oca-ocb-mrp/odoo-bringout-oca-ocb-mrp_subcontracting/mrp_subcontracting/tests/test_subcontracting.py
Ernad Husremovic 6e65e8c877 19.0 vanilla
2026-03-09 09:31:47 +01:00

1694 lines
86 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from odoo import Command
from odoo.exceptions import AccessError, UserError
from odoo.tests import Form
from odoo.tests.common import TransactionCase
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
from odoo.tests import tagged
from dateutil.relativedelta import relativedelta
@tagged('post_install', '-at_install')
class TestSubcontractingBasic(TransactionCase):
def test_subcontracting_location_1(self):
""" Checks the creation and presence of the subcontracting location. """
self.assertTrue(self.env.company.subcontracting_location_id)
self.assertTrue(self.env.company.subcontracting_location_id.active)
company2 = self.env['res.company'].create({'name': 'Test Company'})
self.assertTrue(company2.subcontracting_location_id)
self.assertTrue(self.env.company.subcontracting_location_id != company2.subcontracting_location_id)
def test_duplicating_warehouses_recreates_their_routes_and_operation_types(self):
""" Duplicating a warehouse should result in the creation of new routes and operation types.
Not reusing the existing routes and operation types"""
wh_original = self.env['stock.warehouse'].search([], limit=1)
wh_copy = wh_original.copy(default={'name': 'Dummy Warehouse (copy)', 'code': 'Dummy'})
if 'buy_to_resupply' in wh_original._fields:
# If purchase is installed, the buy route would be reused instead of duplicated.
wh_original.buy_to_resupply = False
wh_original.manufacture_to_resupply = False
# Check if warehouse routes got RECREATED (instead of reused)
route_types = [
"pbm_route_id",
"subcontracting_route_id",
"reception_route_id",
"delivery_route_id"
]
for route_type in route_types:
original_route_set = wh_original[route_type]
copy_route_set = wh_copy[route_type]
error_message = f"At least one {route_type} (route) got reused on duplication (should have been recreated)"
self.assertEqual(len(original_route_set & copy_route_set), 0, error_message)
# Check if warehouse operation types (picking.type) got RECREATED (instead of reused)
operation_types = [
"subcontracting_type_id",
"subcontracting_resupply_type_id",
"pick_type_id",
"pack_type_id",
"out_type_id",
"in_type_id",
"qc_type_id",
"store_type_id",
"int_type_id"
]
for operation_type in operation_types:
original_type_set = wh_original[operation_type]
copy_type_set = wh_copy[operation_type]
error_message = f"At least one {operation_type} (operation_type) got reused on duplication (should have been recreated)"
self.assertEqual(len(original_type_set & copy_type_set), 0, error_message)
def test_warehouse_subcontracting_resupply_type_code(self):
""" Assert that default operation code of resupply subcontractors is 'internal'. """
warehouse = self.env['stock.warehouse'].create({
'name': 'Warehouse',
'code': 'MYWH'
})
self.assertEqual(warehouse.subcontracting_resupply_type_id.code, 'internal')
@tagged('post_install', '-at_install')
class TestSubcontractingFlows(TestMrpSubcontractingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.warehouse = cls.env['stock.warehouse'].create({
'name': 'Subcontracting Warehouse',
'code': 'SBC',
})
def test_flow_1(self):
""" Don't tick any route on the components and trigger the creation of the subcontracting
manufacturing order through a receipt picking. Create a reordering rule in the
subcontracting locations for a component and run the scheduler to resupply. Checks if the
resupplying actually works
"""
# Check subcontracting picking Type
self.assertTrue(all(self.env['stock.warehouse'].search([]).with_context(active_test=False).mapped('subcontracting_type_id.use_create_components_lots')))
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 1
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
# Nothing should be tracked
self.assertTrue(all(m.product_uom_qty == m.quantity for m in picking_receipt.move_ids))
self.assertEqual(picking_receipt.state, 'assigned')
# Check the created manufacturing order
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEqual(len(mo), 1)
self.assertEqual(len(mo.picking_ids), 1)
wh = picking_receipt.picking_type_id.warehouse_id
self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
self.assertFalse(mo.picking_type_id.active)
picking_receipt.move_ids.quantity = 1
picking_receipt.move_ids.picked = True
picking_receipt.button_validate()
self.assertEqual(mo.state, 'done')
self.assertEqual(mo.origin, picking_receipt.name)
# Available quantities should be negative at the subcontracting location for each components
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
self.assertEqual(avail_qty_comp1, -1)
self.assertEqual(avail_qty_comp2, -1)
self.assertEqual(avail_qty_finished, 1)
# Ensure returns to subcontractor location
return_form = Form(self.env['stock.return.picking'].with_context(active_id=picking_receipt.id, active_model='stock.picking'))
return_wizard = return_form.save()
return_wizard.product_return_moves.quantity = 1
return_picking = return_wizard._create_return()
self.assertEqual(len(return_picking), 1)
self.assertEqual(return_picking.move_ids.location_dest_id, self.subcontractor_partner1.property_stock_subcontractor)
def test_flow_2(self):
""" Tick "Resupply Subcontractor on Order" on the components and trigger the creation of
the subcontracting manufacturing order through a receipt picking. Checks if the resupplying
actually works. Also set a different subcontracting location on the partner.
"""
# Tick "resupply subconractor on order"
resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
(self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
# Create a different subcontract location for partner
partner_subcontract_location = self.env['stock.location'].create({
'name': 'Specific partner location',
'location_id': self.env.company.subcontracting_location_id.id,
'usage': 'internal',
'company_id': self.env.company.id,
})
self.subcontractor_partner1.property_stock_subcontractor = partner_subcontract_location.id
# Add a manufacturing lead time to check that the resupply delivery is correctly planned 2 days
# before the subcontracting receipt
self.bom.produce_delay = 2
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 1
move.quantity = 1
move.picked = True
picking_receipt = picking_form.save()
# Pickings should directly be created
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEqual(len(mo.picking_ids), 1)
self.assertEqual(mo.state, 'confirmed')
self.assertEqual(len(mo.picking_ids.move_ids), 2)
picking = mo.picking_ids
wh = picking.picking_type_id.warehouse_id
# The picking should be a delivery order
self.assertEqual(picking.picking_type_id, wh.subcontracting_resupply_type_id)
# The date planned should be correct
self.assertEqual(picking_receipt.scheduled_date, picking.scheduled_date + relativedelta(days=mo.bom_id.produce_delay))
self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
self.assertFalse(mo.picking_type_id.active)
# No manufacturing order for `self.comp2`
comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
self.assertEqual(len(comp2mo), 0)
picking_receipt.move_ids.quantity = 1
picking_receipt.move_ids.picked = True
picking_receipt.button_validate()
self.assertEqual(mo.state, 'done')
self.assertEqual(mo.origin, picking_receipt.name)
# Available quantities should be negative at the subcontracting location for each components
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
self.assertEqual(avail_qty_comp1, -1)
self.assertEqual(avail_qty_comp2, -1)
self.assertEqual(avail_qty_finished, 1)
avail_qty_comp1_in_global_location = self.env['stock.quant']._get_available_quantity(self.comp1, self.env.company.subcontracting_location_id, allow_negative=True)
avail_qty_comp2_in_global_location = self.env['stock.quant']._get_available_quantity(self.comp2, self.env.company.subcontracting_location_id, allow_negative=True)
self.assertEqual(avail_qty_comp1_in_global_location, -1)
self.assertEqual(avail_qty_comp2_in_global_location, -1)
def test_flow_3(self):
""" Tick "Resupply Subcontractor on Order" and "MTO" on the components and trigger the
creation of the subcontracting manufacturing order through a receipt picking. Checks if the
resupplying actually works. One of the component has also "manufacture" set and a BOM
linked. Checks that an MO is created for this one.
"""
# Tick "resupply subconractor on order"
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])]})
# Tick "manufacture" and MTO on self.comp2
mto_route = self.env.ref('stock.route_warehouse0_mto')
mto_route.active = True
manufacture_route = self.env['stock.route'].search([('name', '=', 'Manufacture')])
self.comp2.write({'route_ids': [(4, manufacture_route.id, None)]})
self.comp2.write({'route_ids': [(4, mto_route.id, None)]})
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 1
move.quantity = 1
move.picked = True
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
# Pickings should directly be created
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEqual(mo.state, 'confirmed')
picking_delivery = mo.picking_ids
self.assertEqual(len(picking_delivery), 1)
self.assertEqual(len(picking_delivery.move_ids), 2)
self.assertEqual(picking_delivery.origin, picking_receipt.name)
self.assertEqual(picking_delivery.partner_id, picking_receipt.partner_id.parent_id)
# The picking should be a delivery order
wh = picking_receipt.picking_type_id.warehouse_id
self.assertEqual(mo.picking_ids.picking_type_id, wh.subcontracting_resupply_type_id)
self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
self.assertFalse(mo.picking_type_id.active)
# As well as a manufacturing order for `self.comp2`
comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
self.assertEqual(len(comp2mo), 1)
picking_receipt.move_ids.quantity = 1
picking_receipt.move_ids.picked = True
picking_receipt.button_validate()
self.assertEqual(mo.state, 'done')
self.assertEqual(mo.origin, picking_receipt.name)
# Available quantities should be negative at the subcontracting location for each components
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
self.assertEqual(avail_qty_comp1, -1)
self.assertEqual(avail_qty_comp2, -1)
self.assertEqual(avail_qty_finished, 1)
def test_flow_4(self):
""" Tick "Manufacture" and "MTO" on the components and trigger the
creation of the subcontracting manufacturing order through a receipt
picking. Checks that the delivery and MO for its components are
automatically created.
"""
# Required for `location_id` to be visible in the view
self.env.user.group_ids += self.env.ref('stock.group_stock_multi_locations')
# Tick "manufacture" and MTO on self.comp2
mto_route = self.env.ref('stock.route_warehouse0_mto')
mto_route.active = True
manufacture_route = self.env['stock.route'].search([('name', '=', 'Manufacture')])
self.comp2.write({'route_ids': [(6, None, [manufacture_route.id, mto_route.id])]})
picking_type_in = self.env.ref('stock.picking_type_in')
self.env.ref('mrp_subcontracting.route_resupply_subcontractor_mto').active = False
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
orderpoint_form.product_id = self.comp2
orderpoint_form.product_min_qty = 0.0
orderpoint_form.product_max_qty = 10.0
orderpoint_form.location_id = self.env.company.subcontracting_location_id
orderpoint = orderpoint_form.save()
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = picking_type_in
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 1
move.quantity = 1
move.picked = True
picking_receipt = picking_form.save()
warehouse = picking_receipt.picking_type_id.warehouse_id
# Pickings should directly be created
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEqual(mo.state, 'confirmed')
moves = self.env['stock.move'].search([
('product_id', '=', self.comp2.id),
('location_id', '=', warehouse.lot_stock_id.id),
('location_dest_id', '=', self.env.company.subcontracting_location_id.id)
])
self.assertTrue(moves)
picking_delivery = moves.picking_id
self.assertTrue(picking_delivery)
self.assertEqual(sum(moves.mapped('product_uom_qty')), 11.0)
# As well as a manufacturing order for `self.comp2`
comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
self.assertEqual(len(comp2mo), 1)
def test_flow_5(self):
""" Check that the correct BoM is chosen accordingly to the partner
"""
# We create a second partner of type subcontractor
main_partner_2 = self.env['res.partner'].create({'name': 'main_partner'})
subcontractor_partner2 = self.env['res.partner'].create({
'name': 'subcontractor_partner',
'parent_id': main_partner_2.id,
'company_id': self.env.ref('base.main_company').id
})
# We create a different BoM for the same product
comp3 = self.env['product.product'].create({
'name': 'Component1',
'is_storable': True,
})
bom_form = Form(self.env['mrp.bom'])
bom_form.type = 'subcontract'
bom_form.product_tmpl_id = self.finished.product_tmpl_id
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.comp1
bom_line.product_qty = 1
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = comp3
bom_line.product_qty = 1
bom2 = bom_form.save()
# We assign the second BoM to the new partner
self.bom.write({'subcontractor_ids': [(4, self.subcontractor_partner1.id, None)]})
bom2.write({'subcontractor_ids': [(4, subcontractor_partner2.id, None)]})
# Create a receipt picking from the subcontractor1
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 1
move.quantity = 1
move.picked = True
picking_receipt1 = picking_form.save()
# Create a receipt picking from the subcontractor2
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = subcontractor_partner2
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 1
move.quantity = 1
move.picked = True
picking_receipt2 = picking_form.save()
mo_pick1 = picking_receipt1.move_ids.mapped('move_orig_ids.production_id')
mo_pick2 = picking_receipt2.move_ids.mapped('move_orig_ids.production_id')
self.assertEqual(len(mo_pick1), 1)
self.assertEqual(len(mo_pick2), 1)
self.assertEqual(mo_pick1.bom_id, self.bom)
self.assertEqual(mo_pick2.bom_id, bom2)
def test_flow_6(self):
""" Extra quantity on the move.
"""
# We create a second partner of type subcontractor
main_partner_2 = self.env['res.partner'].create({'name': 'main_partner'})
subcontractor_partner2 = self.env['res.partner'].create({
'name': 'subcontractor_partner',
'parent_id': main_partner_2.id,
'company_id': self.env.ref('base.main_company').id,
})
self.env.invalidate_all()
# We create a different BoM for the same product
comp3 = self.env['product.product'].create({
'name': 'Component3',
'is_storable': True,
})
bom_form = Form(self.env['mrp.bom'])
bom_form.type = 'subcontract'
bom_form.product_tmpl_id = self.finished.product_tmpl_id
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.comp1
bom_line.product_qty = 1
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = comp3
bom_line.product_qty = 2
bom2 = bom_form.save()
# We assign the second BoM to the new partner
self.bom.write({'subcontractor_ids': [(4, self.subcontractor_partner1.id, None)]})
bom2.write({'subcontractor_ids': [(4, subcontractor_partner2.id, None)]})
# Create a receipt picking from the subcontractor1
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = subcontractor_partner2
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 1
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
picking_receipt.move_ids.quantity = 3.0
picking_receipt.move_ids.picked = True
picking_receipt._action_done()
mo = picking_receipt._get_subcontract_production()
move_comp1 = mo.move_raw_ids.filtered(lambda m: m.product_id == self.comp1)
move_comp3 = mo.move_raw_ids.filtered(lambda m: m.product_id == comp3)
self.assertEqual(sum(move_comp1.mapped('product_uom_qty')), 3.0)
self.assertEqual(sum(move_comp3.mapped('product_uom_qty')), 6.0)
self.assertEqual(sum(move_comp1.mapped('quantity')), 3.0)
self.assertEqual(sum(move_comp3.mapped('quantity')), 6.0)
move_finished = mo.move_finished_ids
self.assertEqual(sum(move_finished.mapped('product_uom_qty')), 3.0)
self.assertEqual(sum(move_finished.mapped('quantity')), 3.0)
def test_flow_8(self):
resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
(self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 5
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
picking_receipt.move_ids.quantity = 3
picking_receipt.move_ids.picked = True
Form.from_action(self.env, picking_receipt.button_validate()).save().process()
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_receipt.id)])
self.assertTrue(backorder)
self.assertEqual(backorder.move_ids.product_uom_qty, 2)
mo_done = picking_receipt.move_ids._get_subcontract_production().filtered(lambda p: p.state == 'done')
backorder_mo = backorder.move_ids.move_orig_ids.production_id.filtered(lambda p: p.state != 'done')
self.assertTrue(mo_done)
self.assertEqual(mo_done.qty_produced, 3)
self.assertEqual(mo_done.product_uom_qty, 3)
self.assertTrue(backorder_mo)
self.assertEqual(backorder_mo.product_uom_qty, 2)
self.assertEqual(backorder_mo.qty_produced, 0)
backorder.move_ids.quantity = 2
backorder.move_ids.picked = True
backorder._action_done()
self.assertTrue(picking_receipt.move_ids.move_orig_ids[0].production_id.state == 'done')
def test_flow_9(self):
"""Ensure that cancel the subcontract moves will also delete the
components need for the subcontractor.
"""
resupply_sub_on_order_route = self.env['stock.route'].search([
('name', '=', 'Resupply Subcontractor on Order')
])
(self.comp1 + self.comp2).write({
'route_ids': [(4, resupply_sub_on_order_route.id)]
})
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 5
move.quantity = 5
move.picked = True
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
picking_delivery = self.env['stock.move'].search([
('product_id', 'in', (self.comp1 | self.comp2).ids)
]).picking_id
self.assertTrue(picking_delivery)
self.assertEqual(picking_delivery.state, 'confirmed')
self.assertEqual(self.comp1.virtual_available, -5)
self.assertEqual(self.comp2.virtual_available, -5)
# action_cancel is not call on the picking in order
# to test behavior from other source than picking (e.g. puchase).
picking_receipt.move_ids._action_cancel()
self.assertEqual(picking_delivery.state, 'cancel')
self.assertEqual(self.comp1.virtual_available, 0.0)
self.assertEqual(self.comp1.virtual_available, 0.0)
def test_flow_10(self):
"""Receipts from a children contact of a subcontractor are properly
handled.
"""
# Create a children contact
subcontractor_contact = self.env['res.partner'].create({
'name': 'Test children subcontractor contact',
'parent_id': self.subcontractor_partner1.id,
})
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = subcontractor_contact
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 1
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
# Check that a manufacturing order is created
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEqual(len(mo), 1)
def test_flow_flexible_bom_1(self):
""" Record Component for a bom subcontracted with a flexible and flexible + warning consumption """
self.bom.consumption = 'flexible'
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 1
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
action = picking_receipt.move_ids.action_show_subcontract_details()
mo = self.env['mrp.production'].browse(action['res_id'])
mo_form = Form(mo.with_context(**action['context']), view=action['views'][0][0])
with mo_form.move_raw_ids.edit(0) as move:
self.assertEqual(move.product_id, self.comp1)
self.assertEqual(move.quantity, 0)
move.quantity = 2
mo = mo_form.save()
self.assertEqual(mo.move_raw_ids[0].move_line_ids.quantity, 2)
picking_receipt.button_validate()
self.assertEqual(mo.state, 'done')
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
self.assertEqual(avail_qty_comp1, -2)
def test_mrp_report_bom_structure_subcontracting(self):
self.comp2_bom.write({'type': 'subcontract', 'subcontractor_ids': [Command.link(self.subcontractor_partner1.id)]})
self.finished.seller_ids.price = 10
supplier = self.env['product.supplierinfo'].create({
'product_tmpl_id': self.comp2.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'price': 5,
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.comp2.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'price': 1,
'min_qty': 5,
})
self.assertTrue(supplier.is_subcontractor)
self.comp1.standard_price = 5
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, searchQty=1, searchVariant=False)
subcontracting_values = report_values['lines']['subcontracting']
self.assertEqual(subcontracting_values['name'], self.subcontractor_partner1.display_name)
self.assertEqual(report_values['lines']['bom_cost'], 20) # 10 For subcontracting + 5 for comp1 + 5 for subcontracting of comp2_bom
self.assertEqual(subcontracting_values['bom_cost'], 10)
self.assertEqual(report_values['lines']['components'][0]['bom_cost'], 5)
self.assertEqual(report_values['lines']['components'][1]['bom_cost'], 5)
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, searchQty=3, searchVariant=False)
subcontracting_values = report_values['lines']['subcontracting']
self.assertEqual(report_values['lines']['bom_cost'], 60) # 30 for subcontracting + 15 for comp1 + 15 for subcontracting of comp2_bom
self.assertEqual(subcontracting_values['bom_cost'], 30)
self.assertEqual(report_values['lines']['components'][0]['bom_cost'], 15)
self.assertEqual(report_values['lines']['components'][1]['bom_cost'], 15)
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, searchQty=5, searchVariant=False)
subcontracting_values = report_values['lines']['subcontracting']
self.assertEqual(report_values['lines']['bom_cost'], 80) # 50 for subcontracting + 25 for comp1 + 5 for subcontracting of comp2_bom
self.assertEqual(subcontracting_values['bom_cost'], 50)
self.assertEqual(report_values['lines']['components'][0]['bom_cost'], 25)
self.assertEqual(report_values['lines']['components'][1]['bom_cost'], 5)
def test_several_backorders(self):
def process_picking(picking, qty):
picking.move_ids.quantity = qty
picking.move_ids.picked = True
action = picking.button_validate()
if isinstance(action, dict):
Form.from_action(self.env, action).save().process()
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,
}, {
'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})],
})
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = finished
move.product_uom_qty = 5
picking = picking_form.save()
picking.action_confirm()
supply_picking = self.env['mrp.production'].search([('bom_id', '=', bom.id)]).picking_ids
process_picking(supply_picking, 5)
process_picking(picking, 1.25)
backorder01 = picking.backorder_ids
process_picking(backorder01, 1)
backorder02 = backorder01.backorder_ids
self.assertEqual(backorder02.move_ids.quantity, 2.75)
self.assertEqual(self.env['mrp.production'].search_count([('bom_id', '=', bom.id)]), 3)
def test_several_backorders_2(self):
# This test ensure that the backorders finished moves are correctly made (Production -> Subcontracting -> Stock)
# When the receipt is done, the Subcontracting location should have quantity 0 of the finished product.
# In more detail, this test checks that everything is done correctly
# when the quantity of the backorder is set on the stock.move.line instead of the stock.move,
# it can, for example, happen if the finished product is tracked by Serial Number.
def process_picking_with_backorder(picking, qty):
# Process the picking by putting the given quantity on the stock.move.line
move_line = picking.move_line_ids.ensure_one()
picking.move_ids.quantity = qty
action = picking.button_validate()
if isinstance(action, dict):
Form.from_action(self.env, action).save().process()
return picking.backorder_ids
def check_quants(product, stock_qty, sub_qty, prod_qty):
# Check the quantities of the Stock, Subcontracting and Production locations for the given product
subcontracting_location = self.env.company.subcontracting_location_id
production_location = product.property_stock_production
stock_location = self.env.ref('stock.stock_location_stock')
self.assertEqual(sub_qty, self.env['stock.quant']._gather(product, subcontracting_location).quantity)
self.assertEqual(stock_qty, self.env['stock.quant']._gather(product, stock_location).quantity)
self.assertEqual(prod_qty, self.env['stock.quant']._gather(product, production_location).quantity)
in_pck_type = self.env.ref('stock.picking_type_in')
in_pck_type.write({'show_operations': True})
finished = self.env['product.product'].create({'name': 'Finished Product', 'is_storable': True})
component = self.env['product.product'].create([{'name': 'Component', 'is_storable': True}])
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})],
})
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = in_pck_type
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = finished
move.product_uom_qty = 6
picking = picking_form.save()
picking.action_confirm()
backorder01 = process_picking_with_backorder(picking, 1)
check_quants(product=finished, stock_qty=1, sub_qty=0, prod_qty=-1)
check_quants(product=component, stock_qty=0, sub_qty=-1, prod_qty=1)
backorder02 = process_picking_with_backorder(backorder01, 2)
check_quants(product=finished, stock_qty=3, sub_qty=0, prod_qty=-3)
check_quants(product=component, stock_qty=0, sub_qty=-3, prod_qty=3)
process_picking_with_backorder(backorder02, 3)
check_quants(product=finished, stock_qty=6, sub_qty=0, prod_qty=-6)
check_quants(product=component, stock_qty=0, sub_qty=-6, prod_qty=6)
def test_subcontracting_date_warning(self):
with Form(self.env['stock.picking']) as picking_form:
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 3
move.quantity = 3
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
self.assertEqual(picking_form.json_popover, False)
subcontract = picking_receipt._get_subcontract_production()
self.assertEqual(subcontract.date_start, picking_receipt.scheduled_date)
self.assertEqual(subcontract.date_finished, picking_receipt.scheduled_date)
def test_subcontracting_set_quantity_done(self):
""" Tests to set a quantity done directly on a subcontracted move without using the subcontracting wizard.
Checks that it does the same as it would do with the wizard.
"""
self.bom.consumption = 'flexible'
quantities = [10, 15, 12, 14]
with Form(self.env['stock.picking']) as picking_form:
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = quantities[0]
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
move = picking_receipt.move_ids
for qty in quantities[1:]:
move.quantity = qty
subcontracted = move._get_subcontract_production().filtered(lambda p: p.state != 'cancel')
self.assertEqual(sum(subcontracted.mapped('product_qty')), qty)
self.assertEqual(move.product_uom_qty, quantities[0])
picking_receipt.button_validate()
self.assertEqual(move.product_uom_qty, quantities[0])
self.assertEqual(move.quantity, quantities[-1])
subcontracted = move._get_subcontract_production().filtered(lambda p: p.state == 'done')
self.assertEqual(sum(subcontracted.mapped('qty_produced')), quantities[-1])
def test_change_reception_serial(self):
self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('stock.group_production_lot').id)]})
self.finished.tracking = 'serial'
self.bom.consumption = 'flexible'
finished_lots = self.env['stock.lot'].create([{
'name': 'lot_%s' % number,
'product_id': self.finished.id,
} for number in range(3)])
with Form(self.env['stock.picking']) as picking_form:
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 3
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
subcontract_move = picking_receipt.move_ids.filtered(lambda m: m.is_subcontract)
# Register serial number for each finished product
action = picking_receipt.move_ids.action_show_details()
self.assertEqual(action['name'], 'Detailed Operations', "It should open the detailed operations view.")
with Form(subcontract_move.with_context(action['context']), view=action['view_id']) as move_form:
for idx, lot in enumerate(finished_lots):
with move_form.move_line_ids.edit(idx) as move_line:
move_line.lot_id = lot
move_form.save()
self.assertEqual(len(subcontract_move._get_subcontract_production()), 3)
self.assertEqual(len(subcontract_move._get_subcontract_production().lot_producing_ids), 3)
self.assertRecordValues(subcontract_move._get_subcontract_production().lot_producing_ids.sorted('id'), [
{'id': finished_lots[0].id},
{'id': finished_lots[1].id},
{'id': finished_lots[2].id},
])
new_lot = self.env['stock.lot'].create({
'name': 'lot_alter',
'product_id': self.finished.id,
})
action = picking_receipt.move_ids.action_show_details()
self.assertEqual(action['name'], 'Detailed Operations', "The subcontract record components wizard shouldn't be available now.")
with Form(subcontract_move.with_context(action['context']), view=action['view_id']) as move_form:
with move_form.move_line_ids.edit(2) as move_line:
move_line.lot_id = new_lot
move_form.save()
subcontracted_mo = subcontract_move._get_subcontract_production()
self.assertEqual(len(subcontracted_mo.filtered(lambda p: p.lot_producing_ids == new_lot)), 1)
self.assertEqual(len(subcontracted_mo.filtered(lambda p: p.lot_producing_ids != new_lot)), 2)
def test_decrease_quantity_done(self):
self.bom.consumption = 'flexible'
supplier_location = self.env.ref('stock.stock_location_suppliers')
uom_duo = self.env['uom.uom'].create({
'name': 'Duos',
'relative_factor': 2.0,
'relative_uom_id': self.env.ref('uom.product_uom_unit').id,
})
receipt = self.env['stock.picking'].create({
'partner_id': self.subcontractor_partner1.id,
'location_id': supplier_location.id,
'location_dest_id': self.warehouse.lot_stock_id.id,
'picking_type_id': self.warehouse.in_type_id.id,
'move_ids': [(0, 0, {
'product_id': self.finished.id,
'product_uom_qty': 10.0,
'product_uom': uom_duo.id,
'location_id': supplier_location.id,
'location_dest_id': self.warehouse.lot_stock_id.id,
})],
})
receipt.action_confirm()
productions = self.env['mrp.production'].search([('product_id', '=', self.finished.id)], order='id')
self.assertRecordValues(productions, [
{'qty_producing': 0.0, 'product_qty': 10.0, 'state': 'confirmed'},
])
receipt.move_ids.quantity = 6
productions = self.env['mrp.production'].search([('product_id', '=', self.finished.id)], order='id')
self.assertEqual(receipt.move_ids.product_uom_qty, 10.0, 'Demand should not be impacted')
self.assertRecordValues(productions, [
{'qty_producing': 0.0, 'product_qty': 6.0, 'state': 'confirmed'},
])
receipt.move_ids.quantity = 9
productions = self.env['mrp.production'].search([('product_id', '=', self.finished.id)], order='id')
self.assertEqual(receipt.move_ids.product_uom_qty, 10.0, 'Demand should not be impacted')
self.assertRecordValues(productions, [
{'qty_producing': 0.0, 'product_qty': 9.0, 'state': 'confirmed'},
])
receipt.move_ids.quantity = 7
productions = self.env['mrp.production'].search([('product_id', '=', self.finished.id)], order='id')
self.assertEqual(receipt.move_ids.product_uom_qty, 10.0, 'Demand should not be impacted')
self.assertRecordValues(productions, [
{'qty_producing': 0.0, 'product_qty': 7.0, 'state': 'confirmed'},
])
receipt.move_ids.quantity = 4
productions = self.env['mrp.production'].search([('product_id', '=', self.finished.id)], order='id')
self.assertEqual(receipt.move_ids.product_uom_qty, 10.0, 'Demand should not be impacted')
self.assertRecordValues(productions, [
{'qty_producing': 0.0, 'product_qty': 4.0, 'state': 'confirmed'},
])
receipt.move_ids.quantity = 0
productions = self.env['mrp.production'].search([('product_id', '=', self.finished.id)], order='id')
self.assertEqual(receipt.move_ids.product_uom_qty, 10.0, 'Demand should not be impacted')
self.assertRecordValues(productions, [
{'qty_producing': 0.0, 'product_qty': 10.0, 'state': 'confirmed'},
])
def test_change_partner_subcontracting_location(self):
"""On creating a subcontrating picking, the destination location of the picking is equal to
the subcontracting location of the contact if specified. Otherwise, it will be equal to the
default warehouse subcontracting location.
"""
custom_subcontract_location = self.env['stock.location'].create({
'name': 'custom partner location',
'location_id': self.env.company.subcontracting_location_id.id,
'usage': 'internal',
'company_id': self.env.company.id,
})
subcontractor = self.env['res.partner'].create({'name': 'subcontractor'})
def create_picking(subcontractor):
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.warehouse.subcontracting_resupply_type_id
picking_form.partner_id = subcontractor
with picking_form.move_ids.new() as move:
move.product_id = self.comp1
move.product_uom_qty = 1.0
picking = picking_form.save()
picking.action_confirm()
return picking
picking_with_default_location = create_picking(subcontractor)
self.assertEqual(picking_with_default_location.location_dest_id, self.warehouse.subcontracting_resupply_type_id.default_location_dest_id)
subcontractor.property_stock_subcontractor = custom_subcontract_location.id
picking_with_custom_location = create_picking(subcontractor)
self.assertEqual(picking_with_custom_location.location_dest_id, custom_subcontract_location)
def test_validate_partial_subcontracting_without_backorder(self):
""" Test the validation of a partial subcontracting without creating a backorder."""
self.bom.consumption = 'flexible'
supplier_location = self.env.ref('stock.stock_location_suppliers')
receipt = self.env['stock.picking'].create({
'partner_id': self.subcontractor_partner1.id,
'location_id': supplier_location.id,
'location_dest_id': self.warehouse.lot_stock_id.id,
'picking_type_id': self.warehouse.in_type_id.id,
'move_ids': [Command.create({
'product_id': self.finished.id,
'product_uom_qty': 20.0,
'location_id': supplier_location.id,
'location_dest_id': self.warehouse.lot_stock_id.id,
})],
})
receipt.action_confirm()
self.assertEqual(receipt.state, 'assigned')
receipt.move_ids.quantity = 19.8
# Validate picking without backorder
backorder_wizard_dict = receipt.button_validate()
backorder_wizard_form = Form(self.env[backorder_wizard_dict['res_model']].with_context(backorder_wizard_dict['context']))
backorder_wizard_form.save().process_cancel_backorder()
self.assertEqual(receipt.state, 'done')
productions = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)]).sorted('id')
self.assertRecordValues(productions, [
{'product_qty': 19.8, 'qty_producing': 19.8, 'state': 'done'},
])
def test_replenish_with_subcontracting_bom(self):
""" Checks that a subcontracting bom cannot trigger a 'Manufacture' replenish.
"""
self.assertEqual(self.finished.bom_ids.type, 'subcontract')
self.finished.seller_ids.unlink()
replenish_wizard = self.env['product.replenish'].create({
'product_id': self.finished.id,
'product_tmpl_id': self.finished.product_tmpl_id.id,
'product_uom_id': self.finished.uom_id.id,
'quantity': 1,
'warehouse_id': self.warehouse.id,
})
self.assertFalse(replenish_wizard.allowed_route_ids)
def test_subcontracting_unbuild_warning(self):
with Form(self.env['stock.picking']) as picking_form:
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 3
move.quantity = 3
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
subcontract = picking_receipt._get_subcontract_production()
error_message = "You can't unbuild a subcontracted Manufacturing Order."
with self.assertRaisesRegex(UserError, error_message):
subcontract.button_unbuild()
def test_subcontracted_product_return_locations(self):
"""
Verify that when returning subcontracted and non-subcontracted products:
- the picking has destination location set to the supplier location.
- The returned move line for the subcontracted product has destination location set to the subcontractor's stock location.
- The returned move line for the non-subcontracted product returns to the supplier location.
"""
supplier_location = self.env.ref('stock.stock_location_suppliers')
stock_location = self.warehouse.lot_stock_id
picking_receipt = self.env['stock.picking'].create({
'picking_type_id': self.env.ref('stock.picking_type_in').id,
'partner_id': self.subcontractor_partner1.id,
'location_id': supplier_location.id,
'location_dest_id': stock_location.id,
'move_ids': [
Command.create({
'product_id': self.finished.id,
}),
Command.create({
'product_id': self.comp1.id,
}),
]
})
picking_receipt.action_confirm()
picking_receipt.move_ids.quantity = 1
picking_receipt.move_ids.picked = True
picking_receipt.button_validate()
self.assertEqual(picking_receipt.state, 'done')
# Ensure returns to subcontractor location
return_form = Form(self.env['stock.return.picking'].with_context(active_id=picking_receipt.id, active_model='stock.picking'))
return_wizard = return_form.save()
return_wizard.product_return_moves.quantity = 1
return_picking = return_wizard._create_return()
self.assertEqual(len(return_picking), 1)
self.assertEqual(return_picking.location_dest_id, supplier_location)
self.assertEqual(return_picking.location_id, stock_location)
self.assertRecordValues(return_picking.move_ids.move_line_ids, [
{'product_id': self.finished.id, 'location_id': stock_location.id, 'location_dest_id': self.subcontractor_partner1.property_stock_subcontractor.id},
{'product_id': self.comp1.id, 'location_id': stock_location.id, 'location_dest_id': supplier_location.id}
])
self.assertRecordValues(return_picking.move_ids, [
{'product_id': self.finished.id, 'location_id': stock_location.id, 'location_dest_id': self.subcontractor_partner1.property_stock_subcontractor.id},
{'product_id': self.comp1.id, 'location_id': stock_location.id, 'location_dest_id': supplier_location.id}
])
def test_flow_tracked_1(self):
""" This test mimics test_flow_1 but with a BoM that has tracking included in it.
"""
# Create a receipt picking from the subcontractor
self.finished.tracking = 'lot'
self.comp1.tracking = 'serial'
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.warehouse.in_type_id
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 1
move.picked = True
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
# Check the created manufacturing order
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEqual(len(mo), 1)
self.assertEqual(len(mo.picking_ids), 0)
self.assertEqual(mo.picking_type_id, self.warehouse.subcontracting_type_id)
self.assertFalse(mo.picking_type_id.active)
lot_id = self.env['stock.lot'].create({
'name': 'lot1',
'product_id': self.finished.id,
})
serial_id = self.env['stock.lot'].create({
'name': 'lot1',
'product_id': self.comp1.id,
})
action = picking_receipt.move_ids.action_show_details()
with Form(picking_receipt.move_ids.with_context(action['context']), view=action['view_id']) as move_form:
with move_form.move_line_ids.new() as move_line:
move_line.lot_id = lot_id
move_line.picked = True
move_line.quantity = 1
move_form.save()
action = picking_receipt.move_ids.action_show_subcontract_details()
mo = self.env['mrp.production'].browse(action['res_id'])
action = mo.move_raw_ids[0].action_show_details()
with Form(mo.move_raw_ids[0].with_context(action['context']), view=action['view_id']) as move_form:
with move_form.move_line_ids.new() as move_line:
move_line.lot_id = serial_id
move_line.picked = True
move_line.quantity = 1
move_form.save()
picking_receipt.button_validate()
self.assertEqual(mo.state, 'done')
# Available quantities should be negative at the subcontracting location for each components
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, self.warehouse.lot_stock_id)
self.assertEqual(avail_qty_comp1, -1)
self.assertEqual(avail_qty_comp2, -1)
self.assertEqual(avail_qty_finished, 1)
def test_flow_tracked_only_finished(self):
""" Test when only the finished product is tracked """
self.finished.tracking = "serial"
self.comp1.tracking = "none"
nb_finished_product = 3
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.warehouse.in_type_id
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = nb_finished_product
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
picking_receipt.do_unreserve()
lots = self.env['stock.lot'].create([
{'name': f"subtracked_{i}", 'product_id': self.finished.id}
for i in range(nb_finished_product)
])
move_details = Form(picking_receipt.move_ids, view='stock.view_stock_move_operations')
for lot_id in lots:
with move_details.move_line_ids.new() as ml:
ml.quantity = 1
ml.lot_id = lot_id
move_details.save()
picking_receipt.move_ids.picked = True
picking_receipt.button_validate()
# Check the created manufacturing order
# Should have one mo by serial number
mos = picking_receipt.move_ids.move_orig_ids.production_id
self.assertEqual(len(mos), nb_finished_product)
self.assertEqual(mos.mapped("state"), ["done"] * nb_finished_product)
self.assertEqual(mos.picking_type_id, self.warehouse.subcontracting_type_id)
self.assertFalse(mos.picking_type_id.active)
self.assertEqual(set(mos.lot_producing_ids.mapped("name")), {f"subtracked_{i}" for i in range(nb_finished_product)})
# Available quantities should be negative at the subcontracting location for each components
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished,
self.warehouse.lot_stock_id)
self.assertEqual(avail_qty_comp1, -nb_finished_product)
self.assertEqual(avail_qty_comp2, -nb_finished_product)
self.assertEqual(avail_qty_finished, nb_finished_product)
def test_flow_tracked_backorder(self):
""" This test uses tracked (serial and lot) component and tracked (serial) finished product """
todo_nb = 4
self.comp2.tracking = 'lot'
self.finished.tracking = 'serial'
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.warehouse.in_type_id
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = todo_nb
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
# Check the created manufacturing order
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEqual(len(mo), 1)
self.assertEqual(len(mo.picking_ids), 0)
self.assertEqual(mo.picking_type_id, self.warehouse.subcontracting_type_id)
self.assertFalse(mo.picking_type_id.active)
lot_comp2 = self.env['stock.lot'].create({
'name': 'lot_comp2',
'product_id': self.comp2.id,
})
serials_finished = []
serials_comp1 = []
for i in range(todo_nb):
serials_finished.append(self.env['stock.lot'].create({
'name': 'serial_fin_%s' % i,
'product_id': self.finished.id,
}))
serials_comp1.append(self.env['stock.lot'].create({
'name': 'serials_comp1_%s' % i,
'product_id': self.comp1.id,
}))
# Final product
action = picking_receipt.move_ids.action_show_details()
with Form(picking_receipt.move_ids.with_context(action['context']), view=action['view_id']) as move_form:
for idx, serial in enumerate(serials_finished):
with move_form.move_line_ids.edit(idx) as move_line:
move_line.lot_id = serial
move_line.picked = True
move_line.quantity = 1
move_form.save()
# Components
for mo, compo_1_serial in zip(picking_receipt._get_subcontract_production(), serials_comp1):
action = mo.move_raw_ids[0].action_show_details()
with Form(mo.move_raw_ids[0].with_context(action['context']), view=action['view_id']) as move_form:
with move_form.move_line_ids.new() as move_line:
self.assertEqual(move_line.product_id, self.comp1)
move_line.lot_id = compo_1_serial
move_line.picked = True
move_line.quantity = 1
move_form.save()
action = mo.move_raw_ids[1].action_show_details()
with Form(mo.move_raw_ids[1].with_context(action['context']), view=action['view_id']) as move_form:
with move_form.move_line_ids.new() as move_line:
self.assertEqual(move_line.product_id, self.comp2)
move_line.lot_id = lot_comp2
move_line.picked = True
move_line.quantity = 1
move_form.save()
picking_receipt.button_validate()
self.assertEqual(mo.state, 'done')
# Available quantities should be negative at the subcontracting location for each components
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, self.warehouse.lot_stock_id)
self.assertEqual(avail_qty_comp1, -todo_nb)
self.assertEqual(avail_qty_comp2, -todo_nb)
self.assertEqual(avail_qty_finished, todo_nb)
def test_flow_backorder_production(self):
""" Test subcontracted MO backorder (i.e. through record production window, NOT through
picking backorder). Finished product is serial tracked to ensure subcontracting MO window
is opened. Check that MO backorder auto-reserves components
"""
todo_nb = 3
self.warehouse.subcontracting_to_resupply = True
self.finished.tracking = 'serial'
finished_serials = self.env['stock.lot'].create([{
'name': 'sn_%s' % str(i),
'product_id': self.finished.id,
} for i in range(todo_nb)])
self.env['stock.quant']._update_available_quantity(self.comp1, self.warehouse.lot_stock_id, todo_nb)
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = todo_nb
move.picked = True
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
# Process the delivery of the components
compo_picking = mo.picking_ids
for move in compo_picking.move_ids:
move.quantity = todo_nb
move.picked = True
compo_picking.button_validate()
picking_receipt = self.env['stock.picking'].search([('partner_id', '=', self.subcontractor_partner1.id), ('state', '!=', 'done')])
action = picking_receipt.move_ids.action_show_details()
with Form(picking_receipt.move_ids.with_context(action['context']), view=action['view_id']) as move_form:
for sn in finished_serials:
with move_form.move_line_ids.new() as move_line:
move_line.lot_id = sn
move_line.picked = True
move_form.save()
# Validate the picking
picking_receipt.button_validate()
self.assertEqual(picking_receipt.state, 'done')
def test_flow_subcontracting_portal(self):
# Create a receipt picking from the subcontractor
self.finished.tracking = 'lot'
other_product = self.env['product.product'].create({'name': 'Other Product', 'is_storable': True})
self.portal_user = self.env['res.users'].create({
'name': 'portal user (subcontractor)',
'partner_id': self.subcontractor_partner1.id,
'login': 'subcontractor',
'password': 'subcontractor',
'email': 'subcontractor@subcontracting.portal',
'group_ids': [(6, 0, [self.env.ref('base.group_portal').id, self.env.ref('stock.group_production_lot').id])]
})
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.warehouse.in_type_id
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = 2
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
# Using the subcontractor (portal user)
lot1 = self.env['stock.lot'].with_user(self.portal_user).create({
'name': 'lot1',
'product_id': self.finished.id,
})
lot2 = self.env['stock.lot'].with_user(self.portal_user).create({
'name': 'lot2',
'product_id': self.finished.id,
})
serial1 = self.env['stock.lot'].with_user(self.portal_user).create({
'name': 'lot1',
'product_id': self.comp1.id,
})
serial2 = self.env['stock.lot'].with_user(self.portal_user).create({
'name': 'lot2',
'product_id': self.comp1.id,
})
serial3 = self.env['stock.lot'].with_user(self.portal_user).create({
'name': 'lot3',
'product_id': self.comp1.id,
})
action = picking_receipt.with_user(self.portal_user).with_context({'is_subcontracting_portal': 1}).move_ids.action_show_details()
with Form(picking_receipt.move_ids.with_context(action['context']), view=action['view_id']) as move_form:
with move_form.move_line_ids.edit(0) as move_line:
move_line.lot_id = lot1
move_line.picked = True
move_line.quantity = 1
with move_form.move_line_ids.new() as move_line:
move_line.lot_id = lot2
move_line.picked = True
move_line.quantity = 1
move_form.save()
mo_1, mo_2 = picking_receipt.with_user(self.portal_user)._get_subcontract_production()
# Registering components for the first manufactured product
action = mo_1.move_raw_ids[0].with_user(self.portal_user).with_context({'is_subcontracting_portal': 1}).action_show_details()
with Form(mo_1.move_raw_ids[0].with_user(self.portal_user).with_context(action['context']), view=action['view_id']) as move_form:
with move_form.move_line_ids.new() as move_line:
move_line.lot_id = serial1
move_line.picked = True
move_line.quantity = 1
move_form.save()
# Registering components for the second manufactured product with over-consumption, which leads to a warning
action = mo_2.move_raw_ids[0].with_user(self.portal_user).with_context({'is_subcontracting_portal': 1}).action_show_details()
with Form(mo_2.move_raw_ids[0].with_user(self.portal_user).with_context(action['context']), view=action['view_id']) as move_form:
for compo_serial in (serial2, serial3):
with move_form.move_line_ids.new() as move_line:
move_line.lot_id = compo_serial
move_line.picked = True
move_line.quantity = 1
move_form.save()
action = mo_2.move_raw_ids[1].with_user(self.portal_user).with_context({'is_subcontracting_portal': 1}).action_show_details()
with Form(mo_2.move_raw_ids[1].with_user(self.portal_user).with_context(action['context']), view=action['view_id']) as move_form:
with move_form.move_line_ids.new() as move_line:
move_line.picked = True
move_line.quantity = 2
move_form.save()
# The portal user should not be able to add a product not in the BoM
action = picking_receipt.with_user(self.portal_user).move_ids.action_show_subcontract_details()
mo_form = Form(mo_2.with_context(**action['context']), view=action['views'][1][0])
with self.assertRaises(AccessError):
with mo_form.move_line_raw_ids.new() as move:
move.product_id = other_product
mo = mo_form.save()
# Attempt to validate from the portal user should give an error
with self.assertRaises(UserError):
picking_receipt.with_user(self.portal_user).button_validate()
# Validation from the backend user
picking_receipt.button_validate()
self.assertEqual(mo.state, 'done')
self.assertEqual(mo.move_line_raw_ids[0].quantity, 1)
self.assertEqual(mo.move_line_raw_ids[0].lot_id, serial2)
self.assertEqual(mo.move_line_raw_ids[1].quantity, 1)
self.assertEqual(mo.move_line_raw_ids[1].lot_id, serial3)
self.assertEqual(mo.move_line_raw_ids[2].quantity, 2)
def test_resupply_subcontractor_in_mtso(self):
"""
Check the 'resupply subcontractor on order' route when the associated rule is
updated to 'Take From Stock, if unavailable, Trigger Another Rule' (mtso)
"""
resupply_route = self.env.ref('mrp_subcontracting.route_resupply_subcontractor_mto')
resupply_route.warehouse_ids = [Command.set(self.warehouse.ids)]
resupply_route.rule_ids.procure_method = 'mts_else_mto'
receipt = self.env['stock.picking'].create({
'partner_id': self.subcontractor_partner1.id,
'location_id': self.ref('stock.stock_location_suppliers'),
'location_dest_id': self.warehouse.lot_stock_id.id,
'picking_type_id': self.warehouse.in_type_id.id,
'move_ids': [Command.create({
'product_id': self.finished.id,
'product_uom_qty': 10.0,
'location_id': self.ref('stock.stock_location_suppliers'),
'location_dest_id': self.warehouse.lot_stock_id.id,
})],
})
receipt.action_confirm()
# Note that the subcontractor of the MO is the commercial_partner_id of subcontractor_partner1
subcontracted_mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)], limit=1)
resupply_subcontractor_delivery = self.env['stock.picking'].search([('partner_id', '=', subcontracted_mo.subcontractor_id.id)], limit=1)
self.assertRecordValues(resupply_subcontractor_delivery.move_ids, [
{'product_id': self.comp1.id, 'product_uom_qty': 10.0},
{'product_id': self.comp2.id, 'product_uom_qty': 10.0},
])
class TestSubcontractingSerialMassReceipt(TransactionCase):
def setUp(self):
super().setUp()
self.subcontractor = self.env['res.partner'].create({
'name': 'Subcontractor',
})
self.resupply_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
self.raw_material = self.env['product.product'].create({
'name': 'Component',
'is_storable': True,
'route_ids': [Command.link(self.resupply_route.id)],
})
self.finished = self.env['product.product'].create({
'name': 'Finished',
'is_storable': True,
'tracking': 'serial'
})
self.bom = self.env['mrp.bom'].create({
'product_id': self.finished.id,
'product_tmpl_id': self.finished.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'subcontract',
'subcontractor_ids': [Command.link(self.subcontractor.id)],
'consumption': 'strict',
'bom_line_ids': [
Command.create({'product_id': self.raw_material.id, 'product_qty': 1}),
]
})
def generate_subcontracting_receipt_and_mo(self, product_qty, warehouse=None):
if not warehouse:
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
receipt = self.env['stock.picking'].create({
'picking_type_id': warehouse.in_type_id.id,
'partner_id': self.subcontractor.id,
'location_id': self.ref('stock.stock_location_suppliers'),
'location_dest_id': warehouse.lot_stock_id.id,
'move_ids': [Command.create({
'product_id': self.finished.id,
'product_uom_qty': product_qty,
'product_uom': self.finished.uom_id.id,
'location_id': self.ref('stock.stock_location_suppliers'),
'location_dest_id': warehouse.lot_stock_id.id,
})]
})
receipt.action_confirm()
action = receipt.move_ids.action_show_subcontract_details()
return receipt, self.env['mrp.production'].browse(action['res_id'])
def test_receive_after_resupply(self):
quantities = [5, 4, 1]
# Make needed component stock
self.env['stock.quant']._update_available_quantity(self.raw_material, self.env.ref('stock.stock_location_stock'), sum(quantities))
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = sum(quantities)
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
# Process the delivery of the components
picking_deliver = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)]).picking_ids
picking_deliver.action_assign()
picking_deliver.button_validate()
# Receive
for quantity in quantities:
# Receive <quantity> finished products
picking_receipt.do_unreserve()
lot_name = self.env['stock.lot']._get_next_serial(picking_receipt.company_id, picking_receipt.move_ids[0].product_id) or 'sn#1'
picking_receipt.move_ids[0]._generate_serial_numbers(lot_name, quantity)
picking_receipt.move_ids.picked = True
wizard_data = picking_receipt.button_validate()
if wizard_data is not True:
# Create backorder
Form.from_action(self.env, wizard_data).save().process()
self.assertEqual(picking_receipt.state, 'done')
picking_receipt = picking_receipt.backorder_ids[-1]
self.assertEqual(picking_receipt.state, 'assigned')
self.assertEqual(picking_receipt.state, 'done')
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.env.ref('stock.stock_location_stock')), 0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.subcontractor.property_stock_subcontractor), 0)
def test_receive_no_resupply(self):
quantity = 5
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor
with picking_form.move_ids.new() as move:
move.product_id = self.finished
move.product_uom_qty = quantity
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
picking_receipt.do_unreserve()
# Receive finished products
lot_name = self.env['stock.lot']._get_next_serial(picking_receipt.company_id, picking_receipt.move_ids[0].product_id) or 'sn#1'
picking_receipt.move_ids[0]._generate_serial_numbers(lot_name, quantity)
picking_receipt.move_ids.picked = True
picking_receipt.button_validate()
self.assertEqual(picking_receipt.state, 'done')
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.env.ref('stock.stock_location_stock')), 0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.subcontractor.property_stock_subcontractor, allow_negative=True), -quantity)
def test_bom_subcontracting_product_dynamic_attribute(self):
"""
Test that the report BOM data is available for a product with an dynamic attribute
but without variant.
"""
dynamic_attribute = self.env['product.attribute'].create({
'name': 'flavour',
'create_variant': 'dynamic',
})
value_1 = self.env['product.attribute.value'].create({
'name': 'Vanilla',
'attribute_id': dynamic_attribute.id,
})
value_2 = self.env['product.attribute.value'].create({
'name': 'Chocolate',
'attribute_id': dynamic_attribute.id,
})
product_template = self.env['product.template'].create({
'name': 'Cake',
'uom_id': self.env.ref('uom.product_uom_unit').id,
'is_storable': True,
})
self.env['product.template.attribute.line'].create({
'product_tmpl_id': product_template.id,
'attribute_id': dynamic_attribute.id,
'value_ids': [Command.set([value_1.id, value_2.id])],
})
bom = self.env['mrp.bom'].create({
'product_tmpl_id': product_template.id,
'type': 'subcontract',
'subcontractor_ids': [Command.set([self.subcontractor.id])],
})
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom.id, searchVariant=False)
self.assertTrue(report_values)
def test_subcontracting_multiple_backorders(self):
"""
Check that processing multiple backorders in a raw for a
subcontracted prodcut is well behaved.
"""
subcontracted_produt = self.env['product.product'].create({
'name': 'Lovely product',
'is_storable': True,
})
self.env['mrp.bom'].create({
'product_tmpl_id': subcontracted_produt.product_tmpl_id.id,
'type': 'subcontract',
'subcontractor_ids': [Command.set(self.subcontractor.ids)],
})
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
warehouse.in_type_id.create_backorder = 'always'
receipt = self.env['stock.picking'].create({
'picking_type_id': warehouse.in_type_id.id,
'partner_id': self.subcontractor.id,
'location_id': self.ref('stock.stock_location_suppliers'),
'location_dest_id': warehouse.lot_stock_id.id,
'move_ids': [Command.create({
'product_id': subcontracted_produt.id,
'product_uom_qty': 100,
'product_uom': subcontracted_produt.uom_id.id,
'location_id': self.ref('stock.stock_location_suppliers'),
'location_dest_id': warehouse.lot_stock_id.id,
})],
})
receipt.action_confirm()
action = receipt.move_ids.action_show_subcontract_details()
mo = self.env['mrp.production'].browse(action['res_id'])
with Form(mo.with_context(**action['context']), view=action['views'][0][0]) as mo_form:
mo_form.product_qty = 5.0
self.assertRecordValues(receipt.move_line_ids, [
{'quantity': 5.0, 'state': 'partially_available', 'picked': False}
])
receipt.button_validate()
backorder = receipt.backorder_ids
action = backorder.move_ids.action_show_subcontract_details()
mo = self.env['mrp.production'].browse(action['res_id'])
with Form(mo.with_context(**action['context']), view=action['views'][0][0]) as mo_form:
mo_form.product_qty = 3.0
self.assertRecordValues(backorder.move_line_ids, [
{'quantity': 3.0, 'state': 'partially_available', 'picked': False}
])
backorder.button_validate()
backorder_backorder = backorder.backorder_ids
action = backorder_backorder.move_ids.action_show_subcontract_details()
mo = self.env['mrp.production'].browse(action['res_id'])
with Form(mo.with_context(**action['context']), view=action['views'][0][0]) as mo_form:
mo_form.product_qty = 1.0
self.assertRecordValues(backorder_backorder.move_line_ids, [
{'quantity': 1.0, 'state': 'partially_available', 'picked': False}
])
backorder_backorder.button_validate()
self.assertEqual(subcontracted_produt.qty_available, 9.0)
def test_use_customized_serial_sequence_in_subcontracting_productions(self):
"""
Test that serial numbers are generated with the correct prefix and sequence,
that manually provided serial numbers are correctly applied, and that serial
numbering remains consistent across multiple (subcontracted) manufacturing orders.
"""
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
receipt, mo = self.generate_subcontracting_receipt_and_mo(2, warehouse)
self.finished.lot_sequence_id.prefix = 'TEST'
self.finished.lot_sequence_id.number_next_actual = 1
serials_wizard = Form.from_action(self.env, mo.action_generate_serial())
self.assertEqual(serials_wizard.lot_name, 'TEST0000001')
serials_wizard.save().action_generate_serial_numbers()
serials_wizard.save().action_apply()
self.assertRecordValues(mo._get_subcontract_move().lot_ids.sorted('name'), [
{'name': 'TEST0000001'},
{'name': 'TEST0000002'},
])
self.assertRecordValues(receipt.move_ids[0].lot_ids.sorted('name'), [
{'name': 'TEST0000001'},
{'name': 'TEST0000002'},
])
receipt.button_validate()
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.finished, warehouse.lot_stock_id), 2)
self.assertRecordValues(self.env['stock.lot'].search([('product_id', '=', self.finished.id)]).sorted('name'), [
{'name': 'TEST0000001'},
{'name': 'TEST0000002'},
])
self.assertEqual(self.finished.serial_prefix_format + self.finished.next_serial, 'TEST0000003')
second_receipt, second_mo = self.generate_subcontracting_receipt_and_mo(5, warehouse)
second_serials_wizard = Form.from_action(self.env, second_mo.action_generate_serial())
self.assertEqual(second_serials_wizard.lot_name, 'TEST0000003')
second_serials_wizard.serial_numbers = 'TEST0000005\nLOREM002\nTEST0000003\nIPSUM101\nTEST0000004'
second_serials_wizard.save().action_apply()
self.assertRecordValues(second_mo._get_subcontract_move().lot_ids.sorted('name'), [
{'name': 'IPSUM101'},
{'name': 'LOREM002'},
{'name': 'TEST0000003'},
{'name': 'TEST0000004'},
{'name': 'TEST0000005'},
])
self.assertRecordValues(second_receipt.move_ids[0].lot_ids.sorted('name'), [
{'name': 'IPSUM101'},
{'name': 'LOREM002'},
{'name': 'TEST0000003'},
{'name': 'TEST0000004'},
{'name': 'TEST0000005'},
])
second_receipt.button_validate()
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.finished, warehouse.lot_stock_id), 2 + 5)
self.assertRecordValues(self.env['stock.lot'].search([('product_id', '=', self.finished.id)]).sorted('name'), [
{'name': 'IPSUM101'},
{'name': 'LOREM002'},
{'name': 'TEST0000001'},
{'name': 'TEST0000002'},
{'name': 'TEST0000003'},
{'name': 'TEST0000004'},
{'name': 'TEST0000005'},
])
_third_receipt, third_mo = self.generate_subcontracting_receipt_and_mo(2, warehouse)
third_mo.action_confirm()
third_serials_wizard = Form.from_action(self.env, third_mo.action_generate_serial())
self.assertEqual(third_serials_wizard.lot_name, 'TEST0000006')
@freeze_time('2024-02-03')
def test_use_interpolated_prefix_in_subcontracting_productions(self):
"""
Test that prefixes are correctly interpolated when generating a subcontracting MO
"""
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
receipt, mo = self.generate_subcontracting_receipt_and_mo(2, warehouse)
self.finished.lot_sequence_id.prefix = '%(day)s-%(month)s-'
self.finished.lot_sequence_id.number_next_actual = 1
serials_wizard = Form.from_action(self.env, mo.action_generate_serial())
self.assertEqual(serials_wizard.lot_name, '03-02-0000001')
serials_wizard.save().action_generate_serial_numbers()
serials_wizard.save().action_apply()
self.assertRecordValues(mo._get_subcontract_move().lot_ids.sorted('name'), [
{'name': '03-02-0000001'},
{'name': '03-02-0000002'},
])
self.assertRecordValues(receipt.move_ids.lot_ids.sorted('name'), [
{'name': '03-02-0000001'},
{'name': '03-02-0000002'},
])
receipt.button_validate()
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.finished, warehouse.lot_stock_id), 2)
self.assertRecordValues(self.env['stock.lot'].search([('product_id', '=', self.finished.id)]).sorted('name'), [
{'name': '03-02-0000001'},
{'name': '03-02-0000002'},
])
self.assertEqual(self.finished.next_serial, '0000003')
def test_subcontract_move_lines_are_linked_to_picking(self):
"""Test to ensure that when we generate mass serial numbers for a
subcontracting order, the created move lines are linked to the picking.
Also ensure that applying the serial number generation wizard without
generating any lot/serial numbers raises a UserError."""
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
receipt = self.env['stock.picking'].create({
'picking_type_id': warehouse.in_type_id.id,
'partner_id': self.subcontractor.id,
'move_ids': [Command.create({
'product_id': self.finished.id,
'product_uom_qty': 3,
})],
})
receipt.action_confirm()
mo = self.env['mrp.production'].browse(receipt.move_ids.action_show_subcontract_details()['res_id'])
self.finished.lot_sequence_id.number_next_actual = 1
wizard = Form.from_action(self.env, mo.action_generate_serial()).save()
# Applying the wizard without generating any lot/serial numbers should raise a UserError.
with self.assertRaises(UserError):
wizard.action_apply()
wizard.action_generate_serial_numbers()
wizard.action_apply()
self.assertRecordValues(mo._get_subcontract_move().lot_ids, [
{'name': '0000001'},
{'name': '0000002'},
{'name': '0000003'},
])
receipt.button_validate()
self.assertEqual(receipt.move_line_ids.lot_id, mo._get_subcontract_move().lot_ids)