followup of rev [0]
If these wizards are called on multiple pickings, display the list of
the pickings that could be impacted and allow to select which one should
be impacted.
We also adapt the sanity checks at the start of `button_validte` in
order to specify the concerned pickings if needed. We do not enable the
multi behavior for batch at the moment, so it's only enabled for the
validate multi in the list view.
[0] 6ab4b0d496
task-2069646
closes odoo/odoo#41497
X-original-commit: ff276c6484b138982f185ec9c832f2dbf25baaef
Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
412 lines
16 KiB
Python
412 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo.tests.common import Form, TransactionCase
|
|
from odoo.tests import Form
|
|
|
|
|
|
class TestSaleMrpFlow(TransactionCase):
|
|
|
|
def setUp(self):
|
|
super(TestSaleMrpFlow, self).setUp()
|
|
# Useful models
|
|
self.UoM = self.env['uom.uom']
|
|
self.categ_unit = self.env.ref('uom.product_uom_categ_unit')
|
|
self.categ_kgm = self.env.ref('uom.product_uom_categ_kgm')
|
|
self.stock_location = self.env.ref('stock.stock_location_stock')
|
|
self.warehouse = self.env.ref('stock.warehouse0')
|
|
|
|
self.uom_kg = self.env['uom.uom'].search([('category_id', '=', self.categ_kgm.id), ('uom_type', '=', 'reference')],
|
|
limit=1)
|
|
self.uom_kg.write({
|
|
'name': 'Test-KG',
|
|
'rounding': 0.000001})
|
|
self.uom_gm = self.UoM.create({
|
|
'name': 'Test-G',
|
|
'category_id': self.categ_kgm.id,
|
|
'uom_type': 'smaller',
|
|
'factor': 1000.0,
|
|
'rounding': 0.001})
|
|
self.uom_unit = self.env['uom.uom'].search(
|
|
[('category_id', '=', self.categ_unit.id), ('uom_type', '=', 'reference')], limit=1)
|
|
self.uom_unit.write({
|
|
'name': 'Test-Unit',
|
|
'rounding': 0.01})
|
|
self.uom_dozen = self.UoM.create({
|
|
'name': 'Test-DozenA',
|
|
'category_id': self.categ_unit.id,
|
|
'factor_inv': 12,
|
|
'uom_type': 'bigger',
|
|
'rounding': 0.001})
|
|
|
|
# Creating all components
|
|
self.component_a = self._create_product('Comp A', self.uom_unit)
|
|
self.component_b = self._create_product('Comp B', self.uom_unit)
|
|
self.component_c = self._create_product('Comp C', self.uom_unit)
|
|
self.component_d = self._create_product('Comp D', self.uom_unit)
|
|
self.component_e = self._create_product('Comp E', self.uom_unit)
|
|
self.component_f = self._create_product('Comp F', self.uom_unit)
|
|
self.component_g = self._create_product('Comp G', self.uom_unit)
|
|
|
|
# Create a kit 'kit_1' :
|
|
# -----------------------
|
|
#
|
|
# kit_1 --|- component_a x2
|
|
# |- component_b x1
|
|
# |- component_c x3
|
|
|
|
self.kit_1 = self._create_product('Kit 1', self.uom_unit)
|
|
|
|
self.bom_kit_1 = self.env['mrp.bom'].create({
|
|
'product_tmpl_id': self.kit_1.product_tmpl_id.id,
|
|
'product_qty': 1.0,
|
|
'type': 'phantom'})
|
|
|
|
BomLine = self.env['mrp.bom.line']
|
|
BomLine.create({
|
|
'product_id': self.component_a.id,
|
|
'product_qty': 2.0,
|
|
'bom_id': self.bom_kit_1.id})
|
|
BomLine.create({
|
|
'product_id': self.component_b.id,
|
|
'product_qty': 1.0,
|
|
'bom_id': self.bom_kit_1.id})
|
|
BomLine.create({
|
|
'product_id': self.component_c.id,
|
|
'product_qty': 3.0,
|
|
'bom_id': self.bom_kit_1.id})
|
|
|
|
# Create a kit 'kit_parent' :
|
|
# ---------------------------
|
|
#
|
|
# kit_parent --|- kit_2 x2 --|- component_d x1
|
|
# | |- kit_1 x2 -------|- component_a x2
|
|
# | |- component_b x1
|
|
# | |- component_c x3
|
|
# |
|
|
# |- kit_3 x1 --|- component_f x1
|
|
# | |- component_g x2
|
|
# |
|
|
# |- component_e x1
|
|
|
|
# Creating all kits
|
|
self.kit_2 = self._create_product('Kit 2', self.uom_unit)
|
|
self.kit_3 = self._create_product('kit 3', self.uom_unit)
|
|
self.kit_parent = self._create_product('Kit Parent', self.uom_unit)
|
|
|
|
# Linking the kits and the components via some 'phantom' BoMs
|
|
bom_kit_2 = self.env['mrp.bom'].create({
|
|
'product_tmpl_id': self.kit_2.product_tmpl_id.id,
|
|
'product_qty': 1.0,
|
|
'type': 'phantom'})
|
|
|
|
BomLine.create({
|
|
'product_id': self.component_d.id,
|
|
'product_qty': 1.0,
|
|
'bom_id': bom_kit_2.id})
|
|
BomLine.create({
|
|
'product_id': self.kit_1.id,
|
|
'product_qty': 2.0,
|
|
'bom_id': bom_kit_2.id})
|
|
|
|
bom_kit_parent = self.env['mrp.bom'].create({
|
|
'product_tmpl_id': self.kit_parent.product_tmpl_id.id,
|
|
'product_qty': 1.0,
|
|
'type': 'phantom'})
|
|
|
|
BomLine.create({
|
|
'product_id': self.component_e.id,
|
|
'product_qty': 1.0,
|
|
'bom_id': bom_kit_parent.id})
|
|
BomLine.create({
|
|
'product_id': self.kit_2.id,
|
|
'product_qty': 2.0,
|
|
'bom_id': bom_kit_parent.id})
|
|
|
|
bom_kit_3 = self.env['mrp.bom'].create({
|
|
'product_tmpl_id': self.kit_3.product_tmpl_id.id,
|
|
'product_qty': 1.0,
|
|
'type': 'phantom'})
|
|
|
|
BomLine.create({
|
|
'product_id': self.component_f.id,
|
|
'product_qty': 1.0,
|
|
'bom_id': bom_kit_3.id})
|
|
BomLine.create({
|
|
'product_id': self.component_g.id,
|
|
'product_qty': 2.0,
|
|
'bom_id': bom_kit_3.id})
|
|
|
|
BomLine.create({
|
|
'product_id': self.kit_3.id,
|
|
'product_qty': 2.0,
|
|
'bom_id': bom_kit_parent.id})
|
|
|
|
def _create_product(self, name, uom_id, routes=()):
|
|
p = Form(self.env['product.product'])
|
|
p.name = name
|
|
p.type = 'product'
|
|
p.uom_id = uom_id
|
|
p.uom_po_id = uom_id
|
|
p.route_ids.clear()
|
|
for r in routes:
|
|
p.route_ids.add(r)
|
|
return p.save()
|
|
|
|
# Helper to process quantities based on a dict following this structure :
|
|
#
|
|
# qty_to_process = {
|
|
# product_id: qty
|
|
# }
|
|
|
|
def _process_quantities(self, moves, quantities_to_process):
|
|
""" Helper to process quantities based on a dict following this structure :
|
|
qty_to_process = {
|
|
product_id: qty
|
|
}
|
|
"""
|
|
moves_to_process = moves.filtered(lambda m: m.product_id in quantities_to_process.keys())
|
|
for move in moves_to_process:
|
|
move.write({'quantity_done': quantities_to_process[move.product_id]})
|
|
|
|
def _assert_quantities(self, moves, quantities_to_process):
|
|
""" Helper to check expected quantities based on a dict following this structure :
|
|
qty_to_process = {
|
|
product_id: qty
|
|
...
|
|
}
|
|
"""
|
|
moves_to_process = moves.filtered(lambda m: m.product_id in quantities_to_process.keys())
|
|
for move in moves_to_process:
|
|
self.assertEqual(move.product_uom_qty, quantities_to_process[move.product_id])
|
|
|
|
def _create_move_quantities(self, qty_to_process, components, warehouse):
|
|
""" Helper to creates moves in order to update the quantities of components
|
|
on a specific warehouse. This ensure that all compute fields are triggered.
|
|
The structure of qty_to_process should be the following :
|
|
|
|
qty_to_process = {
|
|
component: (qty, uom),
|
|
...
|
|
}
|
|
"""
|
|
for comp in components:
|
|
f = Form(self.env['stock.move'])
|
|
f.name = 'Test Receipt Components'
|
|
f.location_id = self.env.ref('stock.stock_location_suppliers')
|
|
f.location_dest_id = warehouse.lot_stock_id
|
|
f.product_id = comp
|
|
f.product_uom = qty_to_process[comp][1]
|
|
f.product_uom_qty = qty_to_process[comp][0]
|
|
move = f.save()
|
|
move._action_confirm()
|
|
move._action_assign()
|
|
move_line = move.move_line_ids[0]
|
|
move_line.qty_done = qty_to_process[comp][0]
|
|
move._action_done()
|
|
|
|
def test_01_sale_mrp_kit_qty_delivered(self):
|
|
""" Test that the quantities delivered are correct when
|
|
a kit with subkits is ordered with multiple backorders and returns
|
|
"""
|
|
|
|
# 'kit_parent' structure:
|
|
# ---------------------------
|
|
#
|
|
# kit_parent --|- kit_2 x2 --|- component_d x1
|
|
# | |- kit_1 x2 -------|- component_a x2
|
|
# | |- component_b x1
|
|
# | |- component_c x3
|
|
# |
|
|
# |- kit_3 x1 --|- component_f x1
|
|
# | |- component_g x2
|
|
# |
|
|
# |- component_e x1
|
|
|
|
# Creation of a sale order for x7 kit_parent
|
|
partner = self.env['res.partner'].create({'name': 'My Test Partner'})
|
|
f = Form(self.env['purchase.order'])
|
|
f.partner_id = partner
|
|
with f.order_line.new() as line:
|
|
line.product_id = self.kit_parent
|
|
line.product_qty = 7.0
|
|
line.price_unit = 10
|
|
|
|
po = f.save()
|
|
po.button_confirm()
|
|
|
|
# Check picking creation, its move lines should concern
|
|
# only components. Also checks that the quantities are corresponding
|
|
# to the PO
|
|
self.assertEqual(len(po.picking_ids), 1)
|
|
order_line = po.order_line[0]
|
|
picking_original = po.picking_ids[0]
|
|
move_lines = picking_original.move_lines
|
|
products = move_lines.mapped('product_id')
|
|
kits = [self.kit_parent, self.kit_3, self.kit_2, self.kit_1]
|
|
components = [self.component_a, self.component_b, self.component_c, self.component_d, self.component_e,
|
|
self.component_f, self.component_g]
|
|
expected_quantities = {
|
|
self.component_a: 56.0,
|
|
self.component_b: 28.0,
|
|
self.component_c: 84.0,
|
|
self.component_d: 14.0,
|
|
self.component_e: 7.0,
|
|
self.component_f: 14.0,
|
|
self.component_g: 28.0
|
|
}
|
|
|
|
self.assertEqual(len(move_lines), 7)
|
|
self.assertTrue(not any(kit in products for kit in kits))
|
|
self.assertTrue(all(component in products for component in components))
|
|
self._assert_quantities(move_lines, expected_quantities)
|
|
|
|
# Process only 7 units of each component
|
|
qty_to_process = 7
|
|
move_lines.write({'quantity_done': qty_to_process})
|
|
|
|
# Create a backorder for the missing componenents
|
|
pick = po.picking_ids[0]
|
|
res = pick.button_validate()
|
|
Form(self.env[res['res_model']].with_context(res['context'])).save().process()
|
|
|
|
# Check that a backorded is created
|
|
self.assertEqual(len(po.picking_ids), 2)
|
|
backorder_1 = po.picking_ids - picking_original
|
|
self.assertEqual(backorder_1.backorder_id.id, picking_original.id)
|
|
|
|
# Even if some components are received completely,
|
|
# no KitParent should be received
|
|
self.assertEqual(order_line.qty_received, 0)
|
|
|
|
# Process just enough components to make 1 kit_parent
|
|
qty_to_process = {
|
|
self.component_a: 1,
|
|
self.component_c: 5,
|
|
}
|
|
self._process_quantities(backorder_1.move_lines, qty_to_process)
|
|
|
|
# Create a backorder for the missing componenents
|
|
res = backorder_1.button_validate()
|
|
Form(self.env[res['res_model']].with_context(res['context'])).save().process()
|
|
|
|
# Only 1 kit_parent should be received at this point
|
|
self.assertEqual(order_line.qty_received, 1)
|
|
|
|
# Check that the second backorder is created
|
|
self.assertEqual(len(po.picking_ids), 3)
|
|
backorder_2 = po.picking_ids - picking_original - backorder_1
|
|
self.assertEqual(backorder_2.backorder_id.id, backorder_1.id)
|
|
|
|
# Set the components quantities that backorder_2 should have
|
|
expected_quantities = {
|
|
self.component_a: 48,
|
|
self.component_b: 21,
|
|
self.component_c: 72,
|
|
self.component_d: 7,
|
|
self.component_f: 7,
|
|
self.component_g: 21
|
|
}
|
|
|
|
# Check that the computed quantities are matching the theorical ones.
|
|
# Since component_e was totally processed, this componenent shouldn't be
|
|
# present in backorder_2
|
|
self.assertEqual(len(backorder_2.move_lines), 6)
|
|
move_comp_e = backorder_2.move_lines.filtered(lambda m: m.product_id.id == self.component_e.id)
|
|
self.assertFalse(move_comp_e)
|
|
self._assert_quantities(backorder_2.move_lines, expected_quantities)
|
|
|
|
# Process enough components to make x3 kit_parents
|
|
qty_to_process = {
|
|
self.component_a: 16,
|
|
self.component_b: 5,
|
|
self.component_c: 24,
|
|
self.component_g: 5
|
|
}
|
|
self._process_quantities(backorder_2.move_lines, qty_to_process)
|
|
|
|
# Create a backorder for the missing componenents
|
|
res = backorder_2.button_validate()
|
|
Form(self.env[res['res_model']].with_context(res['context'])).save().process()
|
|
|
|
# Check that x3 kit_parents are indeed received
|
|
self.assertEqual(order_line.qty_received, 3)
|
|
|
|
# Check that the third backorder is created
|
|
self.assertEqual(len(po.picking_ids), 4)
|
|
backorder_3 = po.picking_ids - (picking_original + backorder_1 + backorder_2)
|
|
self.assertEqual(backorder_3.backorder_id.id, backorder_2.id)
|
|
|
|
# Check the components quantities that backorder_3 should have
|
|
expected_quantities = {
|
|
self.component_a: 32,
|
|
self.component_b: 16,
|
|
self.component_c: 48,
|
|
self.component_d: 7,
|
|
self.component_f: 7,
|
|
self.component_g: 16
|
|
}
|
|
self._assert_quantities(backorder_3.move_lines, expected_quantities)
|
|
|
|
# Process all missing components
|
|
self._process_quantities(backorder_3.move_lines, expected_quantities)
|
|
|
|
# Validating the last backorder now it's complete.
|
|
# All kits should be received
|
|
backorder_3.button_validate()
|
|
self.assertEqual(order_line.qty_received, 7.0)
|
|
|
|
# Return all components processed by backorder_3
|
|
stock_return_picking_form = Form(self.env['stock.return.picking']
|
|
.with_context(active_ids=backorder_3.ids, active_id=backorder_3.ids[0],
|
|
active_model='stock.picking'))
|
|
return_wiz = stock_return_picking_form.save()
|
|
for return_move in return_wiz.product_return_moves:
|
|
return_move.write({
|
|
'quantity': expected_quantities[return_move.product_id],
|
|
'to_refund': True
|
|
})
|
|
res = return_wiz.create_returns()
|
|
return_pick = self.env['stock.picking'].browse(res['res_id'])
|
|
|
|
# Process all components and validate the picking
|
|
wiz_act = return_pick.button_validate()
|
|
wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
|
|
wiz.process()
|
|
|
|
# Now quantity received should be 3 again
|
|
self.assertEqual(order_line.qty_received, 3)
|
|
|
|
stock_return_picking_form = Form(self.env['stock.return.picking']
|
|
.with_context(active_ids=return_pick.ids, active_id=return_pick.ids[0],
|
|
active_model='stock.picking'))
|
|
return_wiz = stock_return_picking_form.save()
|
|
for move in return_wiz.product_return_moves:
|
|
move.quantity = expected_quantities[move.product_id]
|
|
res = return_wiz.create_returns()
|
|
return_of_return_pick = self.env['stock.picking'].browse(res['res_id'])
|
|
|
|
# Process all components except one of each
|
|
for move in return_of_return_pick.move_lines:
|
|
move.write({
|
|
'quantity_done': expected_quantities[move.product_id] - 1,
|
|
'to_refund': True
|
|
})
|
|
|
|
wiz_act = return_of_return_pick.button_validate()
|
|
wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
|
|
wiz.process()
|
|
|
|
# As one of each component is missing, only 6 kit_parents should be received
|
|
self.assertEqual(order_line.qty_received, 6)
|
|
|
|
# Check that the 4th backorder is created.
|
|
self.assertEqual(len(po.picking_ids), 7)
|
|
backorder_4 = po.picking_ids - (
|
|
picking_original + backorder_1 + backorder_2 + backorder_3 + return_of_return_pick + return_pick)
|
|
self.assertEqual(backorder_4.backorder_id.id, return_of_return_pick.id)
|
|
|
|
# Check the components quantities that backorder_4 should have
|
|
for move in backorder_4.move_lines:
|
|
self.assertEqual(move.product_qty, 1)
|