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>
713 lines
26 KiB
Python
713 lines
26 KiB
Python
# Copyright 2026 Criptomart
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|
|
|
from datetime import timedelta
|
|
from unittest.mock import patch
|
|
|
|
from odoo import fields
|
|
from odoo.exceptions import UserError
|
|
from odoo.tests.common import TransactionCase
|
|
from odoo.tests.common import tagged
|
|
|
|
|
|
@tagged("post_install", "cron_picking_batch")
|
|
class TestCronPickingBatch(TransactionCase):
|
|
"""Test suite for cron jobs that confirm sale orders and create picking batches."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
# Create consumer groups
|
|
cls.consumer_group_1 = cls.env["res.partner"].create(
|
|
{
|
|
"name": "Consumer Group 1",
|
|
"is_company": True,
|
|
"is_group": True,
|
|
"email": "group1@test.com",
|
|
}
|
|
)
|
|
cls.consumer_group_2 = cls.env["res.partner"].create(
|
|
{
|
|
"name": "Consumer Group 2",
|
|
"is_company": True,
|
|
"is_group": True,
|
|
"email": "group2@test.com",
|
|
}
|
|
)
|
|
# Create test members
|
|
cls.member_1 = cls.env["res.partner"].create(
|
|
{
|
|
"name": "Member 1",
|
|
"email": "member1@test.com",
|
|
"parent_id": cls.consumer_group_1.id,
|
|
}
|
|
)
|
|
cls.member_2 = cls.env["res.partner"].create(
|
|
{
|
|
"name": "Member 2",
|
|
"email": "member2@test.com",
|
|
"parent_id": cls.consumer_group_2.id,
|
|
}
|
|
)
|
|
# Create a test product (storable to generate pickings)
|
|
cls.product = cls.env["product.product"].create(
|
|
{
|
|
"name": "Test Product",
|
|
"is_storable": True, # Odoo 18: storable products generate pickings
|
|
"list_price": 10.0,
|
|
}
|
|
)
|
|
|
|
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.
|
|
|
|
Args:
|
|
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()
|
|
|
|
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",
|
|
"pickup_day": "2", # Wednesday
|
|
"state": state,
|
|
"end_date": end_date,
|
|
}
|
|
)
|
|
|
|
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(
|
|
{
|
|
"partner_id": partner.id,
|
|
"group_order_id": group_order.id,
|
|
"consumer_group_id": consumer_group.id,
|
|
"pickup_date": group_order.pickup_date,
|
|
"order_line": [
|
|
(
|
|
0,
|
|
0,
|
|
{
|
|
"product_id": self.product.id,
|
|
"product_uom_qty": 1,
|
|
"price_unit": 10.0,
|
|
},
|
|
)
|
|
],
|
|
}
|
|
)
|
|
|
|
def test_cron_skips_orders_before_cutoff(self):
|
|
"""Test that cron does NOT confirm orders if cutoff date has not passed."""
|
|
# Create group order with cutoff in future
|
|
group_order = self._create_group_order(cutoff_in_past=False)
|
|
|
|
# Create draft sale orders
|
|
so1 = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
|
so2 = self._create_sale_order(group_order, self.member_2, self.consumer_group_2)
|
|
|
|
self.assertEqual(so1.state, "draft")
|
|
self.assertEqual(so2.state, "draft")
|
|
|
|
# Verify cutoff is in future
|
|
today = fields.Date.today()
|
|
self.assertGreater(
|
|
group_order.cutoff_date,
|
|
today,
|
|
"Cutoff date should be in the future for this test",
|
|
)
|
|
|
|
# Call the confirmation method directly (not full cron to avoid date recalc)
|
|
group_order._confirm_linked_sale_orders()
|
|
|
|
# Sale orders should still be draft (cutoff not passed)
|
|
self.assertEqual(
|
|
so1.state,
|
|
"draft",
|
|
"Sale order should remain draft - cutoff date not yet passed",
|
|
)
|
|
self.assertEqual(
|
|
so2.state,
|
|
"draft",
|
|
"Sale order should remain draft - cutoff date not yet passed",
|
|
)
|
|
|
|
def test_cron_confirms_orders_after_cutoff(self):
|
|
"""Test that cron confirms orders when cutoff date has passed."""
|
|
# Create group order with cutoff yesterday (past)
|
|
group_order = self._create_group_order(cutoff_in_past=True)
|
|
|
|
# Create draft sale orders
|
|
so1 = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
|
so2 = self._create_sale_order(group_order, self.member_2, self.consumer_group_2)
|
|
|
|
self.assertEqual(so1.state, "draft")
|
|
self.assertEqual(so2.state, "draft")
|
|
|
|
# Verify cutoff is in past
|
|
today = fields.Date.today()
|
|
self.assertLess(
|
|
group_order.cutoff_date,
|
|
today,
|
|
"Cutoff date should be in the past for this test",
|
|
)
|
|
|
|
# Call the confirmation method directly
|
|
group_order._confirm_linked_sale_orders()
|
|
|
|
# Refresh records
|
|
so1.invalidate_recordset()
|
|
so2.invalidate_recordset()
|
|
|
|
# Sale orders should be confirmed (cutoff passed)
|
|
self.assertEqual(
|
|
so1.state,
|
|
"sale",
|
|
"Sale order should be confirmed - cutoff date has passed",
|
|
)
|
|
self.assertEqual(
|
|
so2.state,
|
|
"sale",
|
|
"Sale order should be confirmed - cutoff date has passed",
|
|
)
|
|
|
|
def test_cron_freezes_pickup_date_on_confirm(self):
|
|
"""Confirmed orders must keep the cycle pickup date (no next-week drift)."""
|
|
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.assertTrue(
|
|
group_order.pickup_date,
|
|
"Precondition failed: helper must provide a non-empty pickup_date",
|
|
)
|
|
|
|
# Simulate an inconsistent draft date (e.g. stale or shifted value)
|
|
wrong_pickup_date = group_order.pickup_date + timedelta(days=7)
|
|
so.write({"pickup_date": wrong_pickup_date})
|
|
|
|
self.assertEqual(so.state, "draft")
|
|
self.assertEqual(so.pickup_date, wrong_pickup_date)
|
|
|
|
expected_pickup_date = group_order.pickup_date
|
|
|
|
group_order._confirm_linked_sale_orders()
|
|
|
|
so.invalidate_recordset()
|
|
|
|
self.assertEqual(so.state, "sale")
|
|
self.assertEqual(
|
|
so.pickup_date,
|
|
expected_pickup_date,
|
|
"Cron should snapshot and preserve the current cycle pickup_date when confirming",
|
|
)
|
|
|
|
def test_cron_creates_single_picking_batch_for_group_order(self):
|
|
"""Test that cron creates a single picking batch for the whole group order."""
|
|
# Create group order with cutoff yesterday (past)
|
|
group_order = self._create_group_order(cutoff_in_past=True)
|
|
|
|
# Create draft sale orders for different consumer groups
|
|
so1 = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
|
so2 = self._create_sale_order(group_order, self.member_2, self.consumer_group_2)
|
|
|
|
# Call the confirmation method directly
|
|
group_order._confirm_linked_sale_orders()
|
|
|
|
# Refresh records
|
|
so1.invalidate_recordset()
|
|
so2.invalidate_recordset()
|
|
|
|
# Check that pickings were created
|
|
self.assertTrue(so1.picking_ids, "Sale order 1 should have pickings")
|
|
self.assertTrue(so2.picking_ids, "Sale order 2 should have pickings")
|
|
|
|
# Check that all pickings share the same batch
|
|
batch_1 = so1.picking_ids[0].batch_id
|
|
batch_2 = so2.picking_ids[0].batch_id
|
|
|
|
self.assertEqual(
|
|
batch_1.id,
|
|
batch_2.id,
|
|
"Different consumer groups in the same group order should share one batch",
|
|
)
|
|
|
|
# Check that there is only one batch record created
|
|
self.assertEqual(
|
|
self.env["stock.picking.batch"].search_count(
|
|
[("description", "=", group_order.name)]
|
|
),
|
|
1,
|
|
"Only one batch should be created for a single group order",
|
|
)
|
|
|
|
# Check batch description uses the group order name only
|
|
self.assertEqual(
|
|
batch_1.description,
|
|
group_order.name,
|
|
"Batch description should be the group order name",
|
|
)
|
|
|
|
def test_cron_same_consumer_group_same_batch(self):
|
|
"""Test that orders from same consumer group go to same batch."""
|
|
# Create group order with cutoff yesterday (past)
|
|
group_order = self._create_group_order(cutoff_in_past=True)
|
|
|
|
# Create another member in the same group
|
|
member_1b = self.env["res.partner"].create(
|
|
{
|
|
"name": "Member 1B",
|
|
"email": "member1b@test.com",
|
|
"parent_id": self.consumer_group_1.id,
|
|
}
|
|
)
|
|
|
|
# Create two sale orders from same consumer group
|
|
so1 = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
|
so2 = self._create_sale_order(group_order, member_1b, self.consumer_group_1)
|
|
|
|
# Call the confirmation method directly
|
|
group_order._confirm_linked_sale_orders()
|
|
|
|
# Refresh records
|
|
so1.invalidate_recordset()
|
|
so2.invalidate_recordset()
|
|
|
|
# Check that both pickings are in the same batch
|
|
batch_1 = so1.picking_ids[0].batch_id
|
|
batch_2 = so2.picking_ids[0].batch_id
|
|
|
|
self.assertEqual(
|
|
batch_1.id,
|
|
batch_2.id,
|
|
"Same consumer group should have same batch",
|
|
)
|
|
|
|
# Check batch has 2 pickings
|
|
self.assertEqual(
|
|
len(batch_1.picking_ids),
|
|
2,
|
|
"Batch should contain 2 pickings from same consumer group",
|
|
)
|
|
|
|
def test_cron_batch_scheduled_date(self):
|
|
"""Test that batch has a scheduled_date set."""
|
|
# Create group order with cutoff yesterday (past)
|
|
group_order = self._create_group_order(cutoff_in_past=True)
|
|
|
|
# Create a sale order
|
|
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
|
|
|
# Call the confirmation method directly
|
|
group_order._confirm_linked_sale_orders()
|
|
|
|
# Refresh records
|
|
so.invalidate_recordset()
|
|
|
|
# Check batch exists and has scheduled_date
|
|
self.assertTrue(so.picking_ids, "Sale order should have pickings")
|
|
batch = so.picking_ids[0].batch_id
|
|
self.assertTrue(batch, "Picking should have a batch")
|
|
self.assertEqual(
|
|
batch.scheduled_date.date(),
|
|
group_order.pickup_date,
|
|
"Batch scheduled_date should be the pickup date (day before delivery)",
|
|
)
|
|
|
|
def test_cron_does_not_duplicate_batches(self):
|
|
"""Test that running cron twice does not create duplicate batches."""
|
|
# Create group order with cutoff yesterday (past)
|
|
group_order = self._create_group_order(cutoff_in_past=True)
|
|
|
|
# Create a sale order
|
|
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
|
|
|
# Call first time
|
|
group_order._confirm_linked_sale_orders()
|
|
|
|
# Refresh records
|
|
so.invalidate_recordset()
|
|
|
|
self.assertTrue(so.picking_ids, "Sale order should have pickings")
|
|
batch_first = so.picking_ids[0].batch_id
|
|
batch_count_first = self.env["stock.picking.batch"].search_count(
|
|
[("description", "=", group_order.name)]
|
|
)
|
|
|
|
# Call second time
|
|
group_order._confirm_linked_sale_orders()
|
|
so.invalidate_recordset()
|
|
|
|
batch_second = so.picking_ids[0].batch_id
|
|
batch_count_second = self.env["stock.picking.batch"].search_count(
|
|
[("description", "=", group_order.name)]
|
|
)
|
|
|
|
# Should be same batch, no duplicates
|
|
self.assertEqual(
|
|
batch_first.id,
|
|
batch_second.id,
|
|
"Batch should not change after second cron execution",
|
|
)
|
|
self.assertEqual(
|
|
batch_count_first,
|
|
batch_count_second,
|
|
"No new batches should be created on second cron execution",
|
|
)
|
|
|
|
def test_cron_closed_group_order_not_processed(self):
|
|
"""Test that closed group orders are not processed by cron."""
|
|
# Create group order with cutoff yesterday but state=closed
|
|
group_order = self._create_group_order(cutoff_in_past=True, state="closed")
|
|
|
|
# Create draft sale order
|
|
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
|
|
|
|
# Execute full cron (which only processes draft/open orders)
|
|
self.env["group.order"]._cron_update_dates()
|
|
|
|
# Refresh
|
|
so.invalidate_recordset()
|
|
|
|
# Sale order should still be draft (closed group orders not processed)
|
|
self.assertEqual(
|
|
so.state,
|
|
"draft",
|
|
"Sale order should remain draft - group order is closed",
|
|
)
|
|
|
|
def test_cron_confirm_ignores_procurement_usererror(self):
|
|
"""Procurement UserError must not block confirmation in cron flow."""
|
|
group_order = self._create_group_order(cutoff_in_past=True)
|
|
|
|
so_ok = self._create_sale_order(
|
|
group_order, self.member_1, self.consumer_group_1
|
|
)
|
|
so_fail = self._create_sale_order(
|
|
group_order,
|
|
self.member_2,
|
|
self.consumer_group_2,
|
|
)
|
|
|
|
so_ok.write({"name": "SO-OK"})
|
|
so_fail.write({"name": "SO-FAIL"})
|
|
|
|
SaleOrderClass = type(self.env["sale.order"])
|
|
original_action_confirm = SaleOrderClass.action_confirm
|
|
|
|
def _patched_action_confirm(recordset):
|
|
should_fail = any(so.name == "SO-FAIL" for so in recordset)
|
|
if should_fail and not recordset.env.context.get("from_orderpoint"):
|
|
raise UserError()
|
|
return original_action_confirm(recordset)
|
|
|
|
with patch.object(SaleOrderClass, "action_confirm", _patched_action_confirm):
|
|
group_order._confirm_linked_sale_orders()
|
|
|
|
so_ok.invalidate_recordset()
|
|
so_fail.invalidate_recordset()
|
|
|
|
self.assertEqual(
|
|
so_ok.state,
|
|
"sale",
|
|
"The valid order must still be confirmed",
|
|
)
|
|
self.assertTrue(
|
|
so_ok.picking_ids,
|
|
"The valid order should have pickings created",
|
|
)
|
|
self.assertTrue(
|
|
so_ok.picking_ids[0].batch_id,
|
|
"Pickings from valid orders should be batched",
|
|
)
|
|
|
|
self.assertEqual(
|
|
so_fail.state,
|
|
"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")
|