Skip to content

Commit 91787ae

Browse files
committed
[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#87451 X-original-commit: f259d10 Signed-off-by: Tiffany Chang <tic@odoo.com> Signed-off-by: Adrien Widart <awt@odoo.com>
1 parent 8c6b1a1 commit 91787ae

File tree

2 files changed

+191
-4
lines changed

2 files changed

+191
-4
lines changed

addons/mrp_subcontracting_dropshipping/tests/test_sale_dropshipping.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class TestSaleDropshippingFlows(TestMrpSubcontractingCommon):
1212
def setUpClass(cls):
1313
super().setUpClass()
1414

15+
cls.supplier = cls.env["res.partner"].create({"name": "Supplier"})
1516
cls.customer = cls.env["res.partner"].create({"name": "Customer"})
1617
cls.dropship_route = cls.env.ref('stock_dropshipping.route_drop_shipping')
1718

@@ -71,3 +72,181 @@ def test_dropship_with_different_suppliers(self):
7172
# Cancel the second one
7273
sale_order.picking_ids[1].action_cancel()
7374
self.assertEqual(sale_order.order_line.qty_delivered, 1)
75+
76+
def test_return_kit_and_delivered_qty(self):
77+
"""
78+
Sell a kit thanks to the dropshipping route, return it then deliver it again
79+
The delivered quantity should be correctly computed
80+
"""
81+
compo, kit = self.env['product.product'].create([{
82+
'name': n,
83+
'type': 'consu',
84+
'route_ids': [(6, 0, [self.dropship_route.id])],
85+
'seller_ids': [(0, 0, {'name': self.supplier.id})],
86+
} for n in ['Compo', 'Kit']])
87+
88+
self.env['mrp.bom'].create({
89+
'product_tmpl_id': kit.product_tmpl_id.id,
90+
'product_qty': 1,
91+
'type': 'phantom',
92+
'bom_line_ids': [
93+
(0, 0, {'product_id': compo.id, 'product_qty': 1}),
94+
],
95+
})
96+
97+
sale_order = self.env['sale.order'].create({
98+
'partner_id': self.customer.id,
99+
'picking_policy': 'direct',
100+
'order_line': [
101+
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
102+
],
103+
})
104+
sale_order.action_confirm()
105+
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
106+
self.assertEqual(sale_order.order_line.qty_delivered, 0.0)
107+
108+
picking = sale_order.picking_ids
109+
action = picking.button_validate()
110+
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
111+
wizard.process()
112+
self.assertEqual(sale_order.order_line.qty_delivered, 1.0)
113+
114+
for case in ['return', 'deliver again']:
115+
delivered_before_case = 1.0 if case == 'return' else 0.0
116+
delivered_after_case = 0.0 if case == 'return' else 1.0
117+
return_form = Form(self.env['stock.return.picking'].with_context(active_ids=[picking.id], active_id=picking.id, active_model='stock.picking'))
118+
return_wizard = return_form.save()
119+
action = return_wizard.create_returns()
120+
picking = self.env['stock.picking'].browse(action['res_id'])
121+
self.assertEqual(sale_order.order_line.qty_delivered, delivered_before_case, "Incorrect delivered qty for case '%s'" % case)
122+
123+
action = picking.button_validate()
124+
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
125+
wizard.process()
126+
self.assertEqual(sale_order.order_line.qty_delivered, delivered_after_case, "Incorrect delivered qty for case '%s'" % case)
127+
128+
def test_partial_return_kit_and_delivered_qty(self):
129+
"""
130+
Suppose a kit with 4x the same dropshipped component
131+
Suppose a complex delivery process:
132+
- Deliver 2 (with backorder)
133+
- Return 2
134+
- Deliver 1 (with backorder)
135+
- Deliver 1 (process "done")
136+
- Deliver 1 (from the return)
137+
- Deliver 1 (from the return)
138+
The test checks the all-or-nothing policy of the delivered quantity
139+
This quantity should be 1.0 after the last delivery
140+
"""
141+
compo, kit = self.env['product.product'].create([{
142+
'name': n,
143+
'type': 'consu',
144+
'route_ids': [(6, 0, [self.dropship_route.id])],
145+
'seller_ids': [(0, 0, {'name': self.supplier.id})],
146+
} for n in ['Compo', 'Kit']])
147+
148+
self.env['mrp.bom'].create({
149+
'product_tmpl_id': kit.product_tmpl_id.id,
150+
'product_qty': 1,
151+
'type': 'phantom',
152+
'bom_line_ids': [
153+
(0, 0, {'product_id': compo.id, 'product_qty': 4}),
154+
],
155+
})
156+
157+
sale_order = self.env['sale.order'].create({
158+
'partner_id': self.customer.id,
159+
'picking_policy': 'direct',
160+
'order_line': [
161+
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
162+
],
163+
})
164+
sale_order.action_confirm()
165+
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
166+
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 0/4")
167+
168+
picking01 = sale_order.picking_ids
169+
picking01.move_lines.quantity_done = 2
170+
action = picking01.button_validate()
171+
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
172+
wizard.process()
173+
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 2/4")
174+
175+
# Create a return of picking01 (with both components)
176+
return_form = Form(self.env['stock.return.picking'].with_context(active_id=picking01.id, active_model='stock.picking'))
177+
wizard = return_form.save()
178+
wizard.product_return_moves.write({'quantity': 2.0})
179+
res = wizard.create_returns()
180+
return01 = self.env['stock.picking'].browse(res['res_id'])
181+
182+
return01.move_lines.quantity_done = 2
183+
return01.button_validate()
184+
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 0/4")
185+
186+
picking02 = picking01.backorder_ids
187+
picking02.move_lines.quantity_done = 1
188+
action = picking02.button_validate()
189+
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
190+
wizard.process()
191+
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 1/4")
192+
193+
picking03 = picking02.backorder_ids
194+
picking03.move_lines.quantity_done = 1
195+
picking03.button_validate()
196+
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 2/4")
197+
198+
# Create a return of return01 (with 1 component)
199+
return_form = Form(self.env['stock.return.picking'].with_context(active_id=return01.id, active_model='stock.picking'))
200+
wizard = return_form.save()
201+
wizard.product_return_moves.write({'quantity': 1.0})
202+
res = wizard.create_returns()
203+
picking04 = self.env['stock.picking'].browse(res['res_id'])
204+
205+
picking04.move_lines.quantity_done = 1
206+
picking04.button_validate()
207+
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 3/4")
208+
209+
# Create a second return of return01 (with 1 component, the last one)
210+
return_form = Form(self.env['stock.return.picking'].with_context(active_id=return01.id, active_model='stock.picking'))
211+
wizard = return_form.save()
212+
wizard.product_return_moves.write({'quantity': 1.0})
213+
res = wizard.create_returns()
214+
picking04 = self.env['stock.picking'].browse(res['res_id'])
215+
216+
picking04.move_lines.quantity_done = 1
217+
picking04.button_validate()
218+
self.assertEqual(sale_order.order_line.qty_delivered, 1, "Delivered components: 4/4")
219+
220+
def test_cancelled_picking_and_delivered_qty(self):
221+
"""
222+
The delivered quantity should be zero if all SM are cancelled
223+
"""
224+
compo, kit = self.env['product.product'].create([{
225+
'name': n,
226+
'type': 'consu',
227+
'route_ids': [(6, 0, [self.dropship_route.id])],
228+
'seller_ids': [(0, 0, {'name': self.supplier.id})],
229+
} for n in ['Compo', 'Kit']])
230+
231+
self.env['mrp.bom'].create({
232+
'product_tmpl_id': kit.product_tmpl_id.id,
233+
'product_qty': 1,
234+
'type': 'phantom',
235+
'bom_line_ids': [
236+
(0, 0, {'product_id': compo.id, 'product_qty': 1}),
237+
],
238+
})
239+
240+
sale_order = self.env['sale.order'].create({
241+
'partner_id': self.customer.id,
242+
'picking_policy': 'direct',
243+
'order_line': [
244+
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
245+
],
246+
})
247+
sale_order.action_confirm()
248+
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
249+
self.assertEqual(sale_order.order_line.qty_delivered, 0.0)
250+
251+
sale_order.picking_ids.action_cancel()
252+
self.assertEqual(sale_order.order_line.qty_delivered, 0.0)

addons/sale_mrp/models/sale.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Part of Odoo. See LICENSE file for full copyright and licensing details.
33

44
from odoo import api, fields, models, _
5+
from odoo.tools import float_compare
56

67

78
class SaleOrder(models.Model):
@@ -89,17 +90,24 @@ def _compute_qty_delivered(self):
8990
(b.product_tmpl_id == order_line.product_id.product_tmpl_id and not b.product_id)))
9091
if relevant_bom:
9192
# In case of dropship, we use a 'all or nothing' policy since 'bom_line_id' was
92-
# not written on a move coming from a PO.
93+
# not written on a move coming from a PO: all moves (to customer) must be done
94+
# and the returns must be delivered back to the customer
9395
# FIXME: if the components of a kit have different suppliers, multiple PO
9496
# are generated. If one PO is confirmed and all the others are in draft, receiving
9597
# the products for this PO will set the qty_delivered. We might need to check the
9698
# state of all PO as well... but sale_mrp doesn't depend on purchase.
9799
if dropship:
98100
moves = order_line.move_ids.filtered(lambda m: m.state != 'cancel')
99-
if moves and all(m.state == 'done' for m in moves):
100-
order_line.qty_delivered = order_line.product_uom_qty
101+
if any((m.location_dest_id.usage == 'customer' and m.state != 'done')
102+
or (m.location_dest_id.usage != 'customer'
103+
and m.state == 'done'
104+
and float_compare(m.quantity_done,
105+
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'),
106+
precision_rounding=m.product_uom.rounding) > 0)
107+
for m in moves) or not moves:
108+
order_line.qty_delivered = 0
101109
else:
102-
order_line.qty_delivered = 0.0
110+
order_line.qty_delivered = order_line.product_uom_qty
103111
continue
104112
moves = order_line.move_ids.filtered(lambda m: m.state == 'done' and not m.scrapped)
105113
filters = {

0 commit comments

Comments
 (0)