# -*- 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 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)