12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481 |
- # -*- coding: utf-8 -*-
- # Part of Odoo. See LICENSE file for full copyright and licensing details.
- 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)
- @tagged('post_install', '-at_install')
- class TestSubcontractingFlows(TestMrpSubcontractingCommon):
- 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_without_package.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.reserved_availability for m in picking_receipt.move_ids))
- self.assertEqual(picking_receipt.state, 'assigned')
- self.assertEqual(picking_receipt.display_action_record_components, 'hide')
- # 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)
- 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)
- # Create a RR
- pg1 = self.env['procurement.group'].create({})
- self.env['stock.warehouse.orderpoint'].create({
- 'name': 'xxx',
- 'product_id': self.comp1.id,
- 'product_min_qty': 0,
- 'product_max_qty': 0,
- 'location_id': self.env.user.company_id.subcontracting_location_id.id,
- 'group_id': pg1.id,
- })
- # Run the scheduler and check the created picking
- self.env['procurement.group'].run_scheduler()
- picking = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
- self.assertEqual(len(picking), 1)
- self.assertEqual(picking.picking_type_id, wh.subcontracting_resupply_type_id)
- picking_receipt.move_ids.quantity_done = 1
- 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, 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_picking_id, pick_type_id = return_wizard._create_returns()
- return_picking = self.env['stock.picking'].browse(return_picking_id)
- 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 & check rules replication
- reference_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', self.env.company.subcontracting_location_id.id), ('location_dest_id', '=', self.env.company.subcontracting_location_id.id)])
- partner_subcontract_location = self.env['stock.location'].create({
- 'name': 'Specific partner location',
- 'location_id': self.env.ref('stock.stock_location_locations_partner').id,
- 'usage': 'internal',
- 'company_id': self.env.company.id,
- 'is_subcontracting_location': True,
- })
- custom_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', partner_subcontract_location.id), ('location_dest_id', '=', partner_subcontract_location.id)])
- self.assertEqual(reference_location_rules_count, custom_location_rules_count)
- 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.finished.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_without_package.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.assertEqual(picking_receipt.display_action_record_components, 'hide')
- # 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=self.finished.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_done = 1
- 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, 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, 0.0)
- self.assertEqual(avail_qty_comp2_in_global_location, 0.0)
- 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_without_package.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.assertEqual(picking_receipt.display_action_record_components, 'hide')
- # 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)
- # 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_done = 1
- 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, 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.groups_id += 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])]})
- 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 = self.env.ref('stock.picking_type_in')
- picking_form.partner_id = self.subcontractor_partner1
- with picking_form.move_ids_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 1
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
- 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')
- picking_delivery = mo.picking_ids
- self.assertFalse(picking_delivery)
- picking_delivery = self.env['stock.picking'].search([('origin', 'ilike', '%' + picking_receipt.name + '%')])
- self.assertFalse(picking_delivery)
- move = 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(move)
- picking_delivery = move.picking_id
- self.assertTrue(picking_delivery)
- self.assertEqual(move.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',
- 'type': 'product',
- 'categ_id': self.env.ref('product.product_category_all').id,
- })
- 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_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 1
- picking_receipt1 = picking_form.save()
- picking_receipt1.action_confirm()
- # 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_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 1
- picking_receipt2 = picking_form.save()
- picking_receipt2.action_confirm()
- 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',
- 'type': 'product',
- 'categ_id': self.env.ref('product.product_category_all').id,
- })
- 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_without_package.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_done = 3.0
- 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_done')), 3.0)
- self.assertEqual(sum(move_comp3.mapped('quantity_done')), 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_done')), 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_without_package.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_done = 3
- backorder_wiz = picking_receipt.button_validate()
- backorder_wiz = Form(self.env[backorder_wiz['res_model']].with_context(backorder_wiz['context'])).save()
- backorder_wiz.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 = backorder.move_ids.move_orig_ids.production_id.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_done = 2
- 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_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 5
- 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_without_package.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_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 1
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
- self.assertEqual(picking_receipt.display_action_record_components, 'facultative')
- action = picking_receipt.action_record_components()
- mo = self.env['mrp.production'].browse(action['res_id'])
- mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
- mo_form.qty_producing = 1
- with mo_form.move_line_raw_ids.edit(0) as ml:
- self.assertEqual(ml.product_id, self.comp1)
- self.assertEqual(ml.qty_done, 1)
- ml.qty_done = 2
- mo = mo_form.save()
- mo.subcontracting_record_component()
- self.assertEqual(mo.move_raw_ids[0].move_line_ids.qty_done, 2)
- # We should not be able to call the 'record_components' button
- self.assertEqual(picking_receipt.display_action_record_components, 'hide')
- 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_flow_warning_bom_1(self):
- """ Record Component for a bom subcontracted with a flexible and flexible + warning consumption """
- self.bom.consumption = 'warning'
- # 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_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 1
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
- self.assertEqual(picking_receipt.display_action_record_components, 'facultative')
- action = picking_receipt.action_record_components()
- mo = self.env['mrp.production'].browse(action['res_id'])
- mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
- mo_form.qty_producing = 1
- with mo_form.move_line_raw_ids.edit(0) as ml:
- self.assertEqual(ml.product_id, self.comp1)
- self.assertEqual(ml.qty_done, 1)
- ml.qty_done = 2
- mo = mo_form.save()
- action_warning = mo.subcontracting_record_component()
- warning = Form(self.env['mrp.consumption.warning'].with_context(**action_warning['context']))
- warning = warning.save()
- warning.action_cancel()
- action_warning = mo.subcontracting_record_component()
- warning = Form(self.env['mrp.consumption.warning'].with_context(**action_warning['context']))
- warning = warning.save()
- warning.action_confirm()
- self.assertEqual(mo.move_raw_ids[0].move_line_ids.qty_done, 2)
- # We should not be able to call the 'record_components' button
- self.assertEqual(picking_receipt.display_action_record_components, 'hide')
- 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.env['product.supplierinfo'].create({
- 'product_tmpl_id': self.finished.product_tmpl_id.id,
- 'partner_id': self.subcontractor_partner1.id,
- '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(subcontracting_values['prod_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(subcontracting_values['prod_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(subcontracting_values['prod_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_done = qty
- action = picking.button_validate()
- if isinstance(action, dict):
- wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
- wizard.process()
- resupply_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
- finished, component = self.env['product.product'].create([{
- 'name': 'Finished Product',
- 'type': 'product',
- }, {
- 'name': 'Component',
- 'type': 'product',
- '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_without_package.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
- process_picking(backorder02, 0)
- self.assertEqual(backorder02.move_ids.quantity_done, 2.75)
- self.assertEqual(self.env['mrp.production'].search_count([('bom_id', '=', bom.id)]), 3)
- def test_subcontracting_rules_replication(self):
- """ Test activate/archive subcontracting location rules."""
- reference_location_rules = self.env['stock.rule'].search(['|', ('location_src_id', '=', self.env.company.subcontracting_location_id.id), ('location_dest_id', '=', self.env.company.subcontracting_location_id.id)])
- warehouse_related_rules = reference_location_rules.filtered(lambda r: r.warehouse_id)
- company_rules = reference_location_rules - warehouse_related_rules
- # Create a custom subcontracting location
- custom_subcontracting_location = self.env['stock.location'].create({
- 'name': 'Custom Subcontracting Location',
- 'location_id': self.env.ref('stock.stock_location_locations').id,
- 'usage': 'internal',
- 'company_id': self.env.company.id,
- 'is_subcontracting_location': True,
- })
- custom_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', custom_subcontracting_location.id), ('location_dest_id', '=', custom_subcontracting_location.id)])
- self.assertEqual(len(reference_location_rules), custom_location_rules_count)
- # Add a new warehouse
- warehouse = self.env['stock.warehouse'].create({
- 'name': 'Additional Warehouse',
- 'code': 'ADD'
- })
- company_subcontracting_locations_rules_count = self.env['stock.rule'].search_count(['&', ('company_id', '=', warehouse.company_id.id), '|', ('location_src_id.is_subcontracting_location', '=', 'True'), ('location_dest_id.is_subcontracting_location', '=', 'True')])
- self.assertEqual(len(warehouse_related_rules) * 4 + len(company_rules) * 2, company_subcontracting_locations_rules_count)
- # Custom location no longer a subcontracting one
- custom_subcontracting_location.is_subcontracting_location = False
- custom_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', custom_subcontracting_location.id), ('location_dest_id', '=', custom_subcontracting_location.id)])
- self.assertEqual(custom_location_rules_count, 0)
- def test_subcontracting_date_warning(self):
- with Form(self.env['stock.picking'].with_context(default_immediate_transfer=True)) 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_without_package.new() as move:
- move.product_id = self.finished
- move.quantity_done = 3
- picking_receipt = picking_form.save()
- self.assertEqual(picking_form.json_popover, False)
- subcontract = picking_receipt._get_subcontract_production()
- self.assertEqual(subcontract.date_planned_start, picking_receipt.scheduled_date)
- self.assertEqual(subcontract.date_planned_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. Since immediate/planned transfers have different flows, we need to test both.
- """
- self.bom.consumption = 'flexible'
- quantities = [10, 15, 12, 14]
- # For planned transfers, as some triggers are different if it's an immediate transfer.
- 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_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = quantities[0]
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
- self.assertEqual(picking_receipt.immediate_transfer, False)
- move = picking_receipt.move_ids_without_package
- for qty in quantities[1:]:
- move.quantity_done = qty
- subcontracted = move._get_subcontract_production().filtered(lambda p: p.state != 'cancel')
- self.assertEqual(sum(subcontracted.mapped('product_qty')), qty)
- picking_receipt.button_validate()
- self.assertEqual(move.product_uom_qty, quantities[-1])
- self.assertEqual(move.quantity_done, quantities[-1])
- subcontracted = move._get_subcontract_production().filtered(lambda p: p.state == 'done')
- self.assertEqual(sum(subcontracted.mapped('qty_produced')), quantities[-1])
- # Now the same with an immediate transfer
- with Form(self.env['stock.picking'].with_context(default_immediate_transfer=True)) 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_without_package.new() as move:
- move.product_id = self.finished
- move.quantity_done = quantities[0]
- picking_receipt = picking_form.save()
- self.assertEqual(picking_receipt.immediate_transfer, True)
- move = picking_receipt.move_ids_without_package
- subcontracted = move._get_subcontract_production()
- self.assertEqual(subcontracted.product_qty, quantities[0])
- for qty in quantities[1:]:
- move.quantity_done = qty
- subcontracted = move._get_subcontract_production().filtered(lambda p: p.state != 'cancel')
- self.assertEqual(sum(subcontracted.mapped('product_qty')), qty)
- picking_receipt.button_validate()
- self.assertEqual(move.product_uom_qty, quantities[-1])
- self.assertEqual(move.quantity_done, 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.finished.tracking = 'serial'
- self.bom.consumption = 'flexible'
- finished_lots = self.env['stock.lot'].create([{
- 'name': 'lot_%s' % number,
- 'product_id': self.finished.id,
- 'company_id': self.env.company.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_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 3
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
- # Register serial number for each finished product
- for lot in finished_lots:
- action = picking_receipt.move_ids.action_show_details()
- self.assertEqual(action['name'], 'Subcontract', "It should open the subcontract record components wizard instead.")
- mo = self.env['mrp.production'].browse(action['res_id'])
- with Form(mo.with_context(action['context']), view=action['view_id']) as mo_form:
- mo_form.qty_producing = 1
- mo_form.lot_producing_id = lot
- mo_form.save()
- mo.subcontracting_record_component()
- subcontract_move = picking_receipt.move_ids_without_package.filtered(lambda m: m.is_subcontract)
- self.assertEqual(len(subcontract_move._get_subcontract_production()), 3)
- self.assertEqual(len(subcontract_move._get_subcontract_production().lot_producing_id), 3)
- self.assertRecordValues(subcontract_move._get_subcontract_production().lot_producing_id.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,
- 'company_id': self.env.company.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_nosuggest_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_id == new_lot)), 1)
- self.assertEqual(len(subcontracted_mo.filtered(lambda p: p.lot_producing_id != new_lot)), 2)
- def test_multiple_component_records_for_incomplete_move(self):
- self.bom.consumption = 'flexible'
- 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_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 10
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
- move = picking_receipt.move_ids_without_package
- # Register the five first finished products
- action = move.action_show_details()
- mo = self.env['mrp.production'].browse(action['res_id'])
- with Form(mo.with_context(action['context']), view=action['view_id']) as mo_form:
- mo_form.qty_producing = 5
- mo_form.save()
- mo.subcontracting_record_component()
- self.assertEqual(move.quantity_done, 5)
- # Register two other finished products
- action = move.action_show_details()
- mo = self.env['mrp.production'].browse(action['res_id'])
- with Form(mo.with_context(action['context']), view=action['view_id']) as mo_form:
- mo_form.qty_producing = 2
- mo_form.save()
- mo.subcontracting_record_component()
- self.assertEqual(move.quantity_done, 7)
- # Validate picking without backorder
- backorder_wizard_dict = picking_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.assertRecordValues(move._get_subcontract_production(), [
- {'product_qty': 5, 'state': 'done'},
- {'product_qty': 2, 'state': 'done'},
- {'product_qty': 3, 'state': 'cancel'},
- ])
- @tagged('post_install', '-at_install')
- class TestSubcontractingTracking(TransactionCase):
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
- # 1: Create a subcontracting partner
- main_company_1 = cls.env['res.partner'].create({'name': 'main_partner'})
- cls.subcontractor_partner1 = cls.env['res.partner'].create({
- 'name': 'Subcontractor 1',
- 'parent_id': main_company_1.id,
- 'company_id': cls.env.ref('base.main_company').id
- })
- # 2. Create a BOM of subcontracting type
- # 2.1. Comp1 has tracking by lot
- cls.comp1_sn = cls.env['product.product'].create({
- 'name': 'Component1',
- 'type': 'product',
- 'categ_id': cls.env.ref('product.product_category_all').id,
- 'tracking': 'serial'
- })
- cls.comp2 = cls.env['product.product'].create({
- 'name': 'Component2',
- 'type': 'product',
- 'categ_id': cls.env.ref('product.product_category_all').id,
- })
- # 2.2. Finished prodcut has tracking by serial number
- cls.finished_product = cls.env['product.product'].create({
- 'name': 'finished',
- 'type': 'product',
- 'categ_id': cls.env.ref('product.product_category_all').id,
- 'tracking': 'lot'
- })
- bom_form = Form(cls.env['mrp.bom'])
- bom_form.type = 'subcontract'
- bom_form.consumption = 'strict'
- bom_form.subcontractor_ids.add(cls.subcontractor_partner1)
- bom_form.product_tmpl_id = cls.finished_product.product_tmpl_id
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = cls.comp1_sn
- bom_line.product_qty = 1
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = cls.comp2
- bom_line.product_qty = 1
- cls.bom_tracked = bom_form.save()
- 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
- 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_without_package.new() as move:
- move.product_id = self.finished_product
- move.product_uom_qty = 1
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
- # We should be able to call the 'record_components' button
- self.assertEqual(picking_receipt.display_action_record_components, 'mandatory')
- # Check the created manufacturing order
- mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
- self.assertEqual(len(mo), 1)
- self.assertEqual(len(mo.picking_ids), 0)
- 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)
- # Create a RR
- pg1 = self.env['procurement.group'].create({})
- self.env['stock.warehouse.orderpoint'].create({
- 'name': 'xxx',
- 'product_id': self.comp1_sn.id,
- 'product_min_qty': 0,
- 'product_max_qty': 0,
- 'location_id': self.env.user.company_id.subcontracting_location_id.id,
- 'group_id': pg1.id,
- })
- # Run the scheduler and check the created picking
- self.env['procurement.group'].run_scheduler()
- picking = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
- self.assertEqual(len(picking), 1)
- self.assertEqual(picking.picking_type_id, wh.subcontracting_resupply_type_id)
- lot_id = self.env['stock.lot'].create({
- 'name': 'lot1',
- 'product_id': self.finished_product.id,
- 'company_id': self.env.company.id,
- })
- serial_id = self.env['stock.lot'].create({
- 'name': 'lot1',
- 'product_id': self.comp1_sn.id,
- 'company_id': self.env.company.id,
- })
- action = picking_receipt.action_record_components()
- mo = self.env['mrp.production'].browse(action['res_id'])
- mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
- mo_form.qty_producing = 1
- mo_form.lot_producing_id = lot_id
- with mo_form.move_line_raw_ids.edit(0) as ml:
- ml.lot_id = serial_id
- mo = mo_form.save()
- mo.subcontracting_record_component()
- # We should not be able to call the 'record_components' button
- self.assertEqual(picking_receipt.display_action_record_components, 'hide')
- 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_sn, 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_product, 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_tracked_only_finished(self):
- """ Test when only the finished product is tracked """
- self.finished_product.tracking = "serial"
- self.comp1_sn.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.env.ref('stock.picking_type_in')
- picking_form.partner_id = self.subcontractor_partner1
- with picking_form.move_ids_without_package.new() as move:
- move.product_id = self.finished_product
- move.product_uom_qty = nb_finished_product
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
- # We shouldn't be able to call the 'record_components' button
- self.assertEqual(picking_receipt.display_action_record_components, 'hide')
- wh = picking_receipt.picking_type_id.warehouse_id
- lot_names_finished = [f"subtracked_{i}" for i in range(nb_finished_product)]
- move_details = Form(picking_receipt.move_ids, view='stock.view_stock_move_nosuggest_operations')
- for lot_name in lot_names_finished:
- with move_details.move_line_nosuggest_ids.new() as ml:
- ml.qty_done = 1
- ml.lot_name = lot_name
- move_details.save()
- 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, wh.subcontracting_type_id)
- self.assertFalse(mos.picking_type_id.active)
- self.assertEqual(set(mos.lot_producing_id.mapped("name")), set(lot_names_finished))
- # Available quantities should be negative at the subcontracting location for each components
- avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, 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_product, wh.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_product.tracking = 'serial'
- # 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_without_package.new() as move:
- move.product_id = self.finished_product
- move.product_uom_qty = todo_nb
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
- # We should be able to call the 'record_components' button
- self.assertEqual(picking_receipt.display_action_record_components, 'mandatory')
- # Check the created manufacturing order
- mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
- self.assertEqual(len(mo), 1)
- self.assertEqual(len(mo.picking_ids), 0)
- 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)
- lot_comp2 = self.env['stock.lot'].create({
- 'name': 'lot_comp2',
- 'product_id': self.comp2.id,
- 'company_id': self.env.company.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_product.id,
- 'company_id': self.env.company.id,
- }))
- serials_comp1.append(self.env['stock.lot'].create({
- 'name': 'serials_comp1_%s' % i,
- 'product_id': self.comp1_sn.id,
- 'company_id': self.env.company.id,
- }))
- for i in range(todo_nb):
- action = picking_receipt.action_record_components()
- mo = self.env['mrp.production'].browse(action['res_id'])
- mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
- mo_form.lot_producing_id = serials_finished[i]
- with mo_form.move_line_raw_ids.edit(0) as ml:
- self.assertEqual(ml.product_id, self.comp1_sn)
- ml.lot_id = serials_comp1[i]
- with mo_form.move_line_raw_ids.edit(1) as ml:
- self.assertEqual(ml.product_id, self.comp2)
- ml.lot_id = lot_comp2
- mo = mo_form.save()
- mo.subcontracting_record_component()
- # We should not be able to call the 'record_components' button
- self.assertEqual(picking_receipt.display_action_record_components, 'hide')
- picking_receipt.button_validate()
- self.assertEqual(mo.state, 'done')
- self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped("state"), ['done'] * todo_nb)
- self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), todo_nb)
- self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped("qty_produced"), [1] * todo_nb)
- # Available quantities should be negative at the subcontracting location for each components
- avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, 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_product, wh.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_tracked_backorder02(self):
- """ Both component and finished product are tracked by lot. """
- todo_nb = 4
- resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
- finished_product, component = self.env['product.product'].create([{
- 'name': 'SuperProduct',
- 'type': 'product',
- 'tracking': 'lot',
- }, {
- 'name': 'Component',
- 'type': 'product',
- 'tracking': 'lot',
- 'route_ids': [(4, resupply_sub_on_order_route.id)],
- }])
- bom_form = Form(self.env['mrp.bom'])
- bom_form.type = 'subcontract'
- bom_form.subcontractor_ids.add(self.subcontractor_partner1)
- bom_form.product_tmpl_id = finished_product.product_tmpl_id
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = component
- bom_line.product_qty = 1
- bom = bom_form.save()
- finished_lot, component_lot = self.env['stock.lot'].create([{
- 'name': 'lot_%s' % product.name,
- 'product_id': product.id,
- 'company_id': self.env.company.id,
- } for product in [finished_product, component]])
- self.env['stock.quant']._update_available_quantity(component, self.env.ref('stock.stock_location_stock'), todo_nb, lot_id=component_lot)
- # 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_without_package.new() as move:
- move.product_id = finished_product
- move.product_uom_qty = todo_nb
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
- mo = self.env['mrp.production'].search([('bom_id', '=', bom.id)])
- # Process the delivery of the components
- compo_picking = mo.picking_ids
- compo_picking.action_assign()
- wizard_data = compo_picking.button_validate()
- wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
- wizard.process()
- for qty in [3, 1]:
- # Record the receiption of <qty> finished products
- picking_receipt = self.env['stock.picking'].search([('partner_id', '=', self.subcontractor_partner1.id), ('state', '!=', 'done')])
- action = picking_receipt.action_record_components()
- mo = self.env['mrp.production'].browse(action['res_id'])
- mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
- mo_form.qty_producing = qty
- mo_form.lot_producing_id = finished_lot
- with mo_form.move_line_raw_ids.edit(0) as ml:
- ml.lot_id = component_lot
- mo = mo_form.save()
- mo.subcontracting_record_component()
- # Validate the picking and create a backorder
- wizard_data = picking_receipt.button_validate()
- if qty == 3:
- wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
- wizard.process()
- self.assertEqual(picking_receipt.state, 'done')
- 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
- resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
- finished_product, component = self.env['product.product'].create([{
- 'name': 'Pepper Spray',
- 'type': 'product',
- 'tracking': 'serial',
- }, {
- 'name': 'Pepper',
- 'type': 'product',
- 'route_ids': [(4, resupply_sub_on_order_route.id)],
- }])
- bom_form = Form(self.env['mrp.bom'])
- bom_form.type = 'subcontract'
- bom_form.subcontractor_ids.add(self.subcontractor_partner1)
- bom_form.product_tmpl_id = finished_product.product_tmpl_id
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = component
- bom_line.product_qty = 1
- bom = bom_form.save()
- finished_serials = self.env['stock.lot'].create([{
- 'name': 'sn_%s' % str(i),
- 'product_id': finished_product.id,
- 'company_id': self.env.company.id,
- } for i in range(todo_nb)])
- self.env['stock.quant']._update_available_quantity(component, self.env.ref('stock.stock_location_stock'), 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_without_package.new() as move:
- move.product_id = finished_product
- move.product_uom_qty = todo_nb
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
- mo = self.env['mrp.production'].search([('bom_id', '=', bom.id)])
- # Process the delivery of the components
- compo_picking = mo.picking_ids
- compo_picking.action_assign()
- wizard_data = compo_picking.button_validate()
- wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
- wizard.process()
- picking_receipt = self.env['stock.picking'].search([('partner_id', '=', self.subcontractor_partner1.id), ('state', '!=', 'done')])
- for sn in finished_serials:
- # Record the production of each serial number separately
- action = picking_receipt.action_record_components()
- mo = self.env['mrp.production'].browse(action['res_id'])
- self.assertEqual(mo.move_raw_ids.state, 'assigned')
- mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
- mo_form.qty_producing = 1
- mo_form.lot_producing_id = sn
- mo = mo_form.save()
- mo.subcontracting_record_component()
- # Validate the picking
- picking_receipt.button_validate()
- self.assertEqual(picking_receipt.state, 'done')
- @tagged('post_install', '-at_install')
- class TestSubcontractingPortal(TransactionCase):
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
- # 1: Create a subcontracting partner
- main_partner = cls.env['res.partner'].create({'name': 'main_partner'})
- cls.subcontractor_partner1 = cls.env['res.partner'].create({
- 'name': 'subcontractor_partner',
- 'parent_id': main_partner.id,
- 'company_id': cls.env.ref('base.main_company').id,
- })
- # Make the subcontracting partner a portal user
- cls.portal_user = cls.env['res.users'].create({
- 'name': 'portal user (subcontractor)',
- 'partner_id': cls.subcontractor_partner1.id,
- 'login': 'subcontractor',
- 'password': 'subcontractor',
- 'email': 'subcontractor@subcontracting.portal',
- 'groups_id': [(6, 0, [cls.env.ref('base.group_portal').id])]
- })
- # 2. Create a BOM of subcontracting type
- # 2.1. Comp1 has tracking by lot
- cls.comp1_sn = cls.env['product.product'].create({
- 'name': 'Component1',
- 'type': 'product',
- 'categ_id': cls.env.ref('product.product_category_all').id,
- 'tracking': 'serial'
- })
- cls.comp2 = cls.env['product.product'].create({
- 'name': 'Component2',
- 'type': 'product',
- 'categ_id': cls.env.ref('product.product_category_all').id,
- })
- cls.product_not_in_bom = cls.env['product.product'].create({
- 'name': 'Product not in the BoM',
- 'type': 'product',
- })
- # 2.2. Finished prodcut has tracking by serial number
- cls.finished_product = cls.env['product.product'].create({
- 'name': 'finished',
- 'type': 'product',
- 'categ_id': cls.env.ref('product.product_category_all').id,
- 'tracking': 'lot'
- })
- bom_form = Form(cls.env['mrp.bom'])
- bom_form.type = 'subcontract'
- bom_form.consumption = 'warning'
- bom_form.subcontractor_ids.add(cls.subcontractor_partner1)
- bom_form.product_tmpl_id = cls.finished_product.product_tmpl_id
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = cls.comp1_sn
- bom_line.product_qty = 1
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = cls.comp2
- bom_line.product_qty = 1
- cls.bom_tracked = bom_form.save()
- def test_flow_subcontracting_portal(self):
- # 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_without_package.new() as move:
- move.product_id = self.finished_product
- 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_product.id,
- 'company_id': self.env.company.id,
- })
- lot2 = self.env['stock.lot'].with_user(self.portal_user).create({
- 'name': 'lot2',
- 'product_id': self.finished_product.id,
- 'company_id': self.env.company.id,
- })
- serial1 = self.env['stock.lot'].with_user(self.portal_user).create({
- 'name': 'lot1',
- 'product_id': self.comp1_sn.id,
- 'company_id': self.env.company.id,
- })
- serial2 = self.env['stock.lot'].with_user(self.portal_user).create({
- 'name': 'lot2',
- 'product_id': self.comp1_sn.id,
- 'company_id': self.env.company.id,
- })
- serial3 = self.env['stock.lot'].with_user(self.portal_user).create({
- 'name': 'lot3',
- 'product_id': self.comp1_sn.id,
- 'company_id': self.env.company.id,
- })
- action = picking_receipt.with_user(self.portal_user).with_context({'is_subcontracting_portal': 1}).move_ids.action_show_details()
- mo = self.env['mrp.production'].with_user(self.portal_user).browse(action['res_id'])
- mo_form = Form(mo.with_context(action['context']), view=action['view_id'])
- # Registering components for the first manufactured product
- mo_form.qty_producing = 1
- mo_form.lot_producing_id = lot1
- with mo_form.move_line_raw_ids.edit(0) as ml:
- ml.lot_id = serial1
- mo = mo_form.save()
- mo.subcontracting_record_component()
- # Continue record of components with new MO (backorder was when recording first MO)
- action = picking_receipt.with_user(self.portal_user).with_context({'is_subcontracting_portal': 1}).move_ids.action_show_details()
- mo = self.env['mrp.production'].with_user(self.portal_user).browse(action['res_id'])
- mo_form = Form(mo.with_context(action['context']), view=action['view_id'])
- # Registering components for the second manufactured product with over-consumption, which leads to a warning
- mo_form.qty_producing = 1
- mo_form.lot_producing_id = lot2
- with mo_form.move_line_raw_ids.edit(0) as ml:
- ml.lot_id = serial2
- with mo_form.move_line_raw_ids.new() as ml:
- ml.product_id = self.comp1_sn
- ml.lot_id = serial3
- with mo_form.move_line_raw_ids.edit(1) as ml:
- ml.qty_done = 2
- # The portal user should not be able to add a product not in the BoM
- with self.assertRaises(AccessError):
- with mo_form.move_line_raw_ids.new() as ml:
- ml.product_id = self.product_not_in_bom
- mo = mo_form.save()
- action_warning = mo.subcontracting_record_component()
- warning = Form(self.env['mrp.consumption.warning'].with_context(**action_warning['context']))
- warning = warning.save()
- warning.action_confirm()
- # 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].qty_done, 1)
- self.assertEqual(mo.move_line_raw_ids[0].lot_id, serial2)
- self.assertEqual(mo.move_line_raw_ids[1].qty_done, 1)
- self.assertEqual(mo.move_line_raw_ids[1].lot_id, serial3)
- self.assertEqual(mo.move_line_raw_ids[2].qty_done, 2)
- 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',
- 'type': 'product',
- 'route_ids': [Command.link(self.resupply_route.id)],
- })
- self.finished = self.env['product.product'].create({
- 'name': 'Finished',
- 'type': 'product',
- '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 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_without_package.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()
- wizard_data = picking_deliver.button_validate()
- wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
- wizard.process()
- # Receive
- for quantity in quantities:
- # Receive <quantity> finished products
- Form(self.env['stock.assign.serial'].with_context(
- default_move_id=picking_receipt.move_ids[0].id,
- default_next_serial_number=self.env['stock.lot']._get_next_serial(picking_receipt.company_id, picking_receipt.move_ids[0].product_id) or 'sn#1',
- default_next_serial_count=quantity,
- )).save().generate_serial_numbers()
- wizard_data = picking_receipt.button_validate()
- if wizard_data is not True:
- # Create backorder
- wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
- wizard.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_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = quantity
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
- # Receive finished products
- Form(self.env['stock.assign.serial'].with_context(
- default_move_id=picking_receipt.move_ids[0].id,
- default_next_serial_number=self.env['stock.lot']._get_next_serial(picking_receipt.company_id, picking_receipt.move_ids[0].product_id) or 'sn#1',
- default_next_serial_count=quantity,
- )).save().generate_serial_numbers()
- 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)
|