From 76be60d17b34e86725f803db8870b84cb68b5ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Tue, 29 Nov 2022 13:15:50 +0100 Subject: [PATCH 1/3] [ADD] mrp_production_auto_validate --- mrp_production_auto_validate/README.rst | 107 ++++ mrp_production_auto_validate/__init__.py | 2 + mrp_production_auto_validate/__manifest__.py | 19 + .../models/__init__.py | 3 + .../models/mrp_bom.py | 38 ++ .../models/mrp_production.py | 86 ++++ .../models/stock_picking.py | 39 ++ .../readme/CONFIGURE.rst | 13 + .../readme/CONTRIBUTORS.rst | 2 + .../readme/DESCRIPTION.rst | 2 + .../readme/ROADMAP.rst | 3 + .../static/description/index.html | 456 ++++++++++++++++++ .../tests/__init__.py | 1 + .../tests/test_auto_validate.py | 141 ++++++ .../views/mrp_bom.xml | 28 ++ .../views/mrp_production.xml | 17 + .../wizards/__init__.py | 1 + .../wizards/mrp_production_backorder.py | 14 + .../odoo/addons/mrp_production_auto_validate | 1 + setup/mrp_production_auto_validate/setup.py | 6 + 20 files changed, 979 insertions(+) create mode 100644 mrp_production_auto_validate/README.rst create mode 100644 mrp_production_auto_validate/__init__.py create mode 100644 mrp_production_auto_validate/__manifest__.py create mode 100644 mrp_production_auto_validate/models/__init__.py create mode 100644 mrp_production_auto_validate/models/mrp_bom.py create mode 100644 mrp_production_auto_validate/models/mrp_production.py create mode 100644 mrp_production_auto_validate/models/stock_picking.py create mode 100644 mrp_production_auto_validate/readme/CONFIGURE.rst create mode 100644 mrp_production_auto_validate/readme/CONTRIBUTORS.rst create mode 100644 mrp_production_auto_validate/readme/DESCRIPTION.rst create mode 100644 mrp_production_auto_validate/readme/ROADMAP.rst create mode 100644 mrp_production_auto_validate/static/description/index.html create mode 100644 mrp_production_auto_validate/tests/__init__.py create mode 100644 mrp_production_auto_validate/tests/test_auto_validate.py create mode 100644 mrp_production_auto_validate/views/mrp_bom.xml create mode 100644 mrp_production_auto_validate/views/mrp_production.xml create mode 100644 mrp_production_auto_validate/wizards/__init__.py create mode 100644 mrp_production_auto_validate/wizards/mrp_production_backorder.py create mode 120000 setup/mrp_production_auto_validate/odoo/addons/mrp_production_auto_validate create mode 100644 setup/mrp_production_auto_validate/setup.py diff --git a/mrp_production_auto_validate/README.rst b/mrp_production_auto_validate/README.rst new file mode 100644 index 000000000..68ef8295a --- /dev/null +++ b/mrp_production_auto_validate/README.rst @@ -0,0 +1,107 @@ +================================= +Manufacturing Order Auto-Validate +================================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github + :target: https://github.com/OCA/manufacture/tree/14.0/mrp_production_auto_validate + :alt: OCA/manufacture +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/manufacture-14-0/manufacture-14-0-mrp_production_auto_validate + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/129/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Auto-validate manufacturing orders when the "Pick components" transfer operation +is validated. This feature has to be enabled for each Bill of Material to produce. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Warehouse +~~~~~~~~~ + +* Go to *Inventory > Configuration > Settings* to enable the *Multi-Step Routes* option +* Go to *Inventory > Configuration > Warehouses* and enable the manufacturing + with two or three steps on your warehouse (to get a *Pick components* transfer + operation generated when you create a MO). + +Bill of Materials +~~~~~~~~~~~~~~~~~ + +* Go to *Manufacturing > Products > Bills of Materials* and enable the auto-validation + of manufacturing orders for your BoMs (*Order Auto Validation* field). + +Known issues / Roadmap +====================== + +* Add support of auto-validation as soon as we have enough components to produce + at least one finished product. Currently Odoo doesn't support this + (`reservation_state` of MO is still set to "Waiting" in such case). + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Sébastien Alix +* Simone Orsi + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-sebalix| image:: https://github.com/sebalix.png?size=40px + :target: https://github.com/sebalix + :alt: sebalix + +Current `maintainer `__: + +|maintainer-sebalix| + +This module is part of the `OCA/manufacture `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mrp_production_auto_validate/__init__.py b/mrp_production_auto_validate/__init__.py new file mode 100644 index 000000000..aee8895e7 --- /dev/null +++ b/mrp_production_auto_validate/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/mrp_production_auto_validate/__manifest__.py b/mrp_production_auto_validate/__manifest__.py new file mode 100644 index 000000000..6e23d3211 --- /dev/null +++ b/mrp_production_auto_validate/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +{ + "name": "Manufacturing Order Auto-Validate", + "summary": "Manufacturing Order Auto-Validation when components are picked", + "version": "14.0.1.0.0", + "license": "AGPL-3", + "website": "https://github.com/OCA/manufacture", + "author": "Camptocamp, Odoo Community Association (OCA)", + "category": "Manufacturing", + "depends": ["mrp"], + "data": [ + "views/mrp_bom.xml", + "views/mrp_production.xml", + ], + "installable": True, + "development_status": "Beta", + "maintainers": ["sebalix"], +} diff --git a/mrp_production_auto_validate/models/__init__.py b/mrp_production_auto_validate/models/__init__.py new file mode 100644 index 000000000..15d4e50ea --- /dev/null +++ b/mrp_production_auto_validate/models/__init__.py @@ -0,0 +1,3 @@ +from . import mrp_bom +from . import mrp_production +from . import stock_picking diff --git a/mrp_production_auto_validate/models/mrp_bom.py b/mrp_production_auto_validate/models/mrp_bom.py new file mode 100644 index 000000000..e199cfb14 --- /dev/null +++ b/mrp_production_auto_validate/models/mrp_bom.py @@ -0,0 +1,38 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _, api, fields, models + + +class MrpBom(models.Model): + _inherit = "mrp.bom" + + mo_auto_validation = fields.Boolean( + string="Order Auto Validation", + help=( + "Validate automatically the manufacturing order " + "when the 'Pick Components' transfer is validated.\n" + "This behavior is available only if the warehouse is configured " + "with 2 or 3 steps." + ), + default=False, + ) + mo_auto_validation_warning = fields.Char( + string="Order Auto Validation (warning)", + compute="_compute_mo_auto_validation_warning", + ) + + @api.onchange("type") + def onchange_type_auto_validation(self): + if self.type != "normal": + self.mo_auto_validation = self.mo_auto_validation_warning = False + + @api.depends("mo_auto_validation") + def _compute_mo_auto_validation_warning(self): + for bom in self: + bom.mo_auto_validation_warning = False + if bom.mo_auto_validation: + bom.mo_auto_validation_warning = _( + "The Quantity To Produce of an order is now " + "restricted to the BoM Quantity." + ) diff --git a/mrp_production_auto_validate/models/mrp_production.py b/mrp_production_auto_validate/models/mrp_production.py new file mode 100644 index 000000000..6252b9b17 --- /dev/null +++ b/mrp_production_auto_validate/models/mrp_production.py @@ -0,0 +1,86 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging + +from odoo import _, api, exceptions, fields, models, tools + +_logger = logging.getLogger(__name__) + + +class MrpProduction(models.Model): + _inherit = "mrp.production" + + auto_validate = fields.Boolean( + string="Auto Validate", + default=False, + ) + + @api.constrains("bom_id", "auto_validate", "product_qty") + def check_bom_auto_validate(self): + for mo in self: + qty_ok = ( + tools.float_compare( + mo.product_qty, + mo.bom_id.product_qty, + precision_rounding=mo.product_uom_id.rounding, + ) + == 0 + ) + bypass_check = self.env.context.get("disable_check_mo_auto_validate") + if bypass_check: + return + if mo.bom_id and mo.auto_validate and not qty_ok: + raise exceptions.ValidationError( + _( + "The quantity to produce is restricted to {qty} " + "as the BoM is configured with the " + "'Order Auto Validation' option." + ).format(qty=mo.bom_id.product_qty) + ) + + @api.onchange("bom_id") + def _onchange_bom_id(self): + res = super()._onchange_bom_id() + self.auto_validate = self.bom_id.mo_auto_validation + return res + + def _auto_validate_after_picking(self): + self.ensure_one() + if self.state == "progress": + # If the MO is already in progress, we want to call the immediate + # wizard to handle lot/serial number automatically (if any). + action = self._action_generate_immediate_wizard() + self._handle_wiz_mrp_immediate_production(action) + res = self.button_mark_done() + if res is True: + return True + res = self.handle_mark_done_result(res) + # Each call might return a new wizard, loop until we satisfy all of them + while isinstance(res, dict): + res = self.handle_mark_done_result(res) + + def handle_mark_done_result(self, res): + if res["res_model"] == "mrp.production": + # MO has been processed and returned an action to open a backorder + return True + handler_name = "_handle_wiz_" + res["res_model"].replace(".", "_") + handler = getattr(self, handler_name, None) + if not handler: + _logger.warning("'%s' wizard is not supported", res["res_model"]) + return True + return handler(res) + + def _handle_wiz_mrp_immediate_production(self, action): + wiz_model = self.env[action["res_model"]].with_context( + **action.get("context", {}) + ) + wiz = wiz_model.create({}) + return wiz.process() + + def _handle_wiz_mrp_production_backorder(self, action): + wiz_model = self.env[action["res_model"]].with_context( + **action.get("context", {}) + ) + wiz = wiz_model.create({}) + return wiz.action_backorder() diff --git a/mrp_production_auto_validate/models/stock_picking.py b/mrp_production_auto_validate/models/stock_picking.py new file mode 100644 index 000000000..47c5b9c27 --- /dev/null +++ b/mrp_production_auto_validate/models/stock_picking.py @@ -0,0 +1,39 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + def _get_manufacturing_orders(self, states=None): + self.ensure_one() + if states is None: + states = ("confirmed", "progress") + return self.move_lines.move_dest_ids.raw_material_production_id.filtered( + lambda o: o.state in states + ) + + def _action_done(self): + res = super()._action_done() + for picking in self: + if picking.state != "done": + continue + orders = picking._get_manufacturing_orders() + if not orders: + continue + for order in orders: + # NOTE: use of 'reservation_state' doesn't allow to produce + # at least 1 finished product even if there is enough components, + # but Odoo expects to work this way. + if order.auto_validate and order.reservation_state == "assigned": + # 'stock.immediate.transfer' could set the 'skip_immediate' + # key to process the transfer. The same ctx key is used by + # MO validation methods, but they are not the same! + # Unset the key in such case. + # TODO add a test + if order.env.context.get("skip_immediate"): + order = order.with_context(skip_immediate=False) + order._auto_validate_after_picking() + return res diff --git a/mrp_production_auto_validate/readme/CONFIGURE.rst b/mrp_production_auto_validate/readme/CONFIGURE.rst new file mode 100644 index 000000000..4f150bf29 --- /dev/null +++ b/mrp_production_auto_validate/readme/CONFIGURE.rst @@ -0,0 +1,13 @@ +Warehouse +~~~~~~~~~ + +* Go to *Inventory > Configuration > Settings* to enable the *Multi-Step Routes* option +* Go to *Inventory > Configuration > Warehouses* and enable the manufacturing + with two or three steps on your warehouse (to get a *Pick components* transfer + operation generated when you create a MO). + +Bill of Materials +~~~~~~~~~~~~~~~~~ + +* Go to *Manufacturing > Products > Bills of Materials* and enable the auto-validation + of manufacturing orders for your BoMs (*Order Auto Validation* field). diff --git a/mrp_production_auto_validate/readme/CONTRIBUTORS.rst b/mrp_production_auto_validate/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..acb80670a --- /dev/null +++ b/mrp_production_auto_validate/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Sébastien Alix +* Simone Orsi diff --git a/mrp_production_auto_validate/readme/DESCRIPTION.rst b/mrp_production_auto_validate/readme/DESCRIPTION.rst new file mode 100644 index 000000000..996b9a0b3 --- /dev/null +++ b/mrp_production_auto_validate/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +Auto-validate manufacturing orders when the "Pick components" transfer operation +is validated. This feature has to be enabled for each Bill of Material to produce. diff --git a/mrp_production_auto_validate/readme/ROADMAP.rst b/mrp_production_auto_validate/readme/ROADMAP.rst new file mode 100644 index 000000000..3b10d6a05 --- /dev/null +++ b/mrp_production_auto_validate/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +* Add support of auto-validation as soon as we have enough components to produce + at least one finished product. Currently Odoo doesn't support this + (`reservation_state` of MO is still set to "Waiting" in such case). diff --git a/mrp_production_auto_validate/static/description/index.html b/mrp_production_auto_validate/static/description/index.html new file mode 100644 index 000000000..5fefd2707 --- /dev/null +++ b/mrp_production_auto_validate/static/description/index.html @@ -0,0 +1,456 @@ + + + + + + +Manufacturing Order Auto-Validate + + + +
+

Manufacturing Order Auto-Validate

+ + +

Beta License: AGPL-3 OCA/manufacture Translate me on Weblate Try me on Runbot

+

Auto-validate manufacturing orders when the “Pick components” transfer operation +is validated. This feature has to be enabled for each Bill of Material to produce.

+

Table of contents

+ +
+

Configuration

+
+

Warehouse

+
    +
  • Go to Inventory > Configuration > Settings to enable the Multi-Step Routes option
  • +
  • Go to Inventory > Configuration > Warehouses and enable the manufacturing +with two or three steps on your warehouse (to get a Pick components transfer +operation generated when you create a MO).
  • +
+
+
+

Bill of Materials

+
    +
  • Go to Manufacturing > Products > Bills of Materials and enable the auto-validation +of manufacturing orders for your BoMs (Order Auto Validation field).
  • +
+
+
+
+

Known issues / Roadmap

+
    +
  • Add support of auto-validation as soon as we have enough components to produce +at least one finished product. Currently Odoo doesn’t support this +(reservation_state of MO is still set to “Waiting” in such case).
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

sebalix

+

This module is part of the OCA/manufacture project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/mrp_production_auto_validate/tests/__init__.py b/mrp_production_auto_validate/tests/__init__.py new file mode 100644 index 000000000..4342c5e47 --- /dev/null +++ b/mrp_production_auto_validate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_auto_validate diff --git a/mrp_production_auto_validate/tests/test_auto_validate.py b/mrp_production_auto_validate/tests/test_auto_validate.py new file mode 100644 index 000000000..05a543acd --- /dev/null +++ b/mrp_production_auto_validate/tests/test_auto_validate.py @@ -0,0 +1,141 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.exceptions import ValidationError +from odoo.tests.common import Form, SavepointCase + + +class TestManufacturingOrderAutoValidate(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + # Configure the WH to manufacture in at least two steps to get a + # "pick components" transfer operation + cls.wh = cls.env.ref("stock.warehouse0") + cls.wh.manufacture_steps = "pbm" + # Configure the BoM to auto-validate manufacturing orders + # NOTE: to ease tests we take a BoM with only one component + cls.bom = cls.env.ref("mrp.mrp_bom_table_top") # Tracked by S/N + cls.bom.mo_auto_validation = True + + @classmethod + def _create_manufacturing_order(cls, bom, product_qty=1): + with Form(cls.env["mrp.production"]) as form: + form.bom_id = bom + form.product_qty = product_qty + order = form.save() + order.invalidate_cache() + return order + + @classmethod + def _validate_picking(cls, picking, moves=None): + """Validate a stock transfer. + + `moves` can be set with a list of tuples [(move, quantity_done)] to + process the transfer partially. + """ + if moves is None: + moves = [] + for move in picking.move_lines: + # Try to match a move to set a given qty + for move2, qty_done in moves: + if move == move2: + move.quantity_done = qty_done + break + else: + move.quantity_done = move.product_uom_qty + picking._action_done() + + def test_bom_alert(self): + self.assertIn( + "restricted to the BoM Quantity", self.bom.mo_auto_validation_warning + ) + + def test_get_manufacturing_orders_pbm(self): + """Get the MO from transfers in a 2 steps configuration.""" + # WH already configured as 'Pick components and then manufacture (2 steps)' + order = self._create_manufacturing_order(self.bom) + order.action_confirm() + picking_pick = order.picking_ids + self.assertEqual(picking_pick._get_manufacturing_orders(), order) + + def test_get_manufacturing_orders_pbm_sam(self): + """Get the MO from transfers in a 3 steps configuration.""" + self.wh.manufacture_steps = "pbm_sam" + order = self._create_manufacturing_order(self.bom) + order.action_confirm() + picking_pick = order.picking_ids.filtered( + lambda o: "Pick" in o.picking_type_id.name + ) + picking_store = order.picking_ids.filtered( + lambda o: "Store" in o.picking_type_id.name + ) + self.assertEqual(picking_pick._get_manufacturing_orders(), order) + self.assertFalse(picking_store._get_manufacturing_orders()) + + def test_create_order_too_much_qty_to_produce(self): + """Creation of MO for 2 finished product while BoM produces 1.""" + with self.assertRaisesRegex(ValidationError, r"is restricted to"): + self._create_manufacturing_order(self.bom, product_qty=2) + + def test_auto_validate_disabled(self): + """Auto-validation of MO disabled. No change in the standard behavior.""" + self.bom.mo_auto_validation = False + order = self._create_manufacturing_order(self.bom) + order.action_confirm() + self.assertEqual(order.state, "confirmed") + picking = order.picking_ids + picking.action_assign() + self.assertEqual(picking.state, "assigned") + self._validate_picking(picking) + self.assertEqual(picking.state, "done") + self.assertEqual(order.state, "confirmed") + + def test_auto_validate_one_qty_to_produce(self): + """Auto-validation of MO with components for 1 finished product.""" + order = self._create_manufacturing_order(self.bom) + order.action_confirm() + self.assertEqual(order.state, "confirmed") + picking = order.picking_ids + picking.action_assign() + self.assertEqual(picking.state, "assigned") + self._validate_picking(picking) + self.assertEqual(picking.state, "done") + self.assertEqual(order.state, "done") + self.assertTrue(order.lot_producing_id) + + def test_auto_validate_two_qty_to_produce(self): + """Auto-validation of MO with components for 2 finished product.""" + self.bom.product_qty = 2 + order = self._create_manufacturing_order(self.bom, product_qty=2) + order.action_confirm() + self.assertEqual(order.state, "confirmed") + picking = order.picking_ids + picking.action_assign() + self.assertEqual(picking.state, "assigned") + self._validate_picking(picking) + self.assertEqual(picking.state, "done") + self.assertEqual(order.state, "done") + self.assertTrue(order.lot_producing_id) + + def test_auto_validate_with_not_enough_components(self): + """MO not validated: not enough components to produce 1 finished product.""" + self.bom.product_qty = 2 + self.bom.bom_line_ids.product_qty = 4 + order = self._create_manufacturing_order(self.bom, product_qty=2) + order.action_confirm() + self.assertEqual(order.state, "confirmed") + # Process 'Pick components' with not enough qties to process at least + # 1 finished product (2 components required but only 1 transfered) + picking = order.picking_ids + picking.action_assign() + self.assertEqual(picking.state, "assigned") + self._validate_picking(picking, moves=[(picking.move_lines, 1)]) + self.assertEqual(picking.state, "done") + self.assertEqual(picking.backorder_ids.move_lines.product_uom_qty, 3) + # Check that no MO gets validated in the process + order_done = picking._get_manufacturing_orders(states=("done",)) + self.assertFalse(order_done) + self.assertEqual(order.state, "confirmed") + self.assertEqual(order.product_qty, 2) diff --git a/mrp_production_auto_validate/views/mrp_bom.xml b/mrp_production_auto_validate/views/mrp_bom.xml new file mode 100644 index 000000000..6575cdddc --- /dev/null +++ b/mrp_production_auto_validate/views/mrp_bom.xml @@ -0,0 +1,28 @@ + + + + + + mrp.bom.form.inherit + mrp.bom + + + + + + + + + + diff --git a/mrp_production_auto_validate/views/mrp_production.xml b/mrp_production_auto_validate/views/mrp_production.xml new file mode 100644 index 000000000..07217b491 --- /dev/null +++ b/mrp_production_auto_validate/views/mrp_production.xml @@ -0,0 +1,17 @@ + + + + + + mrp.production.form.inherit + mrp.production + + +
+ +
+
+
+ +
diff --git a/mrp_production_auto_validate/wizards/__init__.py b/mrp_production_auto_validate/wizards/__init__.py new file mode 100644 index 000000000..7a6fec6a9 --- /dev/null +++ b/mrp_production_auto_validate/wizards/__init__.py @@ -0,0 +1 @@ +from . import mrp_production_backorder diff --git a/mrp_production_auto_validate/wizards/mrp_production_backorder.py b/mrp_production_auto_validate/wizards/mrp_production_backorder.py new file mode 100644 index 000000000..984479f56 --- /dev/null +++ b/mrp_production_auto_validate/wizards/mrp_production_backorder.py @@ -0,0 +1,14 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import models + + +class MrpProductionBackorder(models.TransientModel): + _inherit = "mrp.production.backorder" + + def action_backorder(self): + # Bypass the 'auto_validate' constraint regarding qty to produce + # when creating a backorder. + self = self.with_context(disable_check_mo_auto_validate=True) + return super().action_backorder() diff --git a/setup/mrp_production_auto_validate/odoo/addons/mrp_production_auto_validate b/setup/mrp_production_auto_validate/odoo/addons/mrp_production_auto_validate new file mode 120000 index 000000000..2a19de960 --- /dev/null +++ b/setup/mrp_production_auto_validate/odoo/addons/mrp_production_auto_validate @@ -0,0 +1 @@ +../../../../mrp_production_auto_validate \ No newline at end of file diff --git a/setup/mrp_production_auto_validate/setup.py b/setup/mrp_production_auto_validate/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/mrp_production_auto_validate/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From bf7e47056ad737bb2245ecb550ffa0ec9ff7b715 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Tue, 10 Jan 2023 18:14:32 +0100 Subject: [PATCH 2/3] mrp_p_auto_validate: fix auto_validate flag Relying on onchange makes this value set only if the BOM is set manually on the Form view, what isn't the case when MO are created by running procurements. --- .../models/mrp_production.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/mrp_production_auto_validate/models/mrp_production.py b/mrp_production_auto_validate/models/mrp_production.py index 6252b9b17..51c0c843a 100644 --- a/mrp_production_auto_validate/models/mrp_production.py +++ b/mrp_production_auto_validate/models/mrp_production.py @@ -13,7 +13,9 @@ class MrpProduction(models.Model): auto_validate = fields.Boolean( string="Auto Validate", - default=False, + compute="_compute_auto_validate", + store=True, + states={"draft": [("readonly", False)]}, ) @api.constrains("bom_id", "auto_validate", "product_qty") @@ -39,11 +41,16 @@ class MrpProduction(models.Model): ).format(qty=mo.bom_id.product_qty) ) - @api.onchange("bom_id") - def _onchange_bom_id(self): - res = super()._onchange_bom_id() - self.auto_validate = self.bom_id.mo_auto_validation - return res + @api.depends("bom_id.mo_auto_validation", "state") + def _compute_auto_validate(self): + for prod in self: + if prod.state != "draft": + # Avoid recomputing the value once the MO is confirmed. + # e.g. if the value changes on the BOM but the MO was already confirmed, + # or if the user forces another value while the MO is in draft, + # we don't want to change the value after confirmation. + continue + prod.auto_validate = prod.bom_id.mo_auto_validation def _auto_validate_after_picking(self): self.ensure_one() From a8e4fa8ddb8b9ce6a8fcbdc3dac914632c1e7057 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Tue, 10 Jan 2023 18:16:09 +0100 Subject: [PATCH 3/3] mrp_p_auto_validate: split qty on create - split values for create in dedicated function - post messages on MO with modified qty - split MOs on create only on procurements --- .../models/__init__.py | 1 + .../models/mrp_production.py | 99 +++++++++++++++++++ .../models/stock_rule.py | 13 +++ .../tests/test_auto_validate.py | 56 +++++++++++ 4 files changed, 169 insertions(+) create mode 100644 mrp_production_auto_validate/models/stock_rule.py diff --git a/mrp_production_auto_validate/models/__init__.py b/mrp_production_auto_validate/models/__init__.py index 15d4e50ea..47c076dd6 100644 --- a/mrp_production_auto_validate/models/__init__.py +++ b/mrp_production_auto_validate/models/__init__.py @@ -1,3 +1,4 @@ from . import mrp_bom from . import mrp_production from . import stock_picking +from . import stock_rule diff --git a/mrp_production_auto_validate/models/mrp_production.py b/mrp_production_auto_validate/models/mrp_production.py index 51c0c843a..7e46ae83b 100644 --- a/mrp_production_auto_validate/models/mrp_production.py +++ b/mrp_production_auto_validate/models/mrp_production.py @@ -21,6 +21,7 @@ class MrpProduction(models.Model): @api.constrains("bom_id", "auto_validate", "product_qty") def check_bom_auto_validate(self): for mo in self: + # FIXME: Handle different UOM between BOM and MO qty_ok = ( tools.float_compare( mo.product_qty, @@ -91,3 +92,101 @@ class MrpProduction(models.Model): ) wiz = wiz_model.create({}) return wiz.action_backorder() + + @api.model_create_multi + def create(self, values_list): + new_values_list, messages_to_post = self.adapt_values_qty_for_auto_validation( + values_list + ) + res = super().create(new_values_list) + if messages_to_post: + for pos, msg in messages_to_post.items(): + prod = res[pos] + prod.message_post(body=msg) + return res + + @api.model + def adapt_values_qty_for_auto_validation(self, values_list): + """Adapt create values according to qty with auto validated BOM + + If MOs are to be created with a BOM having auto validation, we must ensure + the quantity of the MO is equal to the quantity of the BOM. + However when MOs are created through procurements, the requested quantity + is based on the procurement quantity, so we should either + * increase the quantity to match the BOM if procurement value is lower + * split the values to create one MO into multiple values to create multiple + MOs matching the BOM quantity if procurement value is bigger + """ + messages_to_post = {} + if not self.env.context.get("_split_create_values_for_auto_validation"): + return values_list, messages_to_post + new_values_list = [] + for values in values_list: + bom_id = values.get("bom_id") + if not bom_id: + new_values_list.append(values) + continue + bom = self.env["mrp.bom"].browse(bom_id) + if not bom.mo_auto_validation: + new_values_list.append(values) + continue + create_qty = values.get("product_qty") + create_uom = self.env["uom.uom"].browse(values.get("product_uom_id")) + bom_qty = bom.product_qty + bom_uom = bom.product_uom_id + if create_uom != bom_uom: + create_qty = create_uom._compute_quantity(create_qty, bom_uom) + if ( + tools.float_compare( + create_qty, bom_qty, precision_rounding=bom_uom.rounding + ) + == 0 + ): + new_values_list.append(values) + continue + elif ( + tools.float_compare( + create_qty, bom_qty, precision_rounding=bom_uom.rounding + ) + < 0 + ): + procure_qty = values.get("product_qty") + values["product_qty"] = bom_qty + values["product_uom_id"] = bom_uom.id + msg = _( + "Quantity in procurement (%s %s) was increased to %s %s due to auto " + "validation feature preventing to create an MO with a different " + "qty than defined on the BOM." + ) % ( + procure_qty, + create_uom.display_name, + bom_qty, + bom_uom.display_name, + ) + messages_to_post[len(new_values_list)] = msg + new_values_list.append(values) + continue + # If we get here we need to split the prepared MO values + # into multiple MO values to respect BOM qty + while ( + tools.float_compare(create_qty, 0, precision_rounding=bom_uom.rounding) + > 0 + ): + new_values = values.copy() + new_values["product_qty"] = bom_qty + new_values["product_uom_id"] = bom_uom.id + msg = _( + "Quantity in procurement (%s %s) was split to multiple production " + "orders of %s %s due to auto validation feature preventing to " + "set a quantity to produce different than the quantity defined " + "on the Bill of Materials." + ) % ( + values.get("product_qty"), + create_uom.display_name, + bom_qty, + bom_uom.display_name, + ) + messages_to_post[len(new_values_list)] = msg + new_values_list.append(new_values) + create_qty -= bom_qty + return new_values_list, messages_to_post diff --git a/mrp_production_auto_validate/models/stock_rule.py b/mrp_production_auto_validate/models/stock_rule.py new file mode 100644 index 000000000..b59044910 --- /dev/null +++ b/mrp_production_auto_validate/models/stock_rule.py @@ -0,0 +1,13 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import api, models + + +class StockRule(models.Model): + _inherit = "stock.rule" + + @api.model + def _run_manufacture(self, procurements): + return super( + StockRule, self.with_context(_split_create_values_for_auto_validation=True) + )._run_manufacture(procurements) diff --git a/mrp_production_auto_validate/tests/test_auto_validate.py b/mrp_production_auto_validate/tests/test_auto_validate.py index 05a543acd..1a59dc126 100644 --- a/mrp_production_auto_validate/tests/test_auto_validate.py +++ b/mrp_production_auto_validate/tests/test_auto_validate.py @@ -14,10 +14,33 @@ class TestManufacturingOrderAutoValidate(SavepointCase): # "pick components" transfer operation cls.wh = cls.env.ref("stock.warehouse0") cls.wh.manufacture_steps = "pbm" + # Configure the product to be replenished through manufacture route + cls.product_template = cls.env.ref( + "mrp.product_product_computer_desk_head_product_template" + ) + cls.manufacture_route = cls.env.ref("mrp.route_warehouse0_manufacture") + cls.product_template.route_ids = [(6, 0, [cls.manufacture_route.id])] # Configure the BoM to auto-validate manufacturing orders # NOTE: to ease tests we take a BoM with only one component cls.bom = cls.env.ref("mrp.mrp_bom_table_top") # Tracked by S/N cls.bom.mo_auto_validation = True + cls.uom_unit = cls.env.ref("uom.product_uom_unit") + + @classmethod + def _replenish_product(cls, product, product_qty=1, product_uom=None): + if product_uom is None: + product_uom = cls.uom_unit + wiz = ( + cls.env["product.replenish"] + .with_context(default_product_id=product.id) + .create( + { + "quantity": product_qty, + "product_uom_id": product_uom.id, + } + ) + ) + wiz.launch_replenishment() @classmethod def _create_manufacturing_order(cls, bom, product_qty=1): @@ -139,3 +162,36 @@ class TestManufacturingOrderAutoValidate(SavepointCase): self.assertFalse(order_done) self.assertEqual(order.state, "confirmed") self.assertEqual(order.product_qty, 2) + + def test_keep_qty_on_replenishment(self): + existing_mos = self.env["mrp.production"].search([]) + self._replenish_product(self.product_template.product_variant_id, product_qty=1) + created_mos = self.env["mrp.production"].search( + [("id", "not in", existing_mos.ids)] + ) + self.assertEqual(len(created_mos), 1) + self.assertEqual(created_mos.product_qty, self.bom.product_qty) + self.assertFalse(any("split" in m.body for m in created_mos.message_ids)) + self.assertFalse(any("increased" in m.body for m in created_mos.message_ids)) + + def test_split_qty_on_replenishment(self): + existing_mos = self.env["mrp.production"].search([]) + self._replenish_product(self.product_template.product_variant_id, product_qty=3) + created_mos = self.env["mrp.production"].search( + [("id", "not in", existing_mos.ids)] + ) + self.assertEqual(len(created_mos), 3) + for mo in created_mos: + self.assertEqual(mo.product_qty, self.bom.product_qty) + self.assertTrue(any("split" in m.body for m in mo.message_ids)) + + def test_raise_qty_on_replenishment(self): + existing_mos = self.env["mrp.production"].search([]) + self.bom.product_qty = 5 + self._replenish_product(self.product_template.product_variant_id, product_qty=3) + created_mos = self.env["mrp.production"].search( + [("id", "not in", existing_mos.ids)] + ) + self.assertEqual(len(created_mos), 1) + self.assertEqual(created_mos.product_qty, self.bom.product_qty) + self.assertTrue(any("increased" in m.body for m in created_mos.message_ids))