[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

@ -240,6 +240,29 @@ python -m pytest website_sale_aplicoop/tests/ -v
## Changelog ## Changelog
### 18.0.1.3.1 (2026-02-18)
- **Date Calculation Fixes (Critical)**:
- Fixed `_compute_cutoff_date` logic: Changed `days_ahead <= 0` to `days_ahead < 0` to allow cutoff_date to be the 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 that pickup_day >= cutoff_day in weekly orders
- Added `@api.onchange` methods for immediate UI feedback when changing cutoff_day or pickup_day
- **Automatic Date Updates**:
- Created daily cron job `_cron_update_dates` to automatically recalculate dates for active orders
- Ensures computed dates stay current as time passes
- **UI Improvements**:
- Added "Calculated Dates" section in form view showing readonly cutoff_date, pickup_date, and delivery_date
- Improved visibility of automatically calculated dates for administrators
- **Testing**:
- Added 6 regression tests with `@tagged('post_install', 'date_calculations')`:
- `test_cutoff_same_day_as_today_bug_fix`: Validates cutoff can be today
- `test_delivery_date_stored_correctly`: Ensures delivery_date persistence
- `test_constraint_cutoff_before_pickup_invalid`: Tests invalid configurations are rejected
- `test_constraint_cutoff_before_pickup_valid`: Tests valid configurations work
- `test_all_weekday_combinations_consistency`: Tests all 49 date combinations
- `test_cron_update_dates_executes`: Validates cron job execution
- **Documentation**:
- Documented that this is a more robust fix than v18.0.1.2.0, addressing edge cases in date calculations
### 18.0.1.3.0 (2026-02-16) ### 18.0.1.3.0 (2026-02-16)
- **Performance**: Lazy loading of products for faster page loads - **Performance**: Lazy loading of products for faster page loads
- Configurable product pagination (default: 20 per page) - Configurable product pagination (default: 20 per page)
@ -306,7 +329,7 @@ For issues, feature requests, or contributions:
--- ---
**Version:** 18.0.1.2.0 **Version:** 18.0.1.3.1
**Odoo:** 18.0+ **Odoo:** 18.0+
**License:** AGPL-3 **License:** AGPL-3
**Maintainer:** Criptomart SL **Maintainer:** Criptomart SL

View file

@ -3,7 +3,7 @@
{ # noqa: B018 { # noqa: B018
"name": "Website Sale - Aplicoop", "name": "Website Sale - Aplicoop",
"version": "18.0.1.1.1", "version": "18.0.1.3.1",
"category": "Website/Sale", "category": "Website/Sale",
"summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders", "summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders",
"author": "Odoo Community Association (OCA), Criptomart", "author": "Odoo Community Association (OCA), Criptomart",
@ -24,6 +24,8 @@
"data/groups.xml", "data/groups.xml",
# Datos: Menús del website # Datos: Menús del website
"data/website_menus.xml", "data/website_menus.xml",
# Datos: Cron jobs
"data/cron.xml",
# Vistas de seguridad # Vistas de seguridad
"security/ir.model.access.csv", "security/ir.model.access.csv",
"security/record_rules.xml", "security/record_rules.xml",

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Cron job to update dates for active group orders daily -->
<record id="ir_cron_update_group_order_dates" model="ir.cron">
<field name="name">Group Order: Update Dates Daily</field>
<field name="model_id" ref="model_group_order"/>
<field name="state">code</field>
<field name="code">model._cron_update_dates()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="active" eval="True"/>
</record>
</data>
</odoo>

View file

@ -154,7 +154,7 @@ class GroupOrder(models.Model):
delivery_date = fields.Date( delivery_date = fields.Date(
string="Delivery Date", string="Delivery Date",
compute="_compute_delivery_date", compute="_compute_delivery_date",
store=False, store=True,
readonly=True, readonly=True,
help="Calculated delivery date (pickup date + 1 day)", help="Calculated delivery date (pickup date + 1 day)",
) )
@ -503,10 +503,11 @@ class GroupOrder(models.Model):
# Calculate days to NEXT occurrence of cutoff_day # Calculate days to NEXT occurrence of cutoff_day
days_ahead = target_weekday - current_weekday days_ahead = target_weekday - current_weekday
if days_ahead <= 0: if days_ahead < 0:
# Target day already passed this week or is today # Target day already passed this week
# Jump to next week's occurrence # Jump to next week's occurrence
days_ahead += 7 days_ahead += 7
# If days_ahead == 0, cutoff is today (allowed)
record.cutoff_date = reference_date + timedelta(days=days_ahead) record.cutoff_date = reference_date + timedelta(days=days_ahead)
_logger.info( _logger.info(
@ -534,3 +535,64 @@ class GroupOrder(models.Model):
) )
else: else:
record.delivery_date = None record.delivery_date = None
# === Constraints ===
@api.constrains("cutoff_day", "pickup_day", "period")
def _check_cutoff_before_pickup(self):
"""Validate that pickup_day comes after or equals cutoff_day in weekly orders.
For weekly orders, if pickup_day < cutoff_day numerically, it means pickup
would be scheduled BEFORE cutoff in the same week cycle, which is illogical.
Example:
- cutoff_day=3 (Thursday), pickup_day=1 (Tuesday): INVALID
(pickup Tuesday would be before cutoff Thursday)
- cutoff_day=1 (Tuesday), pickup_day=5 (Saturday): VALID
(pickup Saturday is after cutoff Tuesday)
- cutoff_day=5 (Saturday), pickup_day=5 (Saturday): VALID
(same day allowed)
"""
for record in self:
if record.cutoff_day and record.pickup_day and record.period == "weekly":
cutoff = int(record.cutoff_day)
pickup = int(record.pickup_day)
if pickup < cutoff:
pickup_name = dict(self._get_day_selection())[str(pickup)]
cutoff_name = dict(self._get_day_selection())[str(cutoff)]
raise ValidationError(
_(
"For weekly orders, pickup day ({pickup}) must be after or equal to "
"cutoff day ({cutoff}) in the same week. Current configuration would "
"put pickup before cutoff, which is illogical."
).format(pickup=pickup_name, cutoff=cutoff_name)
)
# === Onchange Methods ===
@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")
def _onchange_pickup_day(self):
"""Force recompute pickup_date on UI change for immediate feedback."""
self._compute_pickup_date()
# === Cron Methods ===
@api.model
def _cron_update_dates(self):
"""Cron job to recalculate dates for active orders daily.
This ensures that computed dates stay up-to-date as time passes.
Only updates orders in 'draft' or 'open' states.
"""
orders = self.search([("state", "in", ["draft", "open"])])
_logger.info("Cron: Updating dates for %d active group orders", len(orders))
for order in orders:
order._compute_cutoff_date()
order._compute_pickup_date()
order._compute_delivery_date()
_logger.info("Cron: Date update completed")

View file

@ -1,26 +1,31 @@
# Copyright 2026 Criptomart # Copyright 2026 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) # 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 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): 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): def setUp(self):
super().setUp() super().setUp()
# Create a test group # Create a test group
self.group = self.env['res.partner'].create({ self.group = self.env["res.partner"].create(
'name': 'Test Group', {
'is_company': True, "name": "Test Group",
'email': 'group@test.com', "is_company": True,
}) "email": "group@test.com",
}
)
def test_compute_pickup_date_basic(self): 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 # Use today as reference and calculate next Tuesday
today = fields.Date.today() today = fields.Date.today()
# Find next Sunday (weekday 6) from today # Find next Sunday (weekday 6) from today
@ -32,13 +37,15 @@ class TestDateCalculations(TransactionCase):
# Create order with pickup_day = Tuesday (1), starting on Sunday # Create order with pickup_day = Tuesday (1), starting on Sunday
# NO cutoff_day to avoid dependency on cutoff_date # NO cutoff_day to avoid dependency on cutoff_date
order = self.env['group.order'].create({ order = self.env["group.order"].create(
'name': 'Test Order', {
'group_ids': [(6, 0, [self.group.id])], "name": "Test Order",
'start_date': start_date, # Sunday "group_ids": [(6, 0, [self.group.id])],
'pickup_day': '1', # Tuesday "start_date": start_date, # Sunday
'cutoff_day': False, # Disable to avoid cutoff_date interference "pickup_day": "1", # Tuesday
}) "cutoff_day": False, # Disable to avoid cutoff_date interference
}
)
# Force computation # Force computation
order._compute_pickup_date() order._compute_pickup_date()
@ -48,11 +55,11 @@ class TestDateCalculations(TransactionCase):
self.assertEqual( self.assertEqual(
order.pickup_date, order.pickup_date,
expected_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): 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 # Find next Tuesday from today
today = fields.Date.today() today = fields.Date.today()
days_until_tuesday = (1 - today.weekday()) % 7 days_until_tuesday = (1 - today.weekday()) % 7
@ -62,12 +69,14 @@ class TestDateCalculations(TransactionCase):
start_date = today + timedelta(days=days_until_tuesday) start_date = today + timedelta(days=days_until_tuesday)
# Start on Tuesday, pickup also Tuesday - should return next week's Tuesday # Start on Tuesday, pickup also Tuesday - should return next week's Tuesday
order = self.env['group.order'].create({ order = self.env["group.order"].create(
'name': 'Test Order Same Day', {
'group_ids': [(6, 0, [self.group.id])], "name": "Test Order Same Day",
'start_date': start_date, # Tuesday "group_ids": [(6, 0, [self.group.id])],
'pickup_day': '1', # Tuesday "start_date": start_date, # Tuesday
}) "pickup_day": "1", # Tuesday
}
)
order._compute_pickup_date() order._compute_pickup_date()
@ -76,13 +85,15 @@ class TestDateCalculations(TransactionCase):
self.assertEqual(order.pickup_date, expected_date) self.assertEqual(order.pickup_date, expected_date)
def test_compute_pickup_date_no_start_date(self): def test_compute_pickup_date_no_start_date(self):
'''Test pickup_date calculation when no start_date is set.''' """Test pickup_date calculation when no start_date is set."""
order = self.env['group.order'].create({ order = self.env["group.order"].create(
'name': 'Test Order No Start', {
'group_ids': [(6, 0, [self.group.id])], "name": "Test Order No Start",
'start_date': False, "group_ids": [(6, 0, [self.group.id])],
'pickup_day': '1', # Tuesday "start_date": False,
}) "pickup_day": "1", # Tuesday
}
)
order._compute_pickup_date() order._compute_pickup_date()
@ -93,32 +104,43 @@ class TestDateCalculations(TransactionCase):
self.assertEqual(order.pickup_date.weekday(), 1) # 1 = Tuesday self.assertEqual(order.pickup_date.weekday(), 1) # 1 = Tuesday
def test_compute_pickup_date_without_pickup_day(self): def test_compute_pickup_date_without_pickup_day(self):
'''Test pickup_date is None when pickup_day is not set.''' """Test pickup_date is None when pickup_day is not set."""
order = self.env['group.order'].create({ order = self.env["group.order"].create(
'name': 'Test Order No Pickup Day', {
'group_ids': [(6, 0, [self.group.id])], "name": "Test Order No Pickup Day",
'start_date': fields.Date.today(), "group_ids": [(6, 0, [self.group.id])],
'pickup_day': False, "start_date": fields.Date.today(),
}) "pickup_day": False,
}
)
order._compute_pickup_date() order._compute_pickup_date()
# In Odoo, computed Date fields return False (not None) when no value # In Odoo, computed Date fields return False (not None) when no value
self.assertFalse(order.pickup_date) self.assertFalse(order.pickup_date)
def test_compute_pickup_date_all_weekdays(self): def test_compute_pickup_date_all_weekdays(self):
'''Test pickup_date calculation for each day of the week.''' """Test pickup_date calculation for each day of the week."""
base_date = fields.Date.from_string('2026-02-02') # Monday base_date = fields.Date.from_string("2026-02-02") # Monday
for day_num in range(7): for day_num in range(7):
day_name = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', day_name = [
'Friday', 'Saturday', 'Sunday'][day_num] "Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
][day_num]
order = self.env['group.order'].create({ order = self.env["group.order"].create(
'name': f'Test Order {day_name}', {
'group_ids': [(6, 0, [self.group.id])], "name": f"Test Order {day_name}",
'start_date': base_date, "group_ids": [(6, 0, [self.group.id])],
'pickup_day': str(day_num), "start_date": base_date,
}) "pickup_day": str(day_num),
}
)
order._compute_pickup_date() order._compute_pickup_date()
@ -126,14 +148,14 @@ class TestDateCalculations(TransactionCase):
self.assertEqual( self.assertEqual(
order.pickup_date.weekday(), order.pickup_date.weekday(),
day_num, 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 # Verify it's after start_date
self.assertGreater(order.pickup_date, base_date) self.assertGreater(order.pickup_date, base_date)
def test_compute_delivery_date_basic(self): 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 # Find next Sunday from today
today = fields.Date.today() today = fields.Date.today()
days_until_sunday = (6 - today.weekday()) % 7 days_until_sunday = (6 - today.weekday()) % 7
@ -142,12 +164,14 @@ class TestDateCalculations(TransactionCase):
else: else:
start_date = today + timedelta(days=days_until_sunday) start_date = today + timedelta(days=days_until_sunday)
order = self.env['group.order'].create({ order = self.env["group.order"].create(
'name': 'Test Delivery Date', {
'group_ids': [(6, 0, [self.group.id])], "name": "Test Delivery Date",
'start_date': start_date, # Sunday "group_ids": [(6, 0, [self.group.id])],
'pickup_day': '1', # Tuesday = start_date + 2 days "start_date": start_date, # Sunday
}) "pickup_day": "1", # Tuesday = start_date + 2 days
}
)
order._compute_pickup_date() order._compute_pickup_date()
order._compute_delivery_date() order._compute_delivery_date()
@ -159,13 +183,15 @@ class TestDateCalculations(TransactionCase):
self.assertEqual(order.delivery_date, expected_delivery) self.assertEqual(order.delivery_date, expected_delivery)
def test_compute_delivery_date_without_pickup(self): def test_compute_delivery_date_without_pickup(self):
'''Test delivery_date is None when pickup_date is not set.''' """Test delivery_date is None when pickup_date is not set."""
order = self.env['group.order'].create({ order = self.env["group.order"].create(
'name': 'Test No Delivery', {
'group_ids': [(6, 0, [self.group.id])], "name": "Test No Delivery",
'start_date': fields.Date.today(), "group_ids": [(6, 0, [self.group.id])],
'pickup_day': False, # No pickup day = no pickup_date "start_date": fields.Date.today(),
}) "pickup_day": False, # No pickup day = no pickup_date
}
)
order._compute_pickup_date() order._compute_pickup_date()
order._compute_delivery_date() order._compute_delivery_date()
@ -174,15 +200,17 @@ class TestDateCalculations(TransactionCase):
self.assertFalse(order.delivery_date) self.assertFalse(order.delivery_date)
def test_compute_cutoff_date_basic(self): 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) # Create order with cutoff_day = Sunday (6)
# If today is Sunday, cutoff should be today (days_ahead = 0 is allowed) # If today is Sunday, cutoff should be today (days_ahead = 0 is allowed)
order = self.env['group.order'].create({ order = self.env["group.order"].create(
'name': 'Test Cutoff Date', {
'group_ids': [(6, 0, [self.group.id])], "name": "Test Cutoff Date",
'start_date': fields.Date.from_string('2026-02-01'), # Sunday "group_ids": [(6, 0, [self.group.id])],
'cutoff_day': '6', # Sunday "start_date": fields.Date.from_string("2026-02-01"), # Sunday
}) "cutoff_day": "6", # Sunday
}
)
order._compute_cutoff_date() order._compute_cutoff_date()
@ -193,20 +221,22 @@ class TestDateCalculations(TransactionCase):
self.assertEqual(order.cutoff_date.weekday(), 6) # Sunday self.assertEqual(order.cutoff_date.weekday(), 6) # Sunday
def test_compute_cutoff_date_without_cutoff_day(self): def test_compute_cutoff_date_without_cutoff_day(self):
'''Test cutoff_date is None when cutoff_day is not set.''' """Test cutoff_date is None when cutoff_day is not set."""
order = self.env['group.order'].create({ order = self.env["group.order"].create(
'name': 'Test No Cutoff', {
'group_ids': [(6, 0, [self.group.id])], "name": "Test No Cutoff",
'start_date': fields.Date.today(), "group_ids": [(6, 0, [self.group.id])],
'cutoff_day': False, "start_date": fields.Date.today(),
}) "cutoff_day": False,
}
)
order._compute_cutoff_date() order._compute_cutoff_date()
# In Odoo, computed Date fields return False (not None) when no value # In Odoo, computed Date fields return False (not None) when no value
self.assertFalse(order.cutoff_date) self.assertFalse(order.cutoff_date)
def test_date_dependency_chain(self): 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 # Find next Sunday from today
today = fields.Date.today() today = fields.Date.today()
days_until_sunday = (6 - today.weekday()) % 7 days_until_sunday = (6 - today.weekday()) % 7
@ -215,13 +245,15 @@ class TestDateCalculations(TransactionCase):
else: else:
start_date = today + timedelta(days=days_until_sunday) start_date = today + timedelta(days=days_until_sunday)
order = self.env['group.order'].create({ order = self.env["group.order"].create(
'name': 'Test Date Chain', {
'group_ids': [(6, 0, [self.group.id])], "name": "Test Date Chain",
'start_date': start_date, # Dynamic Sunday "group_ids": [(6, 0, [self.group.id])],
'pickup_day': '1', # Tuesday "start_date": start_date, # Dynamic Sunday
'cutoff_day': '6', # Sunday "pickup_day": "1", # Tuesday
}) "cutoff_day": "6", # Sunday
}
)
# Get initial dates # Get initial dates
initial_pickup = order.pickup_date initial_pickup = order.pickup_date
@ -230,7 +262,7 @@ class TestDateCalculations(TransactionCase):
# Change start_date to a week later # Change start_date to a week later
new_start_date = start_date + timedelta(days=7) 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 # Verify pickup and delivery dates changed
self.assertNotEqual(order.pickup_date, initial_pickup) self.assertNotEqual(order.pickup_date, initial_pickup)
@ -242,12 +274,12 @@ class TestDateCalculations(TransactionCase):
self.assertEqual(delta.days, 1) self.assertEqual(delta.days, 1)
def test_pickup_date_no_extra_week_bug(self): 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, Bug context: Previously when cutoff_day >= pickup_day numerically,
logic incorrectly added 7 extra days even when pickup was already logic incorrectly added 7 extra days even when pickup was already
ahead in the calendar. ahead in the calendar.
''' """
# Scenario: Pickup Tuesday (1) # Scenario: Pickup Tuesday (1)
# Start: Sunday (dynamic) # Start: Sunday (dynamic)
# Expected pickup: Tuesday (2 days later, NOT +9 days) # Expected pickup: Tuesday (2 days later, NOT +9 days)
@ -261,13 +293,15 @@ class TestDateCalculations(TransactionCase):
else: else:
start_date = today + timedelta(days=days_until_sunday) start_date = today + timedelta(days=days_until_sunday)
order = self.env['group.order'].create({ order = self.env["group.order"].create(
'name': 'Regression Test Extra Week', {
'group_ids': [(6, 0, [self.group.id])], "name": "Regression Test Extra Week",
'start_date': start_date, # Sunday (dynamic) "group_ids": [(6, 0, [self.group.id])],
'pickup_day': '1', # Tuesday (numerically < 6) "start_date": start_date, # Sunday (dynamic)
'cutoff_day': False, # Disable to test pure start_date logic "pickup_day": "1", # Tuesday (numerically < 6)
}) "cutoff_day": False, # Disable to test pure start_date logic
}
)
order._compute_pickup_date() order._compute_pickup_date()
@ -276,30 +310,30 @@ class TestDateCalculations(TransactionCase):
self.assertEqual( self.assertEqual(
order.pickup_date, order.pickup_date,
expected, 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 # Verify it's exactly 2 days after start_date
delta = order.pickup_date - order.start_date delta = order.pickup_date - order.start_date
self.assertEqual( self.assertEqual(
delta.days, delta.days, 2, "Pickup should be 2 days after Sunday start_date"
2,
"Pickup should be 2 days after Sunday start_date"
) )
def test_multiple_orders_same_pickup_day(self): def test_multiple_orders_same_pickup_day(self):
'''Test multiple orders with same pickup day get consistent dates.''' """Test multiple orders with same pickup day get consistent dates."""
start = fields.Date.from_string('2026-02-01') start = fields.Date.from_string("2026-02-01")
pickup_day = '1' # Tuesday pickup_day = "1" # Tuesday
orders = [] orders = []
for i in range(3): for i in range(3):
order = self.env['group.order'].create({ order = self.env["group.order"].create(
'name': f'Test Order {i}', {
'group_ids': [(6, 0, [self.group.id])], "name": f"Test Order {i}",
'start_date': start, "group_ids": [(6, 0, [self.group.id])],
'pickup_day': pickup_day, "start_date": start,
}) "pickup_day": pickup_day,
}
)
orders.append(order) orders.append(order)
# All should have same pickup_date # All should have same pickup_date
@ -307,5 +341,229 @@ class TestDateCalculations(TransactionCase):
self.assertEqual( self.assertEqual(
len(set(pickup_dates)), len(set(pickup_dates)),
1, 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}",
) )

View file

@ -63,6 +63,15 @@
<field name="delivery_product_id" invisible="not home_delivery" required="home_delivery" help="Product to use for home delivery"/> <field name="delivery_product_id" invisible="not home_delivery" required="home_delivery" help="Product to use for home delivery"/>
</group> </group>
</group> </group>
<group string="Calculated Dates" name="calculated_dates">
<group>
<field name="cutoff_date" readonly="1" help="Automatically calculated cutoff date"/>
<field name="pickup_date" readonly="1" help="Automatically calculated pickup date"/>
</group>
<group>
<field name="delivery_date" readonly="1" help="Automatically calculated delivery date (pickup + 1 day)"/>
</group>
</group>
<group string="Description"> <group string="Description">
<field name="description" placeholder="Free text description..." nolabel="1"/> <field name="description" placeholder="Free text description..." nolabel="1"/>
</group> </group>