Skip to content

Commit 08e9ebf

Browse files
committed
Automated OrderRound Creation
This feature automatically creates new order rounds based on a configurable schedule, eliminating the need for manual creation every other Sunday. ## How It Works The system uses a cron job that runs every 6 hours to check if a new order round should be created. When the conditions are met, it automatically creates a new order round with all the proper dates calculated based on your configuration. ## Configuration All settings are managed through the Django admin interface under **Constance Config**: ### Core Settings - **AUTO_CREATE_ORDERROUNDS**: Enable/disable the automation (default: False) - **ORDERROUND_INTERVAL_WEEKS**: How often to create new rounds (default: 2 weeks) - **ORDERROUND_CREATE_DAYS_AHEAD**: How far in advance to create rounds (default: 7 days) ### Timing Settings - **ORDERROUND_OPEN_HOUR**: Hour when order rounds open, 24h format (default: 12) - **ORDERROUND_CLOSE_HOUR**: Hour when order rounds close, 24h format (default: 3) - **ORDERROUND_DURATION_HOURS**: How long order rounds stay open (default: 63 hours) - **ORDERROUND_COLLECT_DAYS_AFTER**: Days after closing when products can be collected (default: 0) - **ORDERROUND_COLLECT_HOUR**: Hour when products can be collected (default: 18) ### Coordinator Settings - **ORDERROUND_DEFAULT_TRANSPORT_COORDINATOR**: Default transport coordinator user ID (default: 986) - **ORDERROUND_DEFAULT_PICKUP_LOCATION**: Default pickup location ID (default: 1) ## Setup Instructions 1. **Enable the feature**: - Go to Django Admin → Constance Config - Set `AUTO_CREATE_ORDERROUNDS` to `True` 2. **Configure your schedule**: - Adjust the timing settings to match your current manual schedule - Current defaults: Opens Sunday at 12:00, closes Wednesday at 03:00, collect Wednesday at 18:00 - Transport coordinator automatically set to user ID 986 - Pickup location automatically set to location ID 1 3. **Set up cron** (if not already running): - The system uses django-cron which should already be configured - Make sure your deployment runs: `python manage.py runcrons` ## Manual Controls ### Admin Interface - Go to Django Admin → Ordering → Order Rounds - Select any order round and use the action "Create next order round automatically" - This respects your automation settings but bypasses the timing checks ### Management Command ```bash # Create the next order round python manage.py create_orderround # Preview what would be created (dry run) python manage.py create_orderround --dry-run # Force creation even if disabled or not needed python manage.py create_orderround --force ``` ## How Dates Are Calculated The system calculates dates based on: 1. **Next Opening Date**: - If there's a previous round: adds the interval (e.g., 2 weeks) to the last round's opening date - If no previous rounds: uses the next Sunday 2. **Closing Date**: Opening date + duration hours (e.g., 72 hours later) 3. **Collection Date**: Closing date + collection days (e.g., same day if 0 days after) **Example Schedule**: - **Opens**: Sunday at 12:00 (noon) - **Closes**: Wednesday at 03:00 (3 AM) - 63 hours later - **Collect**: Wednesday at 18:00 (6 PM) - same day as closing ## Safety Features - **No Duplicates**: Won't create a round if one already exists within the configured timeframe - **Manual Override**: You can always create rounds manually - automation won't interfere - **Configuration Checks**: Won't create rounds if automation is disabled - **Error Logging**: All creation attempts are logged for troubleshooting ## Monitoring - Check the **Log** section in Django admin for creation events - Use the management command with `--dry-run` to preview upcoming rounds - The cron job prints status messages that appear in your application logs ## Troubleshooting ### Order rounds aren't being created automatically 1. Check that `AUTO_CREATE_ORDERROUNDS` is set to `True` 2. Verify that django-cron is running (`python manage.py runcrons`) 3. Check if there's already a future round within your `ORDERROUND_CREATE_DAYS_AHEAD` setting 4. Look at the application logs for any error messages from the `AutoCreateOrderRounds` cron job ### Dates are wrong 1. Review your timing configuration in Constance Config 2. Use `python manage.py create_orderround --dry-run` to preview the next round 3. Check that your server timezone is set correctly ### Need to disable temporarily 1. Set `AUTO_CREATE_ORDERROUNDS` to `False` in Constance Config 2. The cron job will continue to run but won't create any rounds 3. You can still use manual creation methods ## Migration from Manual Process 1. **Before enabling**: Make sure your current manual schedule matches the configuration 2. **Enable gradually**: Start with `AUTO_CREATE_ORDERROUNDS = False` and test with the management command 3. **Monitor first few rounds**: Check that the automatically created rounds have the correct dates 4. **Keep manual backup**: You can always create rounds manually if needed
1 parent 94aa133 commit 08e9ebf

File tree

7 files changed

+601
-273
lines changed

7 files changed

+601
-273
lines changed

webapp/ordering/admin.py

Lines changed: 100 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,19 @@
77

88
from finance.models import Balance
99
from vokou.admin import DeleteDisabledMixin
10-
from .models import (Order, OrderProduct, Product, OrderRound, ProductCategory,
11-
OrderProductCorrection, ProductStock, Supplier,
12-
ProductUnit, DraftProduct, PickupLocation)
10+
from .models import (
11+
Order,
12+
OrderProduct,
13+
Product,
14+
OrderRound,
15+
ProductCategory,
16+
OrderProductCorrection,
17+
ProductStock,
18+
Supplier,
19+
ProductUnit,
20+
DraftProduct,
21+
PickupLocation,
22+
)
1323

1424

1525
class OrderProductInline(admin.TabularInline):
@@ -18,36 +28,60 @@ class OrderProductInline(admin.TabularInline):
1828

1929
def create_credit_for_order(modeladmin, request, queryset):
2030
for order in queryset:
21-
Balance.objects.create(user=order.user,
22-
type="CR",
23-
amount=order.total_price,
24-
notes="Bestelling #%d contant betaald"
25-
% order.pk)
31+
Balance.objects.create(
32+
user=order.user, type="CR", amount=order.total_price, notes="Bestelling #%d contant betaald" % order.pk
33+
)
2634

2735

2836
create_credit_for_order.short_description = "Contant betaald"
2937

3038

39+
def create_next_orderround(modeladmin, request, queryset):
40+
"""Admin action to manually create the next order round"""
41+
from ordering.core import create_orderround_automatically
42+
from django.contrib import messages
43+
44+
try:
45+
order_round = create_orderround_automatically()
46+
if order_round:
47+
messages.success(
48+
request,
49+
f"Successfully created order round #{order_round.pk}. "
50+
f"Opens: {order_round.open_for_orders.strftime('%Y-%m-%d %H:%M')}, "
51+
f"Closes: {order_round.closed_for_orders.strftime('%Y-%m-%d %H:%M')}, "
52+
f"Collect: {order_round.collect_datetime.strftime('%Y-%m-%d %H:%M')}",
53+
)
54+
else:
55+
messages.warning(
56+
request, "No new order round was created. Check if automation is enabled and if a new round is needed."
57+
)
58+
except Exception as e:
59+
messages.error(request, f"Failed to create order round: {str(e)}")
60+
61+
62+
create_next_orderround.short_description = "Create next order round automatically"
63+
64+
3165
def dutch_decimal(value):
32-
return str(value).replace('.', ',')
66+
return str(value).replace(".", ",")
3367

3468

3569
def export_orders_for_financial_admin(modeladmin, request, queryset):
3670
field_names = [
37-
'Bestelling ID',
38-
'Lid',
39-
'Datum',
40-
'Ronde',
41-
'Totaalsom producten (incl. marge)',
42-
'ledenbijdrage',
43-
'Transactiekosten',
44-
'Betaald',
45-
'Verrekening debit/credit',
46-
'Totaalbedrag']
47-
48-
response = HttpResponse(content_type='text/csv; charset=utf-8')
49-
response[
50-
'Content-Disposition'] = 'attachment; filename=orders_export.csv'
71+
"Bestelling ID",
72+
"Lid",
73+
"Datum",
74+
"Ronde",
75+
"Totaalsom producten (incl. marge)",
76+
"ledenbijdrage",
77+
"Transactiekosten",
78+
"Betaald",
79+
"Verrekening debit/credit",
80+
"Totaalbedrag",
81+
]
82+
83+
response = HttpResponse(content_type="text/csv; charset=utf-8")
84+
response["Content-Disposition"] = "attachment; filename=orders_export.csv"
5185

5286
writer = csv.writer(response)
5387
writer.writerow(field_names)
@@ -64,27 +98,24 @@ def export_orders_for_financial_admin(modeladmin, request, queryset):
6498
"{0} ({1})".format(order.user, order.user_id),
6599
order.modified.date(),
66100
order.order_round_id,
67-
dd(sum([odp.total_retail_price
68-
for odp in order.orderproducts.all()])),
101+
dd(sum([odp.total_retail_price for odp in order.orderproducts.all()])),
69102
dd(order.member_fee),
70103
dd(order.order_round.transaction_costs),
71-
dd(actually_paid).replace('.', ','),
104+
dd(actually_paid).replace(".", ","),
72105
dd(order.debit.amount - actually_paid),
73-
dd(order.debit.amount)
106+
dd(order.debit.amount),
74107
]
75108
writer.writerow(row)
76109

77110
return response
78111

79112

80-
export_orders_for_financial_admin.short_description = (
81-
"Exporteer voor financiële admin")
113+
export_orders_for_financial_admin.short_description = "Exporteer voor financiële admin"
82114

83115

84116
class OrderAdmin(DeleteDisabledMixin, admin.ModelAdmin):
85-
list_display = ["id", "created", "order_round", "user", "finalized",
86-
"paid", "total_price"]
87-
ordering = ("-id", )
117+
list_display = ["id", "created", "order_round", "user", "finalized", "paid", "total_price"]
118+
ordering = ("-id",)
88119
# inlines = [OrderProductInline] ## causes timeout
89120
list_filter = ("paid", "finalized", "order_round")
90121
actions = (create_credit_for_order, export_orders_for_financial_admin)
@@ -96,6 +127,7 @@ def fn(modeladmin, request, queryset):
96127
for product in queryset:
97128
product.category = category
98129
product.save()
130+
99131
return fn
100132

101133

@@ -114,6 +146,7 @@ def remove_category(_, __, queryset):
114146
for product in queryset:
115147
product.category = None
116148
product.save()
149+
117150
remove_category.short_description = "Remove category from product"
118151
prod_cat_actions.append(remove_category)
119152

@@ -122,51 +155,67 @@ def remove_category(_, __, queryset):
122155

123156

124157
class ProductAdmin(admin.ModelAdmin):
125-
list_display = ["name", 'description', "order_round", "supplier",
126-
"category", "base_price", "maximum_total_order",
127-
"new"]
128-
ordering = ("-id", )
158+
list_display = [
159+
"name",
160+
"description",
161+
"order_round",
162+
"supplier",
163+
"category",
164+
"base_price",
165+
"maximum_total_order",
166+
"new",
167+
]
168+
ordering = ("-id",)
129169
list_filter = ("order_round", "supplier", "category", "new")
130170
actions = prod_cat_actions
131171
list_per_page = 500
132172

133173

134174
class ProductStockAdmin(DeleteDisabledMixin, admin.ModelAdmin):
135-
list_display = ["id", "created", "product", 'amount', "type", "notes"]
175+
list_display = ["id", "created", "product", "amount", "type", "notes"]
136176
ordering = ("-id", "created", "product", "amount", "type", "notes")
137177
list_filter = ("type",)
138-
raw_id_fields = ('product',)
178+
raw_id_fields = ("product",)
139179

140180

141181
class OrderRoundAdmin(DeleteDisabledMixin, admin.ModelAdmin):
142-
list_display = ["id", "open_for_orders", "closed_for_orders",
143-
"collect_datetime", "pickup_location", "markup_percentage",
144-
"transaction_costs", "reminder_hours_before_closing",
145-
"order_placed", "reminder_sent"]
146-
ordering = ("-id", )
182+
list_display = [
183+
"id",
184+
"open_for_orders",
185+
"closed_for_orders",
186+
"collect_datetime",
187+
"pickup_location",
188+
"markup_percentage",
189+
"transaction_costs",
190+
"reminder_hours_before_closing",
191+
"order_placed",
192+
"reminder_sent",
193+
]
194+
ordering = ("-id",)
195+
actions = [create_next_orderround]
147196

148197

149198
class OrderProductCorrectionAdmin(DeleteDisabledMixin, admin.ModelAdmin):
150-
list_display = ["id", 'created', 'order_product', "supplied_percentage",
151-
"notes", "credit", "charge_supplier"]
152-
ordering = ("-id", 'charge_supplier', 'supplied_percentage', 'created')
199+
list_display = ["id", "created", "order_product", "supplied_percentage", "notes", "credit", "charge_supplier"]
200+
ordering = ("-id", "charge_supplier", "supplied_percentage", "created")
153201
list_filter = ("charge_supplier",)
154-
raw_id_fields = ('order_product',)
202+
raw_id_fields = ("order_product",)
155203

156204

157205
class OrderProductAdmin(DeleteDisabledMixin, admin.ModelAdmin):
158-
list_display = ["id", 'order', 'order_paid', "product", "amount",
159-
"stock_product", "base_price", "retail_price"]
160-
ordering = ("-id", 'order', 'product')
206+
list_display = ["id", "order", "order_paid", "product", "amount", "stock_product", "base_price", "retail_price"]
207+
ordering = ("-id", "order", "product")
161208
list_filter = ("order__paid", "product__order_round")
162-
raw_id_fields = ('order', 'product')
209+
raw_id_fields = ("order", "product")
163210

164211
def order_paid(self, obj):
165212
return obj.order.paid is True
213+
166214
order_paid.boolean = True
167215

168216
def stock_product(self, obj):
169217
return obj.product.is_stock_product()
218+
170219
stock_product.boolean = True
171220

172221

0 commit comments

Comments
 (0)