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_backorder_confirmation # noqa: F401
|
||||
from . import stock_picking # 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
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
|
|
@ -21,3 +22,18 @@ class StockMoveLine(models.Model):
|
|||
default=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:
|
||||
batch.summary_line_ids = [fields.Command.clear()]
|
||||
|
||||
def _check_all_products_collected(self):
|
||||
"""Ensure all product summary lines are marked as collected before done."""
|
||||
def _check_all_products_collected(self, product_ids=None):
|
||||
"""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:
|
||||
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:
|
||||
continue
|
||||
|
|
@ -161,10 +170,6 @@ class StockPickingBatch(models.Model):
|
|||
)
|
||||
raise UserError(message)
|
||||
|
||||
def action_done(self):
|
||||
self._check_all_products_collected()
|
||||
return super().action_done()
|
||||
|
||||
|
||||
class StockPickingBatchSummaryLine(models.Model):
|
||||
_name = "stock.picking.batch.summary.line"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import TransactionCase
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
|
|
@ -24,6 +26,13 @@ class TestBatchSummary(TransactionCase):
|
|||
"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(
|
||||
{"name": "Batch Test", "picking_type_id": cls.picking_type.id}
|
||||
|
|
@ -113,6 +122,60 @@ class TestBatchSummary(TransactionCase):
|
|||
picking.batch_id = batch.id
|
||||
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
|
||||
def test_totals_and_pending_with_conversion(self):
|
||||
"""Totals aggregate per product with UoM conversion and pending."""
|
||||
|
|
@ -190,14 +253,6 @@ class TestBatchSummary(TransactionCase):
|
|||
self.assertFalse(batch.summary_line_ids)
|
||||
|
||||
# 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_type_id": self.picking_type.id,
|
||||
|
|
@ -226,8 +281,8 @@ class TestBatchSummary(TransactionCase):
|
|||
)
|
||||
self.env["stock.move"].create(
|
||||
{
|
||||
"name": product2.name,
|
||||
"product_id": product2.id,
|
||||
"name": self.product_2.name,
|
||||
"product_id": self.product_2.id,
|
||||
"product_uom_qty": 10.0,
|
||||
"product_uom": self.uom_unit.id,
|
||||
"picking_id": picking_b.id,
|
||||
|
|
@ -276,7 +331,7 @@ class TestBatchSummary(TransactionCase):
|
|||
# Two products expected
|
||||
products_in_summary = batch.summary_line_ids.mapped("product_id")
|
||||
self.assertIn(self.product, products_in_summary)
|
||||
self.assertIn(product2, products_in_summary)
|
||||
self.assertIn(self.product_2, products_in_summary)
|
||||
|
||||
# Check aggregated quantities
|
||||
line_product1 = batch.summary_line_ids.filtered(
|
||||
|
|
@ -285,12 +340,12 @@ class TestBatchSummary(TransactionCase):
|
|||
self.assertAlmostEqual(line_product1.qty_demanded, 8.0) # 5 + 3
|
||||
|
||||
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)
|
||||
|
||||
def test_done_requires_all_summary_lines_collected(self):
|
||||
"""Batch validation must fail if there are unchecked collected lines."""
|
||||
def test_done_requires_all_summary_lines_collected_without_backorder(self):
|
||||
"""Full validation still blocks if processed products are unchecked."""
|
||||
|
||||
batch = self._create_batch_with_pickings()
|
||||
batch.action_confirm()
|
||||
|
|
@ -302,6 +357,32 @@ class TestBatchSummary(TransactionCase):
|
|||
with self.assertRaises(UserError):
|
||||
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):
|
||||
"""Collected validation helper must pass when all lines are checked."""
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,27 @@
|
|||
<field name="model">stock.move.line</field>
|
||||
<field name="inherit_id" ref="stock_picking_batch.view_move_line_tree"/>
|
||||
<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">
|
||||
<field name="product_categ_id" optional="hide"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='picking_id']" position="after">
|
||||
<field name="picking_partner_id" optional="hide"/>
|
||||
<field name="consumer_group_id" optional="show"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='quantity']" position="after">
|
||||
<field name="is_collected" optional="show" widget="boolean_toggle"/>
|
||||
|
|
|
|||
|
|
@ -28,4 +28,15 @@
|
|||
</xpath>
|
||||
</field>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -768,6 +768,8 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
if not current_user.partner_id:
|
||||
raise ValueError("User has no associated partner") from None
|
||||
|
||||
self._validate_user_group_access(group_order, current_user)
|
||||
|
||||
# Validate items
|
||||
items = data.get("items", [])
|
||||
if not items:
|
||||
|
|
@ -820,6 +822,8 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
if not current_user.partner_id:
|
||||
raise ValueError("User has no associated partner")
|
||||
|
||||
self._validate_user_group_access(group_order, current_user)
|
||||
|
||||
# Validate items
|
||||
items = data.get("items", [])
|
||||
if not items:
|
||||
|
|
@ -887,6 +891,8 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
if not current_user.partner_id:
|
||||
raise ValueError("User has no associated partner")
|
||||
|
||||
self._validate_user_group_access(group_order, current_user)
|
||||
|
||||
# Validate items
|
||||
items = data.get("items", [])
|
||||
if not items:
|
||||
|
|
@ -921,6 +927,29 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
return False
|
||||
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):
|
||||
"""Process cart items and build sale.order line data.
|
||||
|
||||
|
|
@ -1009,6 +1038,35 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
# No salesperson found
|
||||
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(
|
||||
self,
|
||||
group_order,
|
||||
|
|
@ -1022,11 +1080,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
|
||||
Returns the sale.order record.
|
||||
"""
|
||||
# consumer_group_id comes directly from group_order (first/only group)
|
||||
# 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
|
||||
)
|
||||
consumer_group_id = self._validate_user_group_access(group_order, current_user)
|
||||
|
||||
_logger.info(
|
||||
"[CONSUMER_GROUP DEBUG] _create_or_update_sale_order: "
|
||||
|
|
@ -1036,22 +1090,25 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
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:
|
||||
commitment_date = (
|
||||
group_order.delivery_date if is_delivery else group_order.pickup_date
|
||||
)
|
||||
commitment_date = calculated_commitment_date
|
||||
|
||||
if existing_order:
|
||||
# Update existing order with new lines and propagate fields
|
||||
# Use sudo() to avoid permission issues with portal users
|
||||
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:
|
||||
existing_order_sudo.group_order_id = group_order.id
|
||||
existing_order_sudo.pickup_day = group_order.pickup_day
|
||||
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
|
||||
if 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",
|
||||
existing_order.id,
|
||||
commitment_date,
|
||||
is_delivery,
|
||||
effective_home_delivery,
|
||||
consumer_group_id,
|
||||
)
|
||||
return existing_order
|
||||
|
|
@ -1071,7 +1128,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"group_order_id": group_order.id,
|
||||
"pickup_day": group_order.pickup_day,
|
||||
"pickup_date": group_order.pickup_date,
|
||||
"home_delivery": is_delivery,
|
||||
"home_delivery": effective_home_delivery,
|
||||
"consumer_group_id": consumer_group_id,
|
||||
}
|
||||
if commitment_date:
|
||||
|
|
@ -1094,23 +1151,25 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
sale_order.id,
|
||||
group_order.id,
|
||||
group_order.pickup_day,
|
||||
group_order.home_delivery,
|
||||
effective_home_delivery,
|
||||
consumer_group_id,
|
||||
)
|
||||
return 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.
|
||||
|
||||
Returns created sale.order record.
|
||||
"""
|
||||
# consumer_group_id comes directly from group_order (first/only group)
|
||||
# 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
|
||||
)
|
||||
consumer_group_id = self._validate_user_group_access(group_order, current_user)
|
||||
|
||||
_logger.info(
|
||||
"[CONSUMER_GROUP DEBUG] _create_draft_sale_order: "
|
||||
|
|
@ -1120,11 +1179,8 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
consumer_group_id,
|
||||
)
|
||||
|
||||
# commitment_date comes from group_order (delivery_date or pickup_date)
|
||||
commitment_date = (
|
||||
group_order.delivery_date
|
||||
if group_order.home_delivery
|
||||
else group_order.pickup_date
|
||||
effective_home_delivery, commitment_date = self._get_effective_delivery_context(
|
||||
group_order, is_delivery
|
||||
)
|
||||
|
||||
order_vals = {
|
||||
|
|
@ -1134,7 +1190,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"group_order_id": order_id,
|
||||
"pickup_day": group_order.pickup_day,
|
||||
"pickup_date": group_order.pickup_date,
|
||||
"home_delivery": group_order.home_delivery,
|
||||
"home_delivery": effective_home_delivery,
|
||||
"consumer_group_id": consumer_group_id,
|
||||
"commitment_date": commitment_date,
|
||||
}
|
||||
|
|
@ -1374,11 +1430,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
|
||||
All fields (commitment_date, consumer_group_id, etc.) come from group_order.
|
||||
"""
|
||||
# consumer_group_id comes directly from group_order (first/only group)
|
||||
# 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
|
||||
)
|
||||
consumer_group_id = self._validate_user_group_access(group_order, current_user)
|
||||
|
||||
_logger.info(
|
||||
"[CONSUMER_GROUP DEBUG] _merge_or_replace_draft: "
|
||||
|
|
@ -1388,9 +1440,8 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
consumer_group_id,
|
||||
)
|
||||
|
||||
# commitment_date comes directly from selected flow
|
||||
commitment_date = (
|
||||
group_order.delivery_date if is_delivery else group_order.pickup_date
|
||||
effective_home_delivery, commitment_date = self._get_effective_delivery_context(
|
||||
group_order, is_delivery
|
||||
)
|
||||
|
||||
if existing_drafts:
|
||||
|
|
@ -1406,7 +1457,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"group_order_id": order_id,
|
||||
"pickup_day": group_order.pickup_day,
|
||||
"pickup_date": group_order.pickup_date,
|
||||
"home_delivery": is_delivery,
|
||||
"home_delivery": effective_home_delivery,
|
||||
"consumer_group_id": consumer_group_id,
|
||||
"commitment_date": commitment_date,
|
||||
}
|
||||
|
|
@ -1420,7 +1471,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"group_order_id": order_id,
|
||||
"pickup_day": group_order.pickup_day,
|
||||
"pickup_date": group_order.pickup_date,
|
||||
"home_delivery": is_delivery,
|
||||
"home_delivery": effective_home_delivery,
|
||||
"consumer_group_id": consumer_group_id,
|
||||
"commitment_date": commitment_date,
|
||||
}
|
||||
|
|
@ -2240,8 +2291,18 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
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", [])
|
||||
pickup_date = data.get("pickup_date")
|
||||
is_delivery = self._to_bool(data.get("is_delivery", False))
|
||||
if not items:
|
||||
return request.make_response(
|
||||
json.dumps({"error": "No items in cart"}),
|
||||
|
|
@ -2262,7 +2323,12 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
)
|
||||
|
||||
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(
|
||||
|
|
@ -2629,6 +2695,15 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
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
|
||||
items = data.get("items", [])
|
||||
is_delivery = self._to_bool(data.get("is_delivery", False))
|
||||
|
|
@ -2791,27 +2866,23 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
)
|
||||
existing_order = None
|
||||
|
||||
# Get pickup date and delivery info from group order
|
||||
# If delivery, use delivery_date; otherwise use pickup_date
|
||||
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
|
||||
effective_home_delivery, commitment_date = (
|
||||
self._get_effective_delivery_context(group_order, is_delivery)
|
||||
)
|
||||
|
||||
# Create or update sale.order using helper
|
||||
sale_order = self._create_or_update_sale_order(
|
||||
group_order,
|
||||
current_user,
|
||||
sale_order_lines,
|
||||
is_delivery,
|
||||
effective_home_delivery,
|
||||
commitment_date=commitment_date,
|
||||
existing_order=existing_order,
|
||||
)
|
||||
|
||||
# Build confirmation message using helper
|
||||
message_data = self._build_confirmation_message(
|
||||
sale_order, group_order, is_delivery
|
||||
sale_order, group_order, effective_home_delivery
|
||||
)
|
||||
message = message_data["message"]
|
||||
pickup_day_name = message_data["pickup_day"]
|
||||
|
|
|
|||
|
|
@ -799,9 +799,45 @@ class GroupOrder(models.Model):
|
|||
self.delivery_date,
|
||||
)
|
||||
|
||||
sale_orders.action_confirm()
|
||||
# Create picking batches after confirmation
|
||||
self._create_picking_batches_for_sale_orders(sale_orders)
|
||||
# Confirm each order in an isolated savepoint so one bad product
|
||||
# route configuration doesn't block all remaining 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:
|
||||
_logger.exception(
|
||||
"Cron: Error confirming sale orders for group order %s (%s)",
|
||||
|
|
@ -809,6 +845,42 @@ class GroupOrder(models.Model):
|
|||
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):
|
||||
"""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_save_order_endpoints # 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_portal_sale_order_creation # 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)
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -402,3 +404,54 @@ class TestCronPickingBatch(TransactionCase):
|
|||
"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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,37 +20,108 @@ Includes tests for:
|
|||
import json
|
||||
from datetime import date
|
||||
from datetime import timedelta
|
||||
from unittest.mock import Mock
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import http
|
||||
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):
|
||||
"""Test _validate_confirm_json() helper method."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.controller = http.request.env["website.sale"].browse([])
|
||||
self.user = self.env.ref("base.user_admin")
|
||||
self.partner = self.env.ref("base.partner_admin")
|
||||
self.controller = AplicoopWebsiteSale()
|
||||
self.group_1 = self.env["res.partner"].create(
|
||||
{
|
||||
"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
|
||||
self.group_order = self.env["group.order"].create(
|
||||
{
|
||||
"name": "Test Order Phase 3",
|
||||
"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
|
||||
"pickup_day": "5", # Saturday
|
||||
}
|
||||
)
|
||||
|
||||
@patch("odoo.http.request")
|
||||
def test_validate_confirm_json_success(self, mock_request):
|
||||
def test_validate_confirm_json_success(self):
|
||||
"""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 = {
|
||||
"order_id": self.group_order.id,
|
||||
|
|
@ -58,9 +129,10 @@ class TestValidateConfirmJson(TransactionCase):
|
|||
"is_delivery": False,
|
||||
}
|
||||
|
||||
order_id, group_order, current_user, items, is_delivery = (
|
||||
self.controller._validate_confirm_json(data)
|
||||
)
|
||||
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||
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(group_order.id, self.group_order.id)
|
||||
|
|
@ -68,52 +140,51 @@ class TestValidateConfirmJson(TransactionCase):
|
|||
self.assertEqual(len(items), 1)
|
||||
self.assertFalse(is_delivery)
|
||||
|
||||
@patch("odoo.http.request")
|
||||
def test_validate_confirm_json_missing_order_id(self, mock_request):
|
||||
def test_validate_confirm_json_missing_order_id(self):
|
||||
"""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}]}
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
self.controller._validate_confirm_json(data)
|
||||
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||
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, mock_request):
|
||||
def test_validate_confirm_json_order_not_exists(self):
|
||||
"""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 = {
|
||||
"order_id": 99999, # Non-existent ID
|
||||
"items": [{"product_id": 1, "quantity": 2}],
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
self.controller._validate_confirm_json(data)
|
||||
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||
with self.assertRaises(ValueError) as context:
|
||||
self.controller._validate_confirm_json(data)
|
||||
|
||||
self.assertIn("Order", str(context.exception))
|
||||
|
||||
@patch("odoo.http.request")
|
||||
def test_validate_confirm_json_no_items(self, mock_request):
|
||||
def test_validate_confirm_json_no_items(self):
|
||||
"""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 = {
|
||||
"order_id": self.group_order.id,
|
||||
"items": [],
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
self.controller._validate_confirm_json(data)
|
||||
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||
with self.assertRaises(ValueError) as context:
|
||||
self.controller._validate_confirm_json(data)
|
||||
|
||||
self.assertIn("No items in cart", str(context.exception))
|
||||
|
||||
@patch("odoo.http.request")
|
||||
def test_validate_confirm_json_with_delivery_flag(self, mock_request):
|
||||
def test_validate_confirm_json_with_delivery_flag(self):
|
||||
"""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 = {
|
||||
"order_id": self.group_order.id,
|
||||
|
|
@ -121,17 +192,165 @@ class TestValidateConfirmJson(TransactionCase):
|
|||
"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)
|
||||
|
||||
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):
|
||||
"""Test _process_cart_items() helper method."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.controller = http.request.env["website.sale"].browse([])
|
||||
self.controller = AplicoopWebsiteSale()
|
||||
|
||||
# Create test products
|
||||
self.product1 = self.env["product.product"].create(
|
||||
|
|
@ -148,20 +367,28 @@ class TestProcessCartItems(TransactionCase):
|
|||
"type": "consu",
|
||||
}
|
||||
)
|
||||
self.group = self.env["res.partner"].create(
|
||||
{
|
||||
"name": "Cart Test Group",
|
||||
"is_company": True,
|
||||
"is_group": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Create test group order
|
||||
self.group_order = self.env["group.order"].create(
|
||||
{
|
||||
"name": "Test Order for Cart",
|
||||
"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, mock_request):
|
||||
def test_process_cart_items_success(self):
|
||||
"""Test successful cart item processing."""
|
||||
mock_request.env = self.env
|
||||
mock_request.env.lang = "es_ES"
|
||||
request_mock = _build_request_mock(self.env.with_context(lang="es_ES"))
|
||||
|
||||
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(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]["price_unit"], 15.0)
|
||||
|
||||
@patch("odoo.http.request")
|
||||
def test_process_cart_items_uses_list_price_fallback(self, mock_request):
|
||||
def test_process_cart_items_uses_list_price_fallback(self):
|
||||
"""Test cart processing uses list_price when product_price is 0."""
|
||||
mock_request.env = self.env
|
||||
mock_request.env.lang = "es_ES"
|
||||
request_mock = _build_request_mock(self.env.with_context(lang="es_ES"))
|
||||
|
||||
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)
|
||||
# Should use product.list_price as fallback
|
||||
self.assertEqual(result[0][2]["price_unit"], self.product1.list_price)
|
||||
|
||||
@patch("odoo.http.request")
|
||||
def test_process_cart_items_skips_invalid_product(self, mock_request):
|
||||
def test_process_cart_items_skips_invalid_product(self):
|
||||
"""Test cart processing skips non-existent products."""
|
||||
mock_request.env = self.env
|
||||
mock_request.env.lang = "es_ES"
|
||||
request_mock = _build_request_mock(self.env.with_context(lang="es_ES"))
|
||||
|
||||
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
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0][2]["product_id"], self.product1.id)
|
||||
|
||||
@patch("odoo.http.request")
|
||||
def test_process_cart_items_empty_after_filtering(self, mock_request):
|
||||
def test_process_cart_items_empty_after_filtering(self):
|
||||
"""Test cart processing raises error when no valid items remain."""
|
||||
mock_request.env = self.env
|
||||
mock_request.env.lang = "es_ES"
|
||||
request_mock = _build_request_mock(self.env.with_context(lang="es_ES"))
|
||||
|
||||
items = [{"product_id": 99999, "quantity": 1, "product_price": 10.0}]
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
self.controller._process_cart_items(items, self.group_order)
|
||||
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||
with self.assertRaises(ValueError) as context:
|
||||
self.controller._process_cart_items(items, self.group_order)
|
||||
|
||||
self.assertIn("No valid items", str(context.exception))
|
||||
|
||||
@patch("odoo.http.request")
|
||||
def test_process_cart_items_translates_product_name(self, mock_request):
|
||||
def test_process_cart_items_translates_product_name(self):
|
||||
"""Test cart processing uses translated product names."""
|
||||
mock_request.env = self.env
|
||||
mock_request.env.lang = "eu_ES" # Basque
|
||||
request_mock = _build_request_mock(self.env.with_context(lang="eu_ES"))
|
||||
|
||||
# Add translation for product name
|
||||
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 = result[0][2]["name"]
|
||||
|
|
@ -284,21 +508,27 @@ class TestBuildConfirmationMessage(TransactionCase):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.controller = http.request.env["website.sale"].browse([])
|
||||
self.controller = AplicoopWebsiteSale()
|
||||
self.user = self.env.ref("base.user_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
|
||||
pickup_date = date.today() + timedelta(days=5)
|
||||
delivery_date = pickup_date + timedelta(days=1)
|
||||
|
||||
self.group_order = self.env["group.order"].create(
|
||||
{
|
||||
"name": "Test Order Messages",
|
||||
"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_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):
|
||||
"""Test confirmation message for pickup (not delivery)."""
|
||||
mock_request.env = self.env.with_context(lang="es_ES")
|
||||
|
|
@ -333,7 +563,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
|||
# Should have pickup day index
|
||||
self.assertEqual(result["pickup_day_index"], 5)
|
||||
|
||||
@patch("odoo.http.request")
|
||||
@patch(REQUEST_PATCH_TARGET)
|
||||
def test_build_confirmation_message_delivery(self, mock_request):
|
||||
"""Test confirmation message for home delivery."""
|
||||
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)
|
||||
# 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):
|
||||
"""Test confirmation message when no pickup date is set."""
|
||||
mock_request.env = self.env.with_context(lang="es_ES")
|
||||
|
|
@ -363,6 +593,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
|||
{
|
||||
"name": "Order No Dates",
|
||||
"state": "open",
|
||||
"group_ids": [(6, 0, [self.group.id])],
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -384,7 +615,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
|||
# Date fields should be empty
|
||||
self.assertEqual(result["pickup_date"], "")
|
||||
|
||||
@patch("odoo.http.request")
|
||||
@patch(REQUEST_PATCH_TARGET)
|
||||
def test_build_confirmation_message_formats_date(self, mock_request):
|
||||
"""Test confirmation message formats dates correctly (DD/MM/YYYY)."""
|
||||
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}"
|
||||
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):
|
||||
"""Test confirmation message in Spanish (es_ES)."""
|
||||
mock_request.env = self.env.with_context(lang="es_ES")
|
||||
|
|
@ -416,7 +647,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
|||
self.assertIsNotNone(message)
|
||||
# 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):
|
||||
"""Test confirmation message in Basque (eu_ES)."""
|
||||
mock_request.env = self.env.with_context(lang="eu_ES")
|
||||
|
|
@ -429,7 +660,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
|||
self.assertIsNotNone(message)
|
||||
# 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):
|
||||
"""Test confirmation message in Catalan (ca_ES)."""
|
||||
mock_request.env = self.env.with_context(lang="ca_ES")
|
||||
|
|
@ -442,7 +673,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
|||
self.assertIsNotNone(message)
|
||||
# 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):
|
||||
"""Test confirmation message in Galician (gl_ES)."""
|
||||
mock_request.env = self.env.with_context(lang="gl_ES")
|
||||
|
|
@ -455,7 +686,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
|||
self.assertIsNotNone(message)
|
||||
# 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):
|
||||
"""Test confirmation message in Portuguese (pt_PT)."""
|
||||
mock_request.env = self.env.with_context(lang="pt_PT")
|
||||
|
|
@ -468,7 +699,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
|||
self.assertIsNotNone(message)
|
||||
# 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):
|
||||
"""Test confirmation message in French (fr_FR)."""
|
||||
mock_request.env = self.env.with_context(lang="fr_FR")
|
||||
|
|
@ -481,7 +712,7 @@ class TestBuildConfirmationMessage(TransactionCase):
|
|||
self.assertIsNotNone(message)
|
||||
# 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):
|
||||
"""Test confirmation message in Italian (it_IT)."""
|
||||
mock_request.env = self.env.with_context(lang="it_IT")
|
||||
|
|
@ -500,9 +731,29 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.controller = http.request.env["website.sale"].browse([])
|
||||
self.user = self.env.ref("base.user_admin")
|
||||
self.partner = self.env.ref("base.partner_admin")
|
||||
self.controller = AplicoopWebsiteSale()
|
||||
self.consumer_group = self.env["res.partner"].create(
|
||||
{
|
||||
"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
|
||||
self.product = self.env["product.product"].create(
|
||||
|
|
@ -518,19 +769,20 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
|||
{
|
||||
"name": "Integration Test Order",
|
||||
"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_date": date.today() + timedelta(days=5),
|
||||
"home_delivery": True,
|
||||
}
|
||||
)
|
||||
|
||||
@patch("odoo.http.request")
|
||||
def test_confirm_eskaera_full_flow_pickup(self, mock_request):
|
||||
"""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()
|
||||
# Use the real website so pricing helpers can resolve company/fiscal pos
|
||||
self.website = self.env["website"].search([], limit=1)
|
||||
|
||||
# Prepare request data
|
||||
def test_confirm_eskaera_full_flow_pickup(self):
|
||||
"""Test full confirm_eskaera flow for pickup order."""
|
||||
data = {
|
||||
"order_id": self.group_order.id,
|
||||
"items": [
|
||||
|
|
@ -542,11 +794,14 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
|||
],
|
||||
"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")
|
||||
|
||||
# Call confirm_eskaera
|
||||
response = self.controller.confirm_eskaera()
|
||||
with patch(REQUEST_PATCH_TARGET, mock_request):
|
||||
response = self.controller.confirm_eskaera.__wrapped__(self.controller)
|
||||
|
||||
# Verify 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(len(sale_order.order_line), 1)
|
||||
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, mock_request):
|
||||
def test_confirm_eskaera_full_flow_delivery(self):
|
||||
"""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
|
||||
self.group_order.delivery_date = self.group_order.pickup_date + timedelta(
|
||||
days=1
|
||||
)
|
||||
|
||||
# Prepare request data
|
||||
data = {
|
||||
"order_id": self.group_order.id,
|
||||
"items": [
|
||||
|
|
@ -590,11 +844,14 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
|||
],
|
||||
"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")
|
||||
|
||||
# Call confirm_eskaera
|
||||
response = self.controller.confirm_eskaera()
|
||||
with patch(REQUEST_PATCH_TARGET, mock_request):
|
||||
response = self.controller.confirm_eskaera.__wrapped__(self.controller)
|
||||
|
||||
# Verify response
|
||||
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
|
||||
)
|
||||
|
||||
@patch("odoo.http.request")
|
||||
def test_confirm_eskaera_updates_existing_draft(self, mock_request):
|
||||
def test_confirm_eskaera_updates_existing_draft(self):
|
||||
"""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
|
||||
existing_order = self.env["sale.order"].create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"group_order_id": self.group_order.id,
|
||||
"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": [
|
||||
(
|
||||
0,
|
||||
|
|
@ -640,7 +895,6 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
|||
|
||||
existing_order_id = existing_order.id
|
||||
|
||||
# Prepare new request data
|
||||
data = {
|
||||
"order_id": self.group_order.id,
|
||||
"items": [
|
||||
|
|
@ -652,11 +906,14 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
|||
],
|
||||
"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")
|
||||
|
||||
# Call confirm_eskaera
|
||||
response = self.controller.confirm_eskaera()
|
||||
with patch(REQUEST_PATCH_TARGET, mock_request):
|
||||
response = self.controller.confirm_eskaera.__wrapped__(self.controller)
|
||||
|
||||
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(existing_order.order_line[0].product_uom_qty, 5)
|
||||
|
||||
@patch("odoo.http.request")
|
||||
def test_confirm_eskaera_ignores_old_period_draft(self, mock_request):
|
||||
def test_confirm_eskaera_ignores_old_period_draft(self):
|
||||
"""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(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
|
|
@ -706,10 +958,15 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
|||
],
|
||||
"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"))
|
||||
|
||||
self.assertTrue(response_data.get("success"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue