diff --git a/stock_picking_batch_custom/models/stock_picking_batch.py b/stock_picking_batch_custom/models/stock_picking_batch.py
index 6c7a409..8e99785 100644
--- a/stock_picking_batch_custom/models/stock_picking_batch.py
+++ b/stock_picking_batch_custom/models/stock_picking_batch.py
@@ -239,7 +239,7 @@ class StockPickingBatch(models.Model):
"domain": [("batch_id", "=", self.id)],
"context": {
"create": False,
- "delete": True,
+ "delete": False,
},
}
diff --git a/stock_picking_batch_custom/views/stock_picking_batch_views.xml b/stock_picking_batch_custom/views/stock_picking_batch_views.xml
index 5379efe..7c90475 100644
--- a/stock_picking_batch_custom/views/stock_picking_batch_views.xml
+++ b/stock_picking_batch_custom/views/stock_picking_batch_views.xml
@@ -21,14 +21,13 @@
stock.move.line.batch.operator
stock.move.line
-
-
diff --git a/website_sale_aplicoop/CHANGELOG.md b/website_sale_aplicoop/CHANGELOG.md
index 5972f82..1eaa993 100644
--- a/website_sale_aplicoop/CHANGELOG.md
+++ b/website_sale_aplicoop/CHANGELOG.md
@@ -1,28 +1,5 @@
# 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
diff --git a/website_sale_aplicoop/__manifest__.py b/website_sale_aplicoop/__manifest__.py
index 0140a35..f7ac586 100644
--- a/website_sale_aplicoop/__manifest__.py
+++ b/website_sale_aplicoop/__manifest__.py
@@ -3,7 +3,7 @@
{ # noqa: B018
"name": "Website Sale - Aplicoop",
- "version": "18.0.1.11.0",
+ "version": "18.0.1.10.2",
"category": "Website/Sale",
"summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders",
"author": "Odoo Community Association (OCA), Criptomart",
diff --git a/website_sale_aplicoop/controllers/website_sale_validators.py b/website_sale_aplicoop/controllers/website_sale_validators.py
index 8f14309..940a604 100644
--- a/website_sale_aplicoop/controllers/website_sale_validators.py
+++ b/website_sale_aplicoop/controllers/website_sale_validators.py
@@ -65,39 +65,27 @@ 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 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.
+ 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.
"""
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)
diff --git a/website_sale_aplicoop/i18n/es.po b/website_sale_aplicoop/i18n/es.po
index e904244..c2f590b 100644
--- a/website_sale_aplicoop/i18n/es.po
+++ b/website_sale_aplicoop/i18n/es.po
@@ -663,39 +663,26 @@ 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 the "
-"current cycle."
+"Day when purchases stop and the consumer group order is locked for this "
+"week."
msgstr ""
-"Día en que se detienen las compras y el pedido de grupo se bloquea para el "
-"ciclo actual."
+"Día en que se detienen las compras y el pedido del grupo de consumidores se "
+"bloquea para esta semana."
#. 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. 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."
+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"
#. 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). 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"
+msgid "Day when the order closes (empty = permanent)"
+msgstr "Día en que el pedido se cierra (vacío = permanente)"
#. 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 (anchors biweekly/monthly cycles)"
-msgstr ""
-"Día en que el pedido se abre para compras (ancla los ciclos "
-"quincenales/mensuales)"
+msgid "Day when the order opens for purchases"
+msgstr "Día en que el pedido se abre para compras"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop_products
@@ -1133,16 +1120,8 @@ 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 ""
-"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."
+msgid "If empty, the consumer group order is permanent"
+msgstr "Si está vacío, el pedido del grupo de consumidores es permanente"
#. module: website_sale_aplicoop
#: model:ir.model.fields,field_description:website_sale_aplicoop.field_group_order__image
diff --git a/website_sale_aplicoop/i18n/eu.po b/website_sale_aplicoop/i18n/eu.po
index bda4097..def924e 100644
--- a/website_sale_aplicoop/i18n/eu.po
+++ b/website_sale_aplicoop/i18n/eu.po
@@ -661,38 +661,26 @@ 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 the "
-"current cycle."
+"Day when purchases stop and the consumer group order is locked for this "
+"week."
msgstr ""
-"Erosketak gelditzen diren eguna, talde-eskaera uneko ziklorako blokeatuz."
+"Erosketak gelditzen diren eguna eta kontsumitzaileen talde eskaera aste "
+"honetarako "
#. 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. 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."
+msgid "Day when the consumer group order opens for purchases"
+msgstr "Erosketak egiteko kontsumitzaileen talde eskaera irekitzeko eguna"
#. 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). 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"
+msgid "Day when the order closes (empty = permanent)"
+msgstr "Eskaera itxi den eguna (hutsik = iraunkorra)"
#. 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 (anchors biweekly/monthly cycles)"
-msgstr ""
-"Eskaera erosketetarako irekitzen den eguna (hamabostekari/hileko zikloak "
-"ainguratzen ditu)"
+msgid "Day when the order opens for purchases"
+msgstr "Erosketak egiteko eskaera irekitzeko eguna"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop_products
@@ -1130,16 +1118,8 @@ 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 ""
-"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."
+msgid "If empty, the consumer group order is permanent"
+msgstr "Hutsik badago, kontsumitzaileen talde eskaera iraunkorra da"
#. module: website_sale_aplicoop
#: model:ir.model.fields,field_description:website_sale_aplicoop.field_group_order__image
diff --git a/website_sale_aplicoop/models/group_order.py b/website_sale_aplicoop/models/group_order.py
index ef81ddd..97dad3c 100644
--- a/website_sale_aplicoop/models/group_order.py
+++ b/website_sale_aplicoop/models/group_order.py
@@ -4,8 +4,6 @@
import logging
from datetime import timedelta
-from dateutil.relativedelta import relativedelta
-
from odoo import api
from odoo import fields
from odoo import models
@@ -102,17 +100,12 @@ class GroupOrder(models.Model):
start_date = fields.Date(
required=False,
tracking=True,
- 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.",
+ help="Day when the consumer group order opens for purchases",
)
end_date = fields.Date(
required=False,
tracking=True,
- 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.",
+ help="If empty, the consumer group order is permanent",
)
# === Período y días ===
@@ -133,7 +126,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 the current cycle.",
+ help="Day when purchases stop and the consumer group order is locked for this week.",
)
# === Home delivery ===
@@ -750,52 +743,24 @@ class GroupOrder(models.Model):
reference_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")
+ @api.depends("cutoff_day", "start_date")
def _compute_cutoff_date(self):
"""Compute the cutoff date (deadline to place orders before pickup).
- Recurring orders (weekly, biweekly, monthly) use the weekday scheme:
- cutoff_date always falls on cutoff_day.
+ The cutoff date is the NEXT occurrence of cutoff_day from today.
+ This is when members can no longer place orders.
- - 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.
+ 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)
"""
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.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":
+ if record.cutoff_day:
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:
@@ -824,41 +789,7 @@ class GroupOrder(models.Model):
days_ahead,
)
else:
- # 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,
- )
+ record.cutoff_date = None
@api.depends("pickup_date")
def _compute_delivery_date(self):
@@ -881,12 +812,12 @@ class GroupOrder(models.Model):
# === Onchange Methods ===
- @api.onchange("cutoff_day", "start_date", "period", "end_date")
+ @api.onchange("cutoff_day", "start_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", "period", "end_date")
+ @api.onchange("pickup_day", "cutoff_day", "start_date")
def _onchange_pickup_day(self):
"""Force recompute pickup_date on UI change for immediate feedback."""
self._compute_pickup_date()
@@ -932,10 +863,8 @@ 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, close finished one-time orders and recompute
- # dates so recurring orders move to the next cycle.
+ # After confirmation, recompute dates so they advance 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()
@@ -963,31 +892,6 @@ 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.
@@ -1003,17 +907,13 @@ 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, 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.",
+ "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.",
self.id,
self.name,
self.state,
self.period,
self.cutoff_day,
self.start_date,
- self.end_date,
)
return
diff --git a/website_sale_aplicoop/tests/test_cron_picking_batch.py b/website_sale_aplicoop/tests/test_cron_picking_batch.py
index 65b111b..152be12 100644
--- a/website_sale_aplicoop/tests/test_cron_picking_batch.py
+++ b/website_sale_aplicoop/tests/test_cron_picking_batch.py
@@ -59,37 +59,47 @@ class TestCronPickingBatch(TransactionCase):
)
def _create_group_order(self, cutoff_in_past=False, state="open"):
- """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.
+ """Create a group order with cutoff date in past or future.
Args:
- cutoff_in_past: If True, end_date (= cutoff_date) is yesterday.
- If False, end_date is the day after tomorrow.
+ cutoff_in_past: If True, cutoff_date will be yesterday (past).
+ If False, cutoff_date will be tomorrow (future).
state: State of the group order
"""
today = fields.Date.today()
- 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(
+ # Create with basic config first
+ order = 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",
+ "period": "once", # One-time order; dates are set explicitly below
+ "cutoff_day": "0", # Monday
"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(
@@ -441,273 +451,3 @@ 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")
diff --git a/website_sale_aplicoop/views/group_order_views.xml b/website_sale_aplicoop/views/group_order_views.xml
index be74408..edf5f4b 100644
--- a/website_sale_aplicoop/views/group_order_views.xml
+++ b/website_sale_aplicoop/views/group_order_views.xml
@@ -54,8 +54,8 @@
-
-
+
+