Files
odoo_source/addons/purchase_mrp/tests/test_purchase_mrp_flow.py
T
Simon Lejeune 6564c36c74 [REF] stock: immediate and backorder wizard multi
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>
2019-12-06 14:49:38 +00:00

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)