934 lines
39 KiB
Python
934 lines
39 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from datetime import timedelta
|
||
|
|
||
|
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||
|
from odoo.tests.common import Form
|
||
|
from odoo.tests import tagged
|
||
|
from odoo import fields
|
||
|
from odoo.fields import Command
|
||
|
|
||
|
|
||
|
@tagged('post_install', '-at_install')
|
||
|
class TestPurchaseMrpFlow(AccountTestInvoicingCommon):
|
||
|
|
||
|
@classmethod
|
||
|
def setUpClass(cls, chart_template_ref=None):
|
||
|
super().setUpClass(chart_template_ref=chart_template_ref)
|
||
|
# 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.warehouse = cls.env['stock.warehouse'].search([('company_id', '=', cls.env.company.id)])
|
||
|
cls.stock_location = cls.warehouse.lot_stock_id
|
||
|
|
||
|
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.quantity = quantities_to_process[move.product_id]
|
||
|
move.picked = True
|
||
|
|
||
|
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.quantity = qty_to_process[comp][0]
|
||
|
move._action_done()
|
||
|
|
||
|
def test_kit_component_cost(self):
|
||
|
# Set kit and componnet product to automated FIFO
|
||
|
self.kit_1.categ_id.property_cost_method = 'fifo'
|
||
|
self.kit_1.categ_id.property_valuation = 'real_time'
|
||
|
|
||
|
self.kit_1.bom_ids.product_qty = 3
|
||
|
|
||
|
po = Form(self.env['purchase.order'])
|
||
|
po.partner_id = self.env['res.partner'].create({'name': 'Testy'})
|
||
|
with po.order_line.new() as line:
|
||
|
line.product_id = self.kit_1
|
||
|
line.product_qty = 120
|
||
|
line.price_unit = 1260
|
||
|
po = po.save()
|
||
|
po.button_confirm()
|
||
|
po.picking_ids.button_validate()
|
||
|
|
||
|
# Unit price equaly dived among bom lines (cost share not set)
|
||
|
# # price further divided by product qty of each component
|
||
|
components = [
|
||
|
self.component_a,
|
||
|
self.component_b,
|
||
|
self.component_c,
|
||
|
]
|
||
|
|
||
|
self.assertEqual(sum([k.standard_price * k.qty_available for k in components]), 120 * 1260)
|
||
|
|
||
|
def test_kit_component_cost_multi_currency(self):
|
||
|
# Set kit and component product to automated FIFO
|
||
|
kit = self._create_product('Kit', self.uom_unit)
|
||
|
cmp = self._create_product('CMP', self.uom_unit)
|
||
|
|
||
|
bom_kit = self.env['mrp.bom'].create({
|
||
|
'product_tmpl_id': kit.product_tmpl_id.id,
|
||
|
'product_qty': 1.0,
|
||
|
'type': 'phantom'
|
||
|
})
|
||
|
self.env['mrp.bom.line'].create({
|
||
|
'product_id': cmp.id,
|
||
|
'product_qty': 3.0,
|
||
|
'bom_id': bom_kit.id})
|
||
|
|
||
|
kit.categ_id.property_cost_method = 'fifo'
|
||
|
kit.categ_id.property_valuation = 'real_time'
|
||
|
|
||
|
mock_currency = self.env['res.currency'].create({
|
||
|
'name': 'MOCK',
|
||
|
'symbol': 'MC',
|
||
|
})
|
||
|
self.env['res.currency.rate'].create({
|
||
|
'name': '2023-01-01',
|
||
|
'company_rate': 100.0,
|
||
|
'currency_id': mock_currency.id,
|
||
|
'company_id': self.env.company.id,
|
||
|
})
|
||
|
|
||
|
po = Form(self.env['purchase.order'])
|
||
|
po.partner_id = self.env['res.partner'].create({'name': 'Testy'})
|
||
|
po.currency_id = mock_currency
|
||
|
|
||
|
with po.order_line.new() as line:
|
||
|
line.product_id = kit
|
||
|
line.product_qty = 1
|
||
|
line.price_unit = 300.00
|
||
|
|
||
|
po = po.save()
|
||
|
po.button_confirm()
|
||
|
po.picking_ids.button_validate()
|
||
|
|
||
|
layer = po.picking_ids.move_ids.stock_valuation_layer_ids
|
||
|
self.assertEqual(layer.unit_cost, 1)
|
||
|
|
||
|
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': qty_to_process, 'picked': True})
|
||
|
|
||
|
# 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
|
||
|
return_pick.button_validate()
|
||
|
|
||
|
# 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': 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.warehouse
|
||
|
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['res.partner'].create({'name': 'Test Partner'})
|
||
|
|
||
|
# 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.")
|
||
|
|
||
|
def test_procurement_with_preferred_route(self):
|
||
|
"""
|
||
|
3-steps receipts. Suppose a product that has both buy and manufacture
|
||
|
routes. The user runs an orderpoint with the preferred route defined to
|
||
|
"Buy". A purchase order should be generated.
|
||
|
"""
|
||
|
self.warehouse.reception_steps = 'three_steps'
|
||
|
|
||
|
manu_route = self.warehouse.manufacture_pull_id.route_id
|
||
|
buy_route = self.warehouse.buy_pull_id.route_id
|
||
|
|
||
|
# un-prioritize the buy rules
|
||
|
self.env['stock.rule'].search([]).sequence = 1
|
||
|
buy_route.rule_ids.sequence = 2
|
||
|
|
||
|
vendor = self.env['res.partner'].create({'name': 'super vendor'})
|
||
|
|
||
|
product = self.env['product.product'].create({
|
||
|
'name': 'super product',
|
||
|
'type': 'product',
|
||
|
'seller_ids': [(0, 0, {'partner_id': vendor.id})],
|
||
|
'route_ids': [(4, manu_route.id), (4, buy_route.id)],
|
||
|
})
|
||
|
|
||
|
rr = self.env['stock.warehouse.orderpoint'].create({
|
||
|
'name': product.name,
|
||
|
'location_id': self.warehouse.lot_stock_id.id,
|
||
|
'product_id': product.id,
|
||
|
'product_min_qty': 1,
|
||
|
'product_max_qty': 1,
|
||
|
'route_id': buy_route.id,
|
||
|
})
|
||
|
rr.action_replenish()
|
||
|
|
||
|
move_stock, move_check = self.env['stock.move'].search([('product_id', '=', product.id)])
|
||
|
|
||
|
self.assertRecordValues(move_check | move_stock, [
|
||
|
{'location_id': self.warehouse.wh_input_stock_loc_id.id, 'location_dest_id': self.warehouse.wh_qc_stock_loc_id.id, 'state': 'waiting', 'move_dest_ids': move_stock.ids},
|
||
|
{'location_id': self.warehouse.wh_qc_stock_loc_id.id, 'location_dest_id': self.warehouse.lot_stock_id.id, 'state': 'waiting', 'move_dest_ids': []},
|
||
|
])
|
||
|
|
||
|
po = self.env['purchase.order'].search([('partner_id', '=', vendor.id)])
|
||
|
self.assertTrue(po)
|
||
|
|
||
|
po.button_confirm()
|
||
|
move_in = po.picking_ids.move_ids
|
||
|
self.assertEqual(move_in.move_dest_ids.ids, move_check.ids)
|
||
|
|
||
|
def test_procurement_with_preferred_route_2(self):
|
||
|
"""
|
||
|
Check that the route set in the product is taken into account
|
||
|
when the product have a supplier and bom.
|
||
|
"""
|
||
|
manu_route = self.warehouse.manufacture_pull_id.route_id
|
||
|
buy_route = self.warehouse.buy_pull_id.route_id
|
||
|
|
||
|
vendor = self.env['res.partner'].create({'name': 'super vendor'})
|
||
|
|
||
|
product = self.env['product.product'].create({
|
||
|
'name': 'super product',
|
||
|
'type': 'product',
|
||
|
'seller_ids': [(0, 0, {'partner_id': vendor.id})],
|
||
|
'route_ids': buy_route,
|
||
|
})
|
||
|
self.env['mrp.bom'].create({
|
||
|
'product_tmpl_id': product.product_tmpl_id.id,
|
||
|
'product_qty': 1.0,
|
||
|
'product_uom_id': product.uom_id.id,
|
||
|
})
|
||
|
# create a need of the product with a picking
|
||
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||
|
picking = self.env['stock.picking'].create({
|
||
|
'location_id': warehouse.lot_stock_id.id,
|
||
|
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||
|
'picking_type_id': warehouse.out_type_id.id,
|
||
|
'move_ids': [(0, 0, {
|
||
|
'name': product.name,
|
||
|
'product_id': product.id,
|
||
|
'product_uom': product.uom_id.id,
|
||
|
'product_uom_qty': 1,
|
||
|
'location_id': warehouse.lot_stock_id.id,
|
||
|
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||
|
})]
|
||
|
})
|
||
|
picking.action_assign()
|
||
|
self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
|
||
|
orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product.id)])
|
||
|
self.assertEqual(orderpoint_product.route_id, buy_route, "The route buy should be set on the orderpoint")
|
||
|
# Delete the orderpoint to generate a new one with the manufacture route
|
||
|
orderpoint_product.unlink()
|
||
|
# switch the product route to manufacture
|
||
|
product.write({'route_ids': [(3, buy_route.id), (4, manu_route.id)]})
|
||
|
self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
|
||
|
orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product.id)])
|
||
|
self.assertEqual(orderpoint_product.route_id, manu_route, "The route manufacture should be set on the orderpoint")
|
||
|
|
||
|
def test_compute_bom_days_00(self):
|
||
|
"""Check Days to prepare Manufacturing Order are correctly computed when
|
||
|
Security Lead Time and Days to Purchase are set.
|
||
|
"""
|
||
|
purchase_route = self.env.ref("purchase_stock.route_warehouse0_buy")
|
||
|
manufacture_route = self.env['stock.route'].search([('name', '=', 'Manufacture')])
|
||
|
vendor = self.env['res.partner'].create({'name': 'super vendor'})
|
||
|
|
||
|
company_1 = self.kit_parent.bom_ids.company_id
|
||
|
company_2 = self.env['res.company'].create({
|
||
|
'name': 'TestCompany2',
|
||
|
})
|
||
|
|
||
|
company_1.po_lead = 0
|
||
|
company_1.days_to_purchase = 0
|
||
|
company_1.manufacturing_lead = 0
|
||
|
company_2.po_lead = 0
|
||
|
company_2.days_to_purchase = 0
|
||
|
company_2.manufacturing_lead = 0
|
||
|
|
||
|
components = self.component_a | self.component_b | self.component_c | self.component_d | self.component_e | self.component_f | self.component_g
|
||
|
kits = self.kit_parent | self.kit_1 | self.kit_2 | self.kit_3
|
||
|
kits.route_ids = [(6, 0, manufacture_route.ids)]
|
||
|
components.write({
|
||
|
'route_ids': [(6, 0, purchase_route.ids)],
|
||
|
'seller_ids': [(0, 0, {
|
||
|
'partner_id': vendor.id,
|
||
|
'min_qty': 1,
|
||
|
'price': 1,
|
||
|
'delay': 1,
|
||
|
})],
|
||
|
})
|
||
|
|
||
|
bom_kit_parent = self.kit_parent.bom_ids
|
||
|
bom_kit_parent.action_compute_bom_days()
|
||
|
self.assertEqual(bom_kit_parent.days_to_prepare_mo, 1)
|
||
|
|
||
|
# set "Security Lead Time" for Purchase and manufacturing, and "Days to Purchase"
|
||
|
company_1.po_lead = 10
|
||
|
company_1.days_to_purchase = 10
|
||
|
company_1.manufacturing_lead = 10
|
||
|
company_2.po_lead = 20
|
||
|
company_2.days_to_purchase = 20
|
||
|
company_2.manufacturing_lead = 20
|
||
|
|
||
|
# check "Security Lead Time" and "Days to Purchase" will also be included if bom has company_id
|
||
|
bom_kit_parent.action_compute_bom_days()
|
||
|
self.assertEqual(bom_kit_parent.days_to_prepare_mo, 10 + 10 + 10 + 10 + 1)
|
||
|
|
||
|
self.kit_1.bom_ids.company_id = company_2
|
||
|
bom_kit_parent.action_compute_bom_days()
|
||
|
self.assertEqual(bom_kit_parent.days_to_prepare_mo, 20 + 20 + 20 + 10 + 1)
|
||
|
|
||
|
# check "Security Lead Time" and "Days to Purchase" will won't be included if bom doesn't have company_id
|
||
|
kits.bom_ids.company_id = False
|
||
|
bom_kit_parent.action_compute_bom_days()
|
||
|
self.assertEqual(bom_kit_parent.days_to_prepare_mo, 1)
|
||
|
|
||
|
def test_orderpoint_with_manufacture_security_lead_time(self):
|
||
|
"""
|
||
|
Test that a manufacturing order is created with the correct date_start
|
||
|
when we have an order point with the preferred route set to "manufacture"
|
||
|
and the current company has a manufacturing security lead time set.
|
||
|
"""
|
||
|
# set purchase security lead time to 20 days
|
||
|
self.env.company.po_lead = 20
|
||
|
# set manufacturing security lead time to 25 days
|
||
|
self.env.company.manufacturing_lead = 25
|
||
|
product = self.env['product.product'].create({
|
||
|
'name': 'super product',
|
||
|
'type': 'product',
|
||
|
#set route to manufacture + buy
|
||
|
'route_ids': [
|
||
|
(4, self.env.ref('mrp.route_warehouse0_manufacture').id),
|
||
|
(4, self.env.ref('purchase_stock.route_warehouse0_buy').id)
|
||
|
],
|
||
|
'seller_ids': [(0, 0, {
|
||
|
'partner_id': self.env['res.partner'].create({'name': 'super vendor'}).id,
|
||
|
'min_qty': 1,
|
||
|
'price': 1,
|
||
|
})],
|
||
|
})
|
||
|
self.env['mrp.bom'].create({
|
||
|
'product_tmpl_id': product.product_tmpl_id.id,
|
||
|
'produce_delay': 1,
|
||
|
'product_qty': 1,
|
||
|
})
|
||
|
# create a orderpoint to generate a need of the product with perefered route manufacture
|
||
|
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||
|
'product_id': product.id,
|
||
|
'qty_to_order': 5,
|
||
|
'warehouse_id': self.warehouse.id,
|
||
|
'route_id': self.env.ref('mrp.route_warehouse0_manufacture').id,
|
||
|
})
|
||
|
# lead_days_date should be today + manufacturing security lead time + product manufacturing lead time
|
||
|
self.assertEqual(orderpoint.lead_days_date, (fields.Date.today() + timedelta(days=25) + timedelta(days=1)))
|
||
|
orderpoint.action_replenish()
|
||
|
mo = self.env['mrp.production'].search([('product_id', '=', product.id)])
|
||
|
self.assertEqual(mo.product_uom_qty, 5)
|
||
|
self.assertEqual(mo.date_start.date(), fields.Date.today())
|
||
|
|
||
|
def test_mo_overview(self):
|
||
|
component = self.env['product.product'].create({
|
||
|
'name': 'component',
|
||
|
'type': 'product',
|
||
|
'standard_price': 80,
|
||
|
'seller_ids': [(0, 0, {
|
||
|
'partner_id': self.env['res.partner'].create({'name': 'super vendor'}).id,
|
||
|
'min_qty': 3,
|
||
|
'price': 10,
|
||
|
})],
|
||
|
})
|
||
|
finished_product = self.env['product.product'].create({
|
||
|
'name': 'finished_product',
|
||
|
'type': 'product',
|
||
|
})
|
||
|
self.env['mrp.bom'].create({
|
||
|
'product_tmpl_id': finished_product.product_tmpl_id.id,
|
||
|
'product_qty': 1,
|
||
|
'bom_line_ids': [(0, 0, {
|
||
|
'product_id': component.id,
|
||
|
'product_qty': 2,
|
||
|
'product_uom_id': component.uom_id.id
|
||
|
})],
|
||
|
})
|
||
|
mo = self.env['mrp.production'].create({
|
||
|
'product_id': finished_product.id,
|
||
|
'product_qty': 1,
|
||
|
'product_uom_id': finished_product.uom_id.id,
|
||
|
})
|
||
|
self.env.flush_all() # flush to correctly build report
|
||
|
report_values = self.env['report.mrp.report_mo_overview']._get_report_data(mo.id)['components'][0]['summary']
|
||
|
self.assertEqual(report_values['name'], component.name)
|
||
|
self.assertEqual(report_values['quantity'], 2)
|
||
|
self.assertEqual(report_values['mo_cost'], 160)
|
||
|
# Create a second MO with the minimum seller quantity to check that the cost is correctly calculated using the seller's price
|
||
|
mo_2 = self.env['mrp.production'].create({
|
||
|
'product_id': finished_product.id,
|
||
|
'product_qty': 2,
|
||
|
'product_uom_id': finished_product.uom_id.id,
|
||
|
})
|
||
|
self.env.flush_all()
|
||
|
report_values = self.env['report.mrp.report_mo_overview']._get_report_data(mo_2.id)['components'][0]['summary']
|
||
|
self.assertEqual(report_values['quantity'], 4)
|
||
|
self.assertEqual(report_values['mo_cost'], 40)
|
||
|
|
||
|
def test_bom_report_incoming_po(self):
|
||
|
""" Test report bom structure with duplicated components
|
||
|
With enough stock for the first line and two incoming
|
||
|
POs for the second line and third line.
|
||
|
"""
|
||
|
location = self.stock_location
|
||
|
uom_unit = self.env.ref('uom.product_uom_unit')
|
||
|
final_product_tmpl = self.env['product.template'].create({'name': 'Final Product', 'type': 'product'})
|
||
|
component_product = self.env['product.product'].create({'name': 'Compo 1', 'type': 'product'})
|
||
|
|
||
|
self.env['stock.quant']._update_available_quantity(component_product, location, 3.0)
|
||
|
|
||
|
bom = self.env['mrp.bom'].create({
|
||
|
'product_tmpl_id': final_product_tmpl.id,
|
||
|
'product_uom_id': self.uom_unit.id,
|
||
|
'product_qty': 1.0,
|
||
|
'type': 'normal',
|
||
|
'bom_line_ids': [
|
||
|
Command.create({
|
||
|
'product_id': component_product.id,
|
||
|
'product_qty': 3,
|
||
|
'product_uom_id': uom_unit.id,
|
||
|
}),
|
||
|
Command.create({
|
||
|
'product_id': component_product.id,
|
||
|
'product_qty': 3,
|
||
|
'product_uom_id': uom_unit.id,
|
||
|
}),
|
||
|
Command.create({
|
||
|
'product_id': component_product.id,
|
||
|
'product_qty': 4,
|
||
|
'product_uom_id': uom_unit.id,
|
||
|
})
|
||
|
]
|
||
|
})
|
||
|
def create_order(product_id, partner_id, date_order):
|
||
|
f = Form(self.env['purchase.order'])
|
||
|
f.partner_id = partner_id
|
||
|
f.date_order = date_order
|
||
|
with f.order_line.new() as line:
|
||
|
line.product_id = product_id
|
||
|
line.product_qty = 3.0
|
||
|
line.price_unit = 10
|
||
|
return f.save()
|
||
|
partner = self.env['res.partner'].create({'name': 'My Test Partner'})
|
||
|
# Create and confirm two POs with 3 component_product at different date
|
||
|
po_today = create_order(component_product, partner, fields.Datetime.now())
|
||
|
po_5days = create_order(component_product, partner, fields.Datetime.now() + timedelta(days=5))
|
||
|
|
||
|
po_today.button_confirm()
|
||
|
po_5days.button_confirm()
|
||
|
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom.id)
|
||
|
line_values = report_values['lines']['components'][0]
|
||
|
self.assertEqual(line_values['availability_state'], 'estimated', 'The merged components should be estimated.')
|
||
|
|
||
|
def test_bom_report_incoming_po2(self):
|
||
|
""" Test report bom structure with duplicated components
|
||
|
With an incoming PO for the first and second line.
|
||
|
"""
|
||
|
uom_unit = self.env.ref('uom.product_uom_unit')
|
||
|
final_product_tmpl = self.env['product.template'].create({'name': 'Final Product', 'type': 'product'})
|
||
|
component_product = self.env['product.product'].create({'name': 'Compo 1', 'type': 'product'})
|
||
|
|
||
|
bom = self.env['mrp.bom'].create({
|
||
|
'product_tmpl_id': final_product_tmpl.id,
|
||
|
'product_uom_id': self.uom_unit.id,
|
||
|
'product_qty': 1.0,
|
||
|
'type': 'normal',
|
||
|
'bom_line_ids': [
|
||
|
Command.create({
|
||
|
'product_id': component_product.id,
|
||
|
'product_qty': 3,
|
||
|
'product_uom_id': uom_unit.id,
|
||
|
}),
|
||
|
Command.create({
|
||
|
'product_id': component_product.id,
|
||
|
'product_qty': 3,
|
||
|
'product_uom_id': uom_unit.id,
|
||
|
}),
|
||
|
]
|
||
|
})
|
||
|
partner = self.env['res.partner'].create({'name': 'My Test Partner'})
|
||
|
# Create and confirm one PO with 6 component_products.
|
||
|
f = Form(self.env['purchase.order'])
|
||
|
f.partner_id = partner
|
||
|
f.date_order = fields.Datetime.now()
|
||
|
with f.order_line.new() as line:
|
||
|
line.product_id = component_product
|
||
|
line.product_qty = 6.0
|
||
|
line.price_unit = 10
|
||
|
po_today = f.save()
|
||
|
po_today.button_confirm()
|
||
|
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom.id)
|
||
|
line_values = report_values['lines']['components'][0]
|
||
|
self.assertEqual(line_values['availability_state'], 'expected', 'The first component should be expected as there is an incoming PO.')
|