diff --git a/mrp_subcontracting_serial_mass_produce/README.rst b/mrp_subcontracting_serial_mass_produce/README.rst new file mode 100644 index 00000000..c2d07c75 --- /dev/null +++ b/mrp_subcontracting_serial_mass_produce/README.rst @@ -0,0 +1,7 @@ +===================================== +MRP Subcontracting Serial Mass Record +===================================== + +This module automatically records subcontracted components after using +``Mass Produce`` on a subcontracting manufacturing order with serial-tracked +finished products. diff --git a/mrp_subcontracting_serial_mass_produce/__init__.py b/mrp_subcontracting_serial_mass_produce/__init__.py new file mode 100644 index 00000000..60381a54 --- /dev/null +++ b/mrp_subcontracting_serial_mass_produce/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import wizard diff --git a/mrp_subcontracting_serial_mass_produce/__manifest__.py b/mrp_subcontracting_serial_mass_produce/__manifest__.py new file mode 100644 index 00000000..c5153195 --- /dev/null +++ b/mrp_subcontracting_serial_mass_produce/__manifest__.py @@ -0,0 +1,13 @@ +# Copyright 2026 Quartile +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "MRP Subcontracting Serial Mass Produce", + "version": "16.0.1.0.0", + "author": "Quartile, Odoo Community Association (OCA)", + "category": "Manufacturing", + "website": "https://github.com/OCA/manufacture", + "depends": ["mrp_subcontracting"], + "license": "AGPL-3", + "installable": True, +} diff --git a/mrp_subcontracting_serial_mass_produce/readme/CONTRIBUTORS.rst b/mrp_subcontracting_serial_mass_produce/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..161a0923 --- /dev/null +++ b/mrp_subcontracting_serial_mass_produce/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Quartile `_: + + * Toshikimi Shigenobu diff --git a/mrp_subcontracting_serial_mass_produce/readme/DESCRIPTION.rst b/mrp_subcontracting_serial_mass_produce/readme/DESCRIPTION.rst new file mode 100644 index 00000000..ed112fab --- /dev/null +++ b/mrp_subcontracting_serial_mass_produce/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module automatically records subcontracted components after using +``Mass Produce`` on a subcontracting manufacturing order with serial-tracked +finished products. diff --git a/mrp_subcontracting_serial_mass_produce/tests/__init__.py b/mrp_subcontracting_serial_mass_produce/tests/__init__.py new file mode 100644 index 00000000..e0276945 --- /dev/null +++ b/mrp_subcontracting_serial_mass_produce/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_mrp_subcontracting_serial_mass_produce diff --git a/mrp_subcontracting_serial_mass_produce/tests/test_mrp_subcontracting_serial_mass_produce.py b/mrp_subcontracting_serial_mass_produce/tests/test_mrp_subcontracting_serial_mass_produce.py new file mode 100644 index 00000000..95c48c6e --- /dev/null +++ b/mrp_subcontracting_serial_mass_produce/tests/test_mrp_subcontracting_serial_mass_produce.py @@ -0,0 +1,53 @@ +# Copyright 2026 Quartile Limited +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests import Form + +from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon + + +class TestMrpSubcontractingSerialMassProduce(TestMrpSubcontractingCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.finished.tracking = "serial" + cls.subcontract_location = ( + cls.subcontractor_partner1.property_stock_subcontractor + ) + picking_form = Form(cls.env["stock.picking"]) + picking_form.picking_type_id = cls.env.ref("stock.picking_type_in") + picking_form.partner_id = cls.subcontractor_partner1 + with picking_form.move_ids_without_package.new() as move: + move.product_id = cls.finished + move.product_uom_qty = 2 + cls.receipt = picking_form.save() + cls.receipt.action_confirm() + cls.env["stock.quant"]._update_available_quantity( + cls.comp1, cls.subcontract_location, 2 + ) + cls.env["stock.quant"]._update_available_quantity( + cls.comp2, cls.subcontract_location, 2 + ) + + def test_mrp_subcontracting_serial_mass_produce_01(self): + production = self.receipt.move_ids.move_orig_ids.production_id + production.action_assign() + action = production.action_serial_mass_produce_wizard() + wizard = Form(self.env["stock.assign.serial"].with_context(**action["context"])) + wizard.next_serial_number = "SN0001" + wizard.next_serial_count = 2 + action = wizard.save().generate_serial_numbers_production() + wizard = Form(self.env["stock.assign.serial"].browse(action["res_id"])) + wizard.save().apply() + productions = self.receipt.move_ids.move_orig_ids.production_id.sorted("id") + self.assertEqual(len(productions), 2) + self.assertEqual( + productions.mapped("subcontracting_has_been_recorded"), [True, True] + ) + self.assertEqual( + productions.mapped("lot_producing_id.name"), ["SN0001", "SN0002"] + ) + self.assertEqual( + sorted(self.receipt.move_line_ids.mapped("lot_id.name")), + ["SN0001", "SN0002"], + ) diff --git a/mrp_subcontracting_serial_mass_produce/wizard/__init__.py b/mrp_subcontracting_serial_mass_produce/wizard/__init__.py new file mode 100644 index 00000000..f7958e76 --- /dev/null +++ b/mrp_subcontracting_serial_mass_produce/wizard/__init__.py @@ -0,0 +1 @@ +from . import stock_assign_serial_numbers diff --git a/mrp_subcontracting_serial_mass_produce/wizard/stock_assign_serial_numbers.py b/mrp_subcontracting_serial_mass_produce/wizard/stock_assign_serial_numbers.py new file mode 100644 index 00000000..2d3b2f2f --- /dev/null +++ b/mrp_subcontracting_serial_mass_produce/wizard/stock_assign_serial_numbers.py @@ -0,0 +1,26 @@ +# Copyright 2026 Quartile +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class StockAssignSerialNumbers(models.TransientModel): + _inherit = "stock.assign.serial" + + def _assign_serial_numbers(self, cancel_remaining_quantity=False): + serial_numbers = set(self._get_serial_numbers()) + res = super()._assign_serial_numbers(cancel_remaining_quantity) + if not serial_numbers: + return res + productions = ( + self.production_id.procurement_group_id.mrp_production_ids.filtered( + lambda mo: mo.state not in ("done", "cancel") + and not mo.subcontracting_has_been_recorded + and mo._get_subcontract_move() + and mo.lot_producing_id + and mo.lot_producing_id.name in serial_numbers + ) + ) + for production in productions: + production.subcontracting_record_component() + return res diff --git a/setup/mrp_subcontracting_serial_mass_produce/odoo/addons/mrp_subcontracting_serial_mass_produce b/setup/mrp_subcontracting_serial_mass_produce/odoo/addons/mrp_subcontracting_serial_mass_produce new file mode 120000 index 00000000..08c255c1 --- /dev/null +++ b/setup/mrp_subcontracting_serial_mass_produce/odoo/addons/mrp_subcontracting_serial_mass_produce @@ -0,0 +1 @@ +../../../../mrp_subcontracting_serial_mass_produce \ No newline at end of file diff --git a/setup/mrp_subcontracting_serial_mass_produce/setup.py b/setup/mrp_subcontracting_serial_mass_produce/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/mrp_subcontracting_serial_mass_produce/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)