[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
|
|
@ -1,5 +1,28 @@
|
|||
# Changelog - Website Sale Aplicoop
|
||||
|
||||
## [18.0.1.11.0] - 2026-07-15
|
||||
|
||||
### Added
|
||||
|
||||
- **Non-weekly cycle automation**: one-time (special/promotional), biweekly and
|
||||
monthly group orders now follow the same cron confirmation flow as weekly orders
|
||||
(confirm linked sale orders + batch their pickings once the cycle cutoff passes).
|
||||
- **Biweekly/monthly recurrence**: these periods keep the weekday scheme
|
||||
(`cutoff_day`/`pickup_day`) but on a recurrence grid anchored at `start_date`
|
||||
(creation date as fallback): cutoffs advance every 14 days / 1 month (snapped to
|
||||
`cutoff_day`), with automatic catch-up if the cron was down. Previously they
|
||||
behaved as weekly.
|
||||
- **One-time orders**: their single cycle is driven by `end_date`
|
||||
(`cutoff_date = end_date`); once it passes, the cron confirms, batches and closes
|
||||
the group order automatically.
|
||||
|
||||
### Changed
|
||||
|
||||
- `end_date` keeps its "permanent if empty" meaning for all recurring periods;
|
||||
it only closes the cycle for one-time orders.
|
||||
- Draft-cart lookup window on the website is now period-aware (7/14 days, 1 month,
|
||||
or the full order lifetime for one-time orders) instead of always 6 days.
|
||||
|
||||
## [18.0.1.9.0] - 2026-05-20
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
{ # noqa: B018
|
||||
"name": "Website Sale - Aplicoop",
|
||||
"version": "18.0.1.10.2",
|
||||
"version": "18.0.1.11.0",
|
||||
"category": "Website/Sale",
|
||||
"summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders",
|
||||
"author": "Odoo Community Association (OCA), Criptomart",
|
||||
|
|
|
|||
|
|
@ -65,27 +65,39 @@ def _get_salesperson_for_order(self, partner):
|
|||
def _find_recent_draft_order(self, partner_id, group_order, request_obj=None):
|
||||
"""Return the active-cycle draft sale.order for the partner, or empty.
|
||||
|
||||
The active ordering period ends at group_order.cutoff_date and begins
|
||||
6 days earlier (weekly cycle). Drafts created outside this window belong
|
||||
to a previous cycle and must not be reused — otherwise stale carts come
|
||||
back when the user re-enters the order page.
|
||||
The active ordering period ends at group_order.cutoff_date and its length
|
||||
depends on the order period: 7 days (weekly), 14 days (biweekly), one
|
||||
month (monthly). One-time orders have a single cycle starting at
|
||||
start_date (or unbounded when start_date is empty). Drafts created
|
||||
outside this window belong to a previous cycle and must not be reused —
|
||||
otherwise stale carts come back when the user re-enters the order page.
|
||||
"""
|
||||
req = request_obj or request
|
||||
from datetime import timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
if not group_order or not group_order.cutoff_date:
|
||||
return req.env["sale.order"]
|
||||
|
||||
period_start = group_order.cutoff_date - timedelta(days=6)
|
||||
period_end = group_order.cutoff_date
|
||||
if group_order.period == "weekly":
|
||||
period_start = period_end - timedelta(days=6)
|
||||
elif group_order.period == "biweekly":
|
||||
period_start = period_end - timedelta(days=13)
|
||||
elif group_order.period == "monthly":
|
||||
period_start = period_end - relativedelta(months=1) + timedelta(days=1)
|
||||
else: # once: single cycle, bounded by start_date when set
|
||||
period_start = group_order.start_date
|
||||
|
||||
domain = [
|
||||
("partner_id", "=", partner_id),
|
||||
("group_order_id", "=", group_order.id),
|
||||
("state", "=", "draft"),
|
||||
("create_date", ">=", f"{period_start} 00:00:00"),
|
||||
("create_date", "<=", f"{period_end} 23:59:59"),
|
||||
]
|
||||
if period_start:
|
||||
domain.append(("create_date", ">=", f"{period_start} 00:00:00"))
|
||||
|
||||
return (
|
||||
req.env["sale.order"].sudo().search(domain, order="create_date desc", limit=1)
|
||||
|
|
|
|||
|
|
@ -663,26 +663,39 @@ msgstr "Día en que las compras se detienen"
|
|||
#. module: website_sale_aplicoop
|
||||
#: model:ir.model.fields,help:website_sale_aplicoop.field_group_order__cutoff_day
|
||||
msgid ""
|
||||
"Day when purchases stop and the consumer group order is locked for this "
|
||||
"week."
|
||||
"Day when purchases stop and the consumer group order is locked for the "
|
||||
"current cycle."
|
||||
msgstr ""
|
||||
"Día en que se detienen las compras y el pedido del grupo de consumidores se "
|
||||
"bloquea para esta semana."
|
||||
"Día en que se detienen las compras y el pedido de grupo se bloquea para el "
|
||||
"ciclo actual."
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model:ir.model.fields,help:website_sale_aplicoop.field_group_order__start_date
|
||||
msgid "Day when the consumer group order opens for purchases"
|
||||
msgstr "Día en que el pedido del grupo de consumidores se abre para compras"
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
"Día en que el pedido de grupo se abre para compras. En pedidos quincenales y"
|
||||
" mensuales también ancla la recurrencia: los ciclos avanzan cada 14 días / 1"
|
||||
" mes desde el primer día de corte posterior a esta fecha."
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form
|
||||
msgid "Day when the order closes (empty = permanent)"
|
||||
msgstr "Día en que el pedido se cierra (vacío = permanente)"
|
||||
msgid ""
|
||||
"Day when the order closes (empty = permanent). One-time orders are confirmed"
|
||||
" and closed once it has passed"
|
||||
msgstr ""
|
||||
"Día en que se cierra el pedido (vacío = permanente). Los pedidos puntuales "
|
||||
"se confirman y cierran una vez pasada la fecha"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form
|
||||
msgid "Day when the order opens for purchases"
|
||||
msgstr "Día en que el pedido se abre para compras"
|
||||
msgid ""
|
||||
"Day when the order opens for purchases (anchors biweekly/monthly cycles)"
|
||||
msgstr ""
|
||||
"Día en que el pedido se abre para compras (ancla los ciclos "
|
||||
"quincenales/mensuales)"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop_products
|
||||
|
|
@ -1120,8 +1133,16 @@ msgstr "Si está marcado, algunos mensajes tienen un error de entrega."
|
|||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model:ir.model.fields,help:website_sale_aplicoop.field_group_order__end_date
|
||||
msgid "If empty, the consumer group order is permanent"
|
||||
msgstr "Si está vacío, el pedido del grupo de consumidores es permanente"
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
"Pedidos recurrentes (semanal, quincenal, mensual): si está vacía, el pedido "
|
||||
"de grupo es permanente. Pedidos puntuales: esta fecha cierra el ciclo único;"
|
||||
" una vez pasada, el cron confirma los pedidos de venta vinculados y cierra "
|
||||
"el pedido."
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model:ir.model.fields,field_description:website_sale_aplicoop.field_group_order__image
|
||||
|
|
|
|||
|
|
@ -661,26 +661,38 @@ msgstr "Erosketak gelditzen diren eguna"
|
|||
#. module: website_sale_aplicoop
|
||||
#: model:ir.model.fields,help:website_sale_aplicoop.field_group_order__cutoff_day
|
||||
msgid ""
|
||||
"Day when purchases stop and the consumer group order is locked for this "
|
||||
"week."
|
||||
"Day when purchases stop and the consumer group order is locked for the "
|
||||
"current cycle."
|
||||
msgstr ""
|
||||
"Erosketak gelditzen diren eguna eta kontsumitzaileen talde eskaera aste "
|
||||
"honetarako "
|
||||
"Erosketak gelditzen diren eguna, talde-eskaera uneko ziklorako blokeatuz."
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model:ir.model.fields,help:website_sale_aplicoop.field_group_order__start_date
|
||||
msgid "Day when the consumer group order opens for purchases"
|
||||
msgstr "Erosketak egiteko kontsumitzaileen talde eskaera irekitzeko eguna"
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
"Talde-eskaera erosketetarako irekitzen den eguna. Hamabostekari eta hileko "
|
||||
"eskaeretan errepikapena ere ainguratzen du: zikloak 14 egunero / hilabetero "
|
||||
"aurreratzen dira, data honen ondorengo lehen mozketa-egunetik."
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form
|
||||
msgid "Day when the order closes (empty = permanent)"
|
||||
msgstr "Eskaera itxi den eguna (hutsik = iraunkorra)"
|
||||
msgid ""
|
||||
"Day when the order closes (empty = permanent). One-time orders are confirmed"
|
||||
" and closed once it has passed"
|
||||
msgstr ""
|
||||
"Eskaera ixten den eguna (hutsik = iraunkorra). Behin bakarreko eskaerak data"
|
||||
" pasatutakoan berretsi eta ixten dira"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form
|
||||
msgid "Day when the order opens for purchases"
|
||||
msgstr "Erosketak egiteko eskaera irekitzeko eguna"
|
||||
msgid ""
|
||||
"Day when the order opens for purchases (anchors biweekly/monthly cycles)"
|
||||
msgstr ""
|
||||
"Eskaera erosketetarako irekitzen den eguna (hamabostekari/hileko zikloak "
|
||||
"ainguratzen ditu)"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop_products
|
||||
|
|
@ -1118,8 +1130,16 @@ msgstr "Baloratuta badago, mezu batzuek entrega errorea dute."
|
|||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model:ir.model.fields,help:website_sale_aplicoop.field_group_order__end_date
|
||||
msgid "If empty, the consumer group order is permanent"
|
||||
msgstr "Hutsik badago, kontsumitzaileen talde eskaera iraunkorra da"
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
"Eskaera errepikariak (astekoa, hamabostekaria, hilekoa): hutsik badago, "
|
||||
"talde-eskaera iraunkorra da. Behin bakarreko eskaerak: data honek ziklo "
|
||||
"bakarra ixten du; pasatutakoan, cron-ak lotutako salmenta-eskaerak berretsi "
|
||||
"eta eskaera ixten du."
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model:ir.model.fields,field_description:website_sale_aplicoop.field_group_order__image
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -59,47 +59,37 @@ class TestCronPickingBatch(TransactionCase):
|
|||
)
|
||||
|
||||
def _create_group_order(self, cutoff_in_past=False, state="open"):
|
||||
"""Create a group order with cutoff date in past or future.
|
||||
"""Create a one-time group order whose cycle ends in past or future.
|
||||
|
||||
One-time group orders derive cutoff_date from end_date, so the cycle
|
||||
is controlled here through end_date. pickup_date is computed as the
|
||||
next occurrence of pickup_day after end_date.
|
||||
|
||||
Args:
|
||||
cutoff_in_past: If True, cutoff_date will be yesterday (past).
|
||||
If False, cutoff_date will be tomorrow (future).
|
||||
cutoff_in_past: If True, end_date (= cutoff_date) is yesterday.
|
||||
If False, end_date is the day after tomorrow.
|
||||
state: State of the group order
|
||||
"""
|
||||
today = fields.Date.today()
|
||||
|
||||
# Create with basic config first
|
||||
order = self.env["group.order"].create(
|
||||
if cutoff_in_past:
|
||||
end_date = today - timedelta(days=1) # Yesterday
|
||||
else:
|
||||
end_date = today + timedelta(days=2) # Day after tomorrow
|
||||
|
||||
return self.env["group.order"].create(
|
||||
{
|
||||
"name": f"Test Group Order {'past' if cutoff_in_past else 'future'}",
|
||||
"group_ids": [
|
||||
(6, 0, [self.consumer_group_1.id, self.consumer_group_2.id])
|
||||
],
|
||||
"period": "once", # One-time order; dates are set explicitly below
|
||||
"cutoff_day": "0", # Monday
|
||||
"period": "once",
|
||||
"pickup_day": "2", # Wednesday
|
||||
"state": state,
|
||||
"end_date": end_date,
|
||||
}
|
||||
)
|
||||
|
||||
# Directly set cutoff_date and pickup_date (bypass computed logic)
|
||||
if cutoff_in_past:
|
||||
cutoff_date = today - timedelta(days=1) # Yesterday
|
||||
pickup_date = today + timedelta(days=1) # Tomorrow
|
||||
else:
|
||||
cutoff_date = today + timedelta(days=2) # Day after tomorrow
|
||||
pickup_date = today + timedelta(days=4) # 4 days from now
|
||||
|
||||
# Write directly to stored computed fields
|
||||
order.write(
|
||||
{
|
||||
"cutoff_date": cutoff_date,
|
||||
"pickup_date": pickup_date,
|
||||
}
|
||||
)
|
||||
|
||||
return order
|
||||
|
||||
def _create_sale_order(self, group_order, partner, consumer_group):
|
||||
"""Create a draft sale order linked to the group order."""
|
||||
return self.env["sale.order"].create(
|
||||
|
|
@ -451,3 +441,273 @@ class TestCronPickingBatch(TransactionCase):
|
|||
"sale",
|
||||
"The order should be confirmed when cron uses non-blocking procurement context",
|
||||
)
|
||||
|
||||
def test_once_order_closes_after_end_date(self):
|
||||
"""One-time order: full cron confirms, batches and closes it after end_date."""
|
||||
group_order = self._create_group_order(cutoff_in_past=True)
|
||||
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
||||
|
||||
self.env["group.order"]._cron_update_dates()
|
||||
|
||||
so.invalidate_recordset()
|
||||
self.assertEqual(
|
||||
so.state,
|
||||
"sale",
|
||||
"Sale order should be confirmed - end date has passed",
|
||||
)
|
||||
self.assertTrue(so.picking_ids, "Sale order should have pickings")
|
||||
self.assertTrue(
|
||||
so.picking_ids[0].batch_id,
|
||||
"Pickings of a one-time order should be batched",
|
||||
)
|
||||
self.assertEqual(
|
||||
group_order.state,
|
||||
"closed",
|
||||
"One-time group order should be closed after its cycle is confirmed",
|
||||
)
|
||||
|
||||
def test_once_order_before_end_date_stays_open(self):
|
||||
"""One-time order with a future end_date must not be confirmed nor closed."""
|
||||
group_order = self._create_group_order(cutoff_in_past=False)
|
||||
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
||||
end_date_before = group_order.end_date
|
||||
|
||||
group_order._confirm_linked_sale_orders()
|
||||
group_order._close_one_time_order_if_ended()
|
||||
|
||||
so.invalidate_recordset()
|
||||
self.assertEqual(
|
||||
so.state,
|
||||
"draft",
|
||||
"Sale order should remain draft - end date not yet passed",
|
||||
)
|
||||
self.assertEqual(group_order.state, "open", "Group order should stay open")
|
||||
self.assertEqual(
|
||||
group_order.end_date,
|
||||
end_date_before,
|
||||
"end_date must not move while the cycle is still open",
|
||||
)
|
||||
|
||||
def test_once_order_without_end_date_is_skipped(self):
|
||||
"""One-time order without end_date has no cutoff and is never auto-confirmed."""
|
||||
group_order = self.env["group.order"].create(
|
||||
{
|
||||
"name": "Test Group Order permanent once",
|
||||
"group_ids": [(6, 0, [self.consumer_group_1.id])],
|
||||
"period": "once",
|
||||
"pickup_day": "2",
|
||||
"state": "open",
|
||||
}
|
||||
)
|
||||
self.assertFalse(
|
||||
group_order.cutoff_date,
|
||||
"Non-weekly order without end_date should have no cutoff_date",
|
||||
)
|
||||
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
||||
|
||||
group_order._confirm_linked_sale_orders()
|
||||
group_order._close_one_time_order_if_ended()
|
||||
|
||||
so.invalidate_recordset()
|
||||
self.assertEqual(
|
||||
so.state,
|
||||
"draft",
|
||||
"Sale order should remain draft - no end_date to evaluate",
|
||||
)
|
||||
self.assertEqual(group_order.state, "open", "Group order should stay open")
|
||||
|
||||
def _create_recurring_group_order(self, period, start_date, cutoff_day):
|
||||
"""Create a biweekly/monthly group order anchored at start_date."""
|
||||
return self.env["group.order"].create(
|
||||
{
|
||||
"name": f"Test {period} Group Order",
|
||||
"group_ids": [(6, 0, [self.consumer_group_1.id])],
|
||||
"period": period,
|
||||
"cutoff_day": cutoff_day,
|
||||
"pickup_day": "2", # Wednesday
|
||||
"start_date": start_date,
|
||||
"state": "open",
|
||||
}
|
||||
)
|
||||
|
||||
def test_biweekly_cutoff_follows_14_day_grid(self):
|
||||
"""Biweekly cutoff lands on the cutoff_day grid anchored at start_date."""
|
||||
today = fields.Date.today()
|
||||
# Anchor 15 days ago on the same weekday as yesterday: the grid is
|
||||
# [today-15, today-1, today+13, ...] and the compute must pick the
|
||||
# first occurrence that is today or later.
|
||||
anchor = today - timedelta(days=15)
|
||||
cutoff_weekday = (today - timedelta(days=1)).weekday()
|
||||
group_order = self._create_recurring_group_order(
|
||||
"biweekly", anchor, str(cutoff_weekday)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
group_order.cutoff_date,
|
||||
today + timedelta(days=13),
|
||||
"Biweekly cutoff must be the next 14-day grid point, not next week",
|
||||
)
|
||||
self.assertEqual(
|
||||
group_order.cutoff_date.weekday(),
|
||||
cutoff_weekday,
|
||||
"Biweekly cutoff must fall on the configured cutoff_day",
|
||||
)
|
||||
self.assertGreater(
|
||||
group_order.pickup_date,
|
||||
group_order.cutoff_date,
|
||||
"Pickup must be after the cycle cutoff",
|
||||
)
|
||||
|
||||
def test_biweekly_cycle_confirms_and_advances_on_grid(self):
|
||||
"""Biweekly: cron confirms the past cutoff, next cutoff moves +14 days."""
|
||||
today = fields.Date.today()
|
||||
anchor = today - timedelta(days=15)
|
||||
cutoff_weekday = (today - timedelta(days=1)).weekday()
|
||||
group_order = self._create_recurring_group_order(
|
||||
"biweekly", anchor, str(cutoff_weekday)
|
||||
)
|
||||
# Simulate the stored value from the previous cycle (grid point that
|
||||
# passed yesterday), as the daily cron would have left it.
|
||||
group_order.write({"cutoff_date": today - timedelta(days=1)})
|
||||
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
||||
|
||||
group_order._confirm_linked_sale_orders()
|
||||
group_order._close_one_time_order_if_ended()
|
||||
group_order._compute_cutoff_date()
|
||||
group_order._compute_pickup_date()
|
||||
group_order._compute_delivery_date()
|
||||
|
||||
so.invalidate_recordset()
|
||||
self.assertEqual(
|
||||
so.state,
|
||||
"sale",
|
||||
"Sale order should be confirmed - biweekly cutoff has passed",
|
||||
)
|
||||
self.assertTrue(so.picking_ids, "Sale order should have pickings")
|
||||
self.assertTrue(
|
||||
so.picking_ids[0].batch_id,
|
||||
"Pickings of a biweekly order should be batched",
|
||||
)
|
||||
self.assertEqual(
|
||||
group_order.state,
|
||||
"open",
|
||||
"Biweekly group order should stay open for the next cycle",
|
||||
)
|
||||
self.assertEqual(
|
||||
group_order.cutoff_date,
|
||||
today + timedelta(days=13),
|
||||
"Next biweekly cutoff must be 14 days after the confirmed one",
|
||||
)
|
||||
self.assertFalse(
|
||||
group_order.end_date,
|
||||
"end_date must stay empty (permanent recurring order)",
|
||||
)
|
||||
|
||||
def test_biweekly_grid_catches_up_after_downtime(self):
|
||||
"""Biweekly grid skips missed cycles and lands today or later."""
|
||||
today = fields.Date.today()
|
||||
# Grid anchored 43 days back: [-43, -29, -15, -1, +13, ...]
|
||||
anchor = today - timedelta(days=43)
|
||||
cutoff_weekday = (today - timedelta(days=1)).weekday()
|
||||
group_order = self._create_recurring_group_order(
|
||||
"biweekly", anchor, str(cutoff_weekday)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
group_order.cutoff_date,
|
||||
today + timedelta(days=13),
|
||||
"Grid must catch up in 14-day steps from the anchor to today",
|
||||
)
|
||||
|
||||
def test_monthly_cutoff_on_cutoff_day_and_future(self):
|
||||
"""Monthly cutoff falls on cutoff_day, today or later, within one cycle."""
|
||||
today = fields.Date.today()
|
||||
anchor = today - timedelta(days=40)
|
||||
cutoff_weekday = today.weekday()
|
||||
group_order = self._create_recurring_group_order(
|
||||
"monthly", anchor, str(cutoff_weekday)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
group_order.cutoff_date.weekday(),
|
||||
cutoff_weekday,
|
||||
"Monthly cutoff must fall on the configured cutoff_day",
|
||||
)
|
||||
self.assertGreaterEqual(
|
||||
group_order.cutoff_date,
|
||||
today,
|
||||
"Monthly cutoff must be today or in the future",
|
||||
)
|
||||
self.assertLess(
|
||||
group_order.cutoff_date,
|
||||
today + timedelta(days=38),
|
||||
"Monthly cutoff must stay within one monthly cycle from today",
|
||||
)
|
||||
|
||||
def test_monthly_cycle_confirms_and_advances(self):
|
||||
"""Monthly: cron confirms the past cutoff, next cutoff keeps the weekday."""
|
||||
today = fields.Date.today()
|
||||
anchor = today - timedelta(days=40)
|
||||
cutoff_weekday = (today - timedelta(days=1)).weekday()
|
||||
group_order = self._create_recurring_group_order(
|
||||
"monthly", anchor, str(cutoff_weekday)
|
||||
)
|
||||
group_order.write({"cutoff_date": today - timedelta(days=1)})
|
||||
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
||||
|
||||
group_order._confirm_linked_sale_orders()
|
||||
group_order._close_one_time_order_if_ended()
|
||||
group_order._compute_cutoff_date()
|
||||
group_order._compute_pickup_date()
|
||||
group_order._compute_delivery_date()
|
||||
|
||||
so.invalidate_recordset()
|
||||
self.assertEqual(
|
||||
so.state,
|
||||
"sale",
|
||||
"Sale order should be confirmed - monthly cutoff has passed",
|
||||
)
|
||||
self.assertEqual(
|
||||
group_order.state,
|
||||
"open",
|
||||
"Monthly group order should stay open for the next cycle",
|
||||
)
|
||||
self.assertGreaterEqual(
|
||||
group_order.cutoff_date,
|
||||
today,
|
||||
"Next monthly cutoff must be today or in the future",
|
||||
)
|
||||
self.assertEqual(
|
||||
group_order.cutoff_date.weekday(),
|
||||
cutoff_weekday,
|
||||
"Next monthly cutoff must keep the configured cutoff_day",
|
||||
)
|
||||
|
||||
def test_biweekly_without_cutoff_day_is_skipped(self):
|
||||
"""Recurring order without cutoff_day has no cutoff and is never confirmed."""
|
||||
today = fields.Date.today()
|
||||
group_order = self.env["group.order"].create(
|
||||
{
|
||||
"name": "Test biweekly without cutoff_day",
|
||||
"group_ids": [(6, 0, [self.consumer_group_1.id])],
|
||||
"period": "biweekly",
|
||||
"pickup_day": "2",
|
||||
"start_date": today - timedelta(days=15),
|
||||
"state": "open",
|
||||
}
|
||||
)
|
||||
self.assertFalse(
|
||||
group_order.cutoff_date,
|
||||
"Recurring order without cutoff_day should have no cutoff_date",
|
||||
)
|
||||
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
||||
|
||||
group_order._confirm_linked_sale_orders()
|
||||
|
||||
so.invalidate_recordset()
|
||||
self.assertEqual(
|
||||
so.state,
|
||||
"draft",
|
||||
"Sale order should remain draft - no cutoff_day to evaluate",
|
||||
)
|
||||
self.assertEqual(group_order.state, "open", "Group order should stay open")
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@
|
|||
<field name="sequence" help="Sequence for ordering in website list"/>
|
||||
<field name="group_ids" widget="many2many_tags" help="Groups that can participate in this order"/>
|
||||
<field name="type" help="Type of group order: Regular, Special, or Promotional"/>
|
||||
<field name="start_date" help="Day when the order opens for purchases"/>
|
||||
<field name="end_date" help="Day when the order closes (empty = permanent)"/>
|
||||
<field name="start_date" help="Day when the order opens for purchases (anchors biweekly/monthly cycles)"/>
|
||||
<field name="end_date" help="Day when the order closes (empty = permanent). One-time orders are confirmed and closed once it has passed"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="period" help="How often this order repeats"/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue