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 @@ - - + +