addons-cm/website_sale_aplicoop/tests/test_cron_picking_batch.py
snt ce393b6034 [FIX] TestConfirmEskaera_Integration: limpieza de decoradores @patch y corrección de bugs
- Convertir 4 tests de decorador @patch a context manager 'with patch(...)' para evitar RuntimeError en LocalProxy de Werkzeug
- Corregir patrón env(user=..., context=dict(...)) en Odoo 18 (sin .with_context())
- Agregar website real al mock para integración con helpers de pricing (_get_pricing_info)
- Añadir pickup_date en fixture de existing_order para que _find_recent_draft_order localice correctamente
- BUGFIX: Agregar (5,) a order_line para limpiar líneas previas al actualizar pedido existente

Resultado: 0 failed, 0 errors de 4 tests en Docker para TestConfirmEskaera_Integration

BREAKING: _create_or_update_sale_order ahora limpia las líneas anteriores con (5,) antes de asignar las nuevas cuando se actualiza un pedido existente. Comportamiento previo (duplicación de líneas) era un bug.
2026-04-08 17:26:57 +02:00

457 lines
16 KiB
Python

# Copyright 2026 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from datetime import timedelta
from unittest.mock import patch
from odoo import fields
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
from odoo.tests.common import tagged
@tagged("post_install", "cron_picking_batch")
class TestCronPickingBatch(TransactionCase):
"""Test suite for cron jobs that confirm sale orders and create picking batches."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create consumer groups
cls.consumer_group_1 = cls.env["res.partner"].create(
{
"name": "Consumer Group 1",
"is_company": True,
"is_group": True,
"email": "group1@test.com",
}
)
cls.consumer_group_2 = cls.env["res.partner"].create(
{
"name": "Consumer Group 2",
"is_company": True,
"is_group": True,
"email": "group2@test.com",
}
)
# Create test members
cls.member_1 = cls.env["res.partner"].create(
{
"name": "Member 1",
"email": "member1@test.com",
"parent_id": cls.consumer_group_1.id,
}
)
cls.member_2 = cls.env["res.partner"].create(
{
"name": "Member 2",
"email": "member2@test.com",
"parent_id": cls.consumer_group_2.id,
}
)
# Create a test product (storable to generate pickings)
cls.product = cls.env["product.product"].create(
{
"name": "Test Product",
"is_storable": True, # Odoo 18: storable products generate pickings
"list_price": 10.0,
}
)
def _create_group_order(self, cutoff_in_past=False, state="open"):
"""Create a group order with cutoff date in past or future.
Args:
cutoff_in_past: If True, cutoff_date will be yesterday (past).
If False, cutoff_date will be tomorrow (future).
state: State of the group order
"""
today = fields.Date.today()
# Create with basic config first
order = self.env["group.order"].create(
{
"name": f"Test Group Order {'past' if cutoff_in_past else 'future'}",
"group_ids": [
(6, 0, [self.consumer_group_1.id, self.consumer_group_2.id])
],
"period": "once", # One-time order; dates are set explicitly below
"cutoff_day": "0", # Monday
"pickup_day": "2", # Wednesday
"state": state,
}
)
# Directly set cutoff_date and pickup_date (bypass computed logic)
if cutoff_in_past:
cutoff_date = today - timedelta(days=1) # Yesterday
pickup_date = today + timedelta(days=1) # Tomorrow
else:
cutoff_date = today + timedelta(days=2) # Day after tomorrow
pickup_date = today + timedelta(days=4) # 4 days from now
# Write directly to stored computed fields
order.write(
{
"cutoff_date": cutoff_date,
"pickup_date": pickup_date,
}
)
return order
def _create_sale_order(self, group_order, partner, consumer_group):
"""Create a draft sale order linked to the group order."""
return self.env["sale.order"].create(
{
"partner_id": partner.id,
"group_order_id": group_order.id,
"consumer_group_id": consumer_group.id,
"pickup_date": group_order.pickup_date,
"order_line": [
(
0,
0,
{
"product_id": self.product.id,
"product_uom_qty": 1,
"price_unit": 10.0,
},
)
],
}
)
def test_cron_skips_orders_before_cutoff(self):
"""Test that cron does NOT confirm orders if cutoff date has not passed."""
# Create group order with cutoff in future
group_order = self._create_group_order(cutoff_in_past=False)
# Create draft sale orders
so1 = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
so2 = self._create_sale_order(group_order, self.member_2, self.consumer_group_2)
self.assertEqual(so1.state, "draft")
self.assertEqual(so2.state, "draft")
# Verify cutoff is in future
today = fields.Date.today()
self.assertGreater(
group_order.cutoff_date,
today,
"Cutoff date should be in the future for this test",
)
# Call the confirmation method directly (not full cron to avoid date recalc)
group_order._confirm_linked_sale_orders()
# Sale orders should still be draft (cutoff not passed)
self.assertEqual(
so1.state,
"draft",
"Sale order should remain draft - cutoff date not yet passed",
)
self.assertEqual(
so2.state,
"draft",
"Sale order should remain draft - cutoff date not yet passed",
)
def test_cron_confirms_orders_after_cutoff(self):
"""Test that cron confirms orders when cutoff date has passed."""
# Create group order with cutoff yesterday (past)
group_order = self._create_group_order(cutoff_in_past=True)
# Create draft sale orders
so1 = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
so2 = self._create_sale_order(group_order, self.member_2, self.consumer_group_2)
self.assertEqual(so1.state, "draft")
self.assertEqual(so2.state, "draft")
# Verify cutoff is in past
today = fields.Date.today()
self.assertLess(
group_order.cutoff_date,
today,
"Cutoff date should be in the past for this test",
)
# Call the confirmation method directly
group_order._confirm_linked_sale_orders()
# Refresh records
so1.invalidate_recordset()
so2.invalidate_recordset()
# Sale orders should be confirmed (cutoff passed)
self.assertEqual(
so1.state,
"sale",
"Sale order should be confirmed - cutoff date has passed",
)
self.assertEqual(
so2.state,
"sale",
"Sale order should be confirmed - cutoff date has passed",
)
def test_cron_freezes_pickup_date_on_confirm(self):
"""Confirmed orders must keep the cycle pickup date (no next-week drift)."""
group_order = self._create_group_order(cutoff_in_past=True)
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
self.assertTrue(
group_order.pickup_date,
"Precondition failed: helper must provide a non-empty pickup_date",
)
# Simulate an inconsistent draft date (e.g. stale or shifted value)
wrong_pickup_date = group_order.pickup_date + timedelta(days=7)
so.write({"pickup_date": wrong_pickup_date})
self.assertEqual(so.state, "draft")
self.assertEqual(so.pickup_date, wrong_pickup_date)
expected_pickup_date = group_order.pickup_date
group_order._confirm_linked_sale_orders()
so.invalidate_recordset()
self.assertEqual(so.state, "sale")
self.assertEqual(
so.pickup_date,
expected_pickup_date,
"Cron should snapshot and preserve the current cycle pickup_date when confirming",
)
def test_cron_creates_picking_batch_per_consumer_group(self):
"""Test that cron creates separate picking batches per consumer group."""
# Create group order with cutoff yesterday (past)
group_order = self._create_group_order(cutoff_in_past=True)
# Create draft sale orders for different consumer groups
so1 = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
so2 = self._create_sale_order(group_order, self.member_2, self.consumer_group_2)
# Call the confirmation method directly
group_order._confirm_linked_sale_orders()
# Refresh records
so1.invalidate_recordset()
so2.invalidate_recordset()
# Check that pickings were created
self.assertTrue(so1.picking_ids, "Sale order 1 should have pickings")
self.assertTrue(so2.picking_ids, "Sale order 2 should have pickings")
# Check that pickings have batch_id assigned
for picking in so1.picking_ids:
self.assertTrue(
picking.batch_id,
"Picking from SO1 should be assigned to a batch",
)
for picking in so2.picking_ids:
self.assertTrue(
picking.batch_id,
"Picking from SO2 should be assigned to a batch",
)
# Check that batches are different (one per consumer group)
batch_1 = so1.picking_ids[0].batch_id
batch_2 = so2.picking_ids[0].batch_id
self.assertNotEqual(
batch_1.id,
batch_2.id,
"Different consumer groups should have different batches",
)
# Check batch descriptions contain consumer group names
self.assertIn(
self.consumer_group_1.name,
batch_1.description,
"Batch 1 description should include consumer group 1 name",
)
self.assertIn(
self.consumer_group_2.name,
batch_2.description,
"Batch 2 description should include consumer group 2 name",
)
def test_cron_same_consumer_group_same_batch(self):
"""Test that orders from same consumer group go to same batch."""
# Create group order with cutoff yesterday (past)
group_order = self._create_group_order(cutoff_in_past=True)
# Create another member in the same group
member_1b = self.env["res.partner"].create(
{
"name": "Member 1B",
"email": "member1b@test.com",
"parent_id": self.consumer_group_1.id,
}
)
# Create two sale orders from same consumer group
so1 = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
so2 = self._create_sale_order(group_order, member_1b, self.consumer_group_1)
# Call the confirmation method directly
group_order._confirm_linked_sale_orders()
# Refresh records
so1.invalidate_recordset()
so2.invalidate_recordset()
# Check that both pickings are in the same batch
batch_1 = so1.picking_ids[0].batch_id
batch_2 = so2.picking_ids[0].batch_id
self.assertEqual(
batch_1.id,
batch_2.id,
"Same consumer group should have same batch",
)
# Check batch has 2 pickings
self.assertEqual(
len(batch_1.picking_ids),
2,
"Batch should contain 2 pickings from same consumer group",
)
def test_cron_batch_scheduled_date(self):
"""Test that batch has a scheduled_date set."""
# Create group order with cutoff yesterday (past)
group_order = self._create_group_order(cutoff_in_past=True)
# Create a sale order
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
# Call the confirmation method directly
group_order._confirm_linked_sale_orders()
# Refresh records
so.invalidate_recordset()
# Check batch exists and has scheduled_date
self.assertTrue(so.picking_ids, "Sale order should have pickings")
batch = so.picking_ids[0].batch_id
self.assertTrue(batch, "Picking should have a batch")
# scheduled_date should be set (not False/None)
self.assertTrue(
batch.scheduled_date,
"Batch should have a scheduled_date set",
)
def test_cron_does_not_duplicate_batches(self):
"""Test that running cron twice does not create duplicate batches."""
# Create group order with cutoff yesterday (past)
group_order = self._create_group_order(cutoff_in_past=True)
# Create a sale order
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
# Call first time
group_order._confirm_linked_sale_orders()
# Refresh records
so.invalidate_recordset()
self.assertTrue(so.picking_ids, "Sale order should have pickings")
batch_first = so.picking_ids[0].batch_id
batch_count_first = self.env["stock.picking.batch"].search_count([])
# Call second time
group_order._confirm_linked_sale_orders()
batch_second = so.picking_ids[0].batch_id
batch_count_second = self.env["stock.picking.batch"].search_count([])
# Should be same batch, no duplicates
self.assertEqual(
batch_first.id,
batch_second.id,
"Batch should not change after second cron execution",
)
self.assertEqual(
batch_count_first,
batch_count_second,
"No new batches should be created on second cron execution",
)
def test_cron_closed_group_order_not_processed(self):
"""Test that closed group orders are not processed by cron."""
# Create group order with cutoff yesterday but state=closed
group_order = self._create_group_order(cutoff_in_past=True, state="closed")
# Create draft sale order
so = self._create_sale_order(group_order, self.member_1, self.consumer_group_1)
# Execute full cron (which only processes draft/open orders)
self.env["group.order"]._cron_update_dates()
# Refresh
so.invalidate_recordset()
# Sale order should still be draft (closed group orders not processed)
self.assertEqual(
so.state,
"draft",
"Sale order should remain draft - group order is closed",
)
def test_cron_confirm_ignores_procurement_usererror(self):
"""Procurement UserError must not block confirmation in cron flow."""
group_order = self._create_group_order(cutoff_in_past=True)
so_ok = self._create_sale_order(
group_order, self.member_1, self.consumer_group_1
)
so_fail = self._create_sale_order(
group_order,
self.member_2,
self.consumer_group_2,
)
so_ok.write({"name": "SO-OK"})
so_fail.write({"name": "SO-FAIL"})
SaleOrderClass = type(self.env["sale.order"])
original_action_confirm = SaleOrderClass.action_confirm
def _patched_action_confirm(recordset):
should_fail = any(so.name == "SO-FAIL" for so in recordset)
if should_fail and not recordset.env.context.get("from_orderpoint"):
raise UserError("Simulated stock route error")
return original_action_confirm(recordset)
with patch.object(SaleOrderClass, "action_confirm", _patched_action_confirm):
group_order._confirm_linked_sale_orders()
so_ok.invalidate_recordset()
so_fail.invalidate_recordset()
self.assertEqual(
so_ok.state,
"sale",
"The valid order must still be confirmed",
)
self.assertTrue(
so_ok.picking_ids,
"The valid order should have pickings created",
)
self.assertTrue(
so_ok.picking_ids[0].batch_id,
"Pickings from valid orders should be batched",
)
self.assertEqual(
so_fail.state,
"sale",
"The order should be confirmed when cron uses non-blocking procurement context",
)