From 18fd73ed1b2e51d957fcabd921aec838533d1ee7 Mon Sep 17 00:00:00 2001 From: snt Date: Wed, 8 Apr 2026 18:48:18 +0200 Subject: [PATCH] =?UTF-8?q?[IMP]=20stock=5Fpicking=5Fbatch=5Fcustom:=20con?= =?UTF-8?q?figuraciones=20de=20bloqueo=20por=20pesta=C3=B1a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- stock_picking_batch_custom/__manifest__.py | 1 + stock_picking_batch_custom/models/__init__.py | 2 + .../models/res_company.py | 37 ++++++++ .../models/res_config_settings.py | 27 ++++++ .../models/stock_picking_batch.py | 91 ++++++++++++++----- .../tests/test_batch_summary.py | 41 +++++++++ .../views/res_config_settings_views.xml | 37 ++++++++ 7 files changed, 211 insertions(+), 25 deletions(-) create mode 100644 stock_picking_batch_custom/models/res_company.py create mode 100644 stock_picking_batch_custom/models/res_config_settings.py create mode 100644 stock_picking_batch_custom/views/res_config_settings_views.xml diff --git a/stock_picking_batch_custom/__manifest__.py b/stock_picking_batch_custom/__manifest__.py index 27e80bd..1273545 100644 --- a/stock_picking_batch_custom/__manifest__.py +++ b/stock_picking_batch_custom/__manifest__.py @@ -14,6 +14,7 @@ ], "data": [ "security/ir.model.access.csv", + "views/res_config_settings_views.xml", "views/stock_move_line_views.xml", "views/stock_picking_batch_views.xml", ], diff --git a/stock_picking_batch_custom/models/__init__.py b/stock_picking_batch_custom/models/__init__.py index c566790..8be5195 100644 --- a/stock_picking_batch_custom/models/__init__.py +++ b/stock_picking_batch_custom/models/__init__.py @@ -1,3 +1,5 @@ +from . import res_company # noqa: F401 +from . import res_config_settings # noqa: F401 from . import stock_move_line # noqa: F401 from . import stock_backorder_confirmation # noqa: F401 from . import stock_picking # noqa: F401 diff --git a/stock_picking_batch_custom/models/res_company.py b/stock_picking_batch_custom/models/res_company.py new file mode 100644 index 0000000..9ad8ac4 --- /dev/null +++ b/stock_picking_batch_custom/models/res_company.py @@ -0,0 +1,37 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields +from odoo import models + + +class ResCompany(models.Model): + _inherit = "res.company" + + batch_summary_restriction_enabled = fields.Boolean( + string="Enforce Product Summary Restriction", + default=False, + ) + batch_summary_restriction_scope = fields.Selection( + selection=[ + ("processed", "Only processed products"), + ("all", "All summary products"), + ], + string="Product Summary Restriction Scope", + default="processed", + required=True, + ) + + batch_detailed_restriction_enabled = fields.Boolean( + string="Enforce Detailed Operations Restriction", + default=True, + ) + batch_detailed_restriction_scope = fields.Selection( + selection=[ + ("processed", "Only processed lines"), + ("all", "All detailed lines"), + ], + string="Detailed Operations Restriction Scope", + default="processed", + required=True, + ) diff --git a/stock_picking_batch_custom/models/res_config_settings.py b/stock_picking_batch_custom/models/res_config_settings.py new file mode 100644 index 0000000..53100fc --- /dev/null +++ b/stock_picking_batch_custom/models/res_config_settings.py @@ -0,0 +1,27 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields +from odoo import models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + batch_summary_restriction_enabled = fields.Boolean( + related="company_id.batch_summary_restriction_enabled", + readonly=False, + ) + batch_summary_restriction_scope = fields.Selection( + related="company_id.batch_summary_restriction_scope", + readonly=False, + ) + + batch_detailed_restriction_enabled = fields.Boolean( + related="company_id.batch_detailed_restriction_enabled", + readonly=False, + ) + batch_detailed_restriction_scope = fields.Selection( + related="company_id.batch_detailed_restriction_scope", + readonly=False, + ) diff --git a/stock_picking_batch_custom/models/stock_picking_batch.py b/stock_picking_batch_custom/models/stock_picking_batch.py index 40f355f..a8c274b 100644 --- a/stock_picking_batch_custom/models/stock_picking_batch.py +++ b/stock_picking_batch_custom/models/stock_picking_batch.py @@ -140,15 +140,47 @@ class StockPickingBatch(models.Model): else: batch.summary_line_ids = [fields.Command.clear()] - def _check_all_products_collected(self, pickings=None): - """Validate only Detailed Operations collected flags. + def _raise_collected_restriction(self, base_message, product_names): + message = self.env._(base_message) + if product_names: + message += "\n" + self.env._( + "Pending products: %(products)s", products=product_names + ) + raise UserError(message) - Product Summary checkboxes are informative and must not block the flow. - The blocking validation applies to stock.move.line.is_collected in the - detailed operations tab for lines with processed quantity. - """ + def _get_not_collected_summary_lines(self, batch_pickings, scope): + summary_lines = self.summary_line_ids + if scope == "processed": + processed_product_ids = set( + batch_pickings.move_line_ids.filtered( + lambda line: ( + line.move_id.state not in ("cancel", "done") + and line.product_id + and line.move_id.quantity > 0 + ) + ).mapped("product_id.id") + ) + summary_lines = summary_lines.filtered( + lambda line: line.product_id.id in processed_product_ids + ) + return summary_lines.filtered(lambda line: not line.is_collected) + + def _get_not_collected_detailed_lines(self, batch_pickings, scope): + detailed_lines = batch_pickings.move_line_ids.filtered( + lambda line: line.move_id.state not in ("cancel", "done") + and line.product_id + ) + if scope == "processed": + detailed_lines = detailed_lines.filtered( + lambda line: line.move_id.quantity > 0 + ) + return detailed_lines.filtered(lambda line: not line.is_collected) + + def _check_all_products_collected(self, pickings=None): + """Validate collected restrictions based on tab configuration.""" for batch in self: + company = batch.company_id or self.env.company batch_pickings = ( pickings.filtered(lambda p, batch=batch: p.batch_id == batch) if pickings @@ -157,28 +189,37 @@ class StockPickingBatch(models.Model): if not batch_pickings: continue - not_collected_move_lines = batch_pickings.move_line_ids.filtered( - lambda line: ( - line.move_id.state not in ("cancel", "done") - and line.product_id - and line.move_id.quantity > 0 - and not line.is_collected + if company.batch_detailed_restriction_enabled: + not_collected_detailed = batch._get_not_collected_detailed_lines( + batch_pickings, company.batch_detailed_restriction_scope ) - ) - if not not_collected_move_lines: - continue + if not_collected_detailed: + product_names = ", ".join( + sorted( + set( + not_collected_detailed.mapped("product_id.display_name") + ) + ) + ) + batch._raise_collected_restriction( + "You must mark detailed operation lines as collected before validating the batch.", + product_names, + ) - product_names = ", ".join( - sorted(set(not_collected_move_lines.mapped("product_id.display_name"))) - ) - message = batch.env._( - "You must mark detailed operation lines as collected before validating the batch." - ) - if product_names: - message += "\n" + batch.env._( - "Pending products: %(products)s", products=product_names + if company.batch_summary_restriction_enabled: + not_collected_summary = batch._get_not_collected_summary_lines( + batch_pickings, company.batch_summary_restriction_scope ) - raise UserError(message) + if not_collected_summary: + product_names = ", ".join( + sorted( + set(not_collected_summary.mapped("product_id.display_name")) + ) + ) + batch._raise_collected_restriction( + "You must mark product summary lines as collected before validating the batch.", + product_names, + ) class StockPickingBatchSummaryLine(models.Model): diff --git a/stock_picking_batch_custom/tests/test_batch_summary.py b/stock_picking_batch_custom/tests/test_batch_summary.py index d08e40d..ad740f8 100644 --- a/stock_picking_batch_custom/tests/test_batch_summary.py +++ b/stock_picking_batch_custom/tests/test_batch_summary.py @@ -176,6 +176,12 @@ class TestBatchSummary(TransactionCase): batch._compute_summary_line_ids() return batch + def _set_batch_restriction_config(self, **vals): + company = self.env.company + previous = {key: company[key] for key in vals} + company.write(vals) + self.addCleanup(lambda: company.write(previous)) + # Tests def test_totals_and_pending_with_conversion(self): """Totals aggregate per product with UoM conversion and pending.""" @@ -421,3 +427,38 @@ class TestBatchSummary(TransactionCase): # Should not raise batch._check_all_products_collected() + + def test_summary_restriction_blocks_when_enabled(self): + """Product Summary restriction can be enabled independently.""" + + self._set_batch_restriction_config( + batch_summary_restriction_enabled=True, + batch_summary_restriction_scope="processed", + batch_detailed_restriction_enabled=False, + ) + + batch = self._create_batch_with_pickings() + batch.action_confirm() + batch._compute_summary_line_ids() + + # Detailed lines are collected, but Product Summary stays unchecked + batch.move_line_ids.filtered(lambda line: line.quantity > 0).write( + {"is_collected": True} + ) + + with self.assertRaisesRegex(UserError, "product summary"): + batch.action_done() + + def test_detailed_scope_all_blocks_unprocessed_lines(self): + """Detailed scope 'all' must include non-processed detailed lines.""" + + self._set_batch_restriction_config( + batch_summary_restriction_enabled=False, + batch_detailed_restriction_enabled=True, + batch_detailed_restriction_scope="all", + ) + + batch = self._create_partial_batch(extra_move=True) + + with self.assertRaisesRegex(UserError, self.product_2.display_name): + batch._check_all_products_collected(batch.picking_ids) diff --git a/stock_picking_batch_custom/views/res_config_settings_views.xml b/stock_picking_batch_custom/views/res_config_settings_views.xml new file mode 100644 index 0000000..c8b37c5 --- /dev/null +++ b/stock_picking_batch_custom/views/res_config_settings_views.xml @@ -0,0 +1,37 @@ + + + + res.config.settings.view.form.stock.picking.batch.custom + res.config.settings + + + + + +
+
+
+
+
+ Configuración de restricciones para la pestaña Product Summary. +
+
+ + + +
+
+
+
+
+ Configuración de restricciones para la pestaña Operaciones Detalladas. +
+
+
+
+
+