Files
odoo_source/addons/purchase_mrp/tests/test_purchase_mrp_flow.py
T
Arnold Moyaux 35d6c58f86 [REF] purchase_stock, stock_account: remove price difference account
Invoice a different amount than on the purchase order will not
use the price difference account anymore. Instead it will create
a layer with the difference. It means the stock valuation will be
base on real quantity invoiced rather than the purchase order.

Since the price different account does not exists:
- Invoice correction is only done on `account.move.line` linked to a
  `purchase.order.line`. It means that additional lines won't be
 corrected and will debit the stock_input instead (that should be empty
 manually)

Part-of: odoo/odoo#99411
2022-09-12 13:49:16 +02:00

519 lines
21 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 tagged
from odoo import fields
@tagged('post_install', '-at_install')
class TestPurchaseMrpFlow(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Useful models
cls.UoM = cls.env['uom.uom']
cls.categ_unit = cls.env.ref('uom.product_uom_categ_unit')
cls.categ_kgm = cls.env.ref('uom.product_uom_categ_kgm')
cls.stock_location = cls.env.ref('stock.stock_location_stock')
cls.warehouse = cls.env.ref('stock.warehouse0')
grp_uom = cls.env.ref('uom.group_uom')
group_user = cls.env.ref('base.group_user')
group_user.write({'implied_ids': [(4, grp_uom.id)]})
cls.env.user.write({'groups_id': [(4, grp_uom.id)]})
cls.uom_kg = cls.env['uom.uom'].search([('category_id', '=', cls.categ_kgm.id), ('uom_type', '=', 'reference')],
limit=1)
cls.uom_kg.write({
'name': 'Test-KG',
'rounding': 0.000001})
cls.uom_gm = cls.UoM.create({
'name': 'Test-G',
'category_id': cls.categ_kgm.id,
'uom_type': 'smaller',
'factor': 1000.0,
'rounding': 0.001})
cls.uom_unit = cls.env['uom.uom'].search(
[('category_id', '=', cls.categ_unit.id), ('uom_type', '=', 'reference')], limit=1)
cls.uom_unit.write({
'name': 'Test-Unit',
'rounding': 0.01})
cls.uom_dozen = cls.UoM.create({
'name': 'Test-DozenA',
'category_id': cls.categ_unit.id,
'factor_inv': 12,
'uom_type': 'bigger',
'rounding': 0.001})
# Creating all components
cls.component_a = cls._create_product('Comp A', cls.uom_unit)
cls.component_b = cls._create_product('Comp B', cls.uom_unit)
cls.component_c = cls._create_product('Comp C', cls.uom_unit)
cls.component_d = cls._create_product('Comp D', cls.uom_unit)
cls.component_e = cls._create_product('Comp E', cls.uom_unit)
cls.component_f = cls._create_product('Comp F', cls.uom_unit)
cls.component_g = cls._create_product('Comp G', cls.uom_unit)
# Create a kit 'kit_1' :
# -----------------------
#
# kit_1 --|- component_a x2
# |- component_b x1
# |- component_c x3
cls.kit_1 = cls._create_product('Kit 1', cls.uom_unit)
cls.bom_kit_1 = cls.env['mrp.bom'].create({
'product_tmpl_id': cls.kit_1.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom'})
BomLine = cls.env['mrp.bom.line']
BomLine.create({
'product_id': cls.component_a.id,
'product_qty': 2.0,
'bom_id': cls.bom_kit_1.id})
BomLine.create({
'product_id': cls.component_b.id,
'product_qty': 1.0,
'bom_id': cls.bom_kit_1.id})
BomLine.create({
'product_id': cls.component_c.id,
'product_qty': 3.0,
'bom_id': cls.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
cls.kit_2 = cls._create_product('Kit 2', cls.uom_unit)
cls.kit_3 = cls._create_product('kit 3', cls.uom_unit)
cls.kit_parent = cls._create_product('Kit Parent', cls.uom_unit)
# Linking the kits and the components via some 'phantom' BoMs
bom_kit_2 = cls.env['mrp.bom'].create({
'product_tmpl_id': cls.kit_2.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom'})
BomLine.create({
'product_id': cls.component_d.id,
'product_qty': 1.0,
'bom_id': bom_kit_2.id})
BomLine.create({
'product_id': cls.kit_1.id,
'product_qty': 2.0,
'bom_id': bom_kit_2.id})
bom_kit_parent = cls.env['mrp.bom'].create({
'product_tmpl_id': cls.kit_parent.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom'})
BomLine.create({
'product_id': cls.component_e.id,
'product_qty': 1.0,
'bom_id': bom_kit_parent.id})
BomLine.create({
'product_id': cls.kit_2.id,
'product_qty': 2.0,
'bom_id': bom_kit_parent.id})
bom_kit_3 = cls.env['mrp.bom'].create({
'product_tmpl_id': cls.kit_3.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom'})
BomLine.create({
'product_id': cls.component_f.id,
'product_qty': 1.0,
'bom_id': bom_kit_3.id})
BomLine.create({
'product_id': cls.component_g.id,
'product_qty': 2.0,
'bom_id': bom_kit_3.id})
BomLine.create({
'product_id': cls.kit_3.id,
'product_qty': 2.0,
'bom_id': bom_kit_parent.id})
@classmethod
def _create_product(cls, name, uom_id, routes=()):
p = Form(cls.env['product.product'])
p.name = name
p.detailed_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_ids = picking_original.move_ids
products = move_ids.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_ids), 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_ids, expected_quantities)
# Process only 7 units of each component
qty_to_process = 7
move_ids.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_ids, 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_ids), 6)
move_comp_e = backorder_2.move_ids.filtered(lambda m: m.product_id.id == self.component_e.id)
self.assertFalse(move_comp_e)
self._assert_quantities(backorder_2.move_ids, 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_ids, 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_ids, expected_quantities)
# Process all missing components
self._process_quantities(backorder_3.move_ids, 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_ids:
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_ids:
self.assertEqual(move.product_qty, 1)
def test_concurent_procurements(self):
""" Check a production created to fulfill a procurement will not
replenish more that needed if others procurements have the same products
than the production component. """
warehouse = self.env.ref('stock.warehouse0')
buy_route = warehouse.buy_pull_id.route_id
manufacture_route = warehouse.manufacture_pull_id.route_id
vendor1 = self.env['res.partner'].create({'name': 'aaa', 'email': 'from.test@example.com'})
supplier_info1 = self.env['product.supplierinfo'].create({
'partner_id': vendor1.id,
'price': 50,
})
component = self.env['product.product'].create({
'name': 'component',
'type': 'product',
'route_ids': [(4, buy_route.id)],
'seller_ids': [(6, 0, [supplier_info1.id])],
})
finished = self.env['product.product'].create({
'name': 'finished',
'type': 'product',
'route_ids': [(4, manufacture_route.id)],
})
self.env['stock.warehouse.orderpoint'].create({
'name': 'A RR',
'location_id': warehouse.lot_stock_id.id,
'product_id': component.id,
'route_id': buy_route.id,
'product_min_qty': 0,
'product_max_qty': 0,
})
self.env['stock.warehouse.orderpoint'].create({
'name': 'A RR',
'location_id': warehouse.lot_stock_id.id,
'product_id': finished.id,
'route_id': manufacture_route.id,
'product_min_qty': 0,
'product_max_qty': 0,
})
self.env['mrp.bom'].create({
'product_id': finished.id,
'product_tmpl_id': finished.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1.0,
'consumption': 'flexible',
'operation_ids': [
],
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': component.id, 'product_qty': 1}),
]})
# Delivery to trigger replenishment
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = warehouse.out_type_id
with picking_form.move_ids_without_package.new() as move:
move.product_id = finished
move.product_uom_qty = 3
with picking_form.move_ids_without_package.new() as move:
move.product_id = component
move.product_uom_qty = 2
picking = picking_form.save()
picking.action_confirm()
# Find PO
purchase = self.env['purchase.order.line'].search([
('product_id', '=', component.id),
]).order_id
self.assertTrue(purchase)
self.assertEqual(purchase.order_line.product_qty, 5)
def test_01_purchase_mrp_kit_qty_change(self):
self.partner = self.env.ref('base.res_partner_1')
# Create a PO with one unit of the kit product
self.po = self.env['purchase.order'].create({
'partner_id': self.partner.id,
'order_line': [(0, 0, {'name': self.kit_1.name, 'product_id': self.kit_1.id, 'product_qty': 1, 'product_uom': self.kit_1.uom_id.id, 'price_unit': 60.0, 'date_planned': fields.Datetime.now()})],
})
# Validate the PO
self.po.button_confirm()
# Check the component qty in the created picking
self.assertEqual(self.po.picking_ids.move_ids_without_package[0].product_uom_qty, 2, "The quantity of components must be created according to the BOM")
self.assertEqual(self.po.picking_ids.move_ids_without_package[1].product_uom_qty, 1, "The quantity of components must be created according to the BOM")
self.assertEqual(self.po.picking_ids.move_ids_without_package[2].product_uom_qty, 3, "The quantity of components must be created according to the BOM")
# Update the kit quantity in the PO
self.po.order_line[0].product_qty = 2
# Check the component qty after the update
self.assertEqual(self.po.picking_ids.move_ids_without_package[0].product_uom_qty, 4, "The amount of the kit components must be updated when changing the quantity of the kit.")
self.assertEqual(self.po.picking_ids.move_ids_without_package[1].product_uom_qty, 2, "The amount of the kit components must be updated when changing the quantity of the kit.")
self.assertEqual(self.po.picking_ids.move_ids_without_package[2].product_uom_qty, 6, "The amount of the kit components must be updated when changing the quantity of the kit.")