# Copyright 2026 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) from datetime import timedelta from odoo import fields from odoo.tests.common import TransactionCase from odoo.tests.common import tagged @tagged("post_install", "date_calculations") class TestDateCalculations(TransactionCase): """Test suite for date calculation methods in group.order model.""" def setUp(self): super().setUp() # Create a test group self.group = self.env["res.partner"].create( { "name": "Test Group", "is_company": True, "email": "group@test.com", } ) def test_compute_pickup_date_basic(self): """Test pickup_date calculation returns next occurrence of pickup day.""" # Use today as reference and calculate next Tuesday today = fields.Date.today() # Find next Sunday (weekday 6) from today days_until_sunday = (6 - today.weekday()) % 7 if days_until_sunday == 0: # If today is Sunday start_date = today else: start_date = today + timedelta(days=days_until_sunday) # Create order with pickup_day = Tuesday (1), starting on Sunday # NO cutoff_day to avoid dependency on cutoff_date order = self.env["group.order"].create( { "name": "Test Order", "group_ids": [(6, 0, [self.group.id])], "start_date": start_date, # Sunday "pickup_day": "1", # Tuesday "cutoff_day": False, # Disable to avoid cutoff_date interference } ) # Force computation order._compute_pickup_date() # Expected: Next Tuesday after Sunday (2 days later) expected_date = start_date + timedelta(days=2) self.assertEqual( order.pickup_date, expected_date, f"Expected {expected_date}, got {order.pickup_date}", ) def test_compute_pickup_date_same_day(self): """Test pickup_date when start_date is same weekday as pickup_day.""" # Find next Tuesday from today today = fields.Date.today() days_until_tuesday = (1 - today.weekday()) % 7 if days_until_tuesday == 0: # If today is Tuesday start_date = today else: start_date = today + timedelta(days=days_until_tuesday) # Start on Tuesday, pickup also Tuesday - should return next week's Tuesday order = self.env["group.order"].create( { "name": "Test Order Same Day", "group_ids": [(6, 0, [self.group.id])], "start_date": start_date, # Tuesday "pickup_day": "1", # Tuesday } ) order._compute_pickup_date() # Should get next Tuesday (7 days later) expected_date = start_date + timedelta(days=7) self.assertEqual(order.pickup_date, expected_date) def test_compute_pickup_date_no_start_date(self): """Test pickup_date calculation when no start_date is set.""" order = self.env["group.order"].create( { "name": "Test Order No Start", "group_ids": [(6, 0, [self.group.id])], "start_date": False, "pickup_day": "1", # Tuesday } ) order._compute_pickup_date() # Should calculate from today self.assertIsNotNone(order.pickup_date) # Verify it's a future date and falls on Tuesday self.assertGreaterEqual(order.pickup_date, fields.Date.today()) self.assertEqual(order.pickup_date.weekday(), 1) # 1 = Tuesday def test_compute_pickup_date_without_pickup_day(self): """Test pickup_date is None when pickup_day is not set.""" order = self.env["group.order"].create( { "name": "Test Order No Pickup Day", "group_ids": [(6, 0, [self.group.id])], "start_date": fields.Date.today(), "pickup_day": False, } ) order._compute_pickup_date() # In Odoo, computed Date fields return False (not None) when no value self.assertFalse(order.pickup_date) def test_compute_pickup_date_all_weekdays(self): """Test pickup_date calculation for each day of the week.""" base_date = fields.Date.from_string("2026-02-02") # Monday for day_num in range(7): day_name = [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday", ][day_num] order = self.env["group.order"].create( { "name": f"Test Order {day_name}", "group_ids": [(6, 0, [self.group.id])], "start_date": base_date, "pickup_day": str(day_num), } ) order._compute_pickup_date() # Verify the weekday matches self.assertEqual( order.pickup_date.weekday(), day_num, f"Pickup date weekday should be {day_num} ({day_name})", ) # Verify it's after start_date self.assertGreater(order.pickup_date, base_date) def test_compute_delivery_date_basic(self): """Test delivery_date is pickup_date + 1 day.""" # Find next Sunday from today today = fields.Date.today() days_until_sunday = (6 - today.weekday()) % 7 if days_until_sunday == 0: # If today is Sunday start_date = today else: start_date = today + timedelta(days=days_until_sunday) order = self.env["group.order"].create( { "name": "Test Delivery Date", "group_ids": [(6, 0, [self.group.id])], "start_date": start_date, # Sunday "pickup_day": "1", # Tuesday = start_date + 2 days } ) order._compute_pickup_date() order._compute_delivery_date() # Pickup is Tuesday (2 days after Sunday start_date) expected_pickup = start_date + timedelta(days=2) # Delivery should be Wednesday (Tuesday + 1) expected_delivery = expected_pickup + timedelta(days=1) self.assertEqual(order.delivery_date, expected_delivery) def test_compute_delivery_date_without_pickup(self): """Test delivery_date is None when pickup_date is not set.""" order = self.env["group.order"].create( { "name": "Test No Delivery", "group_ids": [(6, 0, [self.group.id])], "start_date": fields.Date.today(), "pickup_day": False, # No pickup day = no pickup_date } ) order._compute_pickup_date() order._compute_delivery_date() # In Odoo, computed Date fields return False (not None) when no value self.assertFalse(order.delivery_date) def test_compute_cutoff_date_basic(self): """Test cutoff_date calculation returns next occurrence of cutoff day.""" # Create order with cutoff_day = Sunday (6) # If today is Sunday, cutoff should be today (days_ahead = 0 is allowed) order = self.env["group.order"].create( { "name": "Test Cutoff Date", "group_ids": [(6, 0, [self.group.id])], "start_date": fields.Date.from_string("2026-02-01"), # Sunday "cutoff_day": "6", # Sunday } ) order._compute_cutoff_date() # When today (in code) matches cutoff_day, days_ahead=0, so cutoff is today # The function uses datetime.now().date(), so we can't predict exact date # Instead verify: cutoff_date is set and falls on correct weekday self.assertIsNotNone(order.cutoff_date) self.assertEqual(order.cutoff_date.weekday(), 6) # Sunday def test_compute_cutoff_date_without_cutoff_day(self): """Test cutoff_date is None when cutoff_day is not set.""" order = self.env["group.order"].create( { "name": "Test No Cutoff", "group_ids": [(6, 0, [self.group.id])], "start_date": fields.Date.today(), "cutoff_day": False, } ) order._compute_cutoff_date() # In Odoo, computed Date fields return False (not None) when no value self.assertFalse(order.cutoff_date) def test_date_dependency_chain(self): """Test that changing start_date triggers recomputation of date fields.""" # Find next Sunday from today today = fields.Date.today() days_until_sunday = (6 - today.weekday()) % 7 if days_until_sunday == 0: # If today is Sunday start_date = today else: start_date = today + timedelta(days=days_until_sunday) order = self.env["group.order"].create( { "name": "Test Date Chain", "group_ids": [(6, 0, [self.group.id])], "start_date": start_date, # Dynamic Sunday "pickup_day": "6", # Sunday (must be >= cutoff_day) "cutoff_day": "5", # Saturday } ) # Get initial dates initial_pickup = order.pickup_date initial_delivery = order.delivery_date # Note: cutoff_date uses datetime.now() not start_date, so won't change # Change start_date to a week later new_start_date = start_date + timedelta(days=7) order.write({"start_date": new_start_date}) # Verify pickup and delivery dates changed self.assertNotEqual(order.pickup_date, initial_pickup) self.assertNotEqual(order.delivery_date, initial_delivery) # Verify dates are still consistent if order.pickup_date and order.delivery_date: delta = order.delivery_date - order.pickup_date self.assertEqual(delta.days, 1) def test_pickup_date_no_extra_week_bug(self): """Regression test: ensure pickup_date doesn't add extra week incorrectly. Bug context: Previously when cutoff_day >= pickup_day numerically, logic incorrectly added 7 extra days even when pickup was already ahead in the calendar. """ # Scenario: Pickup Tuesday (1) # Start: Sunday (dynamic) # Expected pickup: Tuesday (2 days later, NOT +9 days) # NOTE: NO cutoff_day to avoid cutoff_date dependency # Find next Sunday from today today = fields.Date.today() days_until_sunday = (6 - today.weekday()) % 7 if days_until_sunday == 0: # If today is Sunday start_date = today else: start_date = today + timedelta(days=days_until_sunday) order = self.env["group.order"].create( { "name": "Regression Test Extra Week", "group_ids": [(6, 0, [self.group.id])], "start_date": start_date, # Sunday (dynamic) "pickup_day": "1", # Tuesday (numerically < 6) "cutoff_day": False, # Disable to test pure start_date logic } ) order._compute_pickup_date() # Must be 2 days after start_date (Tuesday) expected = start_date + timedelta(days=2) self.assertEqual( order.pickup_date, expected, f"Bug detected: pickup_date should be {expected} not {order.pickup_date}", ) # Verify it's exactly 2 days after start_date delta = order.pickup_date - order.start_date self.assertEqual( delta.days, 2, "Pickup should be 2 days after Sunday start_date" ) def test_multiple_orders_same_pickup_day(self): """Test multiple orders with same pickup day get consistent dates.""" start = fields.Date.from_string("2026-02-01") pickup_day = "1" # Tuesday orders = [] for i in range(3): order = self.env["group.order"].create( { "name": f"Test Order {i}", "group_ids": [(6, 0, [self.group.id])], "start_date": start, "pickup_day": pickup_day, } ) orders.append(order) # All should have same pickup_date pickup_dates = [o.pickup_date for o in orders] self.assertEqual( len(set(pickup_dates)), 1, "All orders with same start_date and pickup_day should have same pickup_date", ) # === NEW REGRESSION TESTS (v18.0.1.3.1) === def test_cutoff_same_day_as_today_bug_fix(self): """Regression test: cutoff_date should allow same day as today. Bug fixed in v18.0.1.3.1: Previously, if cutoff_day == today.weekday(), the system would incorrectly add 7 days, scheduling cutoff for next week. Now cutoff_date can be today if cutoff_day matches today's weekday. """ today = fields.Date.today() cutoff_day = str(today.weekday()) # Same as today order = self.env["group.order"].create( { "name": "Test Cutoff Today", "group_ids": [(6, 0, [self.group.id])], "start_date": today, "cutoff_day": cutoff_day, "period": "weekly", } ) # cutoff_date should be TODAY, not next week self.assertEqual( order.cutoff_date, today, f"Expected cutoff_date={today} (today), got {order.cutoff_date}. " "Cutoff should be allowed on the same day.", ) def test_delivery_date_stored_correctly(self): """Regression test: delivery_date must be stored in database. Bug fixed in v18.0.1.3.1: delivery_date had store=False, causing inconsistent values and inability to search/filter by this field. Now delivery_date is stored (store=True). """ today = fields.Date.today() # Set pickup for next Monday days_until_monday = (0 - today.weekday()) % 7 if days_until_monday == 0: days_until_monday = 7 start_date = today + timedelta(days=days_until_monday - 1) order = self.env["group.order"].create( { "name": "Test Delivery Stored", "group_ids": [(6, 0, [self.group.id])], "start_date": start_date, "pickup_day": "0", # Monday "period": "weekly", } ) # Force computation order._compute_pickup_date() order._compute_delivery_date() expected_delivery = order.pickup_date + timedelta(days=1) self.assertEqual( order.delivery_date, expected_delivery, f"Expected delivery_date={expected_delivery}, got {order.delivery_date}", ) # Verify it's stored: read from database order_from_db = self.env["group.order"].browse(order.id) self.assertEqual( order_from_db.delivery_date, expected_delivery, "delivery_date should be persisted in database (store=True)", ) def test_constraint_cutoff_before_pickup_invalid(self): """Test that any combination of cutoff_day and pickup_day is allowed. As of v18.0.1.3.1, the constraint has been removed to allow flexibility in scheduling. Pickup and cutoff days can now be in any order. This test verifies that no ValidationError is raised even when pickup_day < cutoff_day numerically. """ today = fields.Date.today() # This configuration is now allowed (previously would raise ValidationError) # pickup (Tuesday=1) < cutoff (Thursday=3) order = self.env["group.order"].create( { "name": "Order with earlier pickup than cutoff", "group_ids": [(6, 0, [self.group.id])], "start_date": today, "cutoff_day": "3", # Thursday "pickup_day": "1", # Tuesday (numerically before Thursday) "period": "weekly", } ) self.assertIsNotNone(order, "Order should be created without constraint error") def test_constraint_cutoff_before_pickup_valid(self): """Test constraint allows valid configurations. Valid scenarios: - pickup_day > cutoff_day: pickup is after cutoff ✓ - pickup_day == cutoff_day: same day allowed ✓ """ today = fields.Date.today() # Valid: pickup (Saturday=5) > cutoff (Tuesday=1) order1 = self.env["group.order"].create( { "name": "Valid Order 1", "group_ids": [(6, 0, [self.group.id])], "start_date": today, "cutoff_day": "1", # Tuesday "pickup_day": "5", # Saturday (AFTER Tuesday) "period": "weekly", } ) self.assertTrue(order1.id, "Valid configuration should create order") # Valid: pickup == cutoff (same day) order2 = self.env["group.order"].create( { "name": "Valid Order 2", "group_ids": [(6, 0, [self.group.id])], "start_date": today, "cutoff_day": "5", # Saturday "pickup_day": "5", # Saturday (SAME DAY) "period": "weekly", } ) self.assertTrue(order2.id, "Same day configuration should be allowed") def test_all_weekday_combinations_consistency(self): """Test that all valid weekday combinations produce consistent results. This regression test ensures the date calculation logic works correctly for all 49 combinations of start_date × pickup_day (7 × 7). """ today = fields.Date.today() errors = [] for start_offset in range(7): # 7 possible start days start_date = today + timedelta(days=start_offset) for pickup_weekday in range(7): # 7 possible pickup days order = self.env["group.order"].create( { "name": f"Test S{start_offset}P{pickup_weekday}", "group_ids": [(6, 0, [self.group.id])], "start_date": start_date, "pickup_day": str(pickup_weekday), "period": "weekly", } ) # Validate pickup_date is set if not order.pickup_date: errors.append( f"start_offset={start_offset}, pickup_day={pickup_weekday}: " f"pickup_date is None" ) continue # Validate pickup_date is in the future or today if order.pickup_date < start_date: errors.append( f"start_offset={start_offset}, pickup_day={pickup_weekday}: " f"pickup_date {order.pickup_date} < start_date {start_date}" ) # Validate pickup_date weekday matches pickup_day if order.pickup_date.weekday() != pickup_weekday: errors.append( f"start_offset={start_offset}, pickup_day={pickup_weekday}: " f"pickup_date weekday is {order.pickup_date.weekday()}, " f"expected {pickup_weekday}" ) self.assertEqual( len(errors), 0, f"Found {len(errors)} errors in weekday combinations:\n" + "\n".join(errors), ) def test_cron_update_dates_executes(self): """Test that cron job method executes without errors. New feature in v18.0.1.3.1: Daily cron job to recalculate dates for active orders to keep them up-to-date as time passes. """ today = fields.Date.today() # Create multiple orders in different states orders = [] for i, state in enumerate(["draft", "open", "closed"]): order = self.env["group.order"].create( { "name": f"Test Cron Order {state}", "group_ids": [(6, 0, [self.group.id])], "start_date": today, "pickup_day": str((i + 1) % 7), "cutoff_day": str(i % 7), "period": "weekly", "state": state, } ) orders.append(order) # Execute cron method (should not raise errors) try: self.env["group.order"]._cron_update_dates() except Exception as e: self.fail(f"Cron method raised exception: {e}") # Verify dates are still valid for active orders active_orders = [o for o in orders if o.state in ["draft", "open"]] for order in active_orders: self.assertIsNotNone( order.pickup_date, f"Pickup date should be set for active order {order.name}", ) def test_cron_update_dates_skips_orders_without_cutoff_date(self): """Cron should not crash if an active order lacks cutoff_date. A malformed or partially configured active group order should be logged and skipped for sale-order confirmation, but must not block recomputing the rest of active orders. """ today = fields.Date.today() valid_order = self.env["group.order"].create( { "name": "Valid Cron Order", "group_ids": [(6, 0, [self.group.id])], "start_date": today, "pickup_day": "2", # Wednesday "cutoff_day": "0", # Monday "period": "weekly", "state": "open", } ) invalid_order = self.env["group.order"].create( { "name": "Invalid Cron Order Without Cutoff", "group_ids": [(6, 0, [self.group.id])], "start_date": today, "pickup_day": "2", # Wednesday "cutoff_day": False, "period": "weekly", "state": "open", } ) self.assertFalse( invalid_order.cutoff_date, "Precondition failed: invalid order should not have cutoff_date", ) self.env["group.order"]._cron_update_dates() valid_order.invalidate_recordset() invalid_order.invalidate_recordset() self.assertTrue(valid_order.cutoff_date) self.assertTrue(valid_order.pickup_date) self.assertFalse(invalid_order.cutoff_date)