diff --git a/mrp_byproduct_auto_pick/README.rst b/mrp_byproduct_auto_pick/README.rst new file mode 100644 index 0000000..10810a6 --- /dev/null +++ b/mrp_byproduct_auto_pick/README.rst @@ -0,0 +1,115 @@ +======================= +MRP Byproduct Auto Pick +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:dfcc3449be75e3410a25b67d6cc2215a9fddde1ac736df990144a7b55a651ca7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/18.0/mrp_byproduct_auto_pick + :alt: OCA/manufacture +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/manufacture-18-0/manufacture-18-0-mrp_byproduct_auto_pick + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/manufacture&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +In standard Odoo, the quantity of a byproduct on a manufacturing order +is recomputed from the producing quantity whenever it is (re)evaluated, +including when *Produce All* fills the producing quantity automatically. +As a result, a quantity that an operator entered manually on a byproduct +line is silently reset to the quantity to produce, unless the line +happens to be marked as picked. + +This module adds an *Auto-pick Manually Edited Byproducts* option to +manufacturing orders (defaulted from a company-wide setting) to keep the +manually entered byproduct quantity: when an operator edits the quantity +of a byproduct line on an enabled manufacturing order, the line is +automatically marked as picked so its value is preserved through +*Produce All*. + +Byproduct lines that are not edited keep the standard behavior and still +scale with the producing quantity. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +1. **Company default** -- Go to *Manufacturing > Configuration > + Settings* and tick *Auto-pick Manually Edited Byproducts*. New + manufacturing orders will inherit this value. + +2. **Per manufacturing order** -- The *Auto-pick Manually Edited + Byproducts* checkbox on a manufacturing order can be toggled + individually, overriding the company default for that order. + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Quartile + +Contributors +------------ + +- `Quartile `__: + + - Aung Ko Ko Lin + +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-kanda999| image:: https://github.com/kanda999.png?size=40px + :target: https://github.com/kanda999 + :alt: kanda999 +.. |maintainer-aungkokolin1997| image:: https://github.com/aungkokolin1997.png?size=40px + :target: https://github.com/aungkokolin1997 + :alt: aungkokolin1997 + +Current `maintainers `__: + +|maintainer-kanda999| |maintainer-aungkokolin1997| + +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_byproduct_auto_pick/__init__.py b/mrp_byproduct_auto_pick/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/mrp_byproduct_auto_pick/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mrp_byproduct_auto_pick/__manifest__.py b/mrp_byproduct_auto_pick/__manifest__.py new file mode 100644 index 0000000..3df14ef --- /dev/null +++ b/mrp_byproduct_auto_pick/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "MRP Byproduct Auto Pick", + "summary": "Keep manually entered byproduct quantities instead of " + "resetting them to the quantity to produce", + "version": "18.0.1.0.0", + "category": "Manufacturing", + "license": "AGPL-3", + "author": "Quartile, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/manufacture", + "depends": ["mrp"], + "data": [ + "views/mrp_production_views.xml", + "views/res_config_settings_views.xml", + ], + "maintainers": ["kanda999", "aungkokolin1997"], + "installable": True, +} diff --git a/mrp_byproduct_auto_pick/models/__init__.py b/mrp_byproduct_auto_pick/models/__init__.py new file mode 100644 index 0000000..ab1f55c --- /dev/null +++ b/mrp_byproduct_auto_pick/models/__init__.py @@ -0,0 +1,4 @@ +from . import mrp_production +from . import res_company +from . import res_config_settings +from . import stock_move diff --git a/mrp_byproduct_auto_pick/models/mrp_production.py b/mrp_byproduct_auto_pick/models/mrp_production.py new file mode 100644 index 0000000..a1a0ac4 --- /dev/null +++ b/mrp_byproduct_auto_pick/models/mrp_production.py @@ -0,0 +1,22 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class MrpProduction(models.Model): + _inherit = "mrp.production" + + byproduct_auto_pick = fields.Boolean( + string="Auto-pick Manually Edited Byproducts", + compute="_compute_byproduct_auto_pick", + store=True, + readonly=False, + help="Keep manually entered byproduct quantities on manufacturing " + "orders instead of resetting them to the quantity to produce.", + ) + + @api.depends("company_id") + def _compute_byproduct_auto_pick(self): + for production in self: + production.byproduct_auto_pick = production.company_id.byproduct_auto_pick diff --git a/mrp_byproduct_auto_pick/models/res_company.py b/mrp_byproduct_auto_pick/models/res_company.py new file mode 100644 index 0000000..7475154 --- /dev/null +++ b/mrp_byproduct_auto_pick/models/res_company.py @@ -0,0 +1,10 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + byproduct_auto_pick = fields.Boolean(string="Auto-pick Manually Edited Byproducts") diff --git a/mrp_byproduct_auto_pick/models/res_config_settings.py b/mrp_byproduct_auto_pick/models/res_config_settings.py new file mode 100644 index 0000000..d9d74d9 --- /dev/null +++ b/mrp_byproduct_auto_pick/models/res_config_settings.py @@ -0,0 +1,13 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + byproduct_auto_pick = fields.Boolean( + related="company_id.byproduct_auto_pick", + readonly=False, + ) diff --git a/mrp_byproduct_auto_pick/models/stock_move.py b/mrp_byproduct_auto_pick/models/stock_move.py new file mode 100644 index 0000000..db4c652 --- /dev/null +++ b/mrp_byproduct_auto_pick/models/stock_move.py @@ -0,0 +1,41 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models +from odoo.tools import float_compare + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _is_auto_pick_byproduct(self): + self.ensure_one() + production = self.production_id + return bool(production) and self.product_id != production.product_id + + def _should_auto_pick_byproduct(self): + self.ensure_one() + return self.production_id.byproduct_auto_pick + + def write(self, vals): + # The MO recompute writes byproduct quantities through + # `_set_quantity_done` under the `auto_conso` context; we must not + # pick those. + if self.env.context.get("auto_conso"): + return super().write(vals) + candidates = self.filtered( + lambda move: not move.picked + and move._is_auto_pick_byproduct() + and move._should_auto_pick_byproduct() + ) + quantity_before = {move.id: move.quantity for move in candidates} + res = super().write(vals) + candidates.filtered( + lambda move: float_compare( + move.quantity, + quantity_before[move.id], + precision_rounding=move.product_uom.rounding, + ) + != 0 + ).picked = True + return res diff --git a/mrp_byproduct_auto_pick/pyproject.toml b/mrp_byproduct_auto_pick/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/mrp_byproduct_auto_pick/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/mrp_byproduct_auto_pick/readme/CONFIGURE.md b/mrp_byproduct_auto_pick/readme/CONFIGURE.md new file mode 100644 index 0000000..bc2e443 --- /dev/null +++ b/mrp_byproduct_auto_pick/readme/CONFIGURE.md @@ -0,0 +1,7 @@ +1. **Company default** -- Go to *Manufacturing \> Configuration \> Settings* + and tick *Auto-pick Manually Edited Byproducts*. New manufacturing orders + will inherit this value. + +2. **Per manufacturing order** -- The *Auto-pick Manually Edited Byproducts* + checkbox on a manufacturing order can be toggled individually, overriding + the company default for that order. diff --git a/mrp_byproduct_auto_pick/readme/CONTRIBUTORS.md b/mrp_byproduct_auto_pick/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..faae328 --- /dev/null +++ b/mrp_byproduct_auto_pick/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Quartile](https://www.quartile.co): + - Aung Ko Ko Lin diff --git a/mrp_byproduct_auto_pick/readme/DESCRIPTION.md b/mrp_byproduct_auto_pick/readme/DESCRIPTION.md new file mode 100644 index 0000000..e25bc6b --- /dev/null +++ b/mrp_byproduct_auto_pick/readme/DESCRIPTION.md @@ -0,0 +1,15 @@ +In standard Odoo, the quantity of a byproduct on a manufacturing order is +recomputed from the producing quantity whenever it is (re)evaluated, including +when *Produce All* fills the producing quantity automatically. As a result, a +quantity that an operator entered manually on a byproduct line is silently +reset to the quantity to produce, unless the line happens to be marked as +picked. + +This module adds an *Auto-pick Manually Edited Byproducts* option to +manufacturing orders (defaulted from a company-wide setting) to keep the +manually entered byproduct quantity: when an operator edits the quantity of a +byproduct line on an enabled manufacturing order, the line is automatically +marked as picked so its value is preserved through *Produce All*. + +Byproduct lines that are not edited keep the standard behavior and still scale +with the producing quantity. diff --git a/mrp_byproduct_auto_pick/static/description/index.html b/mrp_byproduct_auto_pick/static/description/index.html new file mode 100644 index 0000000..74ffd52 --- /dev/null +++ b/mrp_byproduct_auto_pick/static/description/index.html @@ -0,0 +1,453 @@ + + + + + +MRP Byproduct Auto Pick + + + +
+

MRP Byproduct Auto Pick

+ + +

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

+

In standard Odoo, the quantity of a byproduct on a manufacturing order +is recomputed from the producing quantity whenever it is (re)evaluated, +including when Produce All fills the producing quantity automatically. +As a result, a quantity that an operator entered manually on a byproduct +line is silently reset to the quantity to produce, unless the line +happens to be marked as picked.

+

This module adds an Auto-pick Manually Edited Byproducts option to +manufacturing orders (defaulted from a company-wide setting) to keep the +manually entered byproduct quantity: when an operator edits the quantity +of a byproduct line on an enabled manufacturing order, the line is +automatically marked as picked so its value is preserved through +Produce All.

+

Byproduct lines that are not edited keep the standard behavior and still +scale with the producing quantity.

+

Table of contents

+ +
+

Configuration

+
    +
  1. Company default – Go to Manufacturing > Configuration > +Settings and tick Auto-pick Manually Edited Byproducts. New +manufacturing orders will inherit this value.
  2. +
  3. Per manufacturing order – The Auto-pick Manually Edited +Byproducts checkbox on a manufacturing order can be toggled +individually, overriding the company default for that order.
  4. +
+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Quartile
  • +
+
+
+

Contributors

+ +
+
+

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 maintainers:

+

kanda999 aungkokolin1997

+

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_byproduct_auto_pick/tests/__init__.py b/mrp_byproduct_auto_pick/tests/__init__.py new file mode 100644 index 0000000..e7057f7 --- /dev/null +++ b/mrp_byproduct_auto_pick/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mrp_byproduct_auto_pick diff --git a/mrp_byproduct_auto_pick/tests/test_mrp_byproduct_auto_pick.py b/mrp_byproduct_auto_pick/tests/test_mrp_byproduct_auto_pick.py new file mode 100644 index 0000000..a993f82 --- /dev/null +++ b/mrp_byproduct_auto_pick/tests/test_mrp_byproduct_auto_pick.py @@ -0,0 +1,142 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.tests import Form, TransactionCase + + +class TestMrpByproductAutoPick(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + manufacture_route = cls.env.ref("mrp.route_warehouse0_manufacture") + cls.finished = cls.env["product.product"].create( + { + "name": "Finished", + "type": "consu", + "is_storable": True, + "route_ids": [Command.set(manufacture_route.ids)], + } + ) + cls.component = cls.env["product.product"].create( + {"name": "Component", "type": "consu"} + ) + cls.byproduct = cls.env["product.product"].create( + {"name": "Byproduct", "type": "consu", "is_storable": True} + ) + # BoM: 1 finished = 1 component (+) 2 byproduct + cls.bom = cls.env["mrp.bom"].create( + { + "product_tmpl_id": cls.finished.product_tmpl_id.id, + "product_qty": 1.0, + "type": "normal", + "bom_line_ids": [ + Command.create({"product_id": cls.component.id, "product_qty": 1.0}) + ], + "byproduct_ids": [ + Command.create({"product_id": cls.byproduct.id, "product_qty": 2.0}) + ], + } + ) + + def _confirm_mo(self, byproduct_auto_pick=True): + mo_form = Form(self.env["mrp.production"]) + mo_form.product_id = self.finished + mo_form.bom_id = self.bom + mo_form.product_qty = 1.0 + mo = mo_form.save() + mo.byproduct_auto_pick = byproduct_auto_pick + mo.action_confirm() + return mo + + def _recompute(self, mo): + """Reproduce the recompute that 'Produce All' triggers.""" + mo.qty_producing = 1.0 + mo._set_qty_producing(False) + + def test_enabled_preserves_manual_quantity(self): + mo = self._confirm_mo(byproduct_auto_pick=True) + move = mo.move_byproduct_ids + self.assertEqual(len(move), 1) + move.quantity = 5.0 + # Editing the byproduct quantity auto-marks the line as picked. + self.assertTrue(move.picked) + self._recompute(mo) + # The manual value survives the recompute. + self.assertEqual(move.quantity, 5.0) + + def test_disabled_reverts_manual_quantity(self): + mo = self._confirm_mo(byproduct_auto_pick=False) + move = mo.move_byproduct_ids + move.quantity = 5.0 + self.assertFalse(move.picked) + self._recompute(mo) + # Standard behavior: reset to the quantity to produce (2 per unit). + self.assertEqual(move.quantity, 2.0) + + def test_tracked_byproduct_pick_on_line_edit(self): + # For tracked byproducts the quantity field is read-only; the operator + # enters quantities through the move lines (lot numbers) instead, which + # the form sends as move_line_ids commands on the move. + self.byproduct.write({"tracking": "lot"}) + mo = self._confirm_mo(byproduct_auto_pick=True) + move = mo.move_byproduct_ids + self.assertFalse(move.picked) + line = move.move_line_ids[:1] + if line: + move.write( + { + "move_line_ids": [ + Command.update( + line.id, {"quantity": 5.0, "lot_name": "BP-LOT-1"} + ) + ] + } + ) + else: + move.write( + { + "move_line_ids": [ + Command.create( + { + "product_id": self.byproduct.id, + "quantity": 5.0, + "lot_name": "BP-LOT-1", + "location_id": move.location_id.id, + "location_dest_id": move.location_dest_id.id, + } + ) + ] + } + ) + # Editing the lot line auto-marks the byproduct as picked. + self.assertTrue(move.picked) + self._recompute(mo) + # The manually entered quantity survives the recompute. + self.assertEqual(move.quantity, 5.0) + + def test_company_default_propagated(self): + self.env.company.byproduct_auto_pick = True + mo_form = Form(self.env["mrp.production"]) + mo_form.product_id = self.finished + mo_form.bom_id = self.bom + mo_form.product_qty = 1.0 + mo = mo_form.save() + # The computed field should pick up the company value. + self.assertTrue(mo.byproduct_auto_pick) + mo.action_confirm() + move = mo.move_byproduct_ids + move.quantity = 5.0 + self.assertTrue(move.picked) + self._recompute(mo) + self.assertEqual(move.quantity, 5.0) + + def test_company_default_off(self): + self.env.company.byproduct_auto_pick = False + mo_form = Form(self.env["mrp.production"]) + mo_form.product_id = self.finished + mo_form.bom_id = self.bom + mo_form.product_qty = 1.0 + mo = mo_form.save() + self.assertFalse(mo.byproduct_auto_pick) diff --git a/mrp_byproduct_auto_pick/views/mrp_production_views.xml b/mrp_byproduct_auto_pick/views/mrp_production_views.xml new file mode 100644 index 0000000..ad5b539 --- /dev/null +++ b/mrp_byproduct_auto_pick/views/mrp_production_views.xml @@ -0,0 +1,18 @@ + + + + mrp.production.form.byproduct.auto.pick + mrp.production + + + + + + + + + + diff --git a/mrp_byproduct_auto_pick/views/res_config_settings_views.xml b/mrp_byproduct_auto_pick/views/res_config_settings_views.xml new file mode 100644 index 0000000..4376acb --- /dev/null +++ b/mrp_byproduct_auto_pick/views/res_config_settings_views.xml @@ -0,0 +1,19 @@ + + + + res.config.settings.form.byproduct.auto.pick + res.config.settings + + + + + + + + + +