Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix part delivery of non-mrp sale stock, fixes T2163 #6

Open
wants to merge 1 commit into
base: 15.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions product_commingle/models/product_commingled.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,17 @@ class ProductCommingled(models.Model):
),
]

@api.constrains("product_id")
def _check_uom_category(self):
@api.constrains("product_id", "parent_product_id")
def _check_uom(self):
for line in self:
parent_product = line.parent_product_id
lines = line
while lines:
if (
parent_product.uom_id.category_id
!= lines.product_id.uom_id.category_id
):
if parent_product.uom_id != lines.product_id.uom_id:
raise ValidationError(
_(
"You cannot mix and match commingled products with"
" different UoM categories!\nParent: %(parent)s,"
" different UoMs!\nParent: %(parent)s,"
" Child: %(child)s"
)
% {"parent": parent_product, "child": lines.product_id}
Expand Down
19 changes: 19 additions & 0 deletions product_commingle_mrp/tests/test_mrp.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,22 @@ def test_simple_stock_move_nested_inside_kit(self):
picking_id.action_confirm()
self.assertEqual(len(picking_id.move_lines), 1)
self.assertEqual(picking_id.move_lines.product_id, self.product_bolta)

# XXX: Taken directly from sale_mrp/models/sale.py#L112-L115

filters = {
"incoming_moves": lambda m: m.location_dest_id.usage == "customer"
and (
not m.origin_returned_move_id
or (m.origin_returned_move_id and m.to_refund)
),
"outgoing_moves": lambda m: m.location_dest_id.usage != "customer"
and m.to_refund,
}

self.assertEqual(
picking_id.move_lines._compute_kit_quantities(
self.product_bolt_kit, 1, self.bom_equiv_kit, filters
),
1.0,
)
29 changes: 21 additions & 8 deletions product_commingle_sale_stock/models/sale.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,27 @@ class SaleOrderLine(models.Model):

def _compute_qty_delivered(self):
res = super(SaleOrderLine, self)._compute_qty_delivered()
for order_line in self.filtered(lambda l: l.product_id.commingled_ok):
# TODO: we aren't supporting partial delivery of commingled products
# due to the complications of kits within commingled products, and
# how we go about dealing with those.
if all(m.state == "done" for m in order_line.move_ids):
order_line.qty_delivered = order_line.product_uom_qty
else:
order_line.qty_delivered = 0.0

filters = {
"incoming_moves": lambda m: m.location_dest_id.usage == "customer"
and (
not m.origin_returned_move_id
or (m.origin_returned_move_id and m.to_refund)
),
"outgoing_moves": lambda m: m.location_dest_id.usage != "customer"
and m.to_refund,
}

for order_line in self.filtered(
lambda l: l.product_id.commingled_ok
and l.qty_delivered_method == "stock_move"
):
done_moves = order_line.mapped("move_ids").filtered(
lambda m: m.state == "done"
)
order_line.qty_delivered = done_moves._compute_commingled_quantities(
order_line.product_id, filters
)
return res

@api.onchange(
Expand Down
48 changes: 47 additions & 1 deletion product_commingle_stock/models/stock_move.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from odoo import api, fields, models
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools import OrderedSet


Expand All @@ -10,6 +11,11 @@ class StockMove(models.Model):
"Commingled Product",
)

product_commingled_id = fields.Many2one(
"product.commingled",
"Commingled Product",
)

@api.model
def _prepare_merge_moves_distinct_fields(self):
distinct_fields = super()._prepare_merge_moves_distinct_fields()
Expand Down Expand Up @@ -87,6 +93,7 @@ def _prepare_commingled_move_values(self, bom_line, line_data, quantity_done):
if self.commingled_original_product_id.id
else self.product_id.id
),
"product_commingled_id": bom_line.id,
}

def _generate_move_commingled(self, bom_line, line_data, quantity_done=None):
Expand All @@ -100,3 +107,42 @@ def _generate_move_commingled(self, bom_line, line_data, quantity_done=None):
if self.state == "assigned":
vals["state"] = "assigned"
return vals

def _compute_commingled_quantities(self, product_id, filters):
"""
Computes the quantity delivered or received when a commingled item is sold or purchased.
:return: The quantity delivered or received
"""

if not product_id.commingled_ok:
raise UserError(
_("Product %(product_id)s is not commingled!")
% {"product_id": product_id}
)

to_find = self.env["product.commingled"]
todo = product_id.commingled_ids

# FIXME: This does not support differing UoM, this is technically
# not supported by product_commingle anyway, however, this I believe
# is the only section of code that would fail.

while todo:
to_find |= product_id.commingled_ids

todo = (
product_id.commingled_ids.mapped("product_id")
.filtered(lambda p: p.commingled_ok)
.mapped("commingled_ids")
)

candidates = self.filtered(lambda m: m.product_commingled_id in (to_find))

incoming_moves = candidates.filtered(filters["incoming_moves"])
outgoing_moves = candidates.filtered(filters["outgoing_moves"])

qty_processed = sum(incoming_moves.mapped("product_qty")) - sum(
outgoing_moves.mapped("product_qty")
)

return qty_processed
127 changes: 127 additions & 0 deletions product_commingle_stock/tests/test_stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ def setUpClass(cls):
cls.stock_location = cls.env.ref("stock.stock_location_stock")
cls.customer_location = cls.env.ref("stock.stock_location_customers")

cls.outbound_move_filters = {
"incoming_moves": lambda m: m.location_dest_id.usage == "customer"
and (
not m.origin_returned_move_id
or (m.origin_returned_move_id and m.to_refund)
),
"outgoing_moves": lambda m: m.location_dest_id.usage != "customer"
and m.to_refund,
}

def test_strict_default(self):
picking_id = self.env["stock.picking"].create(
{
Expand Down Expand Up @@ -47,6 +57,28 @@ def test_strict_default(self):
picking_id.action_confirm()
self.assertEqual(len(picking_id.move_lines), 1)
self.assertEqual(picking_id.move_lines.product_id, self.product_bolta)
self.assertEqual(
picking_id.move_lines.product_commingled_id,
self.product_bolt_equiv.commingled_ids.filtered(
lambda c: c.product_id == self.product_bolta
),
)
self.assertEqual(
picking_id.move_lines._compute_commingled_quantities(
self.product_bolt_equiv, self.outbound_move_filters
),
1,
)

done_moves = picking_id.move_lines.filtered(lambda m: m.state == "done")

self.assertEqual(
done_moves._compute_commingled_quantities(
self.product_bolt_equiv,
self.outbound_move_filters,
),
0,
)

def test_strict_reordered(self):
self.product_bolt_equiv.commingled_ids.filtered(
Expand Down Expand Up @@ -82,6 +114,27 @@ def test_strict_reordered(self):
picking_id.action_confirm()
self.assertEqual(len(picking_id.move_lines), 1)
self.assertEqual(picking_id.move_lines.product_id, self.product_boltb)
self.assertEqual(
picking_id.move_lines.product_commingled_id,
self.product_bolt_equiv.commingled_ids.filtered(
lambda c: c.product_id == self.product_boltb
),
)
self.assertEqual(
picking_id.move_lines._compute_commingled_quantities(
self.product_bolt_equiv, self.outbound_move_filters
),
1,
)

self.assertEqual(
picking_id.move_lines.filtered(
lambda m: m.state == "done"
)._compute_commingled_quantities(
self.product_bolt_equiv, self.outbound_move_filters
),
0,
)

def test_deplete(self):
self.product_bolt_equiv.write({"commingled_policy": "deplete"})
Expand Down Expand Up @@ -125,6 +178,28 @@ def test_deplete(self):
picking_id.action_confirm()
self.assertEqual(len(picking_id.move_lines), 1)
self.assertEqual(picking_id.move_lines.product_id, self.product_boltb)
self.assertEqual(
picking_id.move_lines.product_commingled_id,
self.product_bolt_equiv.commingled_ids.filtered(
lambda c: c.product_id == self.product_boltb
),
)

self.assertEqual(
picking_id.move_lines._compute_commingled_quantities(
self.product_bolt_equiv, self.outbound_move_filters
),
1,
)

self.assertEqual(
picking_id.move_lines.filtered(
lambda m: m.state == "done"
)._compute_commingled_quantities(
self.product_bolt_equiv, self.outbound_move_filters
),
0,
)

def test_deplete_prefer_homogenous(self):
self.product_bolt_equiv.write(
Expand Down Expand Up @@ -173,6 +248,27 @@ def test_deplete_prefer_homogenous(self):
picking_id.action_confirm()
self.assertEqual(len(picking_id.move_lines), 1)
self.assertEqual(picking_id.move_lines.product_id, self.product_bolta)
self.assertEqual(
picking_id.move_lines.product_commingled_id,
self.product_bolt_equiv.commingled_ids.filtered(
lambda c: c.product_id == self.product_bolta
),
)
self.assertEqual(
picking_id.move_lines._compute_commingled_quantities(
self.product_bolt_equiv, self.outbound_move_filters
),
102,
)

self.assertEqual(
picking_id.move_lines.filtered(
lambda m: m.state == "done"
)._compute_commingled_quantities(
self.product_bolt_equiv, self.outbound_move_filters
),
0,
)

def test_deplete_no_prefer_homogenous(self):
self.product_bolt_equiv.write(
Expand Down Expand Up @@ -220,6 +316,21 @@ def test_deplete_no_prefer_homogenous(self):

picking_id.action_confirm()
self.assertEqual(len(picking_id.move_lines), 2)
self.assertEqual(
picking_id.move_lines._compute_commingled_quantities(
self.product_bolt_equiv, self.outbound_move_filters
),
102,
)

self.assertEqual(
picking_id.move_lines.filtered(
lambda m: m.state == "done"
)._compute_commingled_quantities(
self.product_bolt_equiv, self.outbound_move_filters
),
0,
)

def test_deplete_split(self):
self.product_bolt_equiv.write({"commingled_policy": "deplete"})
Expand Down Expand Up @@ -262,3 +373,19 @@ def test_deplete_split(self):

picking_id.action_confirm()
self.assertEqual(len(picking_id.move_lines), 2)

self.assertEqual(
picking_id.move_lines._compute_commingled_quantities(
self.product_bolt_equiv, self.outbound_move_filters
),
102,
)

self.assertEqual(
picking_id.move_lines.filtered(
lambda m: m.state == "done"
)._compute_commingled_quantities(
self.product_bolt_equiv, self.outbound_move_filters
),
0,
)