-
-
Notifications
You must be signed in to change notification settings - Fork 790
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
298 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import purchase_order_line |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* Thierry Ducrest <[email protected]> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
208
purchase_propagate_qty_mrp/tests/test_purchase_propagate_qty_mrp.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}) |
1 change: 1 addition & 0 deletions
1
setup/purchase_propagate_qty_mrp/odoo/addons/purchase_propagate_qty_mrp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../../purchase_propagate_qty_mrp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |