[FIX] sale_mrp: return a dropshipped kit
On a SO, the delivered quantity of a dropshipped kit is incorrect in
case of a return
To reproduce the issue:
1. Activate Dropshipping
2. Create 3 products: KIT AB, A, B:
- All have the Dropship route and a supplier set
3. Create a BOM of type kit: KIT AB is composed of A & B
4. Create a SO for 1 unit of KIT AB, validate
5. Validate the created PO
6. Deliver the products
7. Create a return for these products
- Error: On the SO, the delivered quantity is 0 but the return is
not validated yet. This quantity should still be 1
8. Validate the return
- Error: On the SO, the delivered quantity is now 1, it should be 0
The 'all or nothing' policy should correctly consider the returns: all
moves (to customer) must be done and the returns must be delivered back
to the customer
OPW-2759250
closes odoo/odoo#87596
X-original-commit: f259d10db577bbf1b11fffd028e678b38d5f402d
Signed-off-by: Tiffany Chang <tic@odoo.com>
Signed-off-by: Adrien Widart <awt@odoo.com>
This commit is contained in:
@@ -12,6 +12,7 @@ class TestSaleDropshippingFlows(TestMrpSubcontractingCommon):
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.supplier = cls.env["res.partner"].create({"name": "Supplier"})
|
||||
cls.customer = cls.env["res.partner"].create({"name": "Customer"})
|
||||
cls.dropship_route = cls.env.ref('stock_dropshipping.route_drop_shipping')
|
||||
|
||||
@@ -71,3 +72,181 @@ class TestSaleDropshippingFlows(TestMrpSubcontractingCommon):
|
||||
# Cancel the second one
|
||||
sale_order.picking_ids[1].action_cancel()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, 1)
|
||||
|
||||
def test_return_kit_and_delivered_qty(self):
|
||||
"""
|
||||
Sell a kit thanks to the dropshipping route, return it then deliver it again
|
||||
The delivered quantity should be correctly computed
|
||||
"""
|
||||
compo, kit = self.env['product.product'].create([{
|
||||
'name': n,
|
||||
'type': 'consu',
|
||||
'route_ids': [(6, 0, [self.dropship_route.id])],
|
||||
'seller_ids': [(0, 0, {'partner_id': self.supplier.id})],
|
||||
} for n in ['Compo', 'Kit']])
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': compo.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.customer.id,
|
||||
'picking_policy': 'direct',
|
||||
'order_line': [
|
||||
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
|
||||
],
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, 0.0)
|
||||
|
||||
picking = sale_order.picking_ids
|
||||
action = picking.button_validate()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, 1.0)
|
||||
|
||||
for case in ['return', 'deliver again']:
|
||||
delivered_before_case = 1.0 if case == 'return' else 0.0
|
||||
delivered_after_case = 0.0 if case == 'return' else 1.0
|
||||
return_form = Form(self.env['stock.return.picking'].with_context(active_ids=[picking.id], active_id=picking.id, active_model='stock.picking'))
|
||||
return_wizard = return_form.save()
|
||||
action = return_wizard.create_returns()
|
||||
picking = self.env['stock.picking'].browse(action['res_id'])
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, delivered_before_case, "Incorrect delivered qty for case '%s'" % case)
|
||||
|
||||
action = picking.button_validate()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, delivered_after_case, "Incorrect delivered qty for case '%s'" % case)
|
||||
|
||||
def test_partial_return_kit_and_delivered_qty(self):
|
||||
"""
|
||||
Suppose a kit with 4x the same dropshipped component
|
||||
Suppose a complex delivery process:
|
||||
- Deliver 2 (with backorder)
|
||||
- Return 2
|
||||
- Deliver 1 (with backorder)
|
||||
- Deliver 1 (process "done")
|
||||
- Deliver 1 (from the return)
|
||||
- Deliver 1 (from the return)
|
||||
The test checks the all-or-nothing policy of the delivered quantity
|
||||
This quantity should be 1.0 after the last delivery
|
||||
"""
|
||||
compo, kit = self.env['product.product'].create([{
|
||||
'name': n,
|
||||
'type': 'consu',
|
||||
'route_ids': [(6, 0, [self.dropship_route.id])],
|
||||
'seller_ids': [(0, 0, {'partner_id': self.supplier.id})],
|
||||
} for n in ['Compo', 'Kit']])
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': compo.id, 'product_qty': 4}),
|
||||
],
|
||||
})
|
||||
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.customer.id,
|
||||
'picking_policy': 'direct',
|
||||
'order_line': [
|
||||
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
|
||||
],
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 0/4")
|
||||
|
||||
picking01 = sale_order.picking_ids
|
||||
picking01.move_ids.quantity_done = 2
|
||||
action = picking01.button_validate()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 2/4")
|
||||
|
||||
# Create a return of picking01 (with both components)
|
||||
return_form = Form(self.env['stock.return.picking'].with_context(active_id=picking01.id, active_model='stock.picking'))
|
||||
wizard = return_form.save()
|
||||
wizard.product_return_moves.write({'quantity': 2.0})
|
||||
res = wizard.create_returns()
|
||||
return01 = self.env['stock.picking'].browse(res['res_id'])
|
||||
|
||||
return01.move_ids.quantity_done = 2
|
||||
return01.button_validate()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 0/4")
|
||||
|
||||
picking02 = picking01.backorder_ids
|
||||
picking02.move_ids.quantity_done = 1
|
||||
action = picking02.button_validate()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 1/4")
|
||||
|
||||
picking03 = picking02.backorder_ids
|
||||
picking03.move_ids.quantity_done = 1
|
||||
picking03.button_validate()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 2/4")
|
||||
|
||||
# Create a return of return01 (with 1 component)
|
||||
return_form = Form(self.env['stock.return.picking'].with_context(active_id=return01.id, active_model='stock.picking'))
|
||||
wizard = return_form.save()
|
||||
wizard.product_return_moves.write({'quantity': 1.0})
|
||||
res = wizard.create_returns()
|
||||
picking04 = self.env['stock.picking'].browse(res['res_id'])
|
||||
|
||||
picking04.move_ids.quantity_done = 1
|
||||
picking04.button_validate()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 3/4")
|
||||
|
||||
# Create a second return of return01 (with 1 component, the last one)
|
||||
return_form = Form(self.env['stock.return.picking'].with_context(active_id=return01.id, active_model='stock.picking'))
|
||||
wizard = return_form.save()
|
||||
wizard.product_return_moves.write({'quantity': 1.0})
|
||||
res = wizard.create_returns()
|
||||
picking04 = self.env['stock.picking'].browse(res['res_id'])
|
||||
|
||||
picking04.move_ids.quantity_done = 1
|
||||
picking04.button_validate()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, 1, "Delivered components: 4/4")
|
||||
|
||||
def test_cancelled_picking_and_delivered_qty(self):
|
||||
"""
|
||||
The delivered quantity should be zero if all SM are cancelled
|
||||
"""
|
||||
compo, kit = self.env['product.product'].create([{
|
||||
'name': n,
|
||||
'type': 'consu',
|
||||
'route_ids': [(6, 0, [self.dropship_route.id])],
|
||||
'seller_ids': [(0, 0, {'partner_id': self.supplier.id})],
|
||||
} for n in ['Compo', 'Kit']])
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': compo.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.customer.id,
|
||||
'picking_policy': 'direct',
|
||||
'order_line': [
|
||||
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
|
||||
],
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, 0.0)
|
||||
|
||||
sale_order.picking_ids.action_cancel()
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, 0.0)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.tools import float_compare
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
@@ -45,18 +46,24 @@ class SaleOrderLine(models.Model):
|
||||
(b.product_id == order_line.product_id or
|
||||
(b.product_tmpl_id == order_line.product_id.product_tmpl_id and not b.product_id)))
|
||||
if relevant_bom:
|
||||
# In case of dropship, we use a 'all or nothing' policy since 'bom_line_id' was
|
||||
# not written on a move coming from a PO.
|
||||
# not written on a move coming from a PO: all moves (to customer) must be done
|
||||
# and the returns must be delivered back to the customer
|
||||
# FIXME: if the components of a kit have different suppliers, multiple PO
|
||||
# are generated. If one PO is confirmed and all the others are in draft, receiving
|
||||
# the products for this PO will set the qty_delivered. We might need to check the
|
||||
# state of all PO as well... but sale_mrp doesn't depend on purchase.
|
||||
if dropship:
|
||||
moves = order_line.move_ids.filtered(lambda m: m.state != 'cancel')
|
||||
if moves and all(m.state == 'done' for m in moves):
|
||||
order_line.qty_delivered = order_line.product_uom_qty
|
||||
if any((m.location_dest_id.usage == 'customer' and m.state != 'done')
|
||||
or (m.location_dest_id.usage != 'customer'
|
||||
and m.state == 'done'
|
||||
and float_compare(m.quantity_done,
|
||||
sum(sub_m.product_uom._compute_quantity(sub_m.quantity_done, m.product_uom) for sub_m in m.returned_move_ids if sub_m.state == 'done'),
|
||||
precision_rounding=m.product_uom.rounding) > 0)
|
||||
for m in moves) or not moves:
|
||||
order_line.qty_delivered = 0
|
||||
else:
|
||||
order_line.qty_delivered = 0.0
|
||||
order_line.qty_delivered = order_line.product_uom_qty
|
||||
continue
|
||||
moves = order_line.move_ids.filtered(lambda m: m.state == 'done' and not m.scrapped)
|
||||
filters = {
|
||||
|
||||
Reference in New Issue
Block a user