[FIX] TestConfirmEskaera_Integration: limpieza de decoradores @patch y corrección de bugs
- Convertir 4 tests de decorador @patch a context manager 'with patch(...)' para evitar RuntimeError en LocalProxy de Werkzeug - Corregir patrón env(user=..., context=dict(...)) en Odoo 18 (sin .with_context()) - Agregar website real al mock para integración con helpers de pricing (_get_pricing_info) - Añadir pickup_date en fixture de existing_order para que _find_recent_draft_order localice correctamente - BUGFIX: Agregar (5,) a order_line para limpiar líneas previas al actualizar pedido existente Resultado: 0 failed, 0 errors de 4 tests en Docker para TestConfirmEskaera_Integration BREAKING: _create_or_update_sale_order ahora limpia las líneas anteriores con (5,) antes de asignar las nuevas cuando se actualiza un pedido existente. Comportamiento previo (duplicación de líneas) era un bug.
This commit is contained in:
parent
e9809b90e9
commit
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="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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue