From 6944283ff8fba92b5dc9209d8f474fb061e50fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Wed, 19 Oct 2022 19:20:22 +0200 Subject: [PATCH] [ADD] mrp_subcontracting_no_negative --- mrp_subcontracting_no_negative/README.rst | 1 + mrp_subcontracting_no_negative/__init__.py | 1 + .../__manifest__.py | 21 +++++ .../models/__init__.py | 1 + .../models/stock_picking.py | 24 +++++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 5 ++ .../tests/__init__.py | 1 + .../tests/common.py | 88 +++++++++++++++++++ .../tests/test_mrp_subcontracting.py | 41 +++++++++ .../addons/mrp_subcontracting_no_negative | 1 + setup/mrp_subcontracting_no_negative/setup.py | 6 ++ 12 files changed, 191 insertions(+) create mode 100644 mrp_subcontracting_no_negative/README.rst create mode 100644 mrp_subcontracting_no_negative/__init__.py create mode 100644 mrp_subcontracting_no_negative/__manifest__.py create mode 100644 mrp_subcontracting_no_negative/models/__init__.py create mode 100644 mrp_subcontracting_no_negative/models/stock_picking.py create mode 100644 mrp_subcontracting_no_negative/readme/CONTRIBUTORS.rst create mode 100644 mrp_subcontracting_no_negative/readme/DESCRIPTION.rst create mode 100644 mrp_subcontracting_no_negative/tests/__init__.py create mode 100644 mrp_subcontracting_no_negative/tests/common.py create mode 100644 mrp_subcontracting_no_negative/tests/test_mrp_subcontracting.py create mode 120000 setup/mrp_subcontracting_no_negative/odoo/addons/mrp_subcontracting_no_negative create mode 100644 setup/mrp_subcontracting_no_negative/setup.py diff --git a/mrp_subcontracting_no_negative/README.rst b/mrp_subcontracting_no_negative/README.rst new file mode 100644 index 000000000..7d06e2c40 --- /dev/null +++ b/mrp_subcontracting_no_negative/README.rst @@ -0,0 +1 @@ +botbot diff --git a/mrp_subcontracting_no_negative/__init__.py b/mrp_subcontracting_no_negative/__init__.py new file mode 100644 index 000000000..0650744f6 --- /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 000000000..89865add1 --- /dev/null +++ b/mrp_subcontracting_no_negative/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +{ + "name": "MRP Subcontracting (no negative components)", + "version": "15.0.0.1.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": [ + # core + "mrp_subcontracting", + ], + "data": [], + "installable": True, + "auto_install": True, + "application": False, +} diff --git a/mrp_subcontracting_no_negative/models/__init__.py b/mrp_subcontracting_no_negative/models/__init__.py new file mode 100644 index 000000000..ae4c27227 --- /dev/null +++ b/mrp_subcontracting_no_negative/models/__init__.py @@ -0,0 +1 @@ +from . import stock_picking 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 000000000..4340acde9 --- /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 000000000..c452804a9 --- /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 000000000..e95bd2ec1 --- /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/tests/__init__.py b/mrp_subcontracting_no_negative/tests/__init__.py new file mode 100644 index 000000000..0ce157b49 --- /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 000000000..012e68dd9 --- /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 000000000..ffbb5228a --- /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 000000000..61d377426 --- /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 000000000..28c57bb64 --- /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, +)