purchase_mrp/tests/test_purchase_mrp_flow.py

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.')