Odoo18-Base/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py
2025-01-06 10:57:38 +07:00

959 lines
45 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from datetime import datetime, timedelta
from freezegun import freeze_time
from json import loads
from odoo import Command
from odoo.exceptions import UserError
from odoo.fields import Date
from odoo.tests import Form, tagged, loaded_demo_data
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
_logger = logging.getLogger(__name__)
@tagged('post_install', '-at_install')
class MrpSubcontractingPurchaseTest(TestMrpSubcontractingCommon):
def setUp(self):
super().setUp()
self.finished2, self.comp3 = self.env['product.product'].create([{
'name': 'SuperProduct',
'is_storable': True,
}, {
'name': 'Component',
'type': 'consu',
}])
self.vendor = self.env['res.partner'].create({
'name': 'Vendor',
'company_id': self.env.ref('base.main_company').id,
})
self.bom_finished2 = self.env['mrp.bom'].create({
'product_tmpl_id': self.finished2.product_tmpl_id.id,
'type': 'subcontract',
'subcontractor_ids': [(6, 0, self.subcontractor_partner1.ids)],
'bom_line_ids': [(0, 0, {
'product_id': self.comp3.id,
'product_qty': 1,
})],
})
def test_count_smart_buttons(self):
resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
(self.comp1 + self.comp2).write({'route_ids': [Command.link(resupply_sub_on_order_route.id)]})
# I create a draft Purchase Order for first in move for 10 kg at 50 euro
po = self.env['purchase.order'].create({
'partner_id': self.subcontractor_partner1.id,
'order_line': [Command.create({
'name': 'finished',
'product_id': self.finished.id,
'product_qty': 1.0,
'product_uom': self.finished.uom_id.id,
'price_unit': 50.0}
)],
})
po.button_confirm()
self.assertEqual(po.subcontracting_resupply_picking_count, 1)
action1 = po.action_view_subcontracting_resupply()
picking = self.env[action1['res_model']].browse(action1['res_id'])
self.assertEqual(picking.subcontracting_source_purchase_count, 1)
action2 = picking.action_view_subcontracting_source_purchase()
po_action2 = self.env[action2['res_model']].browse(action2['res_id'])
self.assertEqual(po_action2, po)
def test_decrease_qty(self):
""" Tests when a PO for a subcontracted product has its qty decreased after confirmation
"""
product_qty = 5.0
po = self.env['purchase.order'].create({
'partner_id': self.subcontractor_partner1.id,
'order_line': [Command.create({
'name': 'finished',
'product_id': self.finished.id,
'product_qty': product_qty,
'product_uom': self.finished.uom_id.id,
'price_unit': 50.0}
)],
})
po.button_confirm()
receipt = po.picking_ids
sub_mo = receipt._get_subcontract_production()
self.assertEqual(len(receipt), 1, "A receipt should have been created")
self.assertEqual(receipt.move_ids.product_qty, product_qty, "Qty of subcontracted product to receive is incorrect")
self.assertEqual(len(sub_mo), 1, "A subcontracting MO should have been created")
self.assertEqual(sub_mo.product_qty, product_qty, "Qty of subcontracted product to produce is incorrect")
# create a neg qty to proprogate to receipt
lower_qty = product_qty - 1.0
po.order_line.product_qty = lower_qty
sub_mos = receipt._get_subcontract_production()
self.assertEqual(receipt.move_ids.product_qty, lower_qty, "Qty of subcontracted product to receive should update (not validated yet)")
self.assertEqual(len(sub_mos), 1, "Original subcontract MO should have absorbed qty change")
self.assertEqual(sub_mo.product_qty, lower_qty, "Qty of subcontract MO should update (none validated yet)")
# increase qty again
po.order_line.product_qty = product_qty
sub_mos = receipt._get_subcontract_production()
self.assertEqual(sum(receipt.move_ids.mapped('product_qty')), product_qty, "Qty of subcontracted product to receive should update (not validated yet)")
self.assertEqual(len(sub_mos), 1, "The subcontracted mo should have been updated")
# check that a neg qty can't proprogate once receipt is done
for move in receipt.move_ids:
move.move_line_ids.quantity = move.product_qty
receipt.move_ids.picked = True
receipt.button_validate()
self.assertEqual(receipt.state, 'done')
self.assertEqual(sub_mos.state, 'done')
with self.assertRaises(UserError):
po.order_line.product_qty = lower_qty
def test_purchase_and_return01(self):
"""
The user buys 10 x a subcontracted product P. He receives the 10
products and then does a return with 3 x P. The test ensures that the
final received quantity is correctly computed
"""
po = self.env['purchase.order'].create({
'partner_id': self.subcontractor_partner1.id,
'order_line': [(0, 0, {
'name': self.finished2.name,
'product_id': self.finished2.id,
'product_uom_qty': 10,
'product_uom': self.finished2.uom_id.id,
'price_unit': 1,
})],
})
po.button_confirm()
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_finished2.id)])
self.assertTrue(mo)
receipt = po.picking_ids
receipt.move_ids.quantity = 10
receipt.move_ids.picked = True
receipt.button_validate()
return_form = Form(self.env['stock.return.picking'].with_context(active_id=receipt.id, active_model='stock.picking'))
return_wizard = return_form.save()
return_wizard.product_return_moves.quantity = 3
return_wizard.product_return_moves.to_refund = True
return_picking = return_wizard._create_return()
return_picking.move_ids.quantity = 3
return_picking.move_ids.picked = True
return_picking.button_validate()
self.assertEqual(self.finished2.qty_available, 7.0)
self.assertEqual(po.order_line.qty_received, 7.0)
def test_purchase_and_return02(self):
"""
The user buys 10 x a subcontracted product P. He receives the 10
products and then does a return with 3 x P (with the flag to_refund
disabled and the subcontracting location as return location). The test
ensures that the final received quantity is correctly computed
"""
grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]})
po = self.env['purchase.order'].create({
'partner_id': self.subcontractor_partner1.id,
'order_line': [(0, 0, {
'name': self.finished2.name,
'product_id': self.finished2.id,
'product_uom_qty': 10,
'product_uom': self.finished2.uom_id.id,
'price_unit': 1,
})],
})
po.button_confirm()
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_finished2.id)])
self.assertTrue(mo)
receipt = po.picking_ids
receipt.move_ids.quantity = 10
receipt.move_ids.picked = True
receipt.button_validate()
return_form = Form(self.env['stock.return.picking'].with_context(active_id=receipt.id, active_model='stock.picking'))
return_wizard = return_form.save()
return_wizard.product_return_moves.quantity = 3
return_wizard.product_return_moves.to_refund = False
return_picking = return_wizard._create_return()
return_picking.move_ids.quantity = 3
return_picking.move_ids.picked = True
return_picking.button_validate()
self.assertEqual(self.finished2.qty_available, 7.0)
self.assertEqual(po.order_line.qty_received, 10.0)
def test_orderpoint_warehouse_not_required(self):
"""
The user creates a subcontracted bom for the product,
then we create a po for the subcontracted bom we are gonna get
orderpoints for the components without warehouse.Notice this is
when our subcontracting location is also a replenish location.
The test ensure that we can get those orderpoints without warehouse.
"""
# Create a second warehouse to check which one will be used
self.env['stock.warehouse'].create({'name': 'Second WH', 'code': 'WH02'})
product = self.env['product.product'].create({
'name': 'Product',
'is_storable': True,
})
component = self.env['product.product'].create({
'name': 'Component',
'is_storable': True,
})
subcontractor = self.env['res.partner'].create({
'name': 'Subcontractor',
'property_stock_subcontractor': self.env.company.subcontracting_location_id.id,
})
self.env.company.subcontracting_location_id.replenish_location = True
self.env['mrp.bom'].create({
'product_tmpl_id': product.product_tmpl_id.id,
'product_qty': 1,
'product_uom_id': product.uom_id.id,
'type': 'subcontract',
'subcontractor_ids': [(subcontractor.id)],
'bom_line_ids': [(0, 0, {
'product_id': component.id,
'product_qty': 1,
'product_uom_id': component.uom_id.id,
})],
})
po = self.env['purchase.order'].create({
'partner_id': subcontractor.id,
'order_line': [(0, 0, {
'product_id': product.id,
'product_qty': 1,
'product_uom': product.uom_id.id,
'name': product.name,
'price_unit': 1,
})],
})
po.button_confirm()
self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
orderpoint = self.env['stock.warehouse.orderpoint'].search([('product_id', '=', component.id)])
self.assertTrue(orderpoint)
self.assertEqual(orderpoint.warehouse_id, self.warehouse)
def test_purchase_and_return03(self):
"""
With 2 steps receipt and an input location child of Physical Location (instead of WH)
The user buys 10 x a subcontracted product P. He receives the 10
products and then does a return with 3 x P. The test ensures that the
final received quantity is correctly computed
"""
# Set 2 steps receipt
self.warehouse.write({"reception_steps": "two_steps"})
# Set 'Input' parent location to 'Physical locations'
physical_locations = self.env.ref("stock.stock_location_locations")
input_location = self.warehouse.wh_input_stock_loc_id
input_location.write({"location_id": physical_locations.id})
# Create Purchase
po = self.env['purchase.order'].create({
'partner_id': self.subcontractor_partner1.id,
'order_line': [(0, 0, {
'name': self.finished2.name,
'product_id': self.finished2.id,
'product_uom_qty': 10,
'product_uom': self.finished2.uom_id.id,
'price_unit': 1,
})],
})
po.button_confirm()
# Receive Products
receipt = po.picking_ids
receipt.move_ids.quantity = 10
receipt.move_ids.picked = True
receipt.button_validate()
self.assertEqual(po.order_line.qty_received, 10.0)
# Return Products
return_form = Form(self.env['stock.return.picking'].with_context(active_id=receipt.id, active_model='stock.picking'))
return_wizard = return_form.save()
return_wizard.product_return_moves.quantity = 3
return_wizard.product_return_moves.to_refund = True
return_picking = return_wizard._create_return()
return_picking.move_ids.quantity = 3
return_picking.move_ids.picked = True
return_picking.button_validate()
self.assertEqual(po.order_line.qty_received, 7.0)
def test_subcontracting_resupply_price_diff(self):
"""Test that the price difference is correctly computed when a subcontracted
product is resupplied.
"""
self.env.company.anglo_saxon_accounting = True
resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
(self.comp1 + self.comp2).write({'route_ids': [(6, None, [resupply_sub_on_order_route.id])]})
product_category_all = self.env.ref('product.product_category_all')
product_category_all.property_cost_method = 'standard'
product_category_all.property_valuation = 'real_time'
self._setup_category_stock_journals()
stock_price_diff_acc_id = self.env['account.account'].create({
'name': 'default_account_stock_price_diff',
'code': 'STOCKDIFF',
'reconcile': True,
'account_type': 'asset_current',
})
product_category_all.property_account_creditor_price_difference_categ = stock_price_diff_acc_id
self.comp1.standard_price = 10.0
self.comp2.standard_price = 20.0
self.finished.standard_price = 100
# Create a PO for 1 finished product.
po_form = Form(self.env['purchase.order'])
po_form.partner_id = self.subcontractor_partner1
with po_form.order_line.new() as po_line:
po_line.product_id = self.finished
po_line.product_qty = 2
po_line.price_unit = 50 # should be 70
po = po_form.save()
po.button_confirm()
action = po.action_view_subcontracting_resupply()
resupply_picking = self.env[action['res_model']].browse(action['res_id'])
resupply_picking.move_ids.quantity = 2
resupply_picking.move_ids.picked = True
resupply_picking.button_validate()
action = po.action_view_picking()
final_picking = self.env[action['res_model']].browse(action['res_id'])
final_picking.move_ids.quantity = 2
final_picking.move_ids.picked = True
final_picking.button_validate()
action = po.action_create_invoice()
invoice = self.env['account.move'].browse(action['res_id'])
invoice.invoice_date = Date.today()
invoice.invoice_line_ids.quantity = 1
invoice.action_post()
# price diff line should be 100 - 50 - 10 - 20
price_diff_line = invoice.line_ids.filtered(lambda m: m.account_id == stock_price_diff_acc_id)
self.assertEqual(price_diff_line.credit, 20)
def test_subcontract_product_price_change(self):
""" Create a PO for subcontracted product, receive the product (finish MO),
create vendor bill and edit the product price, confirm the bill.
An extra SVL should be created to correct the valuation of the product
Also check account move data for real time inventory
"""
product_category_all = self.env.ref('product.product_category_all')
product_category_all.property_cost_method = 'fifo'
product_category_all.property_valuation = 'real_time'
in_account = self.env['account.account'].create({
'name': 'IN Account',
'code': '000001',
'account_type': 'asset_current',
})
out_account = self.env['account.account'].create({
'name': 'OUT Account',
'code': '000002',
'account_type': 'asset_current',
})
valu_account = self.env['account.account'].create({
'name': 'VALU Account',
'code': '000003',
'account_type': 'asset_current',
})
production_cost_account = self.env['account.account'].create({
'name': 'PROD COST Account',
'code': '000004',
'account_type': 'asset_current',
})
product_category_all.property_stock_account_input_categ_id = in_account
product_category_all.property_stock_account_output_categ_id = out_account
product_category_all.property_stock_account_production_cost_id = production_cost_account
product_category_all.property_stock_valuation_account_id = valu_account
stock_in_acc_id = product_category_all.property_stock_account_input_categ_id.id
resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
(self.comp1 + self.comp2).write({'route_ids': [Command.link(resupply_sub_on_order_route.id)]})
purchase_comps = self.env['purchase.order'].create({
'partner_id': self.subcontractor_partner1.id, # can be any partner
'order_line': [
Command.create({
'name': self.comp1.name,
'product_id': self.comp1.id,
'product_uom_qty': 1,
'product_uom': self.finished.uom_id.id,
'price_unit': 10,
}),
Command.create({
'name': self.comp2.name,
'product_id': self.comp2.id,
'product_uom_qty': 1,
'product_uom': self.finished.uom_id.id,
'price_unit': 10,
})
],
})
# recieving comp products will set their invetory valuation (creates SVLs)
purchase_comps.button_confirm()
purchase_comps.picking_ids.move_ids.picked = True
purchase_comps.picking_ids.button_validate()
purchase = self.env['purchase.order'].create({
'partner_id': self.subcontractor_partner1.id,
'order_line': [Command.create({
'name': self.finished.name,
'product_id': self.finished.id,
'product_uom_qty': 1,
'product_uom': self.finished.uom_id.id,
'price_unit': 100,
})],
})
# validate subcontractor resupply
purchase.button_confirm()
resupply_picks = purchase._get_subcontracting_resupplies()
resupply_picks.move_ids.picked = True
resupply_picks.button_validate()
# receive subcontracted product (MO will be done)
receipt = purchase.picking_ids
receipt.move_ids.picked = True
receipt.button_validate()
# create bill
purchase.action_create_invoice()
aml = self.env['account.move.line'].search([('purchase_line_id', '=', purchase.order_line.id)])
# add 50 per unit ( 50 x 1 ) = 50 extra valuation
aml.price_unit = 150
aml.move_id.invoice_date = Date.today()
aml.move_id.action_post()
svl = aml.stock_valuation_layer_ids
self.assertEqual(len(svl), 1)
self.assertEqual(svl.value, 50)
# check for the automated inventory valuation
account_move_credit_line = svl.account_move_id.line_ids.filtered(lambda l: l.credit > 0)
self.assertEqual(account_move_credit_line.account_id.id, stock_in_acc_id)
self.assertEqual(account_move_credit_line.credit, 50)
# Total value of subcontracted product = 150 new price + components (10 + 10)
self.assertEqual(self.finished.total_value, 170)
self.assertEqual(self.finished.standard_price, 170)
def test_return_and_decrease_pol_qty(self):
"""
Buy and receive 10 subcontracted products. Return one. Then adapt the
demand on the PO to 9.
"""
po = self.env['purchase.order'].create({
'partner_id': self.subcontractor_partner1.id,
'order_line': [(0, 0, {
'name': self.finished2.name,
'product_id': self.finished2.id,
'product_qty': 10,
'product_uom': self.finished2.uom_id.id,
'price_unit': 1,
})],
})
po.button_confirm()
receipt = po.picking_ids
receipt.move_ids.quantity = 10
receipt.button_validate()
return_form = Form(self.env['stock.return.picking'].with_context(active_id=receipt.id, active_model='stock.picking'))
wizard = return_form.save()
wizard.product_return_moves.quantity = 1.0
return_picking = wizard._create_return()
return_picking.move_ids.quantity = 1.0
return_picking.button_validate()
pol = po.order_line
pol.product_qty = 9.0
stock_location_id = self.warehouse.lot_stock_id
subco_location_id = self.env.company.subcontracting_location_id
self.assertEqual(pol.qty_received, 9.0)
self.assertEqual(pol.product_qty, 9.0)
self.assertEqual(len(po.picking_ids), 2)
self.assertRecordValues(po.picking_ids.move_ids, [
{'location_dest_id': stock_location_id.id, 'quantity': 10.0, 'state': 'done'},
{'location_dest_id': subco_location_id.id, 'quantity': 1.0, 'state': 'done'},
])
def test_subcontracting_lead_days(self):
""" Test the lead days computation for subcontracting. Subcontracting delay =
max(Vendor lead time, Manufacturing lead time + DTPMO) + Days to Purchase + Purchase security lead time
"""
rule = self.env['stock.rule'].search([('action', '=', 'buy')], limit=1)
self.env.company.manufacturing_lead = 114514 # should never be used
self.env.company.po_lead = 1
self.env.company.days_to_purchase = 2
# Case 1 Vendor lead time >= Manufacturing lead time + DTPMO
seller = self.env['product.supplierinfo'].create({
'product_tmpl_id': self.finished.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'price': 12.0,
'delay': 10
})
self.bom.produce_delay = 3
self.bom.days_to_prepare_mo = 4
delays, _ = rule._get_lead_days(self.finished, supplierinfo=seller)
self.assertEqual(delays['total_delay'], seller.delay + self.env.company.po_lead + self.env.company.days_to_purchase)
# Case 2 Vendor lead time < Manufacturing lead time + DTPMO
self.bom.produce_delay = 5
self.bom.days_to_prepare_mo = 6
delays, _ = rule._get_lead_days(self.finished, supplierinfo=seller)
self.assertEqual(delays['total_delay'], self.bom.produce_delay + self.bom.days_to_prepare_mo + self.env.company.po_lead + self.env.company.days_to_purchase)
def test_subcontracting_lead_days_on_overview(self):
"""Test on the BOM overview, the lead days and resupply availability are
correctly computed. The dtpmo on the bom should be used for the lead days,
while the resupply availability should be based on the calculated dtpmo.
"""
# should never be used
self.env.company.manufacturing_lead = 114514
# should be added in all cases
self.env.company.po_lead = 5
self.env.company.days_to_purchase = 5
buy_route_id = self.ref('purchase_stock.route_warehouse0_buy')
(self.finished | self.comp1 | self.comp2).route_ids = [(6, None, [buy_route_id])]
self.comp2_bom.active = False
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.finished.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'price': 648.0,
'delay': 15
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.comp1.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'price': 648.0,
'delay': 10
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.comp2.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'price': 648.0,
'delay': 6
})
self.bom.produce_delay = 10
self.bom.days_to_prepare_mo = 0
# Case 1: Vendor lead time >= Manufacturing lead time + DTPMO on BOM
bom_data = self.env['report.mrp.report_bom_structure']._get_bom_data(self.bom, self.warehouse, self.finished)
self.assertEqual(bom_data['lead_time'], 15 + 5 + 5 + 0,
"Lead time = Purchase lead time(finished) + Days to Purchase + Purchase security lead time + DTPMO on BOM")
# Resupply delay = 0 (received from MRP, where route type != "manufacture")
# Vendor lead time = 15 (finished product supplier delay)
# Manufacture lead time = 10 (BoM.produce_delay)
# Max purchase component delay = max delay(comp1, comp2) + po_lead + days_to_purchase = 20
self.assertEqual(bom_data['resupply_avail_delay'], 0 + 15 + 20 + 5 + 5,
'Resupply avail delay = Resupply delay + Max(Vendor lead time, Manufacture lead time)'
' + Max purchase component delay + Purchase security lead time + Days to Purchase'
)
# Case 2: Vendor lead time < Manufacturing lead time + DTPMO on BOM
self.bom.action_compute_bom_days()
self.assertEqual(self.bom.days_to_prepare_mo, 10 + 5 + 5,
"DTPMO = Purchase lead time(comp1) + Days to Purchase + Purchase security lead time")
self.bom.days_to_prepare_mo = 10
# Temp increase BoM.produce_delay, to check if it is now used in the final calculation
self.bom.produce_delay = 30
bom_data = self.env['report.mrp.report_bom_structure']._get_bom_data(self.bom, self.warehouse, self.finished)
self.assertEqual(bom_data['lead_time'], 30 + 5 + 5 + 10,
"Lead time = Manufacturing lead time + Days to Purchase + Purchase security lead time + DTPMO on BOM")
# Resupply delay = 0 (received from MRP, where route type != "manufacture")
# Vendor lead time = 15 (finished product supplier delay)
# Manufacture lead time = 30 (BoM.produce_delay)
# Max purchase component delay = max delay(comp1, comp2) + po_lead + days_to_purchase = 20
self.assertEqual(bom_data['resupply_avail_delay'], 0 + 30 + 20 + 5 + 5,
'Resupply avail delay = Resupply delay + Max(Vendor lead time, Manufacture lead time)'
' + Max purchase component delay + Purchase security lead time + Days to Purchase'
)
# Continue the test with the original produce_delay
self.bom.produce_delay = 10
# Update stock for components, calculate DTPMO should be 0
self.env['stock.quant']._update_available_quantity(self.comp1, self.env.company.subcontracting_location_id, 100)
self.env['stock.quant']._update_available_quantity(self.comp2, self.env.company.subcontracting_location_id, 100)
self.env.invalidate_all() # invalidate cache to get updated qty_available
# Case 1: Vendor lead time >= Manufacturing lead time + DTPMO on BOM
self.bom.days_to_prepare_mo = 2
bom_data = self.env['report.mrp.report_bom_structure']._get_bom_data(self.bom, self.warehouse, self.finished)
self.assertEqual(bom_data['lead_time'], 15 + 5 + 5,
"Lead time = Purchase lead time(finished) + Days to Purchase + Purchase security lead time")
for component in bom_data['components']:
self.assertEqual(component['availability_state'], 'available')
# Case 2: Vendor lead time < Manufacturing lead time + DTPMO on BOM
self.bom.action_compute_bom_days()
self.assertEqual(self.bom.days_to_prepare_mo, 10 + 5 + 5,
"DTPMO = Purchase lead time(comp1) + Days to Purchase + Purchase security lead time")
bom_data = self.env['report.mrp.report_bom_structure']._get_bom_data(self.bom, self.warehouse, self.finished)
self.assertEqual(bom_data['lead_time'], 10 + 5 + 5 + 20,
"Lead time = Manufacturing lead time + Days to Purchase + Purchase security lead time + DTPMO on BOM")
for component in bom_data['components']:
self.assertEqual(component['availability_state'], 'available')
def test_resupply_order_buy_mto(self):
""" Test a subcontract component can has resupply on order + buy + mto route"""
mto_route = self.env.ref('stock.route_warehouse0_mto')
mto_route.active = True
resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
(self.comp1 + self.comp2).write({
'route_ids': [
Command.link(resupply_sub_on_order_route.id),
Command.link(self.env.ref('purchase_stock.route_warehouse0_buy').id),
Command.link(mto_route.id)],
'seller_ids': [Command.create({
'partner_id': self.vendor.id,
})],
})
po = self.env['purchase.order'].create({
'partner_id': self.subcontractor_partner1.id,
'order_line': [Command.create({
'name': 'finished',
'product_id': self.finished.id,
'product_qty': 1.0,
'product_uom': self.finished.uom_id.id,
'price_unit': 50.0}
)],
})
po.button_confirm()
ressuply_pick = self.env['stock.picking'].search([('location_dest_id', '=', self.env.company.subcontracting_location_id.id)])
self.assertEqual(len(ressuply_pick.move_ids), 2)
self.assertEqual(ressuply_pick.move_ids.mapped('product_id'), self.comp1 | self.comp2)
# should have create a purchase order for the components
comp_po = self.env['purchase.order'].search([('partner_id', '=', self.vendor.id)])
self.assertEqual(len(comp_po.order_line), 2)
self.assertEqual(comp_po.order_line.mapped('product_id'), self.comp1 | self.comp2)
# confirm the po should create stock moves linked to the resupply
comp_po.button_confirm()
comp_receipt = comp_po.picking_ids
self.assertEqual(comp_receipt.move_ids.move_dest_ids, ressuply_pick.move_ids)
# validate the comp receipt should reserve the resupply
self.assertEqual(ressuply_pick.state, 'waiting')
comp_receipt.move_ids.quantity = 1
comp_receipt.move_ids.picked = True
comp_receipt.button_validate()
self.assertEqual(ressuply_pick.state, 'assigned')
def test_update_qty_purchased_with_subcontracted_product(self):
"""
Test That we can update the quantity of a purchase order line with a subcontracted product
"""
mto_route = self.env.ref('stock.route_warehouse0_mto')
buy_route = self.env['stock.route'].search([('name', '=', 'Buy')])
mto_route.active = True
self.finished.route_ids = mto_route.ids + buy_route.ids
seller = self.env['product.supplierinfo'].create({
'partner_id': self.vendor.id,
'price': 12.0,
'delay': 0
})
self.finished.seller_ids = [(6, 0, [seller.id])]
mo = self.env['mrp.production'].create({
'product_id': self.finished2.id,
'product_qty': 3.0,
'move_raw_ids': [(0, 0, {
'product_id': self.finished.id,
'product_uom_qty': 3.0,
'product_uom': self.finished.uom_id.id,
})]
})
mo.action_confirm()
po = self.env['purchase.order.line'].search([('product_id', '=', self.finished.id)]).order_id
po.button_confirm()
self.assertEqual(len(po.picking_ids), 1)
picking = po.picking_ids
picking.move_ids.quantity = 2.0
# When we validate the picking manually, we create a backorder.
Form.from_action(self.env, picking.button_validate()).save().process()
self.assertEqual(len(po.picking_ids), 2)
picking.backorder_ids.action_cancel()
self.assertEqual(picking.backorder_ids.state, 'cancel')
po.order_line.product_qty = 2.0
self.assertEqual(po.order_line.product_qty, 2.0)
def test_mrp_report_bom_structure_subcontracting_quantities(self):
"""Testing quantities and availablility states in subcontracted BoM report
1. Create a BoM of a finished product with a single component
2. Update the on hand quantity of BoM to 100
3. Move 20 components to subcontracting location
4. Check that the free/on-hand quantity of component is 100 (sum of warehouse stock and subcontracting location stock)
5. Check that producible quantity of 'Product' is equal to only subcontractor location stock
6. Check availability states when:
6a. Search quantity <= subcontractor quantity: component is available
6b. Subcontractor quantity <= search quantity <= total quantity: component is available
6c. Total quantity < search quantity: component is unavailable
"""
search_qty_less_than_or_equal_moved = 10
moved_quantity_to_subcontractor = 20
total_component_quantity = 100
search_qty_more_than_total = 110
resupply_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
finished, component = self.env['product.product'].create([{
'name': 'Finished Product',
'is_storable': True,
'seller_ids': [(0, 0, {'partner_id': self.subcontractor_partner1.id})]
}, {
'name': 'Component',
'is_storable': True,
'route_ids': [(4, resupply_route.id)],
}])
bom = self.env['mrp.bom'].create({
'product_tmpl_id': finished.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'subcontract',
'subcontractor_ids': [(4, self.subcontractor_partner1.id)],
'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1.0})],
})
inventory_wizard = self.env['stock.change.product.qty'].create({
'product_id': component.id,
'product_tmpl_id': component.product_tmpl_id.id,
'new_quantity': total_component_quantity,
})
inventory_wizard.change_product_qty()
# Check quantity was updated
self.assertEqual(component.virtual_available, total_component_quantity)
self.assertEqual(component.qty_available, total_component_quantity)
quantity_before_move = self.env['stock.quant']._get_available_quantity(component, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.warehouse.subcontracting_resupply_type_id
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids_without_package.new() as move:
move.product_id = component
move.product_uom_qty = moved_quantity_to_subcontractor
picking = picking_form.save()
picking.action_confirm()
picking.move_ids.quantity = moved_quantity_to_subcontractor
picking.move_ids.picked = True
picking.button_validate()
quantity_after_move = self.env['stock.quant']._get_available_quantity(component, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
self.assertEqual(quantity_after_move, quantity_before_move + moved_quantity_to_subcontractor)
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom.id, searchQty=search_qty_less_than_or_equal_moved, searchVariant=False)
self.assertEqual(report_values['lines']['components'][0]['quantity_available'], moved_quantity_to_subcontractor)
self.assertEqual(report_values['lines']['components'][0]['quantity_on_hand'], moved_quantity_to_subcontractor)
self.assertEqual(report_values['lines']['quantity_available'], 0)
self.assertEqual(report_values['lines']['quantity_on_hand'], 0)
self.assertEqual(report_values['lines']['producible_qty'], moved_quantity_to_subcontractor)
self.assertEqual(report_values['lines']['stock_avail_state'], 'unavailable')
self.assertEqual(report_values['lines']['components'][0]['stock_avail_state'], 'available')
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom.id, searchQty=search_qty_less_than_or_equal_moved, searchVariant=False)
self.assertEqual(report_values['lines']['components'][0]['stock_avail_state'], 'available')
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom.id, searchQty=search_qty_more_than_total, searchVariant=False)
self.assertEqual(report_values['lines']['components'][0]['stock_avail_state'], 'unavailable')
@freeze_time('2024-01-01')
def test_bom_overview_availability_po_lead(self):
# Create routes for components and the main product
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.finished.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'delay': 10
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.comp1.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'delay': 5
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.comp2.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'delay': 5
})
self.bom.produce_delay = 1
self.bom.days_to_prepare_mo = 3
# Security Lead Time for Purchase should always be added
self.env.company.po_lead = 2
# Add 4 units of each component to subcontractor's location
subcontractor_location = self.env.company.subcontracting_location_id
self.env['stock.quant']._update_available_quantity(self.comp1, subcontractor_location, 4)
self.env['stock.quant']._update_available_quantity(self.comp2, subcontractor_location, 4)
# Generate a report for 3 products: all products should be ready for production
bom_data = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, 3)
self.assertTrue(bom_data['lines']['components_available'])
for component in bom_data['lines']['components']:
self.assertEqual(component['quantity_on_hand'], 4)
self.assertEqual(component['availability_state'], 'available')
self.assertEqual(bom_data['lines']['earliest_capacity'], 3)
# 01/11 + 2 days of Security Lead Time = 01/13
self.assertEqual(bom_data['lines']['earliest_date'], '01/13/2024')
self.assertTrue('leftover_capacity' not in bom_data['lines']['earliest_date'])
self.assertTrue('leftover_date' not in bom_data['lines']['earliest_date'])
# Generate a report for 5 products: only 4 products should be ready for production
bom_data = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, 5)
self.assertFalse(bom_data['lines']['components_available'])
for component in bom_data['lines']['components']:
self.assertEqual(component['quantity_on_hand'], 4)
self.assertEqual(component['availability_state'], 'estimated')
self.assertEqual(bom_data['lines']['earliest_capacity'], 4)
# 01/11 + 2 days of Security Lead Time = 01/13
self.assertEqual(bom_data['lines']['earliest_date'], '01/13/2024')
self.assertEqual(bom_data['lines']['leftover_capacity'], 1)
# 01/16 + 2 x 2 days (for components and for final product) = 01/20
self.assertEqual(bom_data['lines']['leftover_date'], '01/20/2024')
def test_location_after_dest_location_update_backorder_production(self):
"""
Buy 2 subcontracted products.
Receive 1 product after changing the destination location.
Create a backorder.
Receive the last one.
Check the locations.
"""
grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
self.env.user.write({'groups_id': [Command.link(grp_multi_loc.id)]})
subcontract_loc = self.env.company.subcontracting_location_id
production_loc = self.finished.property_stock_production
final_loc = self.env['stock.location'].create({
'name': 'Final location',
'location_id': self.env.ref('stock.warehouse0').lot_stock_id.id,
})
# buy 2 subcontracted products
po = self.env['purchase.order'].create({
'partner_id': self.subcontractor_partner1.id,
'order_line': [Command.create({
'name': self.finished.name,
'product_id': self.finished.id,
'product_qty': 2.0,
'product_uom': self.finished.uom_id.id,
'price_unit': 1.0,
})],
})
po.button_confirm()
receipt = po.picking_ids
# receive 1 subcontracted product
receipt.move_ids.quantity = 1
receipt_form = Form(receipt)
# change the destination location
receipt_form.location_dest_id = final_loc
receipt_form.save()
# change the destination location on the move line too
receipt.move_line_ids.location_dest_id = final_loc
# create the backorder
Form.from_action(self.env, receipt.button_validate()).save().process()
backorder = receipt.backorder_ids
# test the stock quantities after receiving 1 product
stock_quants = self.env['stock.quant'].search([('product_id', '=', self.finished.id)])
self.assertEqual(len(stock_quants), 3)
self.assertEqual(stock_quants.filtered(lambda q: q.location_id == final_loc).quantity, 1.0)
self.assertEqual(stock_quants.filtered(lambda q: q.location_id == subcontract_loc).quantity, 0.0)
self.assertEqual(stock_quants.filtered(lambda q: q.location_id == production_loc).quantity, -1.0)
# receive the last subcontracted product
backorder.move_ids.quantity = 1
backorder.button_validate()
# test the final stock quantities
stock_quants = self.env['stock.quant'].search([('product_id', '=', self.finished.id)])
self.assertEqual(len(stock_quants), 3)
self.assertEqual(stock_quants.filtered(lambda q: q.location_id == final_loc).quantity, 2.0)
self.assertEqual(stock_quants.filtered(lambda q: q.location_id == subcontract_loc).quantity, 0.0)
self.assertEqual(stock_quants.filtered(lambda q: q.location_id == production_loc).quantity, -2.0)
def test_return_subcontracted_product_to_supplier_location(self):
"""
Test that we can return subcontracted product to the supplier location.
"""
po = self.env['purchase.order'].create({
'partner_id': self.subcontractor_partner1.id,
'order_line': [Command.create({
'name': self.finished.name,
'product_id': self.finished.id,
'product_qty': 2.0,
'product_uom': self.finished.uom_id.id,
'price_unit': 10.0,
})],
})
po.button_confirm()
self.assertEqual(len(po.picking_ids), 1)
picking = po.picking_ids
picking.button_validate()
self.assertEqual(picking.state, 'done')
# create a return to the vendor location
supplier_location = self.env.ref('stock.stock_location_suppliers')
return_form = Form(self.env['stock.return.picking'].with_context(active_id=picking.id, active_model='stock.picking'))
wizard = return_form.save()
wizard.product_return_moves.quantity = 2.0
return_picking = wizard._create_return()
return_picking.location_dest_id = supplier_location
return_picking.button_validate()
self.assertEqual(return_picking.state, 'done')
def test_global_visibility_days_affect_lead_time(self):
""" Don't count global visibility days more than once, make sure a PO generated from
replenishment/orderpoint has a sensible planned reception date.
"""
wh = self.env.user._get_default_warehouse_id()
self.finished2.seller_ids = [Command.create({
'partner_id': self.subcontractor_partner1.id,
'delay': 0,
})]
final_product = self.finished2
orderpoint = self.env['stock.warehouse.orderpoint'].create({'product_id': final_product.id})
out_picking = self.env['stock.picking'].create({
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': wh.lot_stock_id.id,
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
'move_ids': [Command.create({
'name': 'TGVDALT out move',
'product_id': final_product.id,
'product_uom_qty': 2,
'location_id': wh.lot_stock_id.id,
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
})],
})
out_picking.with_context(global_visibility_days=365).action_assign()
r = orderpoint.action_stock_replenishment_info()
repl_info = self.env[r['res_model']].browse(r['res_id'])
lead_days_date = datetime.strptime(
loads(repl_info.with_context(global_visibility_days=365).json_lead_days)['lead_days_date'],'%m/%d/%Y').date()
self.assertEqual(lead_days_date, Date.today() + timedelta(days=365))
orderpoint.action_replenish()
purchase_order = self.env['purchase.order'].search([
('order_line', 'any', [
('product_id', '=', self.finished2.id),
]),
], limit=1)
self.assertEqual(purchase_order.date_planned.date(), Date.today())