[FIX] website_sale_aplicoop: Critical date calculation fixes (v18.0.1.3.1)

- Fixed _compute_cutoff_date logic: Changed days_ahead <= 0 to days_ahead < 0 to allow cutoff_date same day as today
- Enabled store=True for delivery_date field to persist calculated values and enable database filtering
- Added constraint _check_cutoff_before_pickup to validate pickup_day >= cutoff_day in weekly orders
- Added @api.onchange methods for immediate UI feedback when changing cutoff_day or pickup_day
- Created daily cron job _cron_update_dates to automatically recalculate dates for active orders
- Added 'Calculated Dates' section in form view showing readonly cutoff_date, pickup_date, delivery_date
- Added 6 regression tests with @tagged('post_install', 'date_calculations')
- Updated documentation with comprehensive changelog

This is a more robust fix than v18.0.1.2.0, addressing edge cases in date calculations.
This commit is contained in:
snt 2026-02-18 17:45:45 +01:00
parent c70de71cff
commit 8b0a402ccf
6 changed files with 489 additions and 120 deletions

View file

@ -1,26 +1,31 @@
# Copyright 2026 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from datetime import datetime, timedelta
from datetime import timedelta
from odoo.tests.common import TransactionCase
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.'''
"""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',
})
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.'''
"""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
@ -29,16 +34,18 @@ class TestDateCalculations(TransactionCase):
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
})
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()
@ -48,11 +55,11 @@ class TestDateCalculations(TransactionCase):
self.assertEqual(
order.pickup_date,
expected_date,
f"Expected {expected_date}, got {order.pickup_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.'''
"""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
@ -60,14 +67,16 @@ class TestDateCalculations(TransactionCase):
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 = 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()
@ -76,13 +85,15 @@ class TestDateCalculations(TransactionCase):
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
})
"""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()
@ -93,32 +104,43 @@ class TestDateCalculations(TransactionCase):
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,
})
"""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
"""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]
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 = 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()
@ -126,14 +148,14 @@ class TestDateCalculations(TransactionCase):
self.assertEqual(
order.pickup_date.weekday(),
day_num,
f"Pickup date weekday should be {day_num} ({day_name})"
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.'''
"""Test delivery_date is pickup_date + 1 day."""
# Find next Sunday from today
today = fields.Date.today()
days_until_sunday = (6 - today.weekday()) % 7
@ -141,13 +163,15 @@ class TestDateCalculations(TransactionCase):
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 = 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()
@ -159,13 +183,15 @@ class TestDateCalculations(TransactionCase):
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
})
"""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()
@ -174,15 +200,17 @@ class TestDateCalculations(TransactionCase):
self.assertFalse(order.delivery_date)
def test_compute_cutoff_date_basic(self):
'''Test cutoff_date calculation returns next occurrence of cutoff day.'''
"""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 = 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()
@ -193,20 +221,22 @@ class TestDateCalculations(TransactionCase):
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,
})
"""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.'''
"""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
@ -214,14 +244,16 @@ class TestDateCalculations(TransactionCase):
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
})
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
@ -230,7 +262,7 @@ class TestDateCalculations(TransactionCase):
# Change start_date to a week later
new_start_date = start_date + timedelta(days=7)
order.write({'start_date': new_start_date})
order.write({"start_date": new_start_date})
# Verify pickup and delivery dates changed
self.assertNotEqual(order.pickup_date, initial_pickup)
@ -242,17 +274,17 @@ class TestDateCalculations(TransactionCase):
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.
"""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
@ -260,14 +292,16 @@ class TestDateCalculations(TransactionCase):
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 = 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()
@ -276,30 +310,30 @@ class TestDateCalculations(TransactionCase):
self.assertEqual(
order.pickup_date,
expected,
f"Bug detected: pickup_date should be {expected} not {order.pickup_date}"
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"
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
"""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,
})
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
@ -307,5 +341,229 @@ class TestDateCalculations(TransactionCase):
self.assertEqual(
len(set(pickup_dates)),
1,
"All orders with same start_date and pickup_day should have same pickup_date"
"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}",
)