[IMP] website_sale_aplicoop: automate non-weekly group order cycles
One-time, biweekly and monthly group orders now follow the same cron confirmation flow as weekly ones (confirm sale orders + batch pickings when the cycle cutoff passes): - Biweekly/monthly keep the cutoff_day/pickup_day weekday scheme on a recurrence grid anchored at start_date (creation date as fallback): cutoffs advance +14 days / +1 month snapped to cutoff_day, with catch-up after cron downtime. Previously they behaved as weekly. - One-time orders (specials/promotions) are driven by end_date (cutoff_date = end_date); once passed, the cron confirms, batches and closes the group order. - end_date keeps its "empty = permanent" meaning for recurring orders. - Website draft-cart lookup window is now period-aware instead of assuming a 6-day weekly cycle. - New cron tests for once/biweekly/monthly cycles; i18n es/eu updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d8cd83bdaf
commit
aba22fd230
8 changed files with 510 additions and 74 deletions
|
|
@ -4,6 +4,8 @@
|
|||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
|
@ -100,12 +102,17 @@ class GroupOrder(models.Model):
|
|||
start_date = fields.Date(
|
||||
required=False,
|
||||
tracking=True,
|
||||
help="Day when the consumer group order opens for purchases",
|
||||
help="Day when the consumer group order opens for purchases. For biweekly "
|
||||
"and monthly orders it also anchors the recurrence: cycles advance every "
|
||||
"14 days / 1 month from the first cutoff day after this date.",
|
||||
)
|
||||
end_date = fields.Date(
|
||||
required=False,
|
||||
tracking=True,
|
||||
help="If empty, the consumer group order is permanent",
|
||||
help="Recurring orders (weekly, biweekly, monthly): if empty, the consumer "
|
||||
"group order is permanent. One-time orders: this date closes the single "
|
||||
"cycle; once it has passed, the cron confirms the linked sale orders and "
|
||||
"closes the order.",
|
||||
)
|
||||
|
||||
# === Período y días ===
|
||||
|
|
@ -126,7 +133,7 @@ class GroupOrder(models.Model):
|
|||
selection=_get_day_selection,
|
||||
required=False,
|
||||
tracking=True,
|
||||
help="Day when purchases stop and the consumer group order is locked for this week.",
|
||||
help="Day when purchases stop and the consumer group order is locked for the current cycle.",
|
||||
)
|
||||
|
||||
# === Home delivery ===
|
||||
|
|
@ -743,24 +750,52 @@ class GroupOrder(models.Model):
|
|||
reference_date,
|
||||
)
|
||||
|
||||
@api.depends("cutoff_day", "start_date")
|
||||
@staticmethod
|
||||
def _next_weekday_on_or_after(day, weekday):
|
||||
"""Return the first date on/after `day` that falls on `weekday` (0=Monday)."""
|
||||
return day + timedelta(days=(weekday - day.weekday()) % 7)
|
||||
|
||||
@api.depends("cutoff_day", "start_date", "period", "end_date")
|
||||
def _compute_cutoff_date(self):
|
||||
"""Compute the cutoff date (deadline to place orders before pickup).
|
||||
|
||||
The cutoff date is the NEXT occurrence of cutoff_day from today.
|
||||
This is when members can no longer place orders.
|
||||
Recurring orders (weekly, biweekly, monthly) use the weekday scheme:
|
||||
cutoff_date always falls on cutoff_day.
|
||||
|
||||
Example (as of Monday 2026-02-09):
|
||||
- cutoff_day = 6 (Sunday) → cutoff_date = 2026-02-15 (next Sunday)
|
||||
- pickup_day = 1 (Tuesday) → pickup_date = 2026-02-17 (Tuesday after cutoff)
|
||||
- Weekly: the NEXT occurrence of cutoff_day from today.
|
||||
|
||||
Example (as of Monday 2026-02-09):
|
||||
- cutoff_day = 6 (Sunday) → cutoff_date = 2026-02-15 (next Sunday)
|
||||
- pickup_day = 1 (Tuesday) → pickup_date = 2026-02-17 (Tuesday after)
|
||||
|
||||
- Biweekly/monthly: cycles follow a recurrence grid anchored at
|
||||
start_date (or the creation date as fallback). The first cutoff is
|
||||
the first cutoff_day on/after the anchor; each next cutoff advances
|
||||
+14 days / +1 month (snapped forward to cutoff_day). The computed
|
||||
value is the first grid occurrence that is today or later, so the
|
||||
grid stays stable across daily recomputes and catches up after
|
||||
downtime.
|
||||
|
||||
One-time orders (once) have a single cycle driven by the end date:
|
||||
cutoff_date = end_date. In every case the cron confirms sale orders
|
||||
once cutoff_date has passed, sharing one confirmation flow.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
_logger.info("_compute_cutoff_date called for %d records", len(self))
|
||||
today = datetime.now().date()
|
||||
for record in self:
|
||||
if record.cutoff_day:
|
||||
if record.period == "once":
|
||||
record.cutoff_date = record.end_date or None
|
||||
_logger.info(
|
||||
"Computed cutoff_date for order %d from end_date: %s (period=once)",
|
||||
record.id,
|
||||
record.cutoff_date,
|
||||
)
|
||||
elif not record.cutoff_day:
|
||||
record.cutoff_date = None
|
||||
elif record.period == "weekly":
|
||||
target_weekday = int(record.cutoff_day)
|
||||
today = datetime.now().date()
|
||||
|
||||
# Use today as reference if start_date is in the past, otherwise use start_date
|
||||
if record.start_date and record.start_date < today:
|
||||
|
|
@ -789,7 +824,41 @@ class GroupOrder(models.Model):
|
|||
days_ahead,
|
||||
)
|
||||
else:
|
||||
record.cutoff_date = None
|
||||
# Biweekly/monthly recurrence grid anchored at start_date
|
||||
# (creation date as fallback so the grid stays stable).
|
||||
target_weekday = int(record.cutoff_day)
|
||||
anchor = (
|
||||
record.start_date
|
||||
or (record.create_date and record.create_date.date())
|
||||
or today
|
||||
)
|
||||
first_cutoff = self._next_weekday_on_or_after(anchor, target_weekday)
|
||||
|
||||
if first_cutoff >= today:
|
||||
cutoff = first_cutoff
|
||||
elif record.period == "biweekly":
|
||||
elapsed_days = (today - first_cutoff).days
|
||||
cycles = (elapsed_days + 13) // 14 # ceil to next grid point
|
||||
cutoff = first_cutoff + timedelta(days=14 * cycles)
|
||||
else: # monthly
|
||||
cutoff = first_cutoff
|
||||
months = 0
|
||||
while cutoff < today:
|
||||
months += 1
|
||||
cutoff = self._next_weekday_on_or_after(
|
||||
first_cutoff + relativedelta(months=months),
|
||||
target_weekday,
|
||||
)
|
||||
|
||||
record.cutoff_date = cutoff
|
||||
_logger.info(
|
||||
"Computed cutoff_date for order %d: %s (period=%s, anchor=%s, first=%s)",
|
||||
record.id,
|
||||
record.cutoff_date,
|
||||
record.period,
|
||||
anchor,
|
||||
first_cutoff,
|
||||
)
|
||||
|
||||
@api.depends("pickup_date")
|
||||
def _compute_delivery_date(self):
|
||||
|
|
@ -812,12 +881,12 @@ class GroupOrder(models.Model):
|
|||
|
||||
# === Onchange Methods ===
|
||||
|
||||
@api.onchange("cutoff_day", "start_date")
|
||||
@api.onchange("cutoff_day", "start_date", "period", "end_date")
|
||||
def _onchange_cutoff_day(self):
|
||||
"""Force recompute cutoff_date on UI change for immediate feedback."""
|
||||
self._compute_cutoff_date()
|
||||
|
||||
@api.onchange("pickup_day", "cutoff_day", "start_date")
|
||||
@api.onchange("pickup_day", "cutoff_day", "start_date", "period", "end_date")
|
||||
def _onchange_pickup_day(self):
|
||||
"""Force recompute pickup_date on UI change for immediate feedback."""
|
||||
self._compute_pickup_date()
|
||||
|
|
@ -863,8 +932,10 @@ class GroupOrder(models.Model):
|
|||
try:
|
||||
# Confirm BEFORE recomputing dates: cutoff_date still points to the
|
||||
# current cycle's cutoff (today or past), so the check works correctly.
|
||||
# After confirmation, recompute dates so they advance to the next cycle.
|
||||
# After confirmation, close finished one-time orders and recompute
|
||||
# dates so recurring orders move to the next cycle.
|
||||
order._confirm_linked_sale_orders()
|
||||
order._close_one_time_order_if_ended()
|
||||
order._compute_cutoff_date()
|
||||
order._compute_pickup_date()
|
||||
order._compute_delivery_date()
|
||||
|
|
@ -892,6 +963,31 @@ class GroupOrder(models.Model):
|
|||
failed_orders,
|
||||
)
|
||||
|
||||
def _close_one_time_order_if_ended(self):
|
||||
"""Close one-time orders once their end_date has passed.
|
||||
|
||||
Called by the daily cron right after _confirm_linked_sale_orders(), so
|
||||
the single cycle has already been confirmed and batched. Recurring
|
||||
orders (weekly, biweekly, monthly) are untouched: their cycle advances
|
||||
through the weekday-based cutoff/pickup recomputation.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
today = fields.Date.today()
|
||||
if (
|
||||
self.period == "once"
|
||||
and self.state == "open"
|
||||
and self.end_date
|
||||
and self.end_date < today
|
||||
):
|
||||
self.write({"state": "closed"})
|
||||
_logger.info(
|
||||
"Cron: Closed one-time group order %s (%s) - end date %s passed",
|
||||
self.id,
|
||||
self.name,
|
||||
self.end_date,
|
||||
)
|
||||
|
||||
def _confirm_linked_sale_orders(self):
|
||||
"""Confirm draft/sent sale orders linked to this group order.
|
||||
|
||||
|
|
@ -907,13 +1003,17 @@ class GroupOrder(models.Model):
|
|||
|
||||
if not self.cutoff_date:
|
||||
_logger.warning(
|
||||
"Cron: Group order %s (%s) has no cutoff_date (state=%s, period=%s, cutoff_day=%s, start_date=%s). Skipping sale order confirmation for this cycle.",
|
||||
"Cron: Group order %s (%s) has no cutoff_date (state=%s, period=%s, "
|
||||
"cutoff_day=%s, start_date=%s, end_date=%s). Recurring orders need "
|
||||
"a cutoff_day and one-time orders an end_date to close their cycle. "
|
||||
"Skipping sale order confirmation for this cycle.",
|
||||
self.id,
|
||||
self.name,
|
||||
self.state,
|
||||
self.period,
|
||||
self.cutoff_day,
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
)
|
||||
return
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue