[ADD] website_sale_aplicoop: restore the test coverage lost with the dead code
The dead-code cleanup dropped 11 test files that were never wired into tests/__init__.py. Reviewing what they covered turned up real holes, so the worthwhile ones come back, rewritten against the current schema. Blacklists were the serious gap: product, supplier and category exclusions have absolute priority over product discovery and nothing exercised them. The old file had two separate defects. Its supplier fixtures wrote `main_seller_id` directly, but product_main_seller computes that field from `variant_seller_ids`, so the compute reset it to False and the blacklist had nothing to exclude; they now create real supplierinfo records. Worse, four whole classes asserted against `group_order.product_ids` -- the m2m *input* -- instead of the discovery result, so they set `category_ids` and then checked a field they never touched. Those go through `_get_products_for_group_order` now, and three tests that had no assertions at all got some. The remaining three failures were test bugs too, all Odoo 17->18 leftovers: * Date cases assumed `pickup_date` derives from `start_date`. The chain is cutoff -> pickup -> delivery, and a recurring order whose start date has passed rolls forward to the current cycle, so a 2024 order has no 2024 pickup. The new file anchors on future dates and finds the next 29 February dynamically, with a class documenting the roll-forward itself. * `/eskaera/labels` is `type="json"`; the old test hit it with a plain GET and read the resulting 400 as a bug. It is called over JSON-RPC now, and a test pins the 400 so nobody repeats it. Also `uom.uom.categ` -> `uom.category`. * `price_include` is computed in 18.0, so fixtures must set `price_include_override`. On top of that `_get_price` filters taxes by company and defaults to `env.company`, not the fixture's, which left the tax list empty -- `tax_included` was False for the wrong reason. Two of the portal tests were passing for the wrong reason as well: the access guard bounced them to /eskaera, which also answers 200. Membership has to be set from the member side with `is_group`, and a new test checks the final URL rather than the status alone. Each fixture that can silently build the wrong thing now carries a guard test. Left out on purpose: three files were unimplemented placeholders, and test_draft_persistence still deserves recovering (see the notes file). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3eb79e2431
commit
3e4bd5e5db
6 changed files with 1817 additions and 0 deletions
280
website_sale_aplicoop/tests/test_date_edge_cases.py
Normal file
280
website_sale_aplicoop/tests/test_date_edge_cases.py
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
# Copyright 2025 Criptomart
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
"""Calendar edge cases for the cutoff/pickup/delivery date chain.
|
||||
|
||||
The date chain is `cutoff_date` -> `pickup_date` -> `delivery_date`:
|
||||
|
||||
- `cutoff_date` is the next occurrence of `cutoff_day`, measured from
|
||||
`start_date` when it is in the future, or from **today** when `start_date`
|
||||
already passed. Recurring orders therefore roll forward to the current
|
||||
cycle instead of staying on the cycle they were created in.
|
||||
- `pickup_date` is the next occurrence of `pickup_day` *strictly after*
|
||||
`cutoff_date`.
|
||||
- `delivery_date` is `pickup_date` + 1 day.
|
||||
|
||||
Every fixture below anchors on a **future** start date so the roll-forward
|
||||
does not move the cycle under the test, which keeps the arithmetic exact and
|
||||
the assertions stable over time. `test_past_weekly_order_rolls_forward`
|
||||
covers the roll-forward itself.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
from datetime import timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class DateEdgeCaseCommon:
|
||||
"""Helpers to build orders anchored on chosen calendar dates."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.group = self.env["res.partner"].create(
|
||||
{
|
||||
"name": "Test Group",
|
||||
"is_company": True,
|
||||
}
|
||||
)
|
||||
|
||||
def _create_order(self, start_date, pickup_day, cutoff_day, **overrides):
|
||||
vals = {
|
||||
"name": "Date Edge Case Order",
|
||||
"group_ids": [(6, 0, [self.group.id])],
|
||||
"type": "regular",
|
||||
"start_date": start_date,
|
||||
"end_date": start_date + timedelta(days=7),
|
||||
"period": "weekly",
|
||||
"pickup_day": str(pickup_day),
|
||||
"cutoff_day": str(cutoff_day),
|
||||
}
|
||||
vals.update(overrides)
|
||||
return self.env["group.order"].create(vals)
|
||||
|
||||
@staticmethod
|
||||
def _next_leap_day(reference):
|
||||
"""First 29 February strictly after `reference`."""
|
||||
year = reference.year
|
||||
while True:
|
||||
year += 1
|
||||
try:
|
||||
candidate = date(year, 2, 29)
|
||||
except ValueError:
|
||||
continue
|
||||
if candidate > reference:
|
||||
return candidate
|
||||
|
||||
@staticmethod
|
||||
def _expected_cutoff(start_date, cutoff_day):
|
||||
"""Mirror of `_compute_cutoff_date` for a future weekly start."""
|
||||
days_ahead = cutoff_day - start_date.weekday()
|
||||
if days_ahead < 0:
|
||||
days_ahead += 7
|
||||
return start_date + timedelta(days=days_ahead)
|
||||
|
||||
@staticmethod
|
||||
def _expected_pickup(cutoff_date, pickup_day):
|
||||
"""Mirror of `_compute_pickup_date`: strictly after the cutoff."""
|
||||
days_ahead = pickup_day - cutoff_date.weekday()
|
||||
if days_ahead <= 0:
|
||||
days_ahead += 7
|
||||
return cutoff_date + timedelta(days=days_ahead)
|
||||
|
||||
|
||||
class TestLeapYearHandling(DateEdgeCaseCommon, TransactionCase):
|
||||
"""A 29 February inside the cycle must not shift the dates."""
|
||||
|
||||
def test_pickup_lands_on_leap_day(self):
|
||||
"""A pickup that falls exactly on 29 February is computed as such."""
|
||||
leap_day = self._next_leap_day(date.today())
|
||||
# Anchor the cycle so the cutoff is the day before the leap day; the
|
||||
# next pickup weekday is then the leap day itself.
|
||||
cutoff = leap_day - timedelta(days=1)
|
||||
start = cutoff - timedelta(days=1)
|
||||
|
||||
order = self._create_order(
|
||||
start_date=start,
|
||||
pickup_day=leap_day.weekday(),
|
||||
cutoff_day=cutoff.weekday(),
|
||||
)
|
||||
|
||||
self.assertEqual(order.cutoff_date, cutoff)
|
||||
self.assertEqual(order.pickup_date, leap_day)
|
||||
self.assertEqual(order.pickup_date.day, 29)
|
||||
self.assertEqual(order.pickup_date.month, 2)
|
||||
|
||||
def test_cycle_spans_the_leap_day(self):
|
||||
"""A cycle crossing 29 February keeps the 7-day pickup spacing."""
|
||||
leap_day = self._next_leap_day(date.today())
|
||||
start = leap_day - timedelta(days=3)
|
||||
|
||||
order = self._create_order(
|
||||
start_date=start,
|
||||
pickup_day=(start.weekday() + 4) % 7,
|
||||
cutoff_day=start.weekday(),
|
||||
)
|
||||
|
||||
# cutoff is the start day itself (days_ahead == 0 is allowed)
|
||||
self.assertEqual(order.cutoff_date, start)
|
||||
self.assertEqual(order.pickup_date, start + timedelta(days=4))
|
||||
# The leap day is inside the cycle, so it was counted as a real day.
|
||||
self.assertLess(order.cutoff_date, leap_day)
|
||||
self.assertGreater(order.pickup_date, leap_day)
|
||||
|
||||
def test_delivery_after_leap_day_pickup(self):
|
||||
"""Delivery is pickup + 1 day even when pickup is 29 February."""
|
||||
leap_day = self._next_leap_day(date.today())
|
||||
cutoff = leap_day - timedelta(days=1)
|
||||
start = cutoff - timedelta(days=1)
|
||||
|
||||
order = self._create_order(
|
||||
start_date=start,
|
||||
pickup_day=leap_day.weekday(),
|
||||
cutoff_day=cutoff.weekday(),
|
||||
)
|
||||
|
||||
self.assertEqual(order.pickup_date, leap_day)
|
||||
# 1 March in a leap year.
|
||||
self.assertEqual(order.delivery_date, date(leap_day.year, 3, 1))
|
||||
|
||||
|
||||
class TestMonthAndYearBoundaries(DateEdgeCaseCommon, TransactionCase):
|
||||
"""Cycles crossing a month or year boundary."""
|
||||
|
||||
def _future_day(self, target_day, month_offset=1):
|
||||
"""A future date landing on `target_day` of some upcoming month."""
|
||||
candidate = date.today().replace(day=1) + relativedelta(months=month_offset)
|
||||
return candidate.replace(day=target_day)
|
||||
|
||||
def test_pickup_crosses_month_boundary(self):
|
||||
"""A pickup in the month after the cutoff is computed correctly."""
|
||||
# Anchor on the 28th so a few days forward always lands next month.
|
||||
start = self._future_day(28)
|
||||
pickup_day = (start.weekday() + 5) % 7
|
||||
|
||||
order = self._create_order(
|
||||
start_date=start,
|
||||
pickup_day=pickup_day,
|
||||
cutoff_day=start.weekday(),
|
||||
)
|
||||
|
||||
expected_pickup = start + timedelta(days=5)
|
||||
self.assertEqual(order.cutoff_date, start)
|
||||
self.assertEqual(order.pickup_date, expected_pickup)
|
||||
self.assertNotEqual(order.pickup_date.month, order.cutoff_date.month)
|
||||
|
||||
def test_pickup_crosses_year_boundary(self):
|
||||
"""A cycle spanning 31 December rolls into the next year."""
|
||||
today = date.today()
|
||||
start = date(today.year + 1, 12, 29)
|
||||
pickup_day = (start.weekday() + 4) % 7
|
||||
|
||||
order = self._create_order(
|
||||
start_date=start,
|
||||
pickup_day=pickup_day,
|
||||
cutoff_day=start.weekday(),
|
||||
)
|
||||
|
||||
self.assertEqual(order.cutoff_date, start)
|
||||
self.assertEqual(order.pickup_date, start + timedelta(days=4))
|
||||
self.assertEqual(order.pickup_date.year, start.year + 1)
|
||||
self.assertEqual(order.pickup_date.month, 1)
|
||||
|
||||
def test_last_day_of_month_pickup(self):
|
||||
"""A pickup landing on the last day of the month is kept intact."""
|
||||
start = self._future_day(24)
|
||||
last_day = start + relativedelta(day=31)
|
||||
cutoff = last_day - timedelta(days=1)
|
||||
|
||||
order = self._create_order(
|
||||
start_date=start,
|
||||
pickup_day=last_day.weekday(),
|
||||
cutoff_day=cutoff.weekday(),
|
||||
)
|
||||
|
||||
self.assertEqual(order.cutoff_date, cutoff)
|
||||
self.assertEqual(order.pickup_date, last_day)
|
||||
# Next day already belongs to the following month.
|
||||
self.assertEqual(order.delivery_date.day, 1)
|
||||
|
||||
|
||||
class TestMonthlyRecurrenceBoundaries(DateEdgeCaseCommon, TransactionCase):
|
||||
"""The monthly grid must survive short months and leap years."""
|
||||
|
||||
def test_monthly_grid_from_january_31_anchor(self):
|
||||
"""A 31 January anchor advances into February without overflowing."""
|
||||
today = date.today()
|
||||
anchor = date(today.year + 1, 1, 31)
|
||||
|
||||
order = self._create_order(
|
||||
start_date=anchor,
|
||||
pickup_day=(anchor.weekday() + 2) % 7,
|
||||
cutoff_day=anchor.weekday(),
|
||||
period="monthly",
|
||||
end_date=anchor + relativedelta(months=6),
|
||||
)
|
||||
|
||||
# First grid point is the anchor itself (already on cutoff_day).
|
||||
self.assertEqual(order.cutoff_date, anchor)
|
||||
self.assertEqual(order.pickup_date, anchor + timedelta(days=2))
|
||||
|
||||
def test_monthly_grid_lands_in_february_of_a_leap_year(self):
|
||||
"""A February cycle in a leap year stays inside February."""
|
||||
leap_day = self._next_leap_day(date.today())
|
||||
anchor = date(leap_day.year, 2, 1)
|
||||
# Only meaningful while the anchor is still ahead of us.
|
||||
if anchor <= date.today():
|
||||
self.skipTest("The next leap February already started")
|
||||
|
||||
order = self._create_order(
|
||||
start_date=anchor,
|
||||
pickup_day=(anchor.weekday() + 1) % 7,
|
||||
cutoff_day=anchor.weekday(),
|
||||
period="monthly",
|
||||
end_date=anchor + relativedelta(months=6),
|
||||
)
|
||||
|
||||
self.assertEqual(order.cutoff_date, anchor)
|
||||
self.assertEqual(order.cutoff_date.month, 2)
|
||||
self.assertEqual(order.pickup_date, anchor + timedelta(days=1))
|
||||
|
||||
|
||||
class TestRollForwardBehaviour(DateEdgeCaseCommon, TransactionCase):
|
||||
"""Past-dated recurring orders move to the current cycle."""
|
||||
|
||||
def test_past_weekly_order_rolls_forward(self):
|
||||
"""A weekly order created long ago computes dates for today's cycle.
|
||||
|
||||
This is the documented behaviour of `_compute_cutoff_date`: once
|
||||
`start_date` is in the past the reference becomes today, so the order
|
||||
never keeps stale dates from the cycle it was created in.
|
||||
"""
|
||||
today = date.today()
|
||||
long_ago = today - relativedelta(years=2)
|
||||
|
||||
order = self._create_order(
|
||||
start_date=long_ago,
|
||||
pickup_day=3,
|
||||
cutoff_day=0,
|
||||
end_date=long_ago + timedelta(days=7),
|
||||
)
|
||||
|
||||
expected_cutoff = self._expected_cutoff(today, 0)
|
||||
self.assertEqual(order.cutoff_date, expected_cutoff)
|
||||
self.assertEqual(order.pickup_date, self._expected_pickup(expected_cutoff, 3))
|
||||
# The stale cycle is gone: nothing points back at the creation year.
|
||||
self.assertGreaterEqual(order.cutoff_date, today)
|
||||
self.assertGreater(order.pickup_date, today)
|
||||
|
||||
def test_future_weekly_order_keeps_its_own_cycle(self):
|
||||
"""A future order is anchored on its start date, not on today."""
|
||||
start = date.today() + timedelta(days=30)
|
||||
|
||||
order = self._create_order(start_date=start, pickup_day=3, cutoff_day=0)
|
||||
|
||||
expected_cutoff = self._expected_cutoff(start, 0)
|
||||
self.assertEqual(order.cutoff_date, expected_cutoff)
|
||||
self.assertEqual(order.pickup_date, self._expected_pickup(expected_cutoff, 3))
|
||||
self.assertGreaterEqual(order.cutoff_date, start)
|
||||
Loading…
Add table
Add a link
Reference in a new issue