Compare commits
2 commits
e9809b90e9
...
2237cba034
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2237cba034 | ||
|
|
ce393b6034 |
13 changed files with 857 additions and 192 deletions
|
|
@ -1,2 +1,4 @@
|
||||||
from . import stock_move_line # noqa: F401
|
from . import stock_move_line # noqa: F401
|
||||||
|
from . import stock_backorder_confirmation # noqa: F401
|
||||||
|
from . import stock_picking # noqa: F401
|
||||||
from . import stock_picking_batch # noqa: F401
|
from . import stock_picking_batch # noqa: F401
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class StockBackorderConfirmation(models.TransientModel):
|
||||||
|
_inherit = "stock.backorder.confirmation"
|
||||||
|
|
||||||
|
def _check_batch_summary_lines(self):
|
||||||
|
pickings = self.env["stock.picking"].browse(
|
||||||
|
self.env.context.get("button_validate_picking_ids", [])
|
||||||
|
)
|
||||||
|
for batch in pickings.batch_id:
|
||||||
|
batch._check_all_products_collected()
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
self._check_batch_summary_lines()
|
||||||
|
return super().process()
|
||||||
|
|
||||||
|
def process_cancel_backorder(self):
|
||||||
|
self._check_batch_summary_lines()
|
||||||
|
return super().process_cancel_backorder()
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright 2026 Criptomart
|
# Copyright 2026 Criptomart
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import api
|
||||||
from odoo import fields
|
from odoo import fields
|
||||||
from odoo import models
|
from odoo import models
|
||||||
|
|
||||||
|
|
@ -21,3 +22,18 @@ class StockMoveLine(models.Model):
|
||||||
default=False,
|
default=False,
|
||||||
copy=False,
|
copy=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
consumer_group_id = fields.Many2one(
|
||||||
|
comodel_name="res.partner",
|
||||||
|
compute="_compute_consumer_group_id",
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends("picking_id")
|
||||||
|
def _compute_consumer_group_id(self):
|
||||||
|
for line in self:
|
||||||
|
picking = line.picking_id
|
||||||
|
if picking:
|
||||||
|
line.consumer_group_id = picking.batch_consumer_group_id
|
||||||
|
else:
|
||||||
|
line.consumer_group_id = False
|
||||||
|
|
|
||||||
57
stock_picking_batch_custom/models/stock_picking.py
Normal file
57
stock_picking_batch_custom/models/stock_picking.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import fields
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class StockPicking(models.Model):
|
||||||
|
_inherit = "stock.picking"
|
||||||
|
|
||||||
|
batch_consumer_group_id = fields.Many2one(
|
||||||
|
comodel_name="res.partner",
|
||||||
|
compute="_compute_batch_consumer_group_id",
|
||||||
|
readonly=True,
|
||||||
|
string="Batch Consumer Group",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_batch_consumer_group_id(self):
|
||||||
|
for picking in self:
|
||||||
|
sale = picking.sale_id if "sale_id" in picking._fields else False
|
||||||
|
if sale and "consumer_group_id" in sale._fields:
|
||||||
|
picking.batch_consumer_group_id = sale.consumer_group_id
|
||||||
|
else:
|
||||||
|
picking.batch_consumer_group_id = False
|
||||||
|
|
||||||
|
def _pre_action_done_hook(self):
|
||||||
|
"""Run collected checks only after Odoo resolves backorders.
|
||||||
|
|
||||||
|
This keeps the product summary checkbox informative during the initial
|
||||||
|
batch validation click, but still enforces it once the user confirms the
|
||||||
|
actual picking flow that will be processed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
res = super()._pre_action_done_hook()
|
||||||
|
if res is not True:
|
||||||
|
return res
|
||||||
|
|
||||||
|
self._check_batch_collected_products()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _check_batch_collected_products(self):
|
||||||
|
for batch in self.batch_id:
|
||||||
|
batch_pickings = self.filtered(
|
||||||
|
lambda picking, batch=batch: picking.batch_id == batch
|
||||||
|
)
|
||||||
|
processed_product_ids = (
|
||||||
|
batch_pickings.move_line_ids.filtered(
|
||||||
|
lambda line: line.move_id.state not in ("cancel", "done")
|
||||||
|
and line.product_id
|
||||||
|
and line.quantity
|
||||||
|
)
|
||||||
|
.mapped("product_id")
|
||||||
|
.ids
|
||||||
|
)
|
||||||
|
if not processed_product_ids:
|
||||||
|
continue
|
||||||
|
batch._check_all_products_collected(processed_product_ids)
|
||||||
|
|
@ -140,11 +140,20 @@ class StockPickingBatch(models.Model):
|
||||||
else:
|
else:
|
||||||
batch.summary_line_ids = [fields.Command.clear()]
|
batch.summary_line_ids = [fields.Command.clear()]
|
||||||
|
|
||||||
def _check_all_products_collected(self):
|
def _check_all_products_collected(self, product_ids=None):
|
||||||
"""Ensure all product summary lines are marked as collected before done."""
|
"""Ensure collected is checked for processed products only.
|
||||||
|
|
||||||
|
The product summary remains informative until Odoo knows which products
|
||||||
|
are actually being validated after the backorder decision.
|
||||||
|
"""
|
||||||
|
|
||||||
for batch in self:
|
for batch in self:
|
||||||
not_collected_lines = batch.summary_line_ids.filtered(
|
not_collected_lines = batch.summary_line_ids.filtered(
|
||||||
lambda line: not line.is_collected
|
lambda line: (
|
||||||
|
line.qty_done > 0
|
||||||
|
and not line.is_collected
|
||||||
|
and (not product_ids or line.product_id.id in product_ids)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if not not_collected_lines:
|
if not not_collected_lines:
|
||||||
continue
|
continue
|
||||||
|
|
@ -161,10 +170,6 @@ class StockPickingBatch(models.Model):
|
||||||
)
|
)
|
||||||
raise UserError(message)
|
raise UserError(message)
|
||||||
|
|
||||||
def action_done(self):
|
|
||||||
self._check_all_products_collected()
|
|
||||||
return super().action_done()
|
|
||||||
|
|
||||||
|
|
||||||
class StockPickingBatchSummaryLine(models.Model):
|
class StockPickingBatchSummaryLine(models.Model):
|
||||||
_name = "stock.picking.batch.summary.line"
|
_name = "stock.picking.batch.summary.line"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
# Copyright 2026 Criptomart
|
# Copyright 2026 Criptomart
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.tests import TransactionCase
|
||||||
from odoo.tests import tagged
|
from odoo.tests import tagged
|
||||||
from odoo.tests.common import TransactionCase
|
|
||||||
|
|
||||||
|
|
||||||
@tagged("-at_install", "post_install")
|
@tagged("-at_install", "post_install")
|
||||||
|
|
@ -24,6 +26,13 @@ class TestBatchSummary(TransactionCase):
|
||||||
"uom_po_id": cls.uom_unit.id,
|
"uom_po_id": cls.uom_unit.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
cls.product_2 = cls.env["product.product"].create(
|
||||||
|
{
|
||||||
|
"name": "Test Product 2",
|
||||||
|
"uom_id": cls.uom_unit.id,
|
||||||
|
"uom_po_id": cls.uom_unit.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
cls.batch = cls.env["stock.picking.batch"].create(
|
cls.batch = cls.env["stock.picking.batch"].create(
|
||||||
{"name": "Batch Test", "picking_type_id": cls.picking_type.id}
|
{"name": "Batch Test", "picking_type_id": cls.picking_type.id}
|
||||||
|
|
@ -113,6 +122,60 @@ class TestBatchSummary(TransactionCase):
|
||||||
picking.batch_id = batch.id
|
picking.batch_id = batch.id
|
||||||
return batch
|
return batch
|
||||||
|
|
||||||
|
def _create_partial_batch(self, qty_demanded=2.0, qty_done=1.0, extra_move=False):
|
||||||
|
batch = self.env["stock.picking.batch"].create(
|
||||||
|
{"name": "Batch Partial", "picking_type_id": self.picking_type.id}
|
||||||
|
)
|
||||||
|
batch.picking_type_id.create_backorder = "ask"
|
||||||
|
picking = self.env["stock.picking"].create(
|
||||||
|
{
|
||||||
|
"picking_type_id": self.picking_type.id,
|
||||||
|
"location_id": self.location_src.id,
|
||||||
|
"location_dest_id": self.location_dest.id,
|
||||||
|
"batch_id": batch.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
move = self.env["stock.move"].create(
|
||||||
|
{
|
||||||
|
"name": self.product.name,
|
||||||
|
"product_id": self.product.id,
|
||||||
|
"product_uom_qty": qty_demanded,
|
||||||
|
"product_uom": self.uom_unit.id,
|
||||||
|
"picking_id": picking.id,
|
||||||
|
"location_id": self.location_src.id,
|
||||||
|
"location_dest_id": self.location_dest.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.env["stock.move.line"].create(
|
||||||
|
{
|
||||||
|
"move_id": move.id,
|
||||||
|
"picking_id": picking.id,
|
||||||
|
"product_id": self.product.id,
|
||||||
|
"product_uom_id": self.uom_unit.id,
|
||||||
|
"location_id": self.location_src.id,
|
||||||
|
"location_dest_id": self.location_dest.id,
|
||||||
|
"quantity": qty_done,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if extra_move:
|
||||||
|
self.env["stock.move"].create(
|
||||||
|
{
|
||||||
|
"name": self.product_2.name,
|
||||||
|
"product_id": self.product_2.id,
|
||||||
|
"product_uom_qty": 1.0,
|
||||||
|
"product_uom": self.uom_unit.id,
|
||||||
|
"picking_id": picking.id,
|
||||||
|
"location_id": self.location_src.id,
|
||||||
|
"location_dest_id": self.location_dest.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
batch.action_confirm()
|
||||||
|
batch._compute_summary_line_ids()
|
||||||
|
return batch
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
def test_totals_and_pending_with_conversion(self):
|
def test_totals_and_pending_with_conversion(self):
|
||||||
"""Totals aggregate per product with UoM conversion and pending."""
|
"""Totals aggregate per product with UoM conversion and pending."""
|
||||||
|
|
@ -190,14 +253,6 @@ class TestBatchSummary(TransactionCase):
|
||||||
self.assertFalse(batch.summary_line_ids)
|
self.assertFalse(batch.summary_line_ids)
|
||||||
|
|
||||||
# 2. Create pickings with moves (without batch assignment)
|
# 2. Create pickings with moves (without batch assignment)
|
||||||
product2 = self.env["product.product"].create(
|
|
||||||
{
|
|
||||||
"name": "Test Product 2",
|
|
||||||
"uom_id": self.uom_unit.id,
|
|
||||||
"uom_po_id": self.uom_unit.id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
picking_a = self.env["stock.picking"].create(
|
picking_a = self.env["stock.picking"].create(
|
||||||
{
|
{
|
||||||
"picking_type_id": self.picking_type.id,
|
"picking_type_id": self.picking_type.id,
|
||||||
|
|
@ -226,8 +281,8 @@ class TestBatchSummary(TransactionCase):
|
||||||
)
|
)
|
||||||
self.env["stock.move"].create(
|
self.env["stock.move"].create(
|
||||||
{
|
{
|
||||||
"name": product2.name,
|
"name": self.product_2.name,
|
||||||
"product_id": product2.id,
|
"product_id": self.product_2.id,
|
||||||
"product_uom_qty": 10.0,
|
"product_uom_qty": 10.0,
|
||||||
"product_uom": self.uom_unit.id,
|
"product_uom": self.uom_unit.id,
|
||||||
"picking_id": picking_b.id,
|
"picking_id": picking_b.id,
|
||||||
|
|
@ -276,7 +331,7 @@ class TestBatchSummary(TransactionCase):
|
||||||
# Two products expected
|
# Two products expected
|
||||||
products_in_summary = batch.summary_line_ids.mapped("product_id")
|
products_in_summary = batch.summary_line_ids.mapped("product_id")
|
||||||
self.assertIn(self.product, products_in_summary)
|
self.assertIn(self.product, products_in_summary)
|
||||||
self.assertIn(product2, products_in_summary)
|
self.assertIn(self.product_2, products_in_summary)
|
||||||
|
|
||||||
# Check aggregated quantities
|
# Check aggregated quantities
|
||||||
line_product1 = batch.summary_line_ids.filtered(
|
line_product1 = batch.summary_line_ids.filtered(
|
||||||
|
|
@ -285,12 +340,12 @@ class TestBatchSummary(TransactionCase):
|
||||||
self.assertAlmostEqual(line_product1.qty_demanded, 8.0) # 5 + 3
|
self.assertAlmostEqual(line_product1.qty_demanded, 8.0) # 5 + 3
|
||||||
|
|
||||||
line_product2 = batch.summary_line_ids.filtered(
|
line_product2 = batch.summary_line_ids.filtered(
|
||||||
lambda line: line.product_id == product2
|
lambda line: line.product_id == self.product_2
|
||||||
)
|
)
|
||||||
self.assertAlmostEqual(line_product2.qty_demanded, 10.0)
|
self.assertAlmostEqual(line_product2.qty_demanded, 10.0)
|
||||||
|
|
||||||
def test_done_requires_all_summary_lines_collected(self):
|
def test_done_requires_all_summary_lines_collected_without_backorder(self):
|
||||||
"""Batch validation must fail if there are unchecked collected lines."""
|
"""Full validation still blocks if processed products are unchecked."""
|
||||||
|
|
||||||
batch = self._create_batch_with_pickings()
|
batch = self._create_batch_with_pickings()
|
||||||
batch.action_confirm()
|
batch.action_confirm()
|
||||||
|
|
@ -302,6 +357,32 @@ class TestBatchSummary(TransactionCase):
|
||||||
with self.assertRaises(UserError):
|
with self.assertRaises(UserError):
|
||||||
batch.action_done()
|
batch.action_done()
|
||||||
|
|
||||||
|
def test_partial_validation_waits_for_backorder_wizard_before_blocking(self):
|
||||||
|
"""Partial batch validation must open the backorder wizard first."""
|
||||||
|
|
||||||
|
batch = self._create_partial_batch()
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
type(self.env["stock.picking"]),
|
||||||
|
"_check_backorder",
|
||||||
|
autospec=True,
|
||||||
|
return_value=batch.picking_ids,
|
||||||
|
):
|
||||||
|
action = batch.action_done()
|
||||||
|
|
||||||
|
self.assertIsInstance(action, dict)
|
||||||
|
self.assertEqual(action.get("res_model"), "stock.backorder.confirmation")
|
||||||
|
|
||||||
|
def test_partial_validation_checks_only_processed_products(self):
|
||||||
|
"""Unchecked lines with no processed quantity must remain informative."""
|
||||||
|
|
||||||
|
batch = self._create_partial_batch(extra_move=True)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(UserError, self.product.display_name) as err:
|
||||||
|
batch._check_all_products_collected([self.product.id])
|
||||||
|
|
||||||
|
self.assertNotIn(self.product_2.display_name, str(err.exception))
|
||||||
|
|
||||||
def test_check_all_products_collected_passes_when_all_checked(self):
|
def test_check_all_products_collected_passes_when_all_checked(self):
|
||||||
"""Collected validation helper must pass when all lines are checked."""
|
"""Collected validation helper must pass when all lines are checked."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,27 @@
|
||||||
<field name="model">stock.move.line</field>
|
<field name="model">stock.move.line</field>
|
||||||
<field name="inherit_id" ref="stock_picking_batch.view_move_line_tree"/>
|
<field name="inherit_id" ref="stock_picking_batch.view_move_line_tree"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='picking_id']" position="attributes">
|
||||||
|
<attribute name="optional">hide</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='lot_id']" position="attributes">
|
||||||
|
<attribute name="optional">hide</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='lot_name']" position="attributes">
|
||||||
|
<attribute name="optional">hide</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='location_id']" position="attributes">
|
||||||
|
<attribute name="optional">hide</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='location_dest_id']" position="attributes">
|
||||||
|
<attribute name="optional">hide</attribute>
|
||||||
|
</xpath>
|
||||||
<xpath expr="//field[@name='product_id']" position="after">
|
<xpath expr="//field[@name='product_id']" position="after">
|
||||||
<field name="product_categ_id" optional="hide"/>
|
<field name="product_categ_id" optional="hide"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='picking_id']" position="after">
|
<xpath expr="//field[@name='picking_id']" position="after">
|
||||||
<field name="picking_partner_id" optional="hide"/>
|
<field name="picking_partner_id" optional="hide"/>
|
||||||
|
<field name="consumer_group_id" optional="show"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='quantity']" position="after">
|
<xpath expr="//field[@name='quantity']" position="after">
|
||||||
<field name="is_collected" optional="show" widget="boolean_toggle"/>
|
<field name="is_collected" optional="show" widget="boolean_toggle"/>
|
||||||
|
|
|
||||||
|
|
@ -28,4 +28,15 @@
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="view_stock_picking_batch_picking_tree_consumer_group" model="ir.ui.view">
|
||||||
|
<field name="name">stock.picking.batch.picking.tree.consumer.group</field>
|
||||||
|
<field name="model">stock.picking</field>
|
||||||
|
<field name="inherit_id" ref="stock_picking_batch.stock_picking_view_batch_tree_ref"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='partner_id']" position="after">
|
||||||
|
<field name="batch_consumer_group_id" optional="show"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -768,6 +768,8 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
if not current_user.partner_id:
|
if not current_user.partner_id:
|
||||||
raise ValueError("User has no associated partner") from None
|
raise ValueError("User has no associated partner") from None
|
||||||
|
|
||||||
|
self._validate_user_group_access(group_order, current_user)
|
||||||
|
|
||||||
# Validate items
|
# Validate items
|
||||||
items = data.get("items", [])
|
items = data.get("items", [])
|
||||||
if not items:
|
if not items:
|
||||||
|
|
@ -820,6 +822,8 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
if not current_user.partner_id:
|
if not current_user.partner_id:
|
||||||
raise ValueError("User has no associated partner")
|
raise ValueError("User has no associated partner")
|
||||||
|
|
||||||
|
self._validate_user_group_access(group_order, current_user)
|
||||||
|
|
||||||
# Validate items
|
# Validate items
|
||||||
items = data.get("items", [])
|
items = data.get("items", [])
|
||||||
if not items:
|
if not items:
|
||||||
|
|
@ -887,6 +891,8 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
if not current_user.partner_id:
|
if not current_user.partner_id:
|
||||||
raise ValueError("User has no associated partner")
|
raise ValueError("User has no associated partner")
|
||||||
|
|
||||||
|
self._validate_user_group_access(group_order, current_user)
|
||||||
|
|
||||||
# Validate items
|
# Validate items
|
||||||
items = data.get("items", [])
|
items = data.get("items", [])
|
||||||
if not items:
|
if not items:
|
||||||
|
|
@ -921,6 +927,29 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
return False
|
return False
|
||||||
return bool(value)
|
return bool(value)
|
||||||
|
|
||||||
|
def _get_effective_delivery_context(self, group_order, is_delivery):
|
||||||
|
"""Return effective home delivery flag and commitment date.
|
||||||
|
|
||||||
|
Delivery is effective only when requested by the user and enabled
|
||||||
|
in the group order configuration.
|
||||||
|
"""
|
||||||
|
delivery_requested = self._to_bool(is_delivery)
|
||||||
|
delivery_enabled = bool(group_order and group_order.home_delivery)
|
||||||
|
effective_home_delivery = delivery_requested and delivery_enabled
|
||||||
|
|
||||||
|
if delivery_requested and not delivery_enabled:
|
||||||
|
_logger.info(
|
||||||
|
"Delivery requested but disabled in group order %s; using pickup flow",
|
||||||
|
group_order.id if group_order else "N/A",
|
||||||
|
)
|
||||||
|
|
||||||
|
if effective_home_delivery:
|
||||||
|
commitment_date = group_order.delivery_date or group_order.pickup_date
|
||||||
|
else:
|
||||||
|
commitment_date = group_order.pickup_date if group_order else False
|
||||||
|
|
||||||
|
return effective_home_delivery, commitment_date
|
||||||
|
|
||||||
def _process_cart_items(self, items, group_order, pricelist=None):
|
def _process_cart_items(self, items, group_order, pricelist=None):
|
||||||
"""Process cart items and build sale.order line data.
|
"""Process cart items and build sale.order line data.
|
||||||
|
|
||||||
|
|
@ -1009,6 +1038,35 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
# No salesperson found
|
# No salesperson found
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _get_consumer_group_for_user(self, group_order, current_user):
|
||||||
|
"""Return the matching consumer group for the user in this group order."""
|
||||||
|
partner = current_user.partner_id
|
||||||
|
if not partner or not group_order:
|
||||||
|
return False
|
||||||
|
|
||||||
|
user_group_ids = set(partner.group_ids.ids)
|
||||||
|
for consumer_group in group_order.group_ids:
|
||||||
|
if consumer_group.id in user_group_ids:
|
||||||
|
return consumer_group.id
|
||||||
|
|
||||||
|
_logger.warning(
|
||||||
|
"_get_consumer_group_for_user: User %s (%s) is not member of any "
|
||||||
|
"consumer group in order %s. user_groups=%s, order_groups=%s",
|
||||||
|
current_user.name,
|
||||||
|
current_user.id,
|
||||||
|
group_order.id,
|
||||||
|
sorted(user_group_ids),
|
||||||
|
group_order.group_ids.ids,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _validate_user_group_access(self, group_order, current_user):
|
||||||
|
"""Ensure the user belongs to at least one consumer group of the order."""
|
||||||
|
consumer_group_id = self._get_consumer_group_for_user(group_order, current_user)
|
||||||
|
if not consumer_group_id:
|
||||||
|
raise ValueError("User is not a member of any consumer group in this order")
|
||||||
|
return consumer_group_id
|
||||||
|
|
||||||
def _create_or_update_sale_order(
|
def _create_or_update_sale_order(
|
||||||
self,
|
self,
|
||||||
group_order,
|
group_order,
|
||||||
|
|
@ -1022,11 +1080,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
|
|
||||||
Returns the sale.order record.
|
Returns the sale.order record.
|
||||||
"""
|
"""
|
||||||
# consumer_group_id comes directly from group_order (first/only group)
|
consumer_group_id = self._validate_user_group_access(group_order, current_user)
|
||||||
# No calculations - data propagates directly from the order context
|
|
||||||
consumer_group_id = (
|
|
||||||
group_order.group_ids[0].id if group_order.group_ids else False
|
|
||||||
)
|
|
||||||
|
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"[CONSUMER_GROUP DEBUG] _create_or_update_sale_order: "
|
"[CONSUMER_GROUP DEBUG] _create_or_update_sale_order: "
|
||||||
|
|
@ -1036,22 +1090,25 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
consumer_group_id,
|
consumer_group_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# commitment_date: use provided or calculate from group_order
|
effective_home_delivery, calculated_commitment_date = (
|
||||||
|
self._get_effective_delivery_context(group_order, is_delivery)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Explicit commitment_date wins; otherwise use calculated value
|
||||||
if not commitment_date:
|
if not commitment_date:
|
||||||
commitment_date = (
|
commitment_date = calculated_commitment_date
|
||||||
group_order.delivery_date if is_delivery else group_order.pickup_date
|
|
||||||
)
|
|
||||||
|
|
||||||
if existing_order:
|
if existing_order:
|
||||||
# Update existing order with new lines and propagate fields
|
# Update existing order with new lines and propagate fields
|
||||||
# Use sudo() to avoid permission issues with portal users
|
# Use sudo() to avoid permission issues with portal users
|
||||||
existing_order_sudo = existing_order.sudo()
|
existing_order_sudo = existing_order.sudo()
|
||||||
existing_order_sudo.order_line = sale_order_lines
|
# (5,) clears all existing lines before adding the new ones
|
||||||
|
existing_order_sudo.order_line = [(5,)] + sale_order_lines
|
||||||
if not existing_order_sudo.group_order_id:
|
if not existing_order_sudo.group_order_id:
|
||||||
existing_order_sudo.group_order_id = group_order.id
|
existing_order_sudo.group_order_id = group_order.id
|
||||||
existing_order_sudo.pickup_day = group_order.pickup_day
|
existing_order_sudo.pickup_day = group_order.pickup_day
|
||||||
existing_order_sudo.pickup_date = group_order.pickup_date
|
existing_order_sudo.pickup_date = group_order.pickup_date
|
||||||
existing_order_sudo.home_delivery = is_delivery
|
existing_order_sudo.home_delivery = effective_home_delivery
|
||||||
existing_order_sudo.consumer_group_id = consumer_group_id
|
existing_order_sudo.consumer_group_id = consumer_group_id
|
||||||
if commitment_date:
|
if commitment_date:
|
||||||
existing_order_sudo.commitment_date = commitment_date
|
existing_order_sudo.commitment_date = commitment_date
|
||||||
|
|
@ -1059,7 +1116,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
"Updated existing sale.order %d: commitment_date=%s, home_delivery=%s, consumer_group_id=%s",
|
"Updated existing sale.order %d: commitment_date=%s, home_delivery=%s, consumer_group_id=%s",
|
||||||
existing_order.id,
|
existing_order.id,
|
||||||
commitment_date,
|
commitment_date,
|
||||||
is_delivery,
|
effective_home_delivery,
|
||||||
consumer_group_id,
|
consumer_group_id,
|
||||||
)
|
)
|
||||||
return existing_order
|
return existing_order
|
||||||
|
|
@ -1071,7 +1128,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
"group_order_id": group_order.id,
|
"group_order_id": group_order.id,
|
||||||
"pickup_day": group_order.pickup_day,
|
"pickup_day": group_order.pickup_day,
|
||||||
"pickup_date": group_order.pickup_date,
|
"pickup_date": group_order.pickup_date,
|
||||||
"home_delivery": is_delivery,
|
"home_delivery": effective_home_delivery,
|
||||||
"consumer_group_id": consumer_group_id,
|
"consumer_group_id": consumer_group_id,
|
||||||
}
|
}
|
||||||
if commitment_date:
|
if commitment_date:
|
||||||
|
|
@ -1094,23 +1151,25 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
sale_order.id,
|
sale_order.id,
|
||||||
group_order.id,
|
group_order.id,
|
||||||
group_order.pickup_day,
|
group_order.pickup_day,
|
||||||
group_order.home_delivery,
|
effective_home_delivery,
|
||||||
consumer_group_id,
|
consumer_group_id,
|
||||||
)
|
)
|
||||||
return sale_order
|
return sale_order
|
||||||
|
|
||||||
def _create_draft_sale_order(
|
def _create_draft_sale_order(
|
||||||
self, group_order, current_user, sale_order_lines, order_id, pickup_date=None
|
self,
|
||||||
|
group_order,
|
||||||
|
current_user,
|
||||||
|
sale_order_lines,
|
||||||
|
order_id,
|
||||||
|
pickup_date=None,
|
||||||
|
is_delivery=False,
|
||||||
):
|
):
|
||||||
"""Create a draft sale.order from prepared lines and propagate group fields.
|
"""Create a draft sale.order from prepared lines and propagate group fields.
|
||||||
|
|
||||||
Returns created sale.order record.
|
Returns created sale.order record.
|
||||||
"""
|
"""
|
||||||
# consumer_group_id comes directly from group_order (first/only group)
|
consumer_group_id = self._validate_user_group_access(group_order, current_user)
|
||||||
# No calculations - data propagates directly from the order context
|
|
||||||
consumer_group_id = (
|
|
||||||
group_order.group_ids[0].id if group_order.group_ids else False
|
|
||||||
)
|
|
||||||
|
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"[CONSUMER_GROUP DEBUG] _create_draft_sale_order: "
|
"[CONSUMER_GROUP DEBUG] _create_draft_sale_order: "
|
||||||
|
|
@ -1120,11 +1179,8 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
consumer_group_id,
|
consumer_group_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# commitment_date comes from group_order (delivery_date or pickup_date)
|
effective_home_delivery, commitment_date = self._get_effective_delivery_context(
|
||||||
commitment_date = (
|
group_order, is_delivery
|
||||||
group_order.delivery_date
|
|
||||||
if group_order.home_delivery
|
|
||||||
else group_order.pickup_date
|
|
||||||
)
|
)
|
||||||
|
|
||||||
order_vals = {
|
order_vals = {
|
||||||
|
|
@ -1134,7 +1190,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
"group_order_id": order_id,
|
"group_order_id": order_id,
|
||||||
"pickup_day": group_order.pickup_day,
|
"pickup_day": group_order.pickup_day,
|
||||||
"pickup_date": group_order.pickup_date,
|
"pickup_date": group_order.pickup_date,
|
||||||
"home_delivery": group_order.home_delivery,
|
"home_delivery": effective_home_delivery,
|
||||||
"consumer_group_id": consumer_group_id,
|
"consumer_group_id": consumer_group_id,
|
||||||
"commitment_date": commitment_date,
|
"commitment_date": commitment_date,
|
||||||
}
|
}
|
||||||
|
|
@ -1374,11 +1430,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
|
|
||||||
All fields (commitment_date, consumer_group_id, etc.) come from group_order.
|
All fields (commitment_date, consumer_group_id, etc.) come from group_order.
|
||||||
"""
|
"""
|
||||||
# consumer_group_id comes directly from group_order (first/only group)
|
consumer_group_id = self._validate_user_group_access(group_order, current_user)
|
||||||
# No calculations - data propagates directly from the order context
|
|
||||||
consumer_group_id = (
|
|
||||||
group_order.group_ids[0].id if group_order.group_ids else False
|
|
||||||
)
|
|
||||||
|
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"[CONSUMER_GROUP DEBUG] _merge_or_replace_draft: "
|
"[CONSUMER_GROUP DEBUG] _merge_or_replace_draft: "
|
||||||
|
|
@ -1388,9 +1440,8 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
consumer_group_id,
|
consumer_group_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# commitment_date comes directly from selected flow
|
effective_home_delivery, commitment_date = self._get_effective_delivery_context(
|
||||||
commitment_date = (
|
group_order, is_delivery
|
||||||
group_order.delivery_date if is_delivery else group_order.pickup_date
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing_drafts:
|
if existing_drafts:
|
||||||
|
|
@ -1406,7 +1457,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
"group_order_id": order_id,
|
"group_order_id": order_id,
|
||||||
"pickup_day": group_order.pickup_day,
|
"pickup_day": group_order.pickup_day,
|
||||||
"pickup_date": group_order.pickup_date,
|
"pickup_date": group_order.pickup_date,
|
||||||
"home_delivery": is_delivery,
|
"home_delivery": effective_home_delivery,
|
||||||
"consumer_group_id": consumer_group_id,
|
"consumer_group_id": consumer_group_id,
|
||||||
"commitment_date": commitment_date,
|
"commitment_date": commitment_date,
|
||||||
}
|
}
|
||||||
|
|
@ -1420,7 +1471,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
"group_order_id": order_id,
|
"group_order_id": order_id,
|
||||||
"pickup_day": group_order.pickup_day,
|
"pickup_day": group_order.pickup_day,
|
||||||
"pickup_date": group_order.pickup_date,
|
"pickup_date": group_order.pickup_date,
|
||||||
"home_delivery": is_delivery,
|
"home_delivery": effective_home_delivery,
|
||||||
"consumer_group_id": consumer_group_id,
|
"consumer_group_id": consumer_group_id,
|
||||||
"commitment_date": commitment_date,
|
"commitment_date": commitment_date,
|
||||||
}
|
}
|
||||||
|
|
@ -2240,8 +2291,18 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._validate_user_group_access(group_order, current_user)
|
||||||
|
except ValueError as e:
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({"error": str(e)}),
|
||||||
|
[("Content-Type", "application/json")],
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
items = data.get("items", [])
|
items = data.get("items", [])
|
||||||
pickup_date = data.get("pickup_date")
|
pickup_date = data.get("pickup_date")
|
||||||
|
is_delivery = self._to_bool(data.get("is_delivery", False))
|
||||||
if not items:
|
if not items:
|
||||||
return request.make_response(
|
return request.make_response(
|
||||||
json.dumps({"error": "No items in cart"}),
|
json.dumps({"error": "No items in cart"}),
|
||||||
|
|
@ -2262,7 +2323,12 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
)
|
)
|
||||||
|
|
||||||
sale_order = self._create_draft_sale_order(
|
sale_order = self._create_draft_sale_order(
|
||||||
group_order, current_user, sale_order_lines, order_id, pickup_date
|
group_order,
|
||||||
|
current_user,
|
||||||
|
sale_order_lines,
|
||||||
|
order_id,
|
||||||
|
pickup_date,
|
||||||
|
is_delivery=is_delivery,
|
||||||
)
|
)
|
||||||
|
|
||||||
_logger.info(
|
_logger.info(
|
||||||
|
|
@ -2629,6 +2695,15 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._validate_user_group_access(group_order, current_user)
|
||||||
|
except ValueError as e:
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({"error": str(e)}),
|
||||||
|
[("Content-Type", "application/json")],
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
# Get cart items
|
# Get cart items
|
||||||
items = data.get("items", [])
|
items = data.get("items", [])
|
||||||
is_delivery = self._to_bool(data.get("is_delivery", False))
|
is_delivery = self._to_bool(data.get("is_delivery", False))
|
||||||
|
|
@ -2791,27 +2866,23 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
)
|
)
|
||||||
existing_order = None
|
existing_order = None
|
||||||
|
|
||||||
# Get pickup date and delivery info from group order
|
effective_home_delivery, commitment_date = (
|
||||||
# If delivery, use delivery_date; otherwise use pickup_date
|
self._get_effective_delivery_context(group_order, is_delivery)
|
||||||
commitment_date = None
|
)
|
||||||
if is_delivery and group_order.delivery_date:
|
|
||||||
commitment_date = group_order.delivery_date
|
|
||||||
elif group_order.pickup_date:
|
|
||||||
commitment_date = group_order.pickup_date
|
|
||||||
|
|
||||||
# Create or update sale.order using helper
|
# Create or update sale.order using helper
|
||||||
sale_order = self._create_or_update_sale_order(
|
sale_order = self._create_or_update_sale_order(
|
||||||
group_order,
|
group_order,
|
||||||
current_user,
|
current_user,
|
||||||
sale_order_lines,
|
sale_order_lines,
|
||||||
is_delivery,
|
effective_home_delivery,
|
||||||
commitment_date=commitment_date,
|
commitment_date=commitment_date,
|
||||||
existing_order=existing_order,
|
existing_order=existing_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build confirmation message using helper
|
# Build confirmation message using helper
|
||||||
message_data = self._build_confirmation_message(
|
message_data = self._build_confirmation_message(
|
||||||
sale_order, group_order, is_delivery
|
sale_order, group_order, effective_home_delivery
|
||||||
)
|
)
|
||||||
message = message_data["message"]
|
message = message_data["message"]
|
||||||
pickup_day_name = message_data["pickup_day"]
|
pickup_day_name = message_data["pickup_day"]
|
||||||
|
|
|
||||||
|
|
@ -799,9 +799,45 @@ class GroupOrder(models.Model):
|
||||||
self.delivery_date,
|
self.delivery_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
sale_orders.action_confirm()
|
# Confirm each order in an isolated savepoint so one bad product
|
||||||
# Create picking batches after confirmation
|
# route configuration doesn't block all remaining orders.
|
||||||
self._create_picking_batches_for_sale_orders(sale_orders)
|
confirmed_sale_orders = SaleOrder.browse()
|
||||||
|
failed_sale_orders = SaleOrder.browse()
|
||||||
|
|
||||||
|
for sale_order in sale_orders:
|
||||||
|
try:
|
||||||
|
with self.env.cr.savepoint():
|
||||||
|
# Do not block sales confirmation due to procurement
|
||||||
|
# route issues. This cron confirms business orders first
|
||||||
|
# and handles stock exceptions operationally.
|
||||||
|
sale_order.with_context(from_orderpoint=True).action_confirm()
|
||||||
|
confirmed_sale_orders |= sale_order
|
||||||
|
except Exception:
|
||||||
|
failed_sale_orders |= sale_order
|
||||||
|
_logger.exception(
|
||||||
|
"Cron: Error confirming sale order %s (%s) for group order %s (%s). "
|
||||||
|
"Order skipped; remaining orders will continue.",
|
||||||
|
sale_order.id,
|
||||||
|
sale_order.name,
|
||||||
|
self.id,
|
||||||
|
self.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if confirmed_sale_orders:
|
||||||
|
# Create picking batches only for confirmed sale orders
|
||||||
|
self._create_picking_batches_for_sale_orders(confirmed_sale_orders)
|
||||||
|
self._log_missing_procurement_warnings(confirmed_sale_orders)
|
||||||
|
|
||||||
|
if failed_sale_orders:
|
||||||
|
_logger.warning(
|
||||||
|
"Cron: %d/%d sale orders failed during confirmation for group order %s (%s). "
|
||||||
|
"failed_sale_order_ids=%s",
|
||||||
|
len(failed_sale_orders),
|
||||||
|
len(sale_orders),
|
||||||
|
self.id,
|
||||||
|
self.name,
|
||||||
|
failed_sale_orders.ids,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
_logger.exception(
|
_logger.exception(
|
||||||
"Cron: Error confirming sale orders for group order %s (%s)",
|
"Cron: Error confirming sale orders for group order %s (%s)",
|
||||||
|
|
@ -809,6 +845,42 @@ class GroupOrder(models.Model):
|
||||||
self.name,
|
self.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _log_missing_procurement_warnings(self, sale_orders):
|
||||||
|
"""Log warnings for confirmed orders with stockable lines lacking moves.
|
||||||
|
|
||||||
|
This helps operations detect products with missing route/procurement
|
||||||
|
configuration while still allowing sale order confirmation.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
for sale_order in sale_orders:
|
||||||
|
problematic_lines = sale_order.order_line.filtered(
|
||||||
|
lambda line: line.product_id
|
||||||
|
and getattr(line.product_id, "is_storable", False)
|
||||||
|
and line.product_uom_qty > 0
|
||||||
|
and not line.move_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
if not problematic_lines:
|
||||||
|
continue
|
||||||
|
|
||||||
|
product_labels = []
|
||||||
|
for line in problematic_lines:
|
||||||
|
code = line.product_id.default_code or "NO-CODE"
|
||||||
|
name = line.product_id.display_name or line.product_id.name
|
||||||
|
product_labels.append(f"[{code}] {name}")
|
||||||
|
|
||||||
|
_logger.warning(
|
||||||
|
"Cron: Sale order %s (%s) confirmed but %d stockable lines have no stock moves. "
|
||||||
|
"Likely missing replenishment routes/rules. group_order=%s (%s), products=%s",
|
||||||
|
sale_order.id,
|
||||||
|
sale_order.name,
|
||||||
|
len(problematic_lines),
|
||||||
|
self.id,
|
||||||
|
self.name,
|
||||||
|
product_labels,
|
||||||
|
)
|
||||||
|
|
||||||
def _create_picking_batches_for_sale_orders(self, sale_orders):
|
def _create_picking_batches_for_sale_orders(self, sale_orders):
|
||||||
"""Create stock.picking.batch grouped by consumer_group_id.
|
"""Create stock.picking.batch grouped by consumer_group_id.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from . import test_record_rules # noqa: F401
|
||||||
from . import test_multi_company # noqa: F401
|
from . import test_multi_company # noqa: F401
|
||||||
from . import test_save_order_endpoints # noqa: F401
|
from . import test_save_order_endpoints # noqa: F401
|
||||||
from . import test_date_calculations # noqa: F401
|
from . import test_date_calculations # noqa: F401
|
||||||
|
from . import test_phase3_confirm_eskaera # noqa: F401
|
||||||
from . import test_pricing_with_pricelist # noqa: F401
|
from . import test_pricing_with_pricelist # noqa: F401
|
||||||
from . import test_portal_sale_order_creation # noqa: F401
|
from . import test_portal_sale_order_creation # noqa: F401
|
||||||
from . import test_cron_picking_batch # noqa: F401
|
from . import test_cron_picking_batch # noqa: F401
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@
|
||||||
# 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 timedelta
|
from datetime import timedelta
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from odoo import fields
|
from odoo import fields
|
||||||
|
from odoo.exceptions import UserError
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
from odoo.tests.common import tagged
|
from odoo.tests.common import tagged
|
||||||
|
|
||||||
|
|
@ -402,3 +404,54 @@ class TestCronPickingBatch(TransactionCase):
|
||||||
"draft",
|
"draft",
|
||||||
"Sale order should remain draft - group order is closed",
|
"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",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,37 +20,108 @@ Includes tests for:
|
||||||
import json
|
import json
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import Mock
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from odoo import http
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
from odoo.addons.website_sale_aplicoop.controllers.website_sale import (
|
||||||
|
AplicoopWebsiteSale,
|
||||||
|
)
|
||||||
|
|
||||||
|
REQUEST_PATCH_TARGET = (
|
||||||
|
"odoo.addons.website_sale_aplicoop.controllers.website_sale.request"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_json_response(data, headers=None, status=200):
|
||||||
|
"""Build a lightweight HTTP-like response object for controller tests."""
|
||||||
|
|
||||||
|
raw = data.encode("utf-8") if isinstance(data, str) else data
|
||||||
|
return SimpleNamespace(data=raw, status=status, headers=headers or [])
|
||||||
|
|
||||||
|
|
||||||
|
def _build_request_mock(env, payload=None, website=None):
|
||||||
|
"""Build a request mock compatible with the controller helpers/routes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
env: Odoo Environment to use as request.env.
|
||||||
|
payload: dict that will be JSON-encoded as request.httprequest.data.
|
||||||
|
website: Optional real website record. When not provided a minimal
|
||||||
|
SimpleNamespace stub is used (sufficient for tests that do not
|
||||||
|
call pricing helpers).
|
||||||
|
"""
|
||||||
|
if website is None:
|
||||||
|
website = SimpleNamespace(
|
||||||
|
_get_current_pricelist=lambda: False,
|
||||||
|
show_line_subtotals_tax_selection="tax_excluded",
|
||||||
|
fiscal_position_id=False,
|
||||||
|
company_id=False,
|
||||||
|
)
|
||||||
|
request_mock = SimpleNamespace(
|
||||||
|
env=env,
|
||||||
|
make_response=_make_json_response,
|
||||||
|
website=website,
|
||||||
|
)
|
||||||
|
if payload is not None:
|
||||||
|
request_mock.httprequest = SimpleNamespace(
|
||||||
|
data=json.dumps(payload).encode("utf-8")
|
||||||
|
)
|
||||||
|
return request_mock
|
||||||
|
|
||||||
|
|
||||||
class TestValidateConfirmJson(TransactionCase):
|
class TestValidateConfirmJson(TransactionCase):
|
||||||
"""Test _validate_confirm_json() helper method."""
|
"""Test _validate_confirm_json() helper method."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.controller = http.request.env["website.sale"].browse([])
|
self.controller = AplicoopWebsiteSale()
|
||||||
self.user = self.env.ref("base.user_admin")
|
self.group_1 = self.env["res.partner"].create(
|
||||||
self.partner = self.env.ref("base.partner_admin")
|
{
|
||||||
|
"name": "Consumer Group 1",
|
||||||
|
"is_company": True,
|
||||||
|
"is_group": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.group_2 = self.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Consumer Group 2",
|
||||||
|
"is_company": True,
|
||||||
|
"is_group": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.partner = self.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Phase 3 Member",
|
||||||
|
"email": "phase3-member@test.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.group_2.member_ids = [(4, self.partner.id)]
|
||||||
|
self.user = self.env["res.users"].create(
|
||||||
|
{
|
||||||
|
"name": "Phase 3 User",
|
||||||
|
"login": "phase3-user@test.com",
|
||||||
|
"email": "phase3-user@test.com",
|
||||||
|
"partner_id": self.partner.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Create test group order
|
# Create test group order
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env["group.order"].create(
|
||||||
{
|
{
|
||||||
"name": "Test Order Phase 3",
|
"name": "Test Order Phase 3",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"collection_date": date.today() + timedelta(days=3),
|
"group_ids": [(6, 0, [self.group_1.id, self.group_2.id])],
|
||||||
|
"start_date": date.today(),
|
||||||
|
"end_date": date.today() + timedelta(days=7),
|
||||||
"cutoff_day": "3", # Thursday
|
"cutoff_day": "3", # Thursday
|
||||||
"pickup_day": "5", # Saturday
|
"pickup_day": "5", # Saturday
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_validate_confirm_json_success(self):
|
||||||
def test_validate_confirm_json_success(self, mock_request):
|
|
||||||
"""Test successful validation of confirm JSON data."""
|
"""Test successful validation of confirm JSON data."""
|
||||||
mock_request.env = self.env.with_user(self.user)
|
request_mock = _build_request_mock(self.env(user=self.user))
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"order_id": self.group_order.id,
|
"order_id": self.group_order.id,
|
||||||
|
|
@ -58,9 +129,10 @@ class TestValidateConfirmJson(TransactionCase):
|
||||||
"is_delivery": False,
|
"is_delivery": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
order_id, group_order, current_user, items, is_delivery = (
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
self.controller._validate_confirm_json(data)
|
order_id, group_order, current_user, items, is_delivery = (
|
||||||
)
|
self.controller._validate_confirm_json(data)
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(order_id, self.group_order.id)
|
self.assertEqual(order_id, self.group_order.id)
|
||||||
self.assertEqual(group_order.id, self.group_order.id)
|
self.assertEqual(group_order.id, self.group_order.id)
|
||||||
|
|
@ -68,52 +140,51 @@ class TestValidateConfirmJson(TransactionCase):
|
||||||
self.assertEqual(len(items), 1)
|
self.assertEqual(len(items), 1)
|
||||||
self.assertFalse(is_delivery)
|
self.assertFalse(is_delivery)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_validate_confirm_json_missing_order_id(self):
|
||||||
def test_validate_confirm_json_missing_order_id(self, mock_request):
|
|
||||||
"""Test validation fails without order_id."""
|
"""Test validation fails without order_id."""
|
||||||
mock_request.env = self.env.with_user(self.user)
|
request_mock = _build_request_mock(self.env(user=self.user))
|
||||||
|
|
||||||
data = {"items": [{"product_id": 1, "quantity": 2}]}
|
data = {"items": [{"product_id": 1, "quantity": 2}]}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
self.controller._validate_confirm_json(data)
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.controller._validate_confirm_json(data)
|
||||||
|
|
||||||
self.assertIn("Missing order_id", str(context.exception))
|
self.assertIn("order_id is required", str(context.exception))
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_validate_confirm_json_order_not_exists(self):
|
||||||
def test_validate_confirm_json_order_not_exists(self, mock_request):
|
|
||||||
"""Test validation fails with non-existent order."""
|
"""Test validation fails with non-existent order."""
|
||||||
mock_request.env = self.env.with_user(self.user)
|
request_mock = _build_request_mock(self.env(user=self.user))
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"order_id": 99999, # Non-existent ID
|
"order_id": 99999, # Non-existent ID
|
||||||
"items": [{"product_id": 1, "quantity": 2}],
|
"items": [{"product_id": 1, "quantity": 2}],
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
self.controller._validate_confirm_json(data)
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.controller._validate_confirm_json(data)
|
||||||
|
|
||||||
self.assertIn("Order", str(context.exception))
|
self.assertIn("Order", str(context.exception))
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_validate_confirm_json_no_items(self):
|
||||||
def test_validate_confirm_json_no_items(self, mock_request):
|
|
||||||
"""Test validation fails without items in cart."""
|
"""Test validation fails without items in cart."""
|
||||||
mock_request.env = self.env.with_user(self.user)
|
request_mock = _build_request_mock(self.env(user=self.user))
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"order_id": self.group_order.id,
|
"order_id": self.group_order.id,
|
||||||
"items": [],
|
"items": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
self.controller._validate_confirm_json(data)
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.controller._validate_confirm_json(data)
|
||||||
|
|
||||||
self.assertIn("No items in cart", str(context.exception))
|
self.assertIn("No items in cart", str(context.exception))
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_validate_confirm_json_with_delivery_flag(self):
|
||||||
def test_validate_confirm_json_with_delivery_flag(self, mock_request):
|
|
||||||
"""Test validation correctly handles is_delivery flag."""
|
"""Test validation correctly handles is_delivery flag."""
|
||||||
mock_request.env = self.env.with_user(self.user)
|
request_mock = _build_request_mock(self.env(user=self.user))
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"order_id": self.group_order.id,
|
"order_id": self.group_order.id,
|
||||||
|
|
@ -121,17 +192,165 @@ class TestValidateConfirmJson(TransactionCase):
|
||||||
"is_delivery": True,
|
"is_delivery": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, _, _, is_delivery = self.controller._validate_confirm_json(data)
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
|
_, _, _, _, is_delivery = self.controller._validate_confirm_json(data)
|
||||||
|
|
||||||
self.assertTrue(is_delivery)
|
self.assertTrue(is_delivery)
|
||||||
|
|
||||||
|
def test_validate_confirm_json_user_without_matching_group(self):
|
||||||
|
"""Validation must fail if user is not in any allowed consumer group."""
|
||||||
|
other_partner = self.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Partner without matching group",
|
||||||
|
"email": "nogroup@test.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
other_user = self.env["res.users"].create(
|
||||||
|
{
|
||||||
|
"name": "User without matching group",
|
||||||
|
"login": "nogroup-user@test.com",
|
||||||
|
"email": "nogroup-user@test.com",
|
||||||
|
"partner_id": other_partner.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
request_mock = _build_request_mock(self.env(user=other_user))
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"order_id": self.group_order.id,
|
||||||
|
"items": [{"product_id": 1, "quantity": 1}],
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.controller._validate_confirm_json(data)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"User is not a member of any consumer group in this order",
|
||||||
|
str(context.exception),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestConsumerGroupResolution(TransactionCase):
|
||||||
|
"""Tests for consumer group selection in multi-group orders."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.controller = AplicoopWebsiteSale()
|
||||||
|
self.group_1 = self.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Group A",
|
||||||
|
"is_company": True,
|
||||||
|
"is_group": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.group_2 = self.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Group B",
|
||||||
|
"is_company": True,
|
||||||
|
"is_group": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.group_3 = self.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Group C",
|
||||||
|
"is_company": True,
|
||||||
|
"is_group": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.partner = self.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Member B",
|
||||||
|
"email": "memberb@test.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.group_2.member_ids = [(4, self.partner.id)]
|
||||||
|
self.user = self.env["res.users"].create(
|
||||||
|
{
|
||||||
|
"name": "Member B User",
|
||||||
|
"login": "memberb-user@test.com",
|
||||||
|
"email": "memberb-user@test.com",
|
||||||
|
"partner_id": self.partner.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.product = self.env["product.product"].create(
|
||||||
|
{
|
||||||
|
"name": "Group Resolution Product",
|
||||||
|
"type": "consu",
|
||||||
|
"list_price": 10.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.group_order = self.env["group.order"].create(
|
||||||
|
{
|
||||||
|
"name": "Multi-group order",
|
||||||
|
"state": "open",
|
||||||
|
"group_ids": [(6, 0, [self.group_1.id, self.group_2.id])],
|
||||||
|
"start_date": date.today(),
|
||||||
|
"end_date": date.today() + timedelta(days=7),
|
||||||
|
"cutoff_day": "1",
|
||||||
|
"pickup_day": "2",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.sale_order_lines = [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"product_id": self.product.id,
|
||||||
|
"name": "Test line",
|
||||||
|
"product_uom_qty": 1,
|
||||||
|
"price_unit": 10.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_get_consumer_group_for_user_returns_matching_group(self):
|
||||||
|
"""Should resolve the user's own consumer group, not the first order group."""
|
||||||
|
consumer_group_id = self.controller._get_consumer_group_for_user(
|
||||||
|
self.group_order, self.user
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(consumer_group_id, self.group_2.id)
|
||||||
|
|
||||||
|
def test_get_consumer_group_for_user_uses_first_matching_order_group(self):
|
||||||
|
"""If user belongs to multiple valid groups, use the first in group_order."""
|
||||||
|
self.group_1.member_ids = [(4, self.partner.id)]
|
||||||
|
group_order = self.env["group.order"].create(
|
||||||
|
{
|
||||||
|
"name": "Multi-match order",
|
||||||
|
"state": "open",
|
||||||
|
"group_ids": [
|
||||||
|
(6, 0, [self.group_1.id, self.group_2.id, self.group_3.id])
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
consumer_group_id = self.controller._get_consumer_group_for_user(
|
||||||
|
group_order, self.user
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(consumer_group_id, self.group_1.id)
|
||||||
|
|
||||||
|
def test_create_or_update_sale_order_assigns_user_consumer_group(self):
|
||||||
|
"""Created sale.order must use the consumer group that matches the user."""
|
||||||
|
request_mock = _build_request_mock(self.env(user=self.user))
|
||||||
|
|
||||||
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
|
sale_order = self.controller._create_or_update_sale_order(
|
||||||
|
self.group_order,
|
||||||
|
self.user,
|
||||||
|
self.sale_order_lines,
|
||||||
|
is_delivery=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(sale_order.consumer_group_id.id, self.group_2.id)
|
||||||
|
|
||||||
|
|
||||||
class TestProcessCartItems(TransactionCase):
|
class TestProcessCartItems(TransactionCase):
|
||||||
"""Test _process_cart_items() helper method."""
|
"""Test _process_cart_items() helper method."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.controller = http.request.env["website.sale"].browse([])
|
self.controller = AplicoopWebsiteSale()
|
||||||
|
|
||||||
# Create test products
|
# Create test products
|
||||||
self.product1 = self.env["product.product"].create(
|
self.product1 = self.env["product.product"].create(
|
||||||
|
|
@ -148,20 +367,28 @@ class TestProcessCartItems(TransactionCase):
|
||||||
"type": "consu",
|
"type": "consu",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
self.group = self.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Cart Test Group",
|
||||||
|
"is_company": True,
|
||||||
|
"is_group": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Create test group order
|
# Create test group order
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env["group.order"].create(
|
||||||
{
|
{
|
||||||
"name": "Test Order for Cart",
|
"name": "Test Order for Cart",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
|
"group_ids": [(6, 0, [self.group.id])],
|
||||||
|
"start_date": date.today(),
|
||||||
|
"end_date": date.today() + timedelta(days=7),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_process_cart_items_success(self):
|
||||||
def test_process_cart_items_success(self, mock_request):
|
|
||||||
"""Test successful cart item processing."""
|
"""Test successful cart item processing."""
|
||||||
mock_request.env = self.env
|
request_mock = _build_request_mock(self.env.with_context(lang="es_ES"))
|
||||||
mock_request.env.lang = "es_ES"
|
|
||||||
|
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
|
|
@ -176,7 +403,8 @@ class TestProcessCartItems(TransactionCase):
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
result = self.controller._process_cart_items(items, self.group_order)
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
|
result = self.controller._process_cart_items(items, self.group_order)
|
||||||
|
|
||||||
self.assertEqual(len(result), 2)
|
self.assertEqual(len(result), 2)
|
||||||
self.assertEqual(result[0][0], 0) # Command (0, 0, vals)
|
self.assertEqual(result[0][0], 0) # Command (0, 0, vals)
|
||||||
|
|
@ -185,11 +413,9 @@ class TestProcessCartItems(TransactionCase):
|
||||||
self.assertEqual(result[0][2]["product_uom_qty"], 2)
|
self.assertEqual(result[0][2]["product_uom_qty"], 2)
|
||||||
self.assertEqual(result[0][2]["price_unit"], 15.0)
|
self.assertEqual(result[0][2]["price_unit"], 15.0)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_process_cart_items_uses_list_price_fallback(self):
|
||||||
def test_process_cart_items_uses_list_price_fallback(self, mock_request):
|
|
||||||
"""Test cart processing uses list_price when product_price is 0."""
|
"""Test cart processing uses list_price when product_price is 0."""
|
||||||
mock_request.env = self.env
|
request_mock = _build_request_mock(self.env.with_context(lang="es_ES"))
|
||||||
mock_request.env.lang = "es_ES"
|
|
||||||
|
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
|
|
@ -199,17 +425,16 @@ class TestProcessCartItems(TransactionCase):
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
result = self.controller._process_cart_items(items, self.group_order)
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
|
result = self.controller._process_cart_items(items, self.group_order)
|
||||||
|
|
||||||
self.assertEqual(len(result), 1)
|
self.assertEqual(len(result), 1)
|
||||||
# Should use product.list_price as fallback
|
# Should use product.list_price as fallback
|
||||||
self.assertEqual(result[0][2]["price_unit"], self.product1.list_price)
|
self.assertEqual(result[0][2]["price_unit"], self.product1.list_price)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_process_cart_items_skips_invalid_product(self):
|
||||||
def test_process_cart_items_skips_invalid_product(self, mock_request):
|
|
||||||
"""Test cart processing skips non-existent products."""
|
"""Test cart processing skips non-existent products."""
|
||||||
mock_request.env = self.env
|
request_mock = _build_request_mock(self.env.with_context(lang="es_ES"))
|
||||||
mock_request.env.lang = "es_ES"
|
|
||||||
|
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
|
|
@ -224,30 +449,28 @@ class TestProcessCartItems(TransactionCase):
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
result = self.controller._process_cart_items(items, self.group_order)
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
|
result = self.controller._process_cart_items(items, self.group_order)
|
||||||
|
|
||||||
# Should only process the valid product
|
# Should only process the valid product
|
||||||
self.assertEqual(len(result), 1)
|
self.assertEqual(len(result), 1)
|
||||||
self.assertEqual(result[0][2]["product_id"], self.product1.id)
|
self.assertEqual(result[0][2]["product_id"], self.product1.id)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_process_cart_items_empty_after_filtering(self):
|
||||||
def test_process_cart_items_empty_after_filtering(self, mock_request):
|
|
||||||
"""Test cart processing raises error when no valid items remain."""
|
"""Test cart processing raises error when no valid items remain."""
|
||||||
mock_request.env = self.env
|
request_mock = _build_request_mock(self.env.with_context(lang="es_ES"))
|
||||||
mock_request.env.lang = "es_ES"
|
|
||||||
|
|
||||||
items = [{"product_id": 99999, "quantity": 1, "product_price": 10.0}]
|
items = [{"product_id": 99999, "quantity": 1, "product_price": 10.0}]
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
self.controller._process_cart_items(items, self.group_order)
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.controller._process_cart_items(items, self.group_order)
|
||||||
|
|
||||||
self.assertIn("No valid items", str(context.exception))
|
self.assertIn("No valid items", str(context.exception))
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_process_cart_items_translates_product_name(self):
|
||||||
def test_process_cart_items_translates_product_name(self, mock_request):
|
|
||||||
"""Test cart processing uses translated product names."""
|
"""Test cart processing uses translated product names."""
|
||||||
mock_request.env = self.env
|
request_mock = _build_request_mock(self.env.with_context(lang="eu_ES"))
|
||||||
mock_request.env.lang = "eu_ES" # Basque
|
|
||||||
|
|
||||||
# Add translation for product name
|
# Add translation for product name
|
||||||
self.env["ir.translation"].create(
|
self.env["ir.translation"].create(
|
||||||
|
|
@ -271,7 +494,8 @@ class TestProcessCartItems(TransactionCase):
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
result = self.controller._process_cart_items(items, self.group_order)
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
|
result = self.controller._process_cart_items(items, self.group_order)
|
||||||
|
|
||||||
# Product name should be in Basque context
|
# Product name should be in Basque context
|
||||||
product_name = result[0][2]["name"]
|
product_name = result[0][2]["name"]
|
||||||
|
|
@ -284,21 +508,27 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.controller = http.request.env["website.sale"].browse([])
|
self.controller = AplicoopWebsiteSale()
|
||||||
self.user = self.env.ref("base.user_admin")
|
self.user = self.env.ref("base.user_admin")
|
||||||
self.partner = self.env.ref("base.partner_admin")
|
self.partner = self.env.ref("base.partner_admin")
|
||||||
|
self.group = self.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Message Test Group",
|
||||||
|
"is_company": True,
|
||||||
|
"is_group": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Create test group order with dates
|
# Create test group order with dates
|
||||||
pickup_date = date.today() + timedelta(days=5)
|
|
||||||
delivery_date = pickup_date + timedelta(days=1)
|
|
||||||
|
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env["group.order"].create(
|
||||||
{
|
{
|
||||||
"name": "Test Order Messages",
|
"name": "Test Order Messages",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
|
"group_ids": [(6, 0, [self.group.id])],
|
||||||
|
"start_date": date.today(),
|
||||||
|
"end_date": date.today() + timedelta(days=7),
|
||||||
|
"cutoff_day": "3",
|
||||||
"pickup_day": "5", # Saturday (0=Monday)
|
"pickup_day": "5", # Saturday (0=Monday)
|
||||||
"pickup_date": pickup_date,
|
|
||||||
"delivery_date": delivery_date,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -310,7 +540,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
@patch(REQUEST_PATCH_TARGET)
|
||||||
def test_build_confirmation_message_pickup(self, mock_request):
|
def test_build_confirmation_message_pickup(self, mock_request):
|
||||||
"""Test confirmation message for pickup (not delivery)."""
|
"""Test confirmation message for pickup (not delivery)."""
|
||||||
mock_request.env = self.env.with_context(lang="es_ES")
|
mock_request.env = self.env.with_context(lang="es_ES")
|
||||||
|
|
@ -333,7 +563,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
# Should have pickup day index
|
# Should have pickup day index
|
||||||
self.assertEqual(result["pickup_day_index"], 5)
|
self.assertEqual(result["pickup_day_index"], 5)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
@patch(REQUEST_PATCH_TARGET)
|
||||||
def test_build_confirmation_message_delivery(self, mock_request):
|
def test_build_confirmation_message_delivery(self, mock_request):
|
||||||
"""Test confirmation message for home delivery."""
|
"""Test confirmation message for home delivery."""
|
||||||
mock_request.env = self.env.with_context(lang="es_ES")
|
mock_request.env = self.env.with_context(lang="es_ES")
|
||||||
|
|
@ -353,7 +583,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
# pickup_day_index=5 (Saturday), delivery should be 6 (Sunday)
|
# pickup_day_index=5 (Saturday), delivery should be 6 (Sunday)
|
||||||
# Note: _get_day_names would need to be mocked for exact day name
|
# Note: _get_day_names would need to be mocked for exact day name
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
@patch(REQUEST_PATCH_TARGET)
|
||||||
def test_build_confirmation_message_no_dates(self, mock_request):
|
def test_build_confirmation_message_no_dates(self, mock_request):
|
||||||
"""Test confirmation message when no pickup date is set."""
|
"""Test confirmation message when no pickup date is set."""
|
||||||
mock_request.env = self.env.with_context(lang="es_ES")
|
mock_request.env = self.env.with_context(lang="es_ES")
|
||||||
|
|
@ -363,6 +593,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
{
|
{
|
||||||
"name": "Order No Dates",
|
"name": "Order No Dates",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
|
"group_ids": [(6, 0, [self.group.id])],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -384,7 +615,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
# Date fields should be empty
|
# Date fields should be empty
|
||||||
self.assertEqual(result["pickup_date"], "")
|
self.assertEqual(result["pickup_date"], "")
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
@patch(REQUEST_PATCH_TARGET)
|
||||||
def test_build_confirmation_message_formats_date(self, mock_request):
|
def test_build_confirmation_message_formats_date(self, mock_request):
|
||||||
"""Test confirmation message formats dates correctly (DD/MM/YYYY)."""
|
"""Test confirmation message formats dates correctly (DD/MM/YYYY)."""
|
||||||
mock_request.env = self.env.with_context(lang="es_ES")
|
mock_request.env = self.env.with_context(lang="es_ES")
|
||||||
|
|
@ -402,7 +633,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
date_pattern = r"\d{2}/\d{2}/\d{4}"
|
date_pattern = r"\d{2}/\d{2}/\d{4}"
|
||||||
self.assertRegex(pickup_date_str, date_pattern)
|
self.assertRegex(pickup_date_str, date_pattern)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
@patch(REQUEST_PATCH_TARGET)
|
||||||
def test_build_confirmation_message_multilang_es(self, mock_request):
|
def test_build_confirmation_message_multilang_es(self, mock_request):
|
||||||
"""Test confirmation message in Spanish (es_ES)."""
|
"""Test confirmation message in Spanish (es_ES)."""
|
||||||
mock_request.env = self.env.with_context(lang="es_ES")
|
mock_request.env = self.env.with_context(lang="es_ES")
|
||||||
|
|
@ -416,7 +647,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
self.assertIsNotNone(message)
|
self.assertIsNotNone(message)
|
||||||
# In real scenario, would check for "¡Gracias!" or similar
|
# In real scenario, would check for "¡Gracias!" or similar
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
@patch(REQUEST_PATCH_TARGET)
|
||||||
def test_build_confirmation_message_multilang_eu(self, mock_request):
|
def test_build_confirmation_message_multilang_eu(self, mock_request):
|
||||||
"""Test confirmation message in Basque (eu_ES)."""
|
"""Test confirmation message in Basque (eu_ES)."""
|
||||||
mock_request.env = self.env.with_context(lang="eu_ES")
|
mock_request.env = self.env.with_context(lang="eu_ES")
|
||||||
|
|
@ -429,7 +660,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
self.assertIsNotNone(message)
|
self.assertIsNotNone(message)
|
||||||
# In real scenario, would check for "Eskerrik asko!" or similar
|
# In real scenario, would check for "Eskerrik asko!" or similar
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
@patch(REQUEST_PATCH_TARGET)
|
||||||
def test_build_confirmation_message_multilang_ca(self, mock_request):
|
def test_build_confirmation_message_multilang_ca(self, mock_request):
|
||||||
"""Test confirmation message in Catalan (ca_ES)."""
|
"""Test confirmation message in Catalan (ca_ES)."""
|
||||||
mock_request.env = self.env.with_context(lang="ca_ES")
|
mock_request.env = self.env.with_context(lang="ca_ES")
|
||||||
|
|
@ -442,7 +673,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
self.assertIsNotNone(message)
|
self.assertIsNotNone(message)
|
||||||
# In real scenario, would check for "Gràcies!" or similar
|
# In real scenario, would check for "Gràcies!" or similar
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
@patch(REQUEST_PATCH_TARGET)
|
||||||
def test_build_confirmation_message_multilang_gl(self, mock_request):
|
def test_build_confirmation_message_multilang_gl(self, mock_request):
|
||||||
"""Test confirmation message in Galician (gl_ES)."""
|
"""Test confirmation message in Galician (gl_ES)."""
|
||||||
mock_request.env = self.env.with_context(lang="gl_ES")
|
mock_request.env = self.env.with_context(lang="gl_ES")
|
||||||
|
|
@ -455,7 +686,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
self.assertIsNotNone(message)
|
self.assertIsNotNone(message)
|
||||||
# In real scenario, would check for "Grazas!" or similar
|
# In real scenario, would check for "Grazas!" or similar
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
@patch(REQUEST_PATCH_TARGET)
|
||||||
def test_build_confirmation_message_multilang_pt(self, mock_request):
|
def test_build_confirmation_message_multilang_pt(self, mock_request):
|
||||||
"""Test confirmation message in Portuguese (pt_PT)."""
|
"""Test confirmation message in Portuguese (pt_PT)."""
|
||||||
mock_request.env = self.env.with_context(lang="pt_PT")
|
mock_request.env = self.env.with_context(lang="pt_PT")
|
||||||
|
|
@ -468,7 +699,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
self.assertIsNotNone(message)
|
self.assertIsNotNone(message)
|
||||||
# In real scenario, would check for "Obrigado!" or similar
|
# In real scenario, would check for "Obrigado!" or similar
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
@patch(REQUEST_PATCH_TARGET)
|
||||||
def test_build_confirmation_message_multilang_fr(self, mock_request):
|
def test_build_confirmation_message_multilang_fr(self, mock_request):
|
||||||
"""Test confirmation message in French (fr_FR)."""
|
"""Test confirmation message in French (fr_FR)."""
|
||||||
mock_request.env = self.env.with_context(lang="fr_FR")
|
mock_request.env = self.env.with_context(lang="fr_FR")
|
||||||
|
|
@ -481,7 +712,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
self.assertIsNotNone(message)
|
self.assertIsNotNone(message)
|
||||||
# In real scenario, would check for "Merci!" or similar
|
# In real scenario, would check for "Merci!" or similar
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
@patch(REQUEST_PATCH_TARGET)
|
||||||
def test_build_confirmation_message_multilang_it(self, mock_request):
|
def test_build_confirmation_message_multilang_it(self, mock_request):
|
||||||
"""Test confirmation message in Italian (it_IT)."""
|
"""Test confirmation message in Italian (it_IT)."""
|
||||||
mock_request.env = self.env.with_context(lang="it_IT")
|
mock_request.env = self.env.with_context(lang="it_IT")
|
||||||
|
|
@ -500,9 +731,29 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.controller = http.request.env["website.sale"].browse([])
|
self.controller = AplicoopWebsiteSale()
|
||||||
self.user = self.env.ref("base.user_admin")
|
self.consumer_group = self.env["res.partner"].create(
|
||||||
self.partner = self.env.ref("base.partner_admin")
|
{
|
||||||
|
"name": "Integration Consumer Group",
|
||||||
|
"is_company": True,
|
||||||
|
"is_group": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.partner = self.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Integration Member",
|
||||||
|
"email": "integration-member@test.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.consumer_group.member_ids = [(4, self.partner.id)]
|
||||||
|
self.user = self.env["res.users"].create(
|
||||||
|
{
|
||||||
|
"name": "Integration User",
|
||||||
|
"login": "integration-user@test.com",
|
||||||
|
"email": "integration-user@test.com",
|
||||||
|
"partner_id": self.partner.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Create test product
|
# Create test product
|
||||||
self.product = self.env["product.product"].create(
|
self.product = self.env["product.product"].create(
|
||||||
|
|
@ -518,19 +769,20 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
||||||
{
|
{
|
||||||
"name": "Integration Test Order",
|
"name": "Integration Test Order",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
|
"group_ids": [(6, 0, [self.consumer_group.id])],
|
||||||
|
"start_date": date.today(),
|
||||||
|
"end_date": date.today() + timedelta(days=7),
|
||||||
|
"cutoff_day": "3",
|
||||||
"pickup_day": "5",
|
"pickup_day": "5",
|
||||||
"pickup_date": date.today() + timedelta(days=5),
|
"home_delivery": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
# Use the real website so pricing helpers can resolve company/fiscal pos
|
||||||
def test_confirm_eskaera_full_flow_pickup(self, mock_request):
|
self.website = self.env["website"].search([], limit=1)
|
||||||
"""Test full confirm_eskaera flow for pickup order."""
|
|
||||||
mock_request.env = self.env.with_user(self.user)
|
|
||||||
mock_request.env.lang = "es_ES"
|
|
||||||
mock_request.httprequest = Mock()
|
|
||||||
|
|
||||||
# Prepare request data
|
def test_confirm_eskaera_full_flow_pickup(self):
|
||||||
|
"""Test full confirm_eskaera flow for pickup order."""
|
||||||
data = {
|
data = {
|
||||||
"order_id": self.group_order.id,
|
"order_id": self.group_order.id,
|
||||||
"items": [
|
"items": [
|
||||||
|
|
@ -542,11 +794,14 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
||||||
],
|
],
|
||||||
"is_delivery": False,
|
"is_delivery": False,
|
||||||
}
|
}
|
||||||
|
mock_request = _build_request_mock(
|
||||||
|
self.env(user=self.user, context=dict(self.env.context, lang="es_ES")),
|
||||||
|
data,
|
||||||
|
website=self.website,
|
||||||
|
)
|
||||||
|
|
||||||
mock_request.httprequest.data = json.dumps(data).encode("utf-8")
|
with patch(REQUEST_PATCH_TARGET, mock_request):
|
||||||
|
response = self.controller.confirm_eskaera.__wrapped__(self.controller)
|
||||||
# Call confirm_eskaera
|
|
||||||
response = self.controller.confirm_eskaera()
|
|
||||||
|
|
||||||
# Verify response
|
# Verify response
|
||||||
self.assertIsNotNone(response)
|
self.assertIsNotNone(response)
|
||||||
|
|
@ -565,20 +820,19 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
||||||
self.assertEqual(sale_order.group_order_id.id, self.group_order.id)
|
self.assertEqual(sale_order.group_order_id.id, self.group_order.id)
|
||||||
self.assertEqual(len(sale_order.order_line), 1)
|
self.assertEqual(len(sale_order.order_line), 1)
|
||||||
self.assertEqual(sale_order.order_line[0].product_uom_qty, 3)
|
self.assertEqual(sale_order.order_line[0].product_uom_qty, 3)
|
||||||
|
self.assertFalse(sale_order.home_delivery)
|
||||||
|
self.assertEqual(
|
||||||
|
sale_order.commitment_date.date(),
|
||||||
|
self.group_order.pickup_date,
|
||||||
|
)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_confirm_eskaera_full_flow_delivery(self):
|
||||||
def test_confirm_eskaera_full_flow_delivery(self, mock_request):
|
|
||||||
"""Test full confirm_eskaera flow for delivery order."""
|
"""Test full confirm_eskaera flow for delivery order."""
|
||||||
mock_request.env = self.env.with_user(self.user)
|
|
||||||
mock_request.env.lang = "es_ES"
|
|
||||||
mock_request.httprequest = Mock()
|
|
||||||
|
|
||||||
# Add delivery_date to group order
|
# Add delivery_date to group order
|
||||||
self.group_order.delivery_date = self.group_order.pickup_date + timedelta(
|
self.group_order.delivery_date = self.group_order.pickup_date + timedelta(
|
||||||
days=1
|
days=1
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prepare request data
|
|
||||||
data = {
|
data = {
|
||||||
"order_id": self.group_order.id,
|
"order_id": self.group_order.id,
|
||||||
"items": [
|
"items": [
|
||||||
|
|
@ -590,11 +844,14 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
||||||
],
|
],
|
||||||
"is_delivery": True,
|
"is_delivery": True,
|
||||||
}
|
}
|
||||||
|
mock_request = _build_request_mock(
|
||||||
|
self.env(user=self.user, context=dict(self.env.context, lang="es_ES")),
|
||||||
|
data,
|
||||||
|
website=self.website,
|
||||||
|
)
|
||||||
|
|
||||||
mock_request.httprequest.data = json.dumps(data).encode("utf-8")
|
with patch(REQUEST_PATCH_TARGET, mock_request):
|
||||||
|
response = self.controller.confirm_eskaera.__wrapped__(self.controller)
|
||||||
# Call confirm_eskaera
|
|
||||||
response = self.controller.confirm_eskaera()
|
|
||||||
|
|
||||||
# Verify response
|
# Verify response
|
||||||
response_data = json.loads(response.data.decode("utf-8"))
|
response_data = json.loads(response.data.decode("utf-8"))
|
||||||
|
|
@ -611,19 +868,17 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
||||||
sale_order.commitment_date.date(), self.group_order.delivery_date
|
sale_order.commitment_date.date(), self.group_order.delivery_date
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_confirm_eskaera_updates_existing_draft(self):
|
||||||
def test_confirm_eskaera_updates_existing_draft(self, mock_request):
|
|
||||||
"""Test confirm_eskaera updates existing draft order instead of creating new."""
|
"""Test confirm_eskaera updates existing draft order instead of creating new."""
|
||||||
mock_request.env = self.env.with_user(self.user)
|
|
||||||
mock_request.env.lang = "es_ES"
|
|
||||||
mock_request.httprequest = Mock()
|
|
||||||
|
|
||||||
# Create existing draft order
|
# Create existing draft order
|
||||||
existing_order = self.env["sale.order"].create(
|
existing_order = self.env["sale.order"].create(
|
||||||
{
|
{
|
||||||
"partner_id": self.partner.id,
|
"partner_id": self.partner.id,
|
||||||
"group_order_id": self.group_order.id,
|
"group_order_id": self.group_order.id,
|
||||||
"state": "draft",
|
"state": "draft",
|
||||||
|
# pickup_date must match group_order.pickup_date so
|
||||||
|
# _find_recent_draft_order can locate this draft
|
||||||
|
"pickup_date": self.group_order.pickup_date,
|
||||||
"order_line": [
|
"order_line": [
|
||||||
(
|
(
|
||||||
0,
|
0,
|
||||||
|
|
@ -640,7 +895,6 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
||||||
|
|
||||||
existing_order_id = existing_order.id
|
existing_order_id = existing_order.id
|
||||||
|
|
||||||
# Prepare new request data
|
|
||||||
data = {
|
data = {
|
||||||
"order_id": self.group_order.id,
|
"order_id": self.group_order.id,
|
||||||
"items": [
|
"items": [
|
||||||
|
|
@ -652,11 +906,14 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
||||||
],
|
],
|
||||||
"is_delivery": False,
|
"is_delivery": False,
|
||||||
}
|
}
|
||||||
|
mock_request = _build_request_mock(
|
||||||
|
self.env(user=self.user, context=dict(self.env.context, lang="es_ES")),
|
||||||
|
data,
|
||||||
|
website=self.website,
|
||||||
|
)
|
||||||
|
|
||||||
mock_request.httprequest.data = json.dumps(data).encode("utf-8")
|
with patch(REQUEST_PATCH_TARGET, mock_request):
|
||||||
|
response = self.controller.confirm_eskaera.__wrapped__(self.controller)
|
||||||
# Call confirm_eskaera
|
|
||||||
response = self.controller.confirm_eskaera()
|
|
||||||
|
|
||||||
response_data = json.loads(response.data.decode("utf-8"))
|
response_data = json.loads(response.data.decode("utf-8"))
|
||||||
|
|
||||||
|
|
@ -668,13 +925,8 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
||||||
self.assertEqual(len(existing_order.order_line), 1)
|
self.assertEqual(len(existing_order.order_line), 1)
|
||||||
self.assertEqual(existing_order.order_line[0].product_uom_qty, 5)
|
self.assertEqual(existing_order.order_line[0].product_uom_qty, 5)
|
||||||
|
|
||||||
@patch("odoo.http.request")
|
def test_confirm_eskaera_ignores_old_period_draft(self):
|
||||||
def test_confirm_eskaera_ignores_old_period_draft(self, mock_request):
|
|
||||||
"""Old draft from previous pickup_date must not be reused."""
|
"""Old draft from previous pickup_date must not be reused."""
|
||||||
mock_request.env = self.env.with_user(self.user)
|
|
||||||
mock_request.env.lang = "es_ES"
|
|
||||||
mock_request.httprequest = Mock()
|
|
||||||
|
|
||||||
old_draft = self.env["sale.order"].create(
|
old_draft = self.env["sale.order"].create(
|
||||||
{
|
{
|
||||||
"partner_id": self.partner.id,
|
"partner_id": self.partner.id,
|
||||||
|
|
@ -706,10 +958,15 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
||||||
],
|
],
|
||||||
"is_delivery": False,
|
"is_delivery": False,
|
||||||
}
|
}
|
||||||
|
mock_request = _build_request_mock(
|
||||||
|
self.env(user=self.user, context=dict(self.env.context, lang="es_ES")),
|
||||||
|
data,
|
||||||
|
website=self.website,
|
||||||
|
)
|
||||||
|
|
||||||
mock_request.httprequest.data = json.dumps(data).encode("utf-8")
|
with patch(REQUEST_PATCH_TARGET, mock_request):
|
||||||
|
response = self.controller.confirm_eskaera.__wrapped__(self.controller)
|
||||||
|
|
||||||
response = self.controller.confirm_eskaera()
|
|
||||||
response_data = json.loads(response.data.decode("utf-8"))
|
response_data = json.loads(response.data.decode("utf-8"))
|
||||||
|
|
||||||
self.assertTrue(response_data.get("success"))
|
self.assertTrue(response_data.get("success"))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue