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..47c076dd6 --- /dev/null +++ b/mrp_production_auto_validate/models/__init__.py @@ -0,0 +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_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..7e46ae83b --- /dev/null +++ b/mrp_production_auto_validate/models/mrp_production.py @@ -0,0 +1,192 @@ +# 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", + compute="_compute_auto_validate", + store=True, + states={"draft": [("readonly", False)]}, + ) + + @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, + 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.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() + 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() + + @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_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/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/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..1a59dc126 --- /dev/null +++ b/mrp_production_auto_validate/tests/test_auto_validate.py @@ -0,0 +1,197 @@ +# 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 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): + 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) + + 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)) 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, +)