mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
[ADD] mrp_subcontracting_no_negative
This commit is contained in:
1
mrp_subcontracting_no_negative/README.rst
Normal file
1
mrp_subcontracting_no_negative/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
botbot
|
||||
1
mrp_subcontracting_no_negative/__init__.py
Normal file
1
mrp_subcontracting_no_negative/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
21
mrp_subcontracting_no_negative/__manifest__.py
Normal file
21
mrp_subcontracting_no_negative/__manifest__.py
Normal file
@@ -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,
|
||||
}
|
||||
1
mrp_subcontracting_no_negative/models/__init__.py
Normal file
1
mrp_subcontracting_no_negative/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import stock_picking
|
||||
24
mrp_subcontracting_no_negative/models/stock_picking.py
Normal file
24
mrp_subcontracting_no_negative/models/stock_picking.py
Normal file
@@ -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()
|
||||
1
mrp_subcontracting_no_negative/readme/CONTRIBUTORS.rst
Normal file
1
mrp_subcontracting_no_negative/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1 @@
|
||||
* Sébastien Alix <sebastien.alix@camptocamp.com>
|
||||
5
mrp_subcontracting_no_negative/readme/DESCRIPTION.rst
Normal file
5
mrp_subcontracting_no_negative/readme/DESCRIPTION.rst
Normal file
@@ -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.
|
||||
1
mrp_subcontracting_no_negative/tests/__init__.py
Normal file
1
mrp_subcontracting_no_negative/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_mrp_subcontracting
|
||||
88
mrp_subcontracting_no_negative/tests/common.py
Normal file
88
mrp_subcontracting_no_negative/tests/common.py
Normal file
@@ -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,
|
||||
)
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
../../../../mrp_subcontracting_no_negative
|
||||
6
setup/mrp_subcontracting_no_negative/setup.py
Normal file
6
setup/mrp_subcontracting_no_negative/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
Reference in New Issue
Block a user