# 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.exceptions import ValidationError 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": "1", # Tuesday "cutoff_day": "6", # Sunday } ) # 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 constraint: pickup_day must be >= cutoff_day for weekly orders. New constraint in v18.0.1.3.1: For weekly orders, if pickup_day < cutoff_day numerically, it creates an illogical scenario where pickup would be scheduled before cutoff in the same week cycle. """ today = fields.Date.today() # Invalid configuration: pickup (Tuesday=1) < cutoff (Thursday=3) with self.assertRaises( ValidationError, msg="Should raise ValidationError for pickup_day < cutoff_day", ): self.env["group.order"].create( { "name": "Invalid Order", "group_ids": [(6, 0, [self.group.id])], "start_date": today, "cutoff_day": "3", # Thursday "pickup_day": "1", # Tuesday (BEFORE Thursday) "period": "weekly", } ) 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}", )