Skip to content

Commit

Permalink
Add purchase_propagate_qty_mrp
Browse files Browse the repository at this point in the history
  • Loading branch information
TDu committed Nov 5, 2024
1 parent 4233544 commit d8e06d3
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 0 deletions.
1 change: 1 addition & 0 deletions purchase_propagate_qty_mrp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
16 changes: 16 additions & 0 deletions purchase_propagate_qty_mrp/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2024 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)

{
"name": "Purchase Propagate Quantity MRP",
"version": "14.0.1.0.1",
"summary": "",
"author": "Camptocamp, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/purchase-workflow",
"category": "Purchase Management",
"license": "AGPL-3",
"depends": ["purchase_propagate_qty", "mrp"],
"installable": True,
"auto_install": True,
"application": False,
}
1 change: 1 addition & 0 deletions purchase_propagate_qty_mrp/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import purchase_order_line
59 changes: 59 additions & 0 deletions purchase_propagate_qty_mrp/models/purchase_order_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright 2024 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)

from odoo import _, models
from odoo.exceptions import UserError
from odoo.tools import float_compare


class PurchaseOrderLine(models.Model):
_inherit = "purchase.order.line"

def _propagage_qty_to_moves(self):
kit_purchase_lines = self.env["purchase.order.line"].browse()
for line in self:
if line.state != "purchase":
continue
kit_line = line._propagate_qty_to_moves_mrp()
if kit_line:
kit_purchase_lines |= line
super(PurchaseOrderLine, self - kit_purchase_lines)._propagage_qty_to_moves()

def _propagate_qty_to_moves_mrp(self):
self.ensure_one()
bom = self.env["mrp.bom"].sudo()._bom_find(product=self.product_id)
if not bom or bom.type != "phantom":
return None

Check warning on line 26 in purchase_propagate_qty_mrp/models/purchase_order_line.py

View check run for this annotation

Codecov / codecov/patch

purchase_propagate_qty_mrp/models/purchase_order_line.py#L26

Added line #L26 was not covered by tests
new_kit_quantity = self.product_uom_qty
boms, bom_sub_lines = bom.explode(self.product_id, new_kit_quantity)
for bom_line, bom_line_data in bom_sub_lines:
bom_line_uom = bom_line.product_uom_id
quant_uom = bom_line.product_id.uom_id
new_component_qty, procurement_uom = bom_line_uom._adjust_uom_quantities(
bom_line_data["qty"], quant_uom
)
moves = self.move_ids.filtered(
lambda move: move.product_id == bom_line.product_id
and move.state != "cancel"
)
previous_component_qty = sum(moves.mapped("product_uom_qty"))
removable_qty = moves._get_removable_qty()
quantity_to_remove = previous_component_qty - new_component_qty
if (
float_compare(
removable_qty,
quantity_to_remove,
precision_rounding=procurement_uom.rounding,
)
>= 0
):
moves._deduce_qty(quantity_to_remove, procurement_uom.id)
else:
raise UserError(
_(
"You cannot remove more that what remains to be done. "
"For the kit %s.",
bom_line.product_id.name,
)
)
return self
1 change: 1 addition & 0 deletions purchase_propagate_qty_mrp/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Thierry Ducrest <[email protected]>
4 changes: 4 additions & 0 deletions purchase_propagate_qty_mrp/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This module extends the functionality of the module `purchase_propagate_qty` in purchase-workflow.
It allows to decrease the quantity ordered on a purchase order line for a kit product.
As long as the change will not reduce the purchase quantity below the already received quantity,
even if the kit is partially received.
1 change: 1 addition & 0 deletions purchase_propagate_qty_mrp/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_purchase_propagate_qty_mrp
208 changes: 208 additions & 0 deletions purchase_propagate_qty_mrp/tests/test_purchase_propagate_qty_mrp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# Copyright 2024 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)

from odoo.exceptions import UserError
from odoo.tests.common import SavepointCase


class TestQtyUpdate(SavepointCase):
at_install = False
post_install = True

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.Product = cls.env["product.product"]
cls.seller = cls.env["res.partner"].create({"name": "supplier"})
# Create a kit
cls.product_bed_with_curtain = cls.Product.create(
{
"name": "BED WITH CURTAINS",
"type": "product",
"sale_ok": True,
"purchase_ok": False,
}
)
cls.product_bed_structure = cls.Product.create(
{
"name": "BED STRUCTURE",
"type": "product",
"sale_ok": True,
"purchase_ok": True,
"seller_ids": [(0, 0, {"name": cls.seller.id, "price": 50.0})],
}
)
cls.product_bed_curtain = cls.Product.create(
{
"name": "BED CURTAIN",
"type": "product",
"sale_ok": True,
"purchase_ok": True,
"seller_ids": [(0, 0, {"name": cls.seller.id, "price": 10.0})],
}
)
cls.bom_model = cls.env["mrp.bom"]
# A kit that contains a bed and 2 curtains
cls.bom_bed_with_curtain = cls.bom_model.create(
{
"product_tmpl_id": cls.product_bed_with_curtain.product_tmpl_id.id,
"product_id": cls.product_bed_with_curtain.id,
"type": "phantom",
"bom_line_ids": [
(
0,
0,
{"product_id": cls.product_bed_curtain.id, "product_qty": 2.0},
),
(
0,
0,
{
"product_id": cls.product_bed_structure.id,
"product_qty": 1.0,
},
),
],
}
)

cls.date_planned = "2024-11-04 12:00:00"
cls.po = cls.env["purchase.order"].create(
{
"partner_id": cls.seller.id,
"order_line": [
(
0,
0,
{
"product_id": cls.product_bed_with_curtain.id,
"product_uom": cls.product_bed_with_curtain.uom_id.id,
"name": cls.product_bed_with_curtain.name,
"date_planned": cls.date_planned,
"product_qty": 42.0,
"price_unit": 300.0,
},
),
],
}
)
cls.po.button_confirm()

def _check_moves_quantity(self, moves, values):
"""Quick check of moves and their planned quantity."""
for move in moves:
if move.state == "done":
continue
self.assertEqual(move.product_uom_qty, values[move.product_id])

def _receive_kit(self, purchase_line, quantity_receive, partially=False):
"""Process the reception of a kit quantity in relation to a purchase line.
If partially is set, the quantity of the 2nd component will be divided by half.
Making a partial reception of the kit.
"""
move1 = purchase_line.move_ids.filtered(
lambda r: r.product_id == self.product_bed_structure
)
new_move_vals = move1._split(quantity_receive)
new_move_1 = self.env["stock.move"].create(new_move_vals)
new_move_1._action_confirm(merge=False)
new_move_1._action_assign()
new_move_1.quantity_done = quantity_receive
new_move_1._action_done()
move2 = purchase_line.move_ids.filtered(
lambda r: r.product_id == self.product_bed_curtain
)
receiving = 0
if partially:
receiving = quantity_receive
else:
receiving = quantity_receive * 2
new_move_vals = move2._split(receiving)
new_move_2 = self.env["stock.move"].create(new_move_vals)
new_move_2._action_confirm(merge=False)
new_move_2._action_assign()
new_move_2.quantity_done = receiving
new_move_2._action_done()

def test_purchase_line_qty_decrease_allowed(self):
"""Check decreasing ordered quantity to less than what is left to receive.
Purchased 42 kits
Received nothing yet
Update the purchase quantity to 25
"""
po_line = self.po.order_line[0]
moves = po_line.move_ids
po_line.write({"product_qty": 25})
self._check_moves_quantity(
moves, {self.product_bed_structure: 25, self.product_bed_curtain: 50}
)

def test_purchase_line_qty_decrease_not_allowed(self):
"""Check decreasing the quantity to more than what is left to receive.
Purchased 42 kits
Receive 30 of them
Update the purchase quantity to 29 -> not allowed
"""
po_line = self.po.order_line[0]
self._receive_kit(po_line, 30)
moves = po_line.move_ids
self._check_moves_quantity(
moves, {self.product_bed_structure: 12, self.product_bed_curtain: 24}
)
# Exception raised by Odoo standard
with self.assertRaises(UserError):
po_line.write({"product_qty": 29})

def test_decrease_purchase_qty_nothing_left_to_receive(self):
"""Check decreasing the quantity to the exact amount that has been received.
Purchased 42 kits
Received 30
Update the purchase quantity to 30 -> nothing left to receive
"""
po_line = self.po.order_line[0]
self._receive_kit(po_line, 30)
moves = po_line.move_ids
self._check_moves_quantity(
moves, {self.product_bed_structure: 12, self.product_bed_curtain: 24}
)
po_line.write({"product_qty": 30})
self._check_moves_quantity(
moves, {self.product_bed_structure: 0, self.product_bed_curtain: 0}
)

def test_decrease_purchase_qty_allowed_partially_received_kit(self):
"""Check decrease quantity after partially receiving a kit.
Purchased 42 kits
Receive 14 kits and part of the 15th.
Update the purchase quantity to 15 -> left part of a kit to receive
"""
po_line = self.po.order_line[0]
self._receive_kit(po_line, 15, partially=True)
po_line.write({"product_qty": 15})
moves = po_line.move_ids
self._check_moves_quantity(
moves, {self.product_bed_structure: 0, self.product_bed_curtain: 15}
)

def test_decrease_purchase_qty_not_allowed_partially_received_kit(self):
"""Check decrease quantity after partially receiving a kit.
Purchased 42 kits
Receive 14 kits and part of the 15th.
Update the purchase quantity to 14 -> error raised
"""
po_line = self.po.order_line[0]
self._receive_kit(po_line, 15, partially=True)
error = r".*remains to be done. For the kit BED STRUCTURE."
with self.assertRaisesRegex(UserError, error):
po_line.write({"product_qty": 14})
6 changes: 6 additions & 0 deletions setup/purchase_propagate_qty_mrp/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

0 comments on commit d8e06d3

Please sign in to comment.