diff --git a/mrp_subcontracting_no_negative/README.rst b/mrp_subcontracting_no_negative/README.rst new file mode 100644 index 00000000000..8e201cd2488 --- /dev/null +++ b/mrp_subcontracting_no_negative/README.rst @@ -0,0 +1,90 @@ +=========================================== +MRP Subcontracting (no negative components) +=========================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |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_subcontracting_no_negative + :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_subcontracting_no_negative + :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| + +Disallow negative stock levels in subcontractor locations. + +In standard Odoo it is allowed to validate a subcontractor receipt to get +the finished products even if the components haven't been sent to the +subcontractor. This module prevents this with an error message. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**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 +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Sébastien Alix + +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_subcontracting_no_negative/__init__.py b/mrp_subcontracting_no_negative/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/mrp_subcontracting_no_negative/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mrp_subcontracting_no_negative/__manifest__.py b/mrp_subcontracting_no_negative/__manifest__.py new file mode 100644 index 00000000000..1bf1f2a1b36 --- /dev/null +++ b/mrp_subcontracting_no_negative/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +{ + "name": "MRP Subcontracting (no negative components)", + "version": "16.0.1.0.0", + "development_status": "Alpha", + "license": "AGPL-3", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["sebalix"], + "summary": "Disallow negative stock levels in subcontractor locations.", + "website": "https://github.com/OCA/manufacture", + "category": "Manufacturing", + "depends": [ + "mrp_subcontracting", + ], + "data": [], + "installable": True, + "auto_install": True, + "application": False, +} diff --git a/mrp_subcontracting_no_negative/i18n/mrp_subcontracting_no_negative.pot b/mrp_subcontracting_no_negative/i18n/mrp_subcontracting_no_negative.pot new file mode 100644 index 00000000000..4d9934de879 --- /dev/null +++ b/mrp_subcontracting_no_negative/i18n/mrp_subcontracting_no_negative.pot @@ -0,0 +1,25 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mrp_subcontracting_no_negative +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \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_subcontracting_no_negative +#: model:ir.model,name:mrp_subcontracting_no_negative.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: mrp_subcontracting_no_negative +#: code:addons/mrp_subcontracting_no_negative/models/stock_picking.py:0 +#, python-format +msgid "Unable to reserve components in the location %s." +msgstr "" diff --git a/mrp_subcontracting_no_negative/models/__init__.py b/mrp_subcontracting_no_negative/models/__init__.py new file mode 100644 index 00000000000..3c71fd1b863 --- /dev/null +++ b/mrp_subcontracting_no_negative/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_picking +from . import stock_move diff --git a/mrp_subcontracting_no_negative/models/stock_move.py b/mrp_subcontracting_no_negative/models/stock_move.py new file mode 100644 index 00000000000..2ff4c83bccb --- /dev/null +++ b/mrp_subcontracting_no_negative/models/stock_move.py @@ -0,0 +1,22 @@ +# Copyright 2023 Quartile Limited +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _, exceptions, models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _action_record_components(self): + self.ensure_one() + production = self._get_subcontract_production()[-1:] + if production.reservation_state != "assigned": + production.action_assign() + # Block the reception if components could not be reserved + # NOTE: this also avoids the creation of negative quants + if production.reservation_state != "assigned": + raise exceptions.UserError( + _("Unable to reserve components in the location %s.") + % (production.location_src_id.name) + ) + return super()._action_record_components() diff --git a/mrp_subcontracting_no_negative/models/stock_picking.py b/mrp_subcontracting_no_negative/models/stock_picking.py new file mode 100644 index 00000000000..4340acde993 --- /dev/null +++ b/mrp_subcontracting_no_negative/models/stock_picking.py @@ -0,0 +1,24 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _, exceptions, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + def action_record_components(self): + self.ensure_one() + if self._is_subcontract(): + # Try to reserve the components + for production in self._get_subcontract_production(): + if production.reservation_state != "assigned": + production.action_assign() + # Block the reception if components could not be reserved + # NOTE: this also avoids the creation of negative quants + if production.reservation_state != "assigned": + raise exceptions.UserError( + _("Unable to reserve components in the location %s.") + % (production.location_src_id.name) + ) + return super().action_record_components() diff --git a/mrp_subcontracting_no_negative/readme/CONTRIBUTORS.rst b/mrp_subcontracting_no_negative/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..c452804a90b --- /dev/null +++ b/mrp_subcontracting_no_negative/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Sébastien Alix diff --git a/mrp_subcontracting_no_negative/readme/DESCRIPTION.rst b/mrp_subcontracting_no_negative/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..e95bd2ec1f4 --- /dev/null +++ b/mrp_subcontracting_no_negative/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +Disallow negative stock levels in subcontractor locations. + +In standard Odoo it is allowed to validate a subcontractor receipt to get +the finished products even if the components haven't been sent to the +subcontractor. This module prevents this with an error message. diff --git a/mrp_subcontracting_no_negative/static/description/icon.png b/mrp_subcontracting_no_negative/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/mrp_subcontracting_no_negative/static/description/icon.png differ diff --git a/mrp_subcontracting_no_negative/static/description/index.html b/mrp_subcontracting_no_negative/static/description/index.html new file mode 100644 index 00000000000..09366ccd529 --- /dev/null +++ b/mrp_subcontracting_no_negative/static/description/index.html @@ -0,0 +1,430 @@ + + + + + + +MRP Subcontracting (no negative components) + + + +
+

