From 6a48a4df230dafe6f48b01e642729bd29375cd63 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Fri, 27 Feb 2026 05:34:41 +0000 Subject: [PATCH 1/3] [6115][ADD] mrp_unbuild_lot_location --- mrp_unbuild_lot_location/README.rst | 80 ++++ mrp_unbuild_lot_location/__init__.py | 4 + mrp_unbuild_lot_location/__manifest__.py | 13 + mrp_unbuild_lot_location/i18n/ja.po | 33 ++ mrp_unbuild_lot_location/models/__init__.py | 1 + .../models/mrp_unbuild.py | 47 ++ .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 8 + .../static/description/index.html | 413 ++++++++++++++++++ mrp_unbuild_lot_location/tests/__init__.py | 1 + .../tests/test_mrp_unbuild_lot_location.py | 72 +++ .../views/mrp_unbuild_views.xml | 18 + 12 files changed, 693 insertions(+) create mode 100644 mrp_unbuild_lot_location/README.rst create mode 100644 mrp_unbuild_lot_location/__init__.py create mode 100644 mrp_unbuild_lot_location/__manifest__.py create mode 100644 mrp_unbuild_lot_location/i18n/ja.po create mode 100644 mrp_unbuild_lot_location/models/__init__.py create mode 100644 mrp_unbuild_lot_location/models/mrp_unbuild.py create mode 100644 mrp_unbuild_lot_location/readme/CONTRIBUTORS.rst create mode 100644 mrp_unbuild_lot_location/readme/DESCRIPTION.rst create mode 100644 mrp_unbuild_lot_location/static/description/index.html create mode 100644 mrp_unbuild_lot_location/tests/__init__.py create mode 100644 mrp_unbuild_lot_location/tests/test_mrp_unbuild_lot_location.py create mode 100644 mrp_unbuild_lot_location/views/mrp_unbuild_views.xml diff --git a/mrp_unbuild_lot_location/README.rst b/mrp_unbuild_lot_location/README.rst new file mode 100644 index 00000000..3666a928 --- /dev/null +++ b/mrp_unbuild_lot_location/README.rst @@ -0,0 +1,80 @@ +======================== +MRP Unbuild Lot Location +======================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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/16.0/mrp_unbuild_lot_location + :alt: OCA/manufacture +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/manufacture-16-0/manufacture-16-0-mrp_unbuild_lot_location + :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/16.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +When a manufacturing order is selected on an unbuild order, this module +automatically sets the source location based on where the lot/serial +number of the finished product is currently stocked. + +This prevents inventory discrepancies that could occur when the default +warehouse stock location is used instead of the actual storage location. + +**Table of contents** + +.. contents:: + :local: + +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 +~~~~~~~ + +* Quartile Limited + +Contributors +~~~~~~~~~~~~ + +* `Quartile `_: + + * Toshikimi Shigenobu + +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. + +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. \ No newline at end of file diff --git a/mrp_unbuild_lot_location/__init__.py b/mrp_unbuild_lot_location/__init__.py new file mode 100644 index 00000000..13161700 --- /dev/null +++ b/mrp_unbuild_lot_location/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2025 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/mrp_unbuild_lot_location/__manifest__.py b/mrp_unbuild_lot_location/__manifest__.py new file mode 100644 index 00000000..1fa69fc6 --- /dev/null +++ b/mrp_unbuild_lot_location/__manifest__.py @@ -0,0 +1,13 @@ +# Copyright 2025 Quartile +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "MRP Unbuild Lot Location", + "category": "Manufacturing", + "license": "AGPL-3", + "author": "Quartile, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/manufacture", + "version": "16.0.1.0.0", + "depends": ["mrp"], + "data": ["views/mrp_unbuild_views.xml"], + "installable": True, +} diff --git a/mrp_unbuild_lot_location/i18n/ja.po b/mrp_unbuild_lot_location/i18n/ja.po new file mode 100644 index 00000000..13b3df48 --- /dev/null +++ b/mrp_unbuild_lot_location/i18n/ja.po @@ -0,0 +1,33 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mrp_unbuild_lot_location +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-27 04:51+0000\n" +"PO-Revision-Date: 2026-02-27 04:51+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: mrp_unbuild_lot_location +#: model:ir.model.fields,field_description:mrp_unbuild_lot_location.field_mrp_unbuild__location_id_domain +msgid "Location Id Domain" +msgstr "ロケーションIDドメイン" + +#. module: mrp_unbuild_lot_location +#. odoo-python +#: code:addons/mrp_unbuild_lot_location/models/mrp_unbuild.py:0 +#, python-format +msgid "No stock found for lot/serial number %s." +msgstr "ロット/シリアル番号 %s の在庫がありません。" + +#. module: mrp_unbuild_lot_location +#: model:ir.model,name:mrp_unbuild_lot_location.model_mrp_unbuild +msgid "Unbuild Order" +msgstr "解体オーダ" diff --git a/mrp_unbuild_lot_location/models/__init__.py b/mrp_unbuild_lot_location/models/__init__.py new file mode 100644 index 00000000..d576b950 --- /dev/null +++ b/mrp_unbuild_lot_location/models/__init__.py @@ -0,0 +1 @@ +from . import mrp_unbuild diff --git a/mrp_unbuild_lot_location/models/mrp_unbuild.py b/mrp_unbuild_lot_location/models/mrp_unbuild.py new file mode 100644 index 00000000..0494c656 --- /dev/null +++ b/mrp_unbuild_lot_location/models/mrp_unbuild.py @@ -0,0 +1,47 @@ +# Copyright 2026 Quartile +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models + + +class MrpUnbuild(models.Model): + _inherit = "mrp.unbuild" + + location_id_domain = fields.Many2many( + "stock.location", + compute="_compute_location_id_domain", + ) + + @api.depends("lot_id", "product_id") + def _compute_location_id_domain(self): + for record in self: + if record.lot_id: + quants = ( + self.env["stock.quant"] + .sudo() + .search( + [ + ("product_id", "=", record.product_id.id), + ("lot_id", "=", record.lot_id.id), + ("quantity", ">", 0), + ("location_id.usage", "in", ["internal", "transit"]), + ] + ) + ) + record.location_id_domain = quants.mapped("location_id") + else: + record.location_id_domain = self.env["stock.location"] + + @api.onchange("lot_id") + def _onchange_lot_id(self): + if not self.lot_id: + return + if len(self.location_id_domain) == 1: + self.location_id = self.location_id_domain + elif not self.location_id_domain: + return { + "warning": { + "message": _("No stock found for lot/serial number %s.") + % self.lot_id.name, + } + } diff --git a/mrp_unbuild_lot_location/readme/CONTRIBUTORS.rst b/mrp_unbuild_lot_location/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..d4469251 --- /dev/null +++ b/mrp_unbuild_lot_location/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Quartile `_: + + * Toshikimi Shigenobu diff --git a/mrp_unbuild_lot_location/readme/DESCRIPTION.rst b/mrp_unbuild_lot_location/readme/DESCRIPTION.rst new file mode 100644 index 00000000..b796670a --- /dev/null +++ b/mrp_unbuild_lot_location/readme/DESCRIPTION.rst @@ -0,0 +1,8 @@ +When a lot/serial number is selected on an unbuild order, this module +filters the available source locations to only those where the +lot/serial number is currently stocked. + +If stock exists in a single location, it is automatically set as the +source location. If stock exists in multiple locations, the list is +narrowed down for the user to choose from. A warning is displayed if +no stock is found for the selected lot/serial number. diff --git a/mrp_unbuild_lot_location/static/description/index.html b/mrp_unbuild_lot_location/static/description/index.html new file mode 100644 index 00000000..d0594a4e --- /dev/null +++ b/mrp_unbuild_lot_location/static/description/index.html @@ -0,0 +1,413 @@ + + + + + +MRP Unbuild Location + + + +
+