MRP Subcontracting (no negative components)

+ + +

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

+

Disallow negative stock levels in subcontractor locations.

+

In standard Odoo it is allowed to validate a subcontractor receipt to get +the finished products even if the components haven’t been sent to the +subcontractor. This module prevents this with an error message.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

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

+ +
+
+

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_subcontracting_no_negative/tests/__init__.py b/mrp_subcontracting_no_negative/tests/__init__.py new file mode 100644 index 00000000000..0ce157b49a6 --- /dev/null +++ b/mrp_subcontracting_no_negative/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mrp_subcontracting diff --git a/mrp_subcontracting_no_negative/tests/common.py b/mrp_subcontracting_no_negative/tests/common.py new file mode 100644 index 00000000000..012e68dd97c --- /dev/null +++ b/mrp_subcontracting_no_negative/tests/common.py @@ -0,0 +1,88 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import random +import string + +from odoo.tests import common + + +class Common(common.TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + def _create_subcontractor_receipt(self, vendor, bom): + with common.Form(self.env["stock.picking"]) as form: + form.picking_type_id = self.env.ref("stock.picking_type_in") + form.partner_id = vendor + with form.move_ids_without_package.new() as move: + variant = bom.product_tmpl_id.product_variant_ids + move.product_id = variant + move.product_uom_qty = 1 + picking = form.save() + picking.action_confirm() + return picking + + @classmethod + def _get_subcontracted_bom(cls): + bom = cls.env.ref("mrp_subcontracting.mrp_bom_subcontract") + bom.bom_line_ids.unlink() + component = cls.env.ref("mrp.product_product_computer_desk_head") + component.tracking = "none" + bom.bom_line_ids.create( + { + "bom_id": bom.id, + "product_id": component.id, + "product_qty": 1, + } + ) + return bom + + @classmethod + def _update_qty_in_location( + cls, location, product, quantity, package=None, lot=None, in_date=None + ): + quants = cls.env["stock.quant"]._gather( + product, location, lot_id=lot, package_id=package, strict=True + ) + # this method adds the quantity to the current quantity, so remove it + quantity -= sum(quants.mapped("quantity")) + cls.env["stock.quant"]._update_available_quantity( + product, + location, + quantity, + package_id=package, + lot_id=lot, + in_date=in_date, + ) + + @classmethod + def _update_stock_component_qty(cls, order=None, bom=None, location=None): + if not order and not bom: + return + if order: + bom = order.bom_id + if not location: + location = cls.env.ref("stock.stock_location_stock") + for line in bom.bom_line_ids: + if line.product_id.type != "product": + continue + lot = None + if line.product_id.tracking != "none": + lot_name = "".join( + random.choice(string.ascii_lowercase) for i in range(10) + ) + vals = { + "product_id": line.product_id.id, + "company_id": line.company_id.id, + "name": lot_name, + } + lot = cls.env["stock.production.lot"].create(vals) + cls._update_qty_in_location( + location, + line.product_id, + line.product_qty, + lot=lot, + ) diff --git a/mrp_subcontracting_no_negative/tests/test_mrp_subcontracting.py b/mrp_subcontracting_no_negative/tests/test_mrp_subcontracting.py new file mode 100644 index 00000000000..ffbb5228a4a --- /dev/null +++ b/mrp_subcontracting_no_negative/tests/test_mrp_subcontracting.py @@ -0,0 +1,41 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.exceptions import UserError + +from .common import Common + + +class TestMrpSubcontracting(Common): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.subcontracted_bom = cls._get_subcontracted_bom() + cls.vendor = cls.env.ref("base.res_partner_12") + + def test_no_subcontractor_stock(self): + picking = self._create_subcontractor_receipt( + self.vendor, self.subcontracted_bom + ) + self.assertEqual(picking.state, "assigned") + # No component in the subcontractor location + with self.assertRaisesRegex(UserError, "Unable to reserve"): + picking.action_record_components() + # Try again once the subcontractor received the components + self._update_stock_component_qty( + bom=self.subcontracted_bom, + location=self.vendor.property_stock_subcontractor, + ) + picking.action_record_components() + + def test_with_subcontractor_stock(self): + # Subcontractor has components before we create the receipt + self._update_stock_component_qty( + bom=self.subcontracted_bom, + location=self.vendor.property_stock_subcontractor, + ) + picking = self._create_subcontractor_receipt( + self.vendor, self.subcontracted_bom + ) + self.assertEqual(picking.state, "assigned") + picking.action_record_components() diff --git a/setup/mrp_subcontracting_no_negative/odoo/addons/mrp_subcontracting_no_negative b/setup/mrp_subcontracting_no_negative/odoo/addons/mrp_subcontracting_no_negative new file mode 120000 index 00000000000..61d37742606 --- /dev/null +++ b/setup/mrp_subcontracting_no_negative/odoo/addons/mrp_subcontracting_no_negative @@ -0,0 +1 @@ +../../../../mrp_subcontracting_no_negative \ No newline at end of file diff --git a/setup/mrp_subcontracting_no_negative/setup.py b/setup/mrp_subcontracting_no_negative/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/mrp_subcontracting_no_negative/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)