MRP Unbuild Location

+ + +

Beta License: AGPL-3 qrtl/axls-custom

+

When a manufacturing order is selected on an unbuild order, this module +automatically sets the source location based on where the lot/serial +number of the finished product is currently stocked.

+

This prevents inventory discrepancies that could occur when the default +warehouse stock location is used instead of the actual storage location.

+

Table of contents

+ +
+

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 Limited
  • +
+
+
+

Maintainers

+

This module is part of the qrtl/axls-custom project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/mrp_unbuild_lot_location/tests/__init__.py b/mrp_unbuild_lot_location/tests/__init__.py new file mode 100644 index 00000000..ea457776 --- /dev/null +++ b/mrp_unbuild_lot_location/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mrp_unbuild_lot_location diff --git a/mrp_unbuild_lot_location/tests/test_mrp_unbuild_lot_location.py b/mrp_unbuild_lot_location/tests/test_mrp_unbuild_lot_location.py new file mode 100644 index 00000000..7131e62d --- /dev/null +++ b/mrp_unbuild_lot_location/tests/test_mrp_unbuild_lot_location.py @@ -0,0 +1,72 @@ +# Copyright 2026 Quartile +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class TestMrpUnbuildLotLocation(TransactionCase): + def setUp(self): + super().setUp() + self.stock_location = self.env.ref("stock.warehouse0").lot_stock_id + self.test_location = self.env["stock.location"].create( + { + "name": "Test Shelf", + "location_id": self.stock_location.id, + "usage": "internal", + } + ) + self.product = self.env["product.product"].create( + {"name": "Test Product", "type": "product", "tracking": "lot"} + ) + self.lot_single = self.env["stock.lot"].create( + { + "name": "lot_single", + "product_id": self.product.id, + "company_id": self.env.company.id, + } + ) + self.lot_multiple = self.env["stock.lot"].create( + { + "name": "lot_multiple", + "product_id": self.product.id, + "company_id": self.env.company.id, + } + ) + self.lot_no_stock = self.env["stock.lot"].create( + { + "name": "lot_no_stock", + "product_id": self.product.id, + "company_id": self.env.company.id, + } + ) + self.env["stock.quant"]._update_available_quantity( + self.product, self.test_location, 5, lot_id=self.lot_single + ) + self.env["stock.quant"]._update_available_quantity( + self.product, self.stock_location, 3, lot_id=self.lot_multiple + ) + self.env["stock.quant"]._update_available_quantity( + self.product, self.test_location, 2, lot_id=self.lot_multiple + ) + self.unbuild = self.env["mrp.unbuild"].new({"product_id": self.product.id}) + + def test_location_id_domain_no_lot(self): + """location_id_domain is empty when lot_id is not set.""" + self.assertFalse(self.unbuild.location_id_domain) + + def test_onchange_lot_id_single_location(self): + """When lot has stock in one location, location_id is auto-filled.""" + self.unbuild.lot_id = self.lot_single + self.unbuild._onchange_lot_id() + self.assertEqual(self.unbuild.location_id._origin, self.test_location) + + def test_onchange_lot_id_multiple_locations(self): + """When lot has stock in multiple locations, location_id is not auto-filled.""" + self.unbuild.lot_id = self.lot_multiple + self.assertEqual(len(self.unbuild.location_id_domain), 2) + + def test_onchange_lot_id_no_stock_warning(self): + """When lot has no stock, _onchange_lot_id returns a warning.""" + self.unbuild.lot_id = self.lot_no_stock + result = self.unbuild._onchange_lot_id() + self.assertTrue(result.get("warning")) diff --git a/mrp_unbuild_lot_location/views/mrp_unbuild_views.xml b/mrp_unbuild_lot_location/views/mrp_unbuild_views.xml new file mode 100644 index 00000000..658b432a --- /dev/null +++ b/mrp_unbuild_lot_location/views/mrp_unbuild_views.xml @@ -0,0 +1,18 @@ + + + + mrp.unbuild.form.inherit.location + mrp.unbuild + + + + + + + location_id_domain and [('id', 'in', location_id_domain)] or [('usage', '=', 'internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)] + + + + From 69152c195a9c8c131c43624d05a7dc0d11832850 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Tue, 3 Mar 2026 05:31:59 +0000 Subject: [PATCH 2/3] reflect oca changes --- mrp_unbuild_lot_location/README.rst | 29 ++++++---- .../static/description/index.html | 56 +++++++++++++------ .../odoo/addons/mrp_unbuild_lot_location | 1 + setup/mrp_unbuild_lot_location/setup.py | 6 ++ 4 files changed, 63 insertions(+), 29 deletions(-) create mode 120000 setup/mrp_unbuild_lot_location/odoo/addons/mrp_unbuild_lot_location create mode 100644 setup/mrp_unbuild_lot_location/setup.py diff --git a/mrp_unbuild_lot_location/README.rst b/mrp_unbuild_lot_location/README.rst index 3666a928..262b14ed 100644 --- a/mrp_unbuild_lot_location/README.rst +++ b/mrp_unbuild_lot_location/README.rst @@ -2,10 +2,13 @@ MRP Unbuild Lot Location ======================== -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4f9697c06fd8dbb3a982668b55249262d59735b25801624826dd0d07eb0c3677 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status @@ -19,18 +22,20 @@ MRP Unbuild Lot Location .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png :target: https://translation.odoo-community.org/projects/manufacture-16-0/manufacture-16-0-mrp_unbuild_lot_location :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/16.0 - :alt: Try me on Runbot +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/manufacture&target_branch=16.0 + :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| -When a manufacturing order is selected on an unbuild order, this module -automatically sets the source location based on where the lot/serial -number of the finished product is currently stocked. +When a lot/serial number is selected on an unbuild order, this module +filters the available source locations to only those where the +lot/serial number is currently stocked. -This prevents inventory discrepancies that could occur when the default -warehouse stock location is used instead of the actual storage location. +If stock exists in a single location, it is automatically set as the +source location. If stock exists in multiple locations, the list is +narrowed down for the user to choose from. A warning is displayed if +no stock is found for the selected lot/serial number. **Table of contents** @@ -42,7 +47,7 @@ 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 +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. @@ -53,7 +58,7 @@ Credits Authors ~~~~~~~ -* Quartile Limited +* Quartile Contributors ~~~~~~~~~~~~ @@ -77,4 +82,4 @@ promote its widespread use. 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. \ No newline at end of file +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mrp_unbuild_lot_location/static/description/index.html b/mrp_unbuild_lot_location/static/description/index.html index d0594a4e..c8a9ea17 100644 --- a/mrp_unbuild_lot_location/static/description/index.html +++ b/mrp_unbuild_lot_location/static/description/index.html @@ -3,7 +3,7 @@ -MRP Unbuild Location +MRP Unbuild Lot Location -
-

MRP Unbuild Location

+
+

MRP Unbuild Lot Location

-

Beta License: AGPL-3 qrtl/axls-custom

-

When a manufacturing order is selected on an unbuild order, this module -automatically sets the source location based on where the lot/serial -number of the finished product is currently stocked.

-

This prevents inventory discrepancies that could occur when the default -warehouse stock location is used instead of the actual storage location.

+

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

+

When a lot/serial number is selected on an unbuild order, this module +filters the available source locations to only those where the +lot/serial number is currently stocked.

+

If stock exists in a single location, it is automatically set as the +source location. If stock exists in multiple locations, the list is +narrowed down for the user to choose from. A warning is displayed if +no stock is found for the selected lot/serial number.

Table of contents

Bug Tracker

-

Bugs are tracked on GitHub Issues. +

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.

+feedback.

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

@@ -399,13 +402,32 @@

Credits

Authors

    -
  • Quartile Limited
  • +
  • Quartile
  • +
+
+
+

Contributors

+
    +
  • Quartile:

    +
    +
      +
    • Toshikimi Shigenobu
    • +
    +
    +
-

Maintainers

-

This module is part of the qrtl/axls-custom project on GitHub.

-

You are welcome to contribute.

+

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.

+

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/setup/mrp_unbuild_lot_location/odoo/addons/mrp_unbuild_lot_location b/setup/mrp_unbuild_lot_location/odoo/addons/mrp_unbuild_lot_location new file mode 120000 index 00000000..563d6c23 --- /dev/null +++ b/setup/mrp_unbuild_lot_location/odoo/addons/mrp_unbuild_lot_location @@ -0,0 +1 @@ +../../../../mrp_unbuild_lot_location \ No newline at end of file diff --git a/setup/mrp_unbuild_lot_location/setup.py b/setup/mrp_unbuild_lot_location/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/mrp_unbuild_lot_location/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From c086643f931863aff47039aaab6f1a6443f3a396 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Wed, 11 Mar 2026 08:43:13 +0000 Subject: [PATCH 3/3] reflect oca changes --- mrp_unbuild_lot_location/README.rst | 2 +- mrp_unbuild_lot_location/__manifest__.py | 3 +- .../models/mrp_unbuild.py | 66 +++++++----- .../readme/CONTRIBUTORS.rst | 2 +- .../static/description/index.html | 10 +- .../tests/test_mrp_unbuild_lot_location.py | 102 ++++++++++-------- .../views/mrp_unbuild_views.xml | 4 +- 7 files changed, 110 insertions(+), 79 deletions(-) diff --git a/mrp_unbuild_lot_location/README.rst b/mrp_unbuild_lot_location/README.rst index 262b14ed..bb40ccfe 100644 --- a/mrp_unbuild_lot_location/README.rst +++ b/mrp_unbuild_lot_location/README.rst @@ -64,8 +64,8 @@ Contributors ~~~~~~~~~~~~ * `Quartile `_: - * Toshikimi Shigenobu + * Aung Ko Ko Lin Maintainers ~~~~~~~~~~~ diff --git a/mrp_unbuild_lot_location/__manifest__.py b/mrp_unbuild_lot_location/__manifest__.py index 1fa69fc6..e29f4b88 100644 --- a/mrp_unbuild_lot_location/__manifest__.py +++ b/mrp_unbuild_lot_location/__manifest__.py @@ -1,7 +1,8 @@ -# Copyright 2025 Quartile +# Copyright 2026 Quartile (https://www.quartile.co) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { "name": "MRP Unbuild Lot Location", + "summary": "Filter unbuild source locations by lot/serial stock", "category": "Manufacturing", "license": "AGPL-3", "author": "Quartile, Odoo Community Association (OCA)", diff --git a/mrp_unbuild_lot_location/models/mrp_unbuild.py b/mrp_unbuild_lot_location/models/mrp_unbuild.py index 0494c656..fca74317 100644 --- a/mrp_unbuild_lot_location/models/mrp_unbuild.py +++ b/mrp_unbuild_lot_location/models/mrp_unbuild.py @@ -1,47 +1,63 @@ -# Copyright 2026 Quartile +# 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 +from odoo.exceptions import ValidationError +from odoo.osv import expression class MrpUnbuild(models.Model): _inherit = "mrp.unbuild" - location_id_domain = fields.Many2many( - "stock.location", - compute="_compute_location_id_domain", - ) + location_id_domain = fields.Binary(compute="_compute_location_id_domain") - @api.depends("lot_id", "product_id") + @api.depends("lot_id", "product_id", "company_id") def _compute_location_id_domain(self): for record in self: + # Standard location filter (applies even without a lot) + base_domain = [ + ("usage", "in", ["internal", "transit"]), + "|", + ("company_id", "=", False), + ("company_id", "=", record.company_id.id), + ] if record.lot_id: - quants = ( + location_ids = ( self.env["stock.quant"] - .sudo() .search( [ ("product_id", "=", record.product_id.id), ("lot_id", "=", record.lot_id.id), ("quantity", ">", 0), - ("location_id.usage", "in", ["internal", "transit"]), ] ) + .location_id.ids ) - record.location_id_domain = quants.mapped("location_id") - else: - record.location_id_domain = self.env["stock.location"] + base_domain = expression.AND( + [ + base_domain, + [("id", "in", location_ids)], + ] + ) + record.location_id_domain = base_domain + + @api.depends("company_id", "location_id_domain") + def _compute_location_id(self): + super()._compute_location_id() + for record in self.filtered("lot_id"): + locations = self.env["stock.location"].search(record.location_id_domain) + if len(locations) == 1: + record.location_id = locations + return - @api.onchange("lot_id") - def _onchange_lot_id(self): - if not self.lot_id: - return - if len(self.location_id_domain) == 1: - self.location_id = self.location_id_domain - elif not self.location_id_domain: - return { - "warning": { - "message": _("No stock found for lot/serial number %s.") - % self.lot_id.name, - } - } + @api.constrains("lot_id") + def _check_lot_location(self): + for record in self: + if not record.lot_id: + continue + if not self.env["stock.location"].search( + record.location_id_domain, limit=1 + ): + raise ValidationError( + _("No stock found for lot/serial number %s.") % record.lot_id.name + ) diff --git a/mrp_unbuild_lot_location/readme/CONTRIBUTORS.rst b/mrp_unbuild_lot_location/readme/CONTRIBUTORS.rst index d4469251..cb949fb0 100644 --- a/mrp_unbuild_lot_location/readme/CONTRIBUTORS.rst +++ b/mrp_unbuild_lot_location/readme/CONTRIBUTORS.rst @@ -1,3 +1,3 @@ * `Quartile `_: - * Toshikimi Shigenobu + * Aung Ko Ko Lin diff --git a/mrp_unbuild_lot_location/static/description/index.html b/mrp_unbuild_lot_location/static/description/index.html index c8a9ea17..32240446 100644 --- a/mrp_unbuild_lot_location/static/description/index.html +++ b/mrp_unbuild_lot_location/static/description/index.html @@ -407,13 +407,15 @@

Authors

Contributors

-
diff --git a/mrp_unbuild_lot_location/tests/test_mrp_unbuild_lot_location.py b/mrp_unbuild_lot_location/tests/test_mrp_unbuild_lot_location.py index 7131e62d..a85cf50d 100644 --- a/mrp_unbuild_lot_location/tests/test_mrp_unbuild_lot_location.py +++ b/mrp_unbuild_lot_location/tests/test_mrp_unbuild_lot_location.py @@ -1,72 +1,86 @@ # Copyright 2026 Quartile # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.exceptions import ValidationError from odoo.tests.common import TransactionCase class TestMrpUnbuildLotLocation(TransactionCase): - def setUp(self): - super().setUp() - self.stock_location = self.env.ref("stock.warehouse0").lot_stock_id - self.test_location = self.env["stock.location"].create( + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.warehouse = cls.env["stock.warehouse"].create( { - "name": "Test Shelf", - "location_id": self.stock_location.id, + "name": "Test Warehouse", + "code": "TWH", + "company_id": cls.env.company.id, + } + ) + cls.location_1 = cls.env["stock.location"].create( + { + "name": "Location 1", + "location_id": cls.warehouse.lot_stock_id.id, "usage": "internal", } ) - self.product = self.env["product.product"].create( + cls.location_2 = cls.env["stock.location"].create( + { + "name": "Location 2", + "location_id": cls.warehouse.lot_stock_id.id, + "usage": "internal", + } + ) + cls.product = cls.env["product.product"].create( {"name": "Test Product", "type": "product", "tracking": "lot"} ) - self.lot_single = self.env["stock.lot"].create( + cls.lot_1 = cls.env["stock.lot"].create( { - "name": "lot_single", - "product_id": self.product.id, - "company_id": self.env.company.id, + "name": "lot_1", + "product_id": cls.product.id, + "company_id": cls.env.company.id, } ) - self.lot_multiple = self.env["stock.lot"].create( + cls.lot_2 = cls.env["stock.lot"].create( { - "name": "lot_multiple", - "product_id": self.product.id, - "company_id": self.env.company.id, + "name": "lot_2", + "product_id": cls.product.id, + "company_id": cls.env.company.id, } ) - self.lot_no_stock = self.env["stock.lot"].create( + cls.lot_3 = cls.env["stock.lot"].create( { - "name": "lot_no_stock", - "product_id": self.product.id, - "company_id": self.env.company.id, + "name": "lot_3", + "product_id": cls.product.id, + "company_id": cls.env.company.id, } ) - self.env["stock.quant"]._update_available_quantity( - self.product, self.test_location, 5, lot_id=self.lot_single + cls.env["stock.quant"]._update_available_quantity( + cls.product, cls.location_1, 5, lot_id=cls.lot_1 ) - self.env["stock.quant"]._update_available_quantity( - self.product, self.stock_location, 3, lot_id=self.lot_multiple + cls.env["stock.quant"]._update_available_quantity( + cls.product, cls.location_1, 2, lot_id=cls.lot_2 ) - self.env["stock.quant"]._update_available_quantity( - self.product, self.test_location, 2, lot_id=self.lot_multiple + cls.env["stock.quant"]._update_available_quantity( + cls.product, cls.location_2, 3, lot_id=cls.lot_2 ) - self.unbuild = self.env["mrp.unbuild"].new({"product_id": self.product.id}) + cls.unbuild = cls.env["mrp.unbuild"].create({"product_id": cls.product.id}) def test_location_id_domain_no_lot(self): - """location_id_domain is empty when lot_id is not set.""" - self.assertFalse(self.unbuild.location_id_domain) - - def test_onchange_lot_id_single_location(self): - """When lot has stock in one location, location_id is auto-filled.""" - self.unbuild.lot_id = self.lot_single - self.unbuild._onchange_lot_id() - self.assertEqual(self.unbuild.location_id._origin, self.test_location) - - def test_onchange_lot_id_multiple_locations(self): - """When lot has stock in multiple locations, location_id is not auto-filled.""" - self.unbuild.lot_id = self.lot_multiple - self.assertEqual(len(self.unbuild.location_id_domain), 2) + # When lot_id is not set, domain returns all internal/transit locations. + locations = self.env["stock.location"].search(self.unbuild.location_id_domain) + self.assertIn(self.location_1, locations) + self.assertIn(self.location_2, locations) - def test_onchange_lot_id_no_stock_warning(self): - """When lot has no stock, _onchange_lot_id returns a warning.""" - self.unbuild.lot_id = self.lot_no_stock - result = self.unbuild._onchange_lot_id() - self.assertTrue(result.get("warning")) + def test_lot_location_resolution(self): + # When lot has stock in one location, location_id is auto-filled. + self.unbuild.lot_id = self.lot_1 + locations = self.env["stock.location"].search(self.unbuild.location_id_domain) + self.assertEqual(self.unbuild.location_id, self.location_1) + # When lot has stock in multiple locations, location_id is not auto-filled. + self.unbuild.lot_id = self.lot_2 + locations = self.env["stock.location"].search(self.unbuild.location_id_domain) + self.assertIn(self.location_1, locations) + self.assertIn(self.location_2, locations) + # When lot has no stock, saving raises a ValidationError. + with self.assertRaises(ValidationError): + self.unbuild.lot_id = self.lot_3 diff --git a/mrp_unbuild_lot_location/views/mrp_unbuild_views.xml b/mrp_unbuild_lot_location/views/mrp_unbuild_views.xml index 658b432a..a09cecf1 100644 --- a/mrp_unbuild_lot_location/views/mrp_unbuild_views.xml +++ b/mrp_unbuild_lot_location/views/mrp_unbuild_views.xml @@ -9,9 +9,7 @@ - location_id_domain and [('id', 'in', location_id_domain)] or [('usage', '=', 'internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)] + location_id_domain