test_subcontracting.py 74 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from odoo import Command
  4. from odoo.exceptions import AccessError, UserError
  5. from odoo.tests import Form
  6. from odoo.tests.common import TransactionCase
  7. from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
  8. from odoo.tests import tagged
  9. from dateutil.relativedelta import relativedelta
  10. @tagged('post_install', '-at_install')
  11. class TestSubcontractingBasic(TransactionCase):
  12. def test_subcontracting_location_1(self):
  13. """ Checks the creation and presence of the subcontracting location. """
  14. self.assertTrue(self.env.company.subcontracting_location_id)
  15. self.assertTrue(self.env.company.subcontracting_location_id.active)
  16. company2 = self.env['res.company'].create({'name': 'Test Company'})
  17. self.assertTrue(company2.subcontracting_location_id)
  18. self.assertTrue(self.env.company.subcontracting_location_id != company2.subcontracting_location_id)
  19. @tagged('post_install', '-at_install')
  20. class TestSubcontractingFlows(TestMrpSubcontractingCommon):
  21. def test_flow_1(self):
  22. """ Don't tick any route on the components and trigger the creation of the subcontracting
  23. manufacturing order through a receipt picking. Create a reordering rule in the
  24. subcontracting locations for a component and run the scheduler to resupply. Checks if the
  25. resupplying actually works
  26. """
  27. # Check subcontracting picking Type
  28. self.assertTrue(all(self.env['stock.warehouse'].search([]).with_context(active_test=False).mapped('subcontracting_type_id.use_create_components_lots')))
  29. # Create a receipt picking from the subcontractor
  30. picking_form = Form(self.env['stock.picking'])
  31. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  32. picking_form.partner_id = self.subcontractor_partner1
  33. with picking_form.move_ids_without_package.new() as move:
  34. move.product_id = self.finished
  35. move.product_uom_qty = 1
  36. picking_receipt = picking_form.save()
  37. picking_receipt.action_confirm()
  38. # Nothing should be tracked
  39. self.assertTrue(all(m.product_uom_qty == m.reserved_availability for m in picking_receipt.move_ids))
  40. self.assertEqual(picking_receipt.state, 'assigned')
  41. self.assertEqual(picking_receipt.display_action_record_components, 'hide')
  42. # Check the created manufacturing order
  43. mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
  44. self.assertEqual(len(mo), 1)
  45. self.assertEqual(len(mo.picking_ids), 0)
  46. wh = picking_receipt.picking_type_id.warehouse_id
  47. self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
  48. self.assertFalse(mo.picking_type_id.active)
  49. # Create a RR
  50. pg1 = self.env['procurement.group'].create({})
  51. self.env['stock.warehouse.orderpoint'].create({
  52. 'name': 'xxx',
  53. 'product_id': self.comp1.id,
  54. 'product_min_qty': 0,
  55. 'product_max_qty': 0,
  56. 'location_id': self.env.user.company_id.subcontracting_location_id.id,
  57. 'group_id': pg1.id,
  58. })
  59. # Run the scheduler and check the created picking
  60. self.env['procurement.group'].run_scheduler()
  61. picking = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
  62. self.assertEqual(len(picking), 1)
  63. self.assertEqual(picking.picking_type_id, wh.subcontracting_resupply_type_id)
  64. picking_receipt.move_ids.quantity_done = 1
  65. picking_receipt.button_validate()
  66. self.assertEqual(mo.state, 'done')
  67. # Available quantities should be negative at the subcontracting location for each components
  68. avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  69. avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  70. avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
  71. self.assertEqual(avail_qty_comp1, -1)
  72. self.assertEqual(avail_qty_comp2, -1)
  73. self.assertEqual(avail_qty_finished, 1)
  74. # Ensure returns to subcontractor location
  75. return_form = Form(self.env['stock.return.picking'].with_context(active_id=picking_receipt.id, active_model='stock.picking'))
  76. return_wizard = return_form.save()
  77. return_picking_id, pick_type_id = return_wizard._create_returns()
  78. return_picking = self.env['stock.picking'].browse(return_picking_id)
  79. self.assertEqual(len(return_picking), 1)
  80. self.assertEqual(return_picking.move_ids.location_dest_id, self.subcontractor_partner1.property_stock_subcontractor)
  81. def test_flow_2(self):
  82. """ Tick "Resupply Subcontractor on Order" on the components and trigger the creation of
  83. the subcontracting manufacturing order through a receipt picking. Checks if the resupplying
  84. actually works. Also set a different subcontracting location on the partner.
  85. """
  86. # Tick "resupply subconractor on order"
  87. resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
  88. (self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
  89. # Create a different subcontract location & check rules replication
  90. 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)])
  91. partner_subcontract_location = self.env['stock.location'].create({
  92. 'name': 'Specific partner location',
  93. 'location_id': self.env.ref('stock.stock_location_locations_partner').id,
  94. 'usage': 'internal',
  95. 'company_id': self.env.company.id,
  96. 'is_subcontracting_location': True,
  97. })
  98. custom_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', partner_subcontract_location.id), ('location_dest_id', '=', partner_subcontract_location.id)])
  99. self.assertEqual(reference_location_rules_count, custom_location_rules_count)
  100. self.subcontractor_partner1.property_stock_subcontractor = partner_subcontract_location.id
  101. # Add a manufacturing lead time to check that the resupply delivery is correctly planned 2 days
  102. # before the subcontracting receipt
  103. self.finished.produce_delay = 2
  104. # Create a receipt picking from the subcontractor
  105. picking_form = Form(self.env['stock.picking'])
  106. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  107. picking_form.partner_id = self.subcontractor_partner1
  108. with picking_form.move_ids_without_package.new() as move:
  109. move.product_id = self.finished
  110. move.product_uom_qty = 1
  111. picking_receipt = picking_form.save()
  112. picking_receipt.action_confirm()
  113. # Nothing should be tracked
  114. self.assertEqual(picking_receipt.display_action_record_components, 'hide')
  115. # Pickings should directly be created
  116. mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
  117. self.assertEqual(len(mo.picking_ids), 1)
  118. self.assertEqual(mo.state, 'confirmed')
  119. self.assertEqual(len(mo.picking_ids.move_ids), 2)
  120. picking = mo.picking_ids
  121. wh = picking.picking_type_id.warehouse_id
  122. # The picking should be a delivery order
  123. self.assertEqual(picking.picking_type_id, wh.subcontracting_resupply_type_id)
  124. # The date planned should be correct
  125. self.assertEqual(picking_receipt.scheduled_date, picking.scheduled_date + relativedelta(days=self.finished.produce_delay))
  126. self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
  127. self.assertFalse(mo.picking_type_id.active)
  128. # No manufacturing order for `self.comp2`
  129. comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
  130. self.assertEqual(len(comp2mo), 0)
  131. picking_receipt.move_ids.quantity_done = 1
  132. picking_receipt.button_validate()
  133. self.assertEqual(mo.state, 'done')
  134. # Available quantities should be negative at the subcontracting location for each components
  135. avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  136. avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  137. avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
  138. self.assertEqual(avail_qty_comp1, -1)
  139. self.assertEqual(avail_qty_comp2, -1)
  140. self.assertEqual(avail_qty_finished, 1)
  141. avail_qty_comp1_in_global_location = self.env['stock.quant']._get_available_quantity(self.comp1, self.env.company.subcontracting_location_id, allow_negative=True)
  142. avail_qty_comp2_in_global_location = self.env['stock.quant']._get_available_quantity(self.comp2, self.env.company.subcontracting_location_id, allow_negative=True)
  143. self.assertEqual(avail_qty_comp1_in_global_location, 0.0)
  144. self.assertEqual(avail_qty_comp2_in_global_location, 0.0)
  145. def test_flow_3(self):
  146. """ Tick "Resupply Subcontractor on Order" and "MTO" on the components and trigger the
  147. creation of the subcontracting manufacturing order through a receipt picking. Checks if the
  148. resupplying actually works. One of the component has also "manufacture" set and a BOM
  149. linked. Checks that an MO is created for this one.
  150. """
  151. # Tick "resupply subconractor on order"
  152. resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
  153. (self.comp1 + self.comp2).write({'route_ids': [(6, None, [resupply_sub_on_order_route.id])]})
  154. # Tick "manufacture" and MTO on self.comp2
  155. mto_route = self.env.ref('stock.route_warehouse0_mto')
  156. mto_route.active = True
  157. manufacture_route = self.env['stock.route'].search([('name', '=', 'Manufacture')])
  158. self.comp2.write({'route_ids': [(4, manufacture_route.id, None)]})
  159. self.comp2.write({'route_ids': [(4, mto_route.id, None)]})
  160. # Create a receipt picking from the subcontractor
  161. picking_form = Form(self.env['stock.picking'])
  162. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  163. picking_form.partner_id = self.subcontractor_partner1
  164. with picking_form.move_ids_without_package.new() as move:
  165. move.product_id = self.finished
  166. move.product_uom_qty = 1
  167. picking_receipt = picking_form.save()
  168. picking_receipt.action_confirm()
  169. # Nothing should be tracked
  170. self.assertEqual(picking_receipt.display_action_record_components, 'hide')
  171. # Pickings should directly be created
  172. mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
  173. self.assertEqual(mo.state, 'confirmed')
  174. picking_delivery = mo.picking_ids
  175. self.assertEqual(len(picking_delivery), 1)
  176. self.assertEqual(len(picking_delivery.move_ids), 2)
  177. self.assertEqual(picking_delivery.origin, picking_receipt.name)
  178. self.assertEqual(picking_delivery.partner_id, picking_receipt.partner_id)
  179. # The picking should be a delivery order
  180. wh = picking_receipt.picking_type_id.warehouse_id
  181. self.assertEqual(mo.picking_ids.picking_type_id, wh.subcontracting_resupply_type_id)
  182. self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
  183. self.assertFalse(mo.picking_type_id.active)
  184. # As well as a manufacturing order for `self.comp2`
  185. comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
  186. self.assertEqual(len(comp2mo), 1)
  187. picking_receipt.move_ids.quantity_done = 1
  188. picking_receipt.button_validate()
  189. self.assertEqual(mo.state, 'done')
  190. # Available quantities should be negative at the subcontracting location for each components
  191. avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  192. avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  193. avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
  194. self.assertEqual(avail_qty_comp1, -1)
  195. self.assertEqual(avail_qty_comp2, -1)
  196. self.assertEqual(avail_qty_finished, 1)
  197. def test_flow_4(self):
  198. """ Tick "Manufacture" and "MTO" on the components and trigger the
  199. creation of the subcontracting manufacturing order through a receipt
  200. picking. Checks that the delivery and MO for its components are
  201. automatically created.
  202. """
  203. # Required for `location_id` to be visible in the view
  204. self.env.user.groups_id += self.env.ref('stock.group_stock_multi_locations')
  205. # Tick "manufacture" and MTO on self.comp2
  206. mto_route = self.env.ref('stock.route_warehouse0_mto')
  207. mto_route.active = True
  208. manufacture_route = self.env['stock.route'].search([('name', '=', 'Manufacture')])
  209. self.comp2.write({'route_ids': [(6, None, [manufacture_route.id, mto_route.id])]})
  210. orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
  211. orderpoint_form.product_id = self.comp2
  212. orderpoint_form.product_min_qty = 0.0
  213. orderpoint_form.product_max_qty = 10.0
  214. orderpoint_form.location_id = self.env.company.subcontracting_location_id
  215. orderpoint = orderpoint_form.save()
  216. # Create a receipt picking from the subcontractor
  217. picking_form = Form(self.env['stock.picking'])
  218. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  219. picking_form.partner_id = self.subcontractor_partner1
  220. with picking_form.move_ids_without_package.new() as move:
  221. move.product_id = self.finished
  222. move.product_uom_qty = 1
  223. picking_receipt = picking_form.save()
  224. picking_receipt.action_confirm()
  225. warehouse = picking_receipt.picking_type_id.warehouse_id
  226. # Pickings should directly be created
  227. mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
  228. self.assertEqual(mo.state, 'confirmed')
  229. picking_delivery = mo.picking_ids
  230. self.assertFalse(picking_delivery)
  231. picking_delivery = self.env['stock.picking'].search([('origin', 'ilike', '%' + picking_receipt.name + '%')])
  232. self.assertFalse(picking_delivery)
  233. move = self.env['stock.move'].search([
  234. ('product_id', '=', self.comp2.id),
  235. ('location_id', '=', warehouse.lot_stock_id.id),
  236. ('location_dest_id', '=', self.env.company.subcontracting_location_id.id)
  237. ])
  238. self.assertTrue(move)
  239. picking_delivery = move.picking_id
  240. self.assertTrue(picking_delivery)
  241. self.assertEqual(move.product_uom_qty, 11.0)
  242. # As well as a manufacturing order for `self.comp2`
  243. comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
  244. self.assertEqual(len(comp2mo), 1)
  245. def test_flow_5(self):
  246. """ Check that the correct BoM is chosen accordingly to the partner
  247. """
  248. # We create a second partner of type subcontractor
  249. main_partner_2 = self.env['res.partner'].create({'name': 'main_partner'})
  250. subcontractor_partner2 = self.env['res.partner'].create({
  251. 'name': 'subcontractor_partner',
  252. 'parent_id': main_partner_2.id,
  253. 'company_id': self.env.ref('base.main_company').id
  254. })
  255. # We create a different BoM for the same product
  256. comp3 = self.env['product.product'].create({
  257. 'name': 'Component1',
  258. 'type': 'product',
  259. 'categ_id': self.env.ref('product.product_category_all').id,
  260. })
  261. bom_form = Form(self.env['mrp.bom'])
  262. bom_form.type = 'subcontract'
  263. bom_form.product_tmpl_id = self.finished.product_tmpl_id
  264. with bom_form.bom_line_ids.new() as bom_line:
  265. bom_line.product_id = self.comp1
  266. bom_line.product_qty = 1
  267. with bom_form.bom_line_ids.new() as bom_line:
  268. bom_line.product_id = comp3
  269. bom_line.product_qty = 1
  270. bom2 = bom_form.save()
  271. # We assign the second BoM to the new partner
  272. self.bom.write({'subcontractor_ids': [(4, self.subcontractor_partner1.id, None)]})
  273. bom2.write({'subcontractor_ids': [(4, subcontractor_partner2.id, None)]})
  274. # Create a receipt picking from the subcontractor1
  275. picking_form = Form(self.env['stock.picking'])
  276. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  277. picking_form.partner_id = self.subcontractor_partner1
  278. with picking_form.move_ids_without_package.new() as move:
  279. move.product_id = self.finished
  280. move.product_uom_qty = 1
  281. picking_receipt1 = picking_form.save()
  282. picking_receipt1.action_confirm()
  283. # Create a receipt picking from the subcontractor2
  284. picking_form = Form(self.env['stock.picking'])
  285. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  286. picking_form.partner_id = subcontractor_partner2
  287. with picking_form.move_ids_without_package.new() as move:
  288. move.product_id = self.finished
  289. move.product_uom_qty = 1
  290. picking_receipt2 = picking_form.save()
  291. picking_receipt2.action_confirm()
  292. mo_pick1 = picking_receipt1.move_ids.mapped('move_orig_ids.production_id')
  293. mo_pick2 = picking_receipt2.move_ids.mapped('move_orig_ids.production_id')
  294. self.assertEqual(len(mo_pick1), 1)
  295. self.assertEqual(len(mo_pick2), 1)
  296. self.assertEqual(mo_pick1.bom_id, self.bom)
  297. self.assertEqual(mo_pick2.bom_id, bom2)
  298. def test_flow_6(self):
  299. """ Extra quantity on the move.
  300. """
  301. # We create a second partner of type subcontractor
  302. main_partner_2 = self.env['res.partner'].create({'name': 'main_partner'})
  303. subcontractor_partner2 = self.env['res.partner'].create({
  304. 'name': 'subcontractor_partner',
  305. 'parent_id': main_partner_2.id,
  306. 'company_id': self.env.ref('base.main_company').id,
  307. })
  308. self.env.invalidate_all()
  309. # We create a different BoM for the same product
  310. comp3 = self.env['product.product'].create({
  311. 'name': 'Component3',
  312. 'type': 'product',
  313. 'categ_id': self.env.ref('product.product_category_all').id,
  314. })
  315. bom_form = Form(self.env['mrp.bom'])
  316. bom_form.type = 'subcontract'
  317. bom_form.product_tmpl_id = self.finished.product_tmpl_id
  318. with bom_form.bom_line_ids.new() as bom_line:
  319. bom_line.product_id = self.comp1
  320. bom_line.product_qty = 1
  321. with bom_form.bom_line_ids.new() as bom_line:
  322. bom_line.product_id = comp3
  323. bom_line.product_qty = 2
  324. bom2 = bom_form.save()
  325. # We assign the second BoM to the new partner
  326. self.bom.write({'subcontractor_ids': [(4, self.subcontractor_partner1.id, None)]})
  327. bom2.write({'subcontractor_ids': [(4, subcontractor_partner2.id, None)]})
  328. # Create a receipt picking from the subcontractor1
  329. picking_form = Form(self.env['stock.picking'])
  330. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  331. picking_form.partner_id = subcontractor_partner2
  332. with picking_form.move_ids_without_package.new() as move:
  333. move.product_id = self.finished
  334. move.product_uom_qty = 1
  335. picking_receipt = picking_form.save()
  336. picking_receipt.action_confirm()
  337. picking_receipt.move_ids.quantity_done = 3.0
  338. picking_receipt._action_done()
  339. mo = picking_receipt._get_subcontract_production()
  340. move_comp1 = mo.move_raw_ids.filtered(lambda m: m.product_id == self.comp1)
  341. move_comp3 = mo.move_raw_ids.filtered(lambda m: m.product_id == comp3)
  342. self.assertEqual(sum(move_comp1.mapped('product_uom_qty')), 3.0)
  343. self.assertEqual(sum(move_comp3.mapped('product_uom_qty')), 6.0)
  344. self.assertEqual(sum(move_comp1.mapped('quantity_done')), 3.0)
  345. self.assertEqual(sum(move_comp3.mapped('quantity_done')), 6.0)
  346. move_finished = mo.move_finished_ids
  347. self.assertEqual(sum(move_finished.mapped('product_uom_qty')), 3.0)
  348. self.assertEqual(sum(move_finished.mapped('quantity_done')), 3.0)
  349. def test_flow_8(self):
  350. resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
  351. (self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
  352. # Create a receipt picking from the subcontractor
  353. picking_form = Form(self.env['stock.picking'])
  354. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  355. picking_form.partner_id = self.subcontractor_partner1
  356. with picking_form.move_ids_without_package.new() as move:
  357. move.product_id = self.finished
  358. move.product_uom_qty = 5
  359. picking_receipt = picking_form.save()
  360. picking_receipt.action_confirm()
  361. picking_receipt.move_ids.quantity_done = 3
  362. backorder_wiz = picking_receipt.button_validate()
  363. backorder_wiz = Form(self.env[backorder_wiz['res_model']].with_context(backorder_wiz['context'])).save()
  364. backorder_wiz.process()
  365. backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_receipt.id)])
  366. self.assertTrue(backorder)
  367. self.assertEqual(backorder.move_ids.product_uom_qty, 2)
  368. mo_done = backorder.move_ids.move_orig_ids.production_id.filtered(lambda p: p.state == 'done')
  369. backorder_mo = backorder.move_ids.move_orig_ids.production_id.filtered(lambda p: p.state != 'done')
  370. self.assertTrue(mo_done)
  371. self.assertEqual(mo_done.qty_produced, 3)
  372. self.assertEqual(mo_done.product_uom_qty, 3)
  373. self.assertTrue(backorder_mo)
  374. self.assertEqual(backorder_mo.product_uom_qty, 2)
  375. self.assertEqual(backorder_mo.qty_produced, 0)
  376. backorder.move_ids.quantity_done = 2
  377. backorder._action_done()
  378. self.assertTrue(picking_receipt.move_ids.move_orig_ids[0].production_id.state == 'done')
  379. def test_flow_9(self):
  380. """Ensure that cancel the subcontract moves will also delete the
  381. components need for the subcontractor.
  382. """
  383. resupply_sub_on_order_route = self.env['stock.route'].search([
  384. ('name', '=', 'Resupply Subcontractor on Order')
  385. ])
  386. (self.comp1 + self.comp2).write({
  387. 'route_ids': [(4, resupply_sub_on_order_route.id)]
  388. })
  389. picking_form = Form(self.env['stock.picking'])
  390. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  391. picking_form.partner_id = self.subcontractor_partner1
  392. with picking_form.move_ids_without_package.new() as move:
  393. move.product_id = self.finished
  394. move.product_uom_qty = 5
  395. picking_receipt = picking_form.save()
  396. picking_receipt.action_confirm()
  397. picking_delivery = self.env['stock.move'].search([
  398. ('product_id', 'in', (self.comp1 | self.comp2).ids)
  399. ]).picking_id
  400. self.assertTrue(picking_delivery)
  401. self.assertEqual(picking_delivery.state, 'confirmed')
  402. self.assertEqual(self.comp1.virtual_available, -5)
  403. self.assertEqual(self.comp2.virtual_available, -5)
  404. # action_cancel is not call on the picking in order
  405. # to test behavior from other source than picking (e.g. puchase).
  406. picking_receipt.move_ids._action_cancel()
  407. self.assertEqual(picking_delivery.state, 'cancel')
  408. self.assertEqual(self.comp1.virtual_available, 0.0)
  409. self.assertEqual(self.comp1.virtual_available, 0.0)
  410. def test_flow_10(self):
  411. """Receipts from a children contact of a subcontractor are properly
  412. handled.
  413. """
  414. # Create a children contact
  415. subcontractor_contact = self.env['res.partner'].create({
  416. 'name': 'Test children subcontractor contact',
  417. 'parent_id': self.subcontractor_partner1.id,
  418. })
  419. # Create a receipt picking from the subcontractor
  420. picking_form = Form(self.env['stock.picking'])
  421. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  422. picking_form.partner_id = subcontractor_contact
  423. with picking_form.move_ids_without_package.new() as move:
  424. move.product_id = self.finished
  425. move.product_uom_qty = 1
  426. picking_receipt = picking_form.save()
  427. picking_receipt.action_confirm()
  428. # Check that a manufacturing order is created
  429. mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
  430. self.assertEqual(len(mo), 1)
  431. def test_flow_flexible_bom_1(self):
  432. """ Record Component for a bom subcontracted with a flexible and flexible + warning consumption """
  433. self.bom.consumption = 'flexible'
  434. # Create a receipt picking from the subcontractor
  435. picking_form = Form(self.env['stock.picking'])
  436. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  437. picking_form.partner_id = self.subcontractor_partner1
  438. with picking_form.move_ids_without_package.new() as move:
  439. move.product_id = self.finished
  440. move.product_uom_qty = 1
  441. picking_receipt = picking_form.save()
  442. picking_receipt.action_confirm()
  443. self.assertEqual(picking_receipt.display_action_record_components, 'facultative')
  444. action = picking_receipt.action_record_components()
  445. mo = self.env['mrp.production'].browse(action['res_id'])
  446. mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
  447. mo_form.qty_producing = 1
  448. with mo_form.move_line_raw_ids.edit(0) as ml:
  449. self.assertEqual(ml.product_id, self.comp1)
  450. self.assertEqual(ml.qty_done, 1)
  451. ml.qty_done = 2
  452. mo = mo_form.save()
  453. mo.subcontracting_record_component()
  454. self.assertEqual(mo.move_raw_ids[0].move_line_ids.qty_done, 2)
  455. # We should not be able to call the 'record_components' button
  456. self.assertEqual(picking_receipt.display_action_record_components, 'hide')
  457. picking_receipt.button_validate()
  458. self.assertEqual(mo.state, 'done')
  459. avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  460. self.assertEqual(avail_qty_comp1, -2)
  461. def test_flow_warning_bom_1(self):
  462. """ Record Component for a bom subcontracted with a flexible and flexible + warning consumption """
  463. self.bom.consumption = 'warning'
  464. # Create a receipt picking from the subcontractor
  465. picking_form = Form(self.env['stock.picking'])
  466. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  467. picking_form.partner_id = self.subcontractor_partner1
  468. with picking_form.move_ids_without_package.new() as move:
  469. move.product_id = self.finished
  470. move.product_uom_qty = 1
  471. picking_receipt = picking_form.save()
  472. picking_receipt.action_confirm()
  473. self.assertEqual(picking_receipt.display_action_record_components, 'facultative')
  474. action = picking_receipt.action_record_components()
  475. mo = self.env['mrp.production'].browse(action['res_id'])
  476. mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
  477. mo_form.qty_producing = 1
  478. with mo_form.move_line_raw_ids.edit(0) as ml:
  479. self.assertEqual(ml.product_id, self.comp1)
  480. self.assertEqual(ml.qty_done, 1)
  481. ml.qty_done = 2
  482. mo = mo_form.save()
  483. action_warning = mo.subcontracting_record_component()
  484. warning = Form(self.env['mrp.consumption.warning'].with_context(**action_warning['context']))
  485. warning = warning.save()
  486. warning.action_cancel()
  487. action_warning = mo.subcontracting_record_component()
  488. warning = Form(self.env['mrp.consumption.warning'].with_context(**action_warning['context']))
  489. warning = warning.save()
  490. warning.action_confirm()
  491. self.assertEqual(mo.move_raw_ids[0].move_line_ids.qty_done, 2)
  492. # We should not be able to call the 'record_components' button
  493. self.assertEqual(picking_receipt.display_action_record_components, 'hide')
  494. picking_receipt.button_validate()
  495. self.assertEqual(mo.state, 'done')
  496. avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  497. self.assertEqual(avail_qty_comp1, -2)
  498. def test_mrp_report_bom_structure_subcontracting(self):
  499. self.comp2_bom.write({'type': 'subcontract', 'subcontractor_ids': [Command.link(self.subcontractor_partner1.id)]})
  500. self.env['product.supplierinfo'].create({
  501. 'product_tmpl_id': self.finished.product_tmpl_id.id,
  502. 'partner_id': self.subcontractor_partner1.id,
  503. 'price': 10,
  504. })
  505. supplier = self.env['product.supplierinfo'].create({
  506. 'product_tmpl_id': self.comp2.product_tmpl_id.id,
  507. 'partner_id': self.subcontractor_partner1.id,
  508. 'price': 5,
  509. })
  510. self.env['product.supplierinfo'].create({
  511. 'product_tmpl_id': self.comp2.product_tmpl_id.id,
  512. 'partner_id': self.subcontractor_partner1.id,
  513. 'price': 1,
  514. 'min_qty': 5,
  515. })
  516. self.assertTrue(supplier.is_subcontractor)
  517. self.comp1.standard_price = 5
  518. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, searchQty=1, searchVariant=False)
  519. subcontracting_values = report_values['lines']['subcontracting']
  520. self.assertEqual(subcontracting_values['name'], self.subcontractor_partner1.display_name)
  521. self.assertEqual(report_values['lines']['bom_cost'], 20) # 10 For subcontracting + 5 for comp1 + 5 for subcontracting of comp2_bom
  522. self.assertEqual(subcontracting_values['bom_cost'], 10)
  523. self.assertEqual(subcontracting_values['prod_cost'], 10)
  524. self.assertEqual(report_values['lines']['components'][0]['bom_cost'], 5)
  525. self.assertEqual(report_values['lines']['components'][1]['bom_cost'], 5)
  526. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, searchQty=3, searchVariant=False)
  527. subcontracting_values = report_values['lines']['subcontracting']
  528. self.assertEqual(report_values['lines']['bom_cost'], 60) # 30 for subcontracting + 15 for comp1 + 15 for subcontracting of comp2_bom
  529. self.assertEqual(subcontracting_values['bom_cost'], 30)
  530. self.assertEqual(subcontracting_values['prod_cost'], 30)
  531. self.assertEqual(report_values['lines']['components'][0]['bom_cost'], 15)
  532. self.assertEqual(report_values['lines']['components'][1]['bom_cost'], 15)
  533. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, searchQty=5, searchVariant=False)
  534. subcontracting_values = report_values['lines']['subcontracting']
  535. self.assertEqual(report_values['lines']['bom_cost'], 80) # 50 for subcontracting + 25 for comp1 + 5 for subcontracting of comp2_bom
  536. self.assertEqual(subcontracting_values['bom_cost'], 50)
  537. self.assertEqual(subcontracting_values['prod_cost'], 50)
  538. self.assertEqual(report_values['lines']['components'][0]['bom_cost'], 25)
  539. self.assertEqual(report_values['lines']['components'][1]['bom_cost'], 5)
  540. def test_several_backorders(self):
  541. def process_picking(picking, qty):
  542. picking.move_ids.quantity_done = qty
  543. action = picking.button_validate()
  544. if isinstance(action, dict):
  545. wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
  546. wizard.process()
  547. resupply_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
  548. finished, component = self.env['product.product'].create([{
  549. 'name': 'Finished Product',
  550. 'type': 'product',
  551. }, {
  552. 'name': 'Component',
  553. 'type': 'product',
  554. 'route_ids': [(4, resupply_route.id)],
  555. }])
  556. bom = self.env['mrp.bom'].create({
  557. 'product_tmpl_id': finished.product_tmpl_id.id,
  558. 'product_qty': 1.0,
  559. 'type': 'subcontract',
  560. 'subcontractor_ids': [(4, self.subcontractor_partner1.id)],
  561. 'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1.0})],
  562. })
  563. picking_form = Form(self.env['stock.picking'])
  564. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  565. picking_form.partner_id = self.subcontractor_partner1
  566. with picking_form.move_ids_without_package.new() as move:
  567. move.product_id = finished
  568. move.product_uom_qty = 5
  569. picking = picking_form.save()
  570. picking.action_confirm()
  571. supply_picking = self.env['mrp.production'].search([('bom_id', '=', bom.id)]).picking_ids
  572. process_picking(supply_picking, 5)
  573. process_picking(picking, 1.25)
  574. backorder01 = picking.backorder_ids
  575. process_picking(backorder01, 1)
  576. backorder02 = backorder01.backorder_ids
  577. process_picking(backorder02, 0)
  578. self.assertEqual(backorder02.move_ids.quantity_done, 2.75)
  579. self.assertEqual(self.env['mrp.production'].search_count([('bom_id', '=', bom.id)]), 3)
  580. def test_subcontracting_rules_replication(self):
  581. """ Test activate/archive subcontracting location rules."""
  582. 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)])
  583. warehouse_related_rules = reference_location_rules.filtered(lambda r: r.warehouse_id)
  584. company_rules = reference_location_rules - warehouse_related_rules
  585. # Create a custom subcontracting location
  586. custom_subcontracting_location = self.env['stock.location'].create({
  587. 'name': 'Custom Subcontracting Location',
  588. 'location_id': self.env.ref('stock.stock_location_locations').id,
  589. 'usage': 'internal',
  590. 'company_id': self.env.company.id,
  591. 'is_subcontracting_location': True,
  592. })
  593. custom_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', custom_subcontracting_location.id), ('location_dest_id', '=', custom_subcontracting_location.id)])
  594. self.assertEqual(len(reference_location_rules), custom_location_rules_count)
  595. # Add a new warehouse
  596. warehouse = self.env['stock.warehouse'].create({
  597. 'name': 'Additional Warehouse',
  598. 'code': 'ADD'
  599. })
  600. 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')])
  601. self.assertEqual(len(warehouse_related_rules) * 4 + len(company_rules) * 2, company_subcontracting_locations_rules_count)
  602. # Custom location no longer a subcontracting one
  603. custom_subcontracting_location.is_subcontracting_location = False
  604. custom_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', custom_subcontracting_location.id), ('location_dest_id', '=', custom_subcontracting_location.id)])
  605. self.assertEqual(custom_location_rules_count, 0)
  606. def test_subcontracting_date_warning(self):
  607. with Form(self.env['stock.picking'].with_context(default_immediate_transfer=True)) as picking_form:
  608. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  609. picking_form.partner_id = self.subcontractor_partner1
  610. with picking_form.move_ids_without_package.new() as move:
  611. move.product_id = self.finished
  612. move.quantity_done = 3
  613. picking_receipt = picking_form.save()
  614. self.assertEqual(picking_form.json_popover, False)
  615. subcontract = picking_receipt._get_subcontract_production()
  616. self.assertEqual(subcontract.date_planned_start, picking_receipt.scheduled_date)
  617. self.assertEqual(subcontract.date_planned_finished, picking_receipt.scheduled_date)
  618. def test_subcontracting_set_quantity_done(self):
  619. """ Tests to set a quantity done directly on a subcontracted move without using the subcontracting wizard. Checks that it does the same
  620. as it would do with the wizard. Since immediate/planned transfers have different flows, we need to test both.
  621. """
  622. self.bom.consumption = 'flexible'
  623. quantities = [10, 15, 12, 14]
  624. # For planned transfers, as some triggers are different if it's an immediate transfer.
  625. with Form(self.env['stock.picking']) as picking_form:
  626. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  627. picking_form.partner_id = self.subcontractor_partner1
  628. with picking_form.move_ids_without_package.new() as move:
  629. move.product_id = self.finished
  630. move.product_uom_qty = quantities[0]
  631. picking_receipt = picking_form.save()
  632. picking_receipt.action_confirm()
  633. self.assertEqual(picking_receipt.immediate_transfer, False)
  634. move = picking_receipt.move_ids_without_package
  635. for qty in quantities[1:]:
  636. move.quantity_done = qty
  637. subcontracted = move._get_subcontract_production().filtered(lambda p: p.state != 'cancel')
  638. self.assertEqual(sum(subcontracted.mapped('product_qty')), qty)
  639. picking_receipt.button_validate()
  640. self.assertEqual(move.product_uom_qty, quantities[-1])
  641. self.assertEqual(move.quantity_done, quantities[-1])
  642. subcontracted = move._get_subcontract_production().filtered(lambda p: p.state == 'done')
  643. self.assertEqual(sum(subcontracted.mapped('qty_produced')), quantities[-1])
  644. # Now the same with an immediate transfer
  645. with Form(self.env['stock.picking'].with_context(default_immediate_transfer=True)) as picking_form:
  646. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  647. picking_form.partner_id = self.subcontractor_partner1
  648. with picking_form.move_ids_without_package.new() as move:
  649. move.product_id = self.finished
  650. move.quantity_done = quantities[0]
  651. picking_receipt = picking_form.save()
  652. self.assertEqual(picking_receipt.immediate_transfer, True)
  653. move = picking_receipt.move_ids_without_package
  654. subcontracted = move._get_subcontract_production()
  655. self.assertEqual(subcontracted.product_qty, quantities[0])
  656. for qty in quantities[1:]:
  657. move.quantity_done = qty
  658. subcontracted = move._get_subcontract_production().filtered(lambda p: p.state != 'cancel')
  659. self.assertEqual(sum(subcontracted.mapped('product_qty')), qty)
  660. picking_receipt.button_validate()
  661. self.assertEqual(move.product_uom_qty, quantities[-1])
  662. self.assertEqual(move.quantity_done, quantities[-1])
  663. subcontracted = move._get_subcontract_production().filtered(lambda p: p.state == 'done')
  664. self.assertEqual(sum(subcontracted.mapped('qty_produced')), quantities[-1])
  665. def test_change_reception_serial(self):
  666. self.finished.tracking = 'serial'
  667. self.bom.consumption = 'flexible'
  668. finished_lots = self.env['stock.lot'].create([{
  669. 'name': 'lot_%s' % number,
  670. 'product_id': self.finished.id,
  671. 'company_id': self.env.company.id,
  672. } for number in range(3)])
  673. with Form(self.env['stock.picking']) as picking_form:
  674. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  675. picking_form.partner_id = self.subcontractor_partner1
  676. with picking_form.move_ids_without_package.new() as move:
  677. move.product_id = self.finished
  678. move.product_uom_qty = 3
  679. picking_receipt = picking_form.save()
  680. picking_receipt.action_confirm()
  681. # Register serial number for each finished product
  682. for lot in finished_lots:
  683. action = picking_receipt.move_ids.action_show_details()
  684. self.assertEqual(action['name'], 'Subcontract', "It should open the subcontract record components wizard instead.")
  685. mo = self.env['mrp.production'].browse(action['res_id'])
  686. with Form(mo.with_context(action['context']), view=action['view_id']) as mo_form:
  687. mo_form.qty_producing = 1
  688. mo_form.lot_producing_id = lot
  689. mo_form.save()
  690. mo.subcontracting_record_component()
  691. subcontract_move = picking_receipt.move_ids_without_package.filtered(lambda m: m.is_subcontract)
  692. self.assertEqual(len(subcontract_move._get_subcontract_production()), 3)
  693. self.assertEqual(len(subcontract_move._get_subcontract_production().lot_producing_id), 3)
  694. self.assertRecordValues(subcontract_move._get_subcontract_production().lot_producing_id.sorted('id'), [
  695. {'id': finished_lots[0].id},
  696. {'id': finished_lots[1].id},
  697. {'id': finished_lots[2].id},
  698. ])
  699. new_lot = self.env['stock.lot'].create({
  700. 'name': 'lot_alter',
  701. 'product_id': self.finished.id,
  702. 'company_id': self.env.company.id,
  703. })
  704. action = picking_receipt.move_ids.action_show_details()
  705. self.assertEqual(action['name'], 'Detailed Operations', "The subcontract record components wizard shouldn't be available now.")
  706. with Form(subcontract_move.with_context(action['context']), view=action['view_id']) as move_form:
  707. with move_form.move_line_nosuggest_ids.edit(2) as move_line:
  708. move_line.lot_id = new_lot
  709. move_form.save()
  710. subcontracted_mo = subcontract_move._get_subcontract_production()
  711. self.assertEqual(len(subcontracted_mo.filtered(lambda p: p.lot_producing_id == new_lot)), 1)
  712. self.assertEqual(len(subcontracted_mo.filtered(lambda p: p.lot_producing_id != new_lot)), 2)
  713. def test_multiple_component_records_for_incomplete_move(self):
  714. self.bom.consumption = 'flexible'
  715. with Form(self.env['stock.picking']) as picking_form:
  716. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  717. picking_form.partner_id = self.subcontractor_partner1
  718. with picking_form.move_ids_without_package.new() as move:
  719. move.product_id = self.finished
  720. move.product_uom_qty = 10
  721. picking_receipt = picking_form.save()
  722. picking_receipt.action_confirm()
  723. move = picking_receipt.move_ids_without_package
  724. # Register the five first finished products
  725. action = move.action_show_details()
  726. mo = self.env['mrp.production'].browse(action['res_id'])
  727. with Form(mo.with_context(action['context']), view=action['view_id']) as mo_form:
  728. mo_form.qty_producing = 5
  729. mo_form.save()
  730. mo.subcontracting_record_component()
  731. self.assertEqual(move.quantity_done, 5)
  732. # Register two other finished products
  733. action = move.action_show_details()
  734. mo = self.env['mrp.production'].browse(action['res_id'])
  735. with Form(mo.with_context(action['context']), view=action['view_id']) as mo_form:
  736. mo_form.qty_producing = 2
  737. mo_form.save()
  738. mo.subcontracting_record_component()
  739. self.assertEqual(move.quantity_done, 7)
  740. # Validate picking without backorder
  741. backorder_wizard_dict = picking_receipt.button_validate()
  742. backorder_wizard_form = Form(self.env[backorder_wizard_dict['res_model']].with_context(backorder_wizard_dict['context']))
  743. backorder_wizard_form.save().process_cancel_backorder()
  744. self.assertRecordValues(move._get_subcontract_production(), [
  745. {'product_qty': 5, 'state': 'done'},
  746. {'product_qty': 2, 'state': 'done'},
  747. {'product_qty': 3, 'state': 'cancel'},
  748. ])
  749. @tagged('post_install', '-at_install')
  750. class TestSubcontractingTracking(TransactionCase):
  751. @classmethod
  752. def setUpClass(cls):
  753. super().setUpClass()
  754. # 1: Create a subcontracting partner
  755. main_company_1 = cls.env['res.partner'].create({'name': 'main_partner'})
  756. cls.subcontractor_partner1 = cls.env['res.partner'].create({
  757. 'name': 'Subcontractor 1',
  758. 'parent_id': main_company_1.id,
  759. 'company_id': cls.env.ref('base.main_company').id
  760. })
  761. # 2. Create a BOM of subcontracting type
  762. # 2.1. Comp1 has tracking by lot
  763. cls.comp1_sn = cls.env['product.product'].create({
  764. 'name': 'Component1',
  765. 'type': 'product',
  766. 'categ_id': cls.env.ref('product.product_category_all').id,
  767. 'tracking': 'serial'
  768. })
  769. cls.comp2 = cls.env['product.product'].create({
  770. 'name': 'Component2',
  771. 'type': 'product',
  772. 'categ_id': cls.env.ref('product.product_category_all').id,
  773. })
  774. # 2.2. Finished prodcut has tracking by serial number
  775. cls.finished_product = cls.env['product.product'].create({
  776. 'name': 'finished',
  777. 'type': 'product',
  778. 'categ_id': cls.env.ref('product.product_category_all').id,
  779. 'tracking': 'lot'
  780. })
  781. bom_form = Form(cls.env['mrp.bom'])
  782. bom_form.type = 'subcontract'
  783. bom_form.consumption = 'strict'
  784. bom_form.subcontractor_ids.add(cls.subcontractor_partner1)
  785. bom_form.product_tmpl_id = cls.finished_product.product_tmpl_id
  786. with bom_form.bom_line_ids.new() as bom_line:
  787. bom_line.product_id = cls.comp1_sn
  788. bom_line.product_qty = 1
  789. with bom_form.bom_line_ids.new() as bom_line:
  790. bom_line.product_id = cls.comp2
  791. bom_line.product_qty = 1
  792. cls.bom_tracked = bom_form.save()
  793. def test_flow_tracked_1(self):
  794. """ This test mimics test_flow_1 but with a BoM that has tracking included in it.
  795. """
  796. # Create a receipt picking from the subcontractor
  797. picking_form = Form(self.env['stock.picking'])
  798. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  799. picking_form.partner_id = self.subcontractor_partner1
  800. with picking_form.move_ids_without_package.new() as move:
  801. move.product_id = self.finished_product
  802. move.product_uom_qty = 1
  803. picking_receipt = picking_form.save()
  804. picking_receipt.action_confirm()
  805. # We should be able to call the 'record_components' button
  806. self.assertEqual(picking_receipt.display_action_record_components, 'mandatory')
  807. # Check the created manufacturing order
  808. mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
  809. self.assertEqual(len(mo), 1)
  810. self.assertEqual(len(mo.picking_ids), 0)
  811. wh = picking_receipt.picking_type_id.warehouse_id
  812. self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
  813. self.assertFalse(mo.picking_type_id.active)
  814. # Create a RR
  815. pg1 = self.env['procurement.group'].create({})
  816. self.env['stock.warehouse.orderpoint'].create({
  817. 'name': 'xxx',
  818. 'product_id': self.comp1_sn.id,
  819. 'product_min_qty': 0,
  820. 'product_max_qty': 0,
  821. 'location_id': self.env.user.company_id.subcontracting_location_id.id,
  822. 'group_id': pg1.id,
  823. })
  824. # Run the scheduler and check the created picking
  825. self.env['procurement.group'].run_scheduler()
  826. picking = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
  827. self.assertEqual(len(picking), 1)
  828. self.assertEqual(picking.picking_type_id, wh.subcontracting_resupply_type_id)
  829. lot_id = self.env['stock.lot'].create({
  830. 'name': 'lot1',
  831. 'product_id': self.finished_product.id,
  832. 'company_id': self.env.company.id,
  833. })
  834. serial_id = self.env['stock.lot'].create({
  835. 'name': 'lot1',
  836. 'product_id': self.comp1_sn.id,
  837. 'company_id': self.env.company.id,
  838. })
  839. action = picking_receipt.action_record_components()
  840. mo = self.env['mrp.production'].browse(action['res_id'])
  841. mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
  842. mo_form.qty_producing = 1
  843. mo_form.lot_producing_id = lot_id
  844. with mo_form.move_line_raw_ids.edit(0) as ml:
  845. ml.lot_id = serial_id
  846. mo = mo_form.save()
  847. mo.subcontracting_record_component()
  848. # We should not be able to call the 'record_components' button
  849. self.assertEqual(picking_receipt.display_action_record_components, 'hide')
  850. picking_receipt.button_validate()
  851. self.assertEqual(mo.state, 'done')
  852. # Available quantities should be negative at the subcontracting location for each components
  853. avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  854. avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  855. avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_product, wh.lot_stock_id)
  856. self.assertEqual(avail_qty_comp1, -1)
  857. self.assertEqual(avail_qty_comp2, -1)
  858. self.assertEqual(avail_qty_finished, 1)
  859. def test_flow_tracked_only_finished(self):
  860. """ Test when only the finished product is tracked """
  861. self.finished_product.tracking = "serial"
  862. self.comp1_sn.tracking = "none"
  863. nb_finished_product = 3
  864. # Create a receipt picking from the subcontractor
  865. picking_form = Form(self.env['stock.picking'])
  866. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  867. picking_form.partner_id = self.subcontractor_partner1
  868. with picking_form.move_ids_without_package.new() as move:
  869. move.product_id = self.finished_product
  870. move.product_uom_qty = nb_finished_product
  871. picking_receipt = picking_form.save()
  872. picking_receipt.action_confirm()
  873. # We shouldn't be able to call the 'record_components' button
  874. self.assertEqual(picking_receipt.display_action_record_components, 'hide')
  875. wh = picking_receipt.picking_type_id.warehouse_id
  876. lot_names_finished = [f"subtracked_{i}" for i in range(nb_finished_product)]
  877. move_details = Form(picking_receipt.move_ids, view='stock.view_stock_move_nosuggest_operations')
  878. for lot_name in lot_names_finished:
  879. with move_details.move_line_nosuggest_ids.new() as ml:
  880. ml.qty_done = 1
  881. ml.lot_name = lot_name
  882. move_details.save()
  883. picking_receipt.button_validate()
  884. # Check the created manufacturing order
  885. # Should have one mo by serial number
  886. mos = picking_receipt.move_ids.move_orig_ids.production_id
  887. self.assertEqual(len(mos), nb_finished_product)
  888. self.assertEqual(mos.mapped("state"), ["done"] * nb_finished_product)
  889. self.assertEqual(mos.picking_type_id, wh.subcontracting_type_id)
  890. self.assertFalse(mos.picking_type_id.active)
  891. self.assertEqual(set(mos.lot_producing_id.mapped("name")), set(lot_names_finished))
  892. # Available quantities should be negative at the subcontracting location for each components
  893. avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  894. avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  895. avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_product, wh.lot_stock_id)
  896. self.assertEqual(avail_qty_comp1, -nb_finished_product)
  897. self.assertEqual(avail_qty_comp2, -nb_finished_product)
  898. self.assertEqual(avail_qty_finished, nb_finished_product)
  899. def test_flow_tracked_backorder(self):
  900. """ This test uses tracked (serial and lot) component and tracked (serial) finished product """
  901. todo_nb = 4
  902. self.comp2.tracking = 'lot'
  903. self.finished_product.tracking = 'serial'
  904. # Create a receipt picking from the subcontractor
  905. picking_form = Form(self.env['stock.picking'])
  906. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  907. picking_form.partner_id = self.subcontractor_partner1
  908. with picking_form.move_ids_without_package.new() as move:
  909. move.product_id = self.finished_product
  910. move.product_uom_qty = todo_nb
  911. picking_receipt = picking_form.save()
  912. picking_receipt.action_confirm()
  913. # We should be able to call the 'record_components' button
  914. self.assertEqual(picking_receipt.display_action_record_components, 'mandatory')
  915. # Check the created manufacturing order
  916. mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
  917. self.assertEqual(len(mo), 1)
  918. self.assertEqual(len(mo.picking_ids), 0)
  919. wh = picking_receipt.picking_type_id.warehouse_id
  920. self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
  921. self.assertFalse(mo.picking_type_id.active)
  922. lot_comp2 = self.env['stock.lot'].create({
  923. 'name': 'lot_comp2',
  924. 'product_id': self.comp2.id,
  925. 'company_id': self.env.company.id,
  926. })
  927. serials_finished = []
  928. serials_comp1 = []
  929. for i in range(todo_nb):
  930. serials_finished.append(self.env['stock.lot'].create({
  931. 'name': 'serial_fin_%s' % i,
  932. 'product_id': self.finished_product.id,
  933. 'company_id': self.env.company.id,
  934. }))
  935. serials_comp1.append(self.env['stock.lot'].create({
  936. 'name': 'serials_comp1_%s' % i,
  937. 'product_id': self.comp1_sn.id,
  938. 'company_id': self.env.company.id,
  939. }))
  940. for i in range(todo_nb):
  941. action = picking_receipt.action_record_components()
  942. mo = self.env['mrp.production'].browse(action['res_id'])
  943. mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
  944. mo_form.lot_producing_id = serials_finished[i]
  945. with mo_form.move_line_raw_ids.edit(0) as ml:
  946. self.assertEqual(ml.product_id, self.comp1_sn)
  947. ml.lot_id = serials_comp1[i]
  948. with mo_form.move_line_raw_ids.edit(1) as ml:
  949. self.assertEqual(ml.product_id, self.comp2)
  950. ml.lot_id = lot_comp2
  951. mo = mo_form.save()
  952. mo.subcontracting_record_component()
  953. # We should not be able to call the 'record_components' button
  954. self.assertEqual(picking_receipt.display_action_record_components, 'hide')
  955. picking_receipt.button_validate()
  956. self.assertEqual(mo.state, 'done')
  957. self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped("state"), ['done'] * todo_nb)
  958. self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), todo_nb)
  959. self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped("qty_produced"), [1] * todo_nb)
  960. # Available quantities should be negative at the subcontracting location for each components
  961. avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  962. avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
  963. avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_product, wh.lot_stock_id)
  964. self.assertEqual(avail_qty_comp1, -todo_nb)
  965. self.assertEqual(avail_qty_comp2, -todo_nb)
  966. self.assertEqual(avail_qty_finished, todo_nb)
  967. def test_flow_tracked_backorder02(self):
  968. """ Both component and finished product are tracked by lot. """
  969. todo_nb = 4
  970. resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
  971. finished_product, component = self.env['product.product'].create([{
  972. 'name': 'SuperProduct',
  973. 'type': 'product',
  974. 'tracking': 'lot',
  975. }, {
  976. 'name': 'Component',
  977. 'type': 'product',
  978. 'tracking': 'lot',
  979. 'route_ids': [(4, resupply_sub_on_order_route.id)],
  980. }])
  981. bom_form = Form(self.env['mrp.bom'])
  982. bom_form.type = 'subcontract'
  983. bom_form.subcontractor_ids.add(self.subcontractor_partner1)
  984. bom_form.product_tmpl_id = finished_product.product_tmpl_id
  985. with bom_form.bom_line_ids.new() as bom_line:
  986. bom_line.product_id = component
  987. bom_line.product_qty = 1
  988. bom = bom_form.save()
  989. finished_lot, component_lot = self.env['stock.lot'].create([{
  990. 'name': 'lot_%s' % product.name,
  991. 'product_id': product.id,
  992. 'company_id': self.env.company.id,
  993. } for product in [finished_product, component]])
  994. self.env['stock.quant']._update_available_quantity(component, self.env.ref('stock.stock_location_stock'), todo_nb, lot_id=component_lot)
  995. # Create a receipt picking from the subcontractor
  996. picking_form = Form(self.env['stock.picking'])
  997. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  998. picking_form.partner_id = self.subcontractor_partner1
  999. with picking_form.move_ids_without_package.new() as move:
  1000. move.product_id = finished_product
  1001. move.product_uom_qty = todo_nb
  1002. picking_receipt = picking_form.save()
  1003. picking_receipt.action_confirm()
  1004. mo = self.env['mrp.production'].search([('bom_id', '=', bom.id)])
  1005. # Process the delivery of the components
  1006. compo_picking = mo.picking_ids
  1007. compo_picking.action_assign()
  1008. wizard_data = compo_picking.button_validate()
  1009. wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
  1010. wizard.process()
  1011. for qty in [3, 1]:
  1012. # Record the receiption of <qty> finished products
  1013. picking_receipt = self.env['stock.picking'].search([('partner_id', '=', self.subcontractor_partner1.id), ('state', '!=', 'done')])
  1014. action = picking_receipt.action_record_components()
  1015. mo = self.env['mrp.production'].browse(action['res_id'])
  1016. mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
  1017. mo_form.qty_producing = qty
  1018. mo_form.lot_producing_id = finished_lot
  1019. with mo_form.move_line_raw_ids.edit(0) as ml:
  1020. ml.lot_id = component_lot
  1021. mo = mo_form.save()
  1022. mo.subcontracting_record_component()
  1023. # Validate the picking and create a backorder
  1024. wizard_data = picking_receipt.button_validate()
  1025. if qty == 3:
  1026. wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
  1027. wizard.process()
  1028. self.assertEqual(picking_receipt.state, 'done')
  1029. def test_flow_backorder_production(self):
  1030. """ Test subcontracted MO backorder (i.e. through record production window, NOT through
  1031. picking backorder). Finished product is serial tracked to ensure subcontracting MO window
  1032. is opened. Check that MO backorder auto-reserves components
  1033. """
  1034. todo_nb = 3
  1035. resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
  1036. finished_product, component = self.env['product.product'].create([{
  1037. 'name': 'Pepper Spray',
  1038. 'type': 'product',
  1039. 'tracking': 'serial',
  1040. }, {
  1041. 'name': 'Pepper',
  1042. 'type': 'product',
  1043. 'route_ids': [(4, resupply_sub_on_order_route.id)],
  1044. }])
  1045. bom_form = Form(self.env['mrp.bom'])
  1046. bom_form.type = 'subcontract'
  1047. bom_form.subcontractor_ids.add(self.subcontractor_partner1)
  1048. bom_form.product_tmpl_id = finished_product.product_tmpl_id
  1049. with bom_form.bom_line_ids.new() as bom_line:
  1050. bom_line.product_id = component
  1051. bom_line.product_qty = 1
  1052. bom = bom_form.save()
  1053. finished_serials = self.env['stock.lot'].create([{
  1054. 'name': 'sn_%s' % str(i),
  1055. 'product_id': finished_product.id,
  1056. 'company_id': self.env.company.id,
  1057. } for i in range(todo_nb)])
  1058. self.env['stock.quant']._update_available_quantity(component, self.env.ref('stock.stock_location_stock'), todo_nb)
  1059. # Create a receipt picking from the subcontractor
  1060. picking_form = Form(self.env['stock.picking'])
  1061. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  1062. picking_form.partner_id = self.subcontractor_partner1
  1063. with picking_form.move_ids_without_package.new() as move:
  1064. move.product_id = finished_product
  1065. move.product_uom_qty = todo_nb
  1066. picking_receipt = picking_form.save()
  1067. picking_receipt.action_confirm()
  1068. mo = self.env['mrp.production'].search([('bom_id', '=', bom.id)])
  1069. # Process the delivery of the components
  1070. compo_picking = mo.picking_ids
  1071. compo_picking.action_assign()
  1072. wizard_data = compo_picking.button_validate()
  1073. wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
  1074. wizard.process()
  1075. picking_receipt = self.env['stock.picking'].search([('partner_id', '=', self.subcontractor_partner1.id), ('state', '!=', 'done')])
  1076. for sn in finished_serials:
  1077. # Record the production of each serial number separately
  1078. action = picking_receipt.action_record_components()
  1079. mo = self.env['mrp.production'].browse(action['res_id'])
  1080. self.assertEqual(mo.move_raw_ids.state, 'assigned')
  1081. mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
  1082. mo_form.qty_producing = 1
  1083. mo_form.lot_producing_id = sn
  1084. mo = mo_form.save()
  1085. mo.subcontracting_record_component()
  1086. # Validate the picking
  1087. picking_receipt.button_validate()
  1088. self.assertEqual(picking_receipt.state, 'done')
  1089. @tagged('post_install', '-at_install')
  1090. class TestSubcontractingPortal(TransactionCase):
  1091. @classmethod
  1092. def setUpClass(cls):
  1093. super().setUpClass()
  1094. # 1: Create a subcontracting partner
  1095. main_partner = cls.env['res.partner'].create({'name': 'main_partner'})
  1096. cls.subcontractor_partner1 = cls.env['res.partner'].create({
  1097. 'name': 'subcontractor_partner',
  1098. 'parent_id': main_partner.id,
  1099. 'company_id': cls.env.ref('base.main_company').id,
  1100. })
  1101. # Make the subcontracting partner a portal user
  1102. cls.portal_user = cls.env['res.users'].create({
  1103. 'name': 'portal user (subcontractor)',
  1104. 'partner_id': cls.subcontractor_partner1.id,
  1105. 'login': 'subcontractor',
  1106. 'password': 'subcontractor',
  1107. 'email': 'subcontractor@subcontracting.portal',
  1108. 'groups_id': [(6, 0, [cls.env.ref('base.group_portal').id])]
  1109. })
  1110. # 2. Create a BOM of subcontracting type
  1111. # 2.1. Comp1 has tracking by lot
  1112. cls.comp1_sn = cls.env['product.product'].create({
  1113. 'name': 'Component1',
  1114. 'type': 'product',
  1115. 'categ_id': cls.env.ref('product.product_category_all').id,
  1116. 'tracking': 'serial'
  1117. })
  1118. cls.comp2 = cls.env['product.product'].create({
  1119. 'name': 'Component2',
  1120. 'type': 'product',
  1121. 'categ_id': cls.env.ref('product.product_category_all').id,
  1122. })
  1123. cls.product_not_in_bom = cls.env['product.product'].create({
  1124. 'name': 'Product not in the BoM',
  1125. 'type': 'product',
  1126. })
  1127. # 2.2. Finished prodcut has tracking by serial number
  1128. cls.finished_product = cls.env['product.product'].create({
  1129. 'name': 'finished',
  1130. 'type': 'product',
  1131. 'categ_id': cls.env.ref('product.product_category_all').id,
  1132. 'tracking': 'lot'
  1133. })
  1134. bom_form = Form(cls.env['mrp.bom'])
  1135. bom_form.type = 'subcontract'
  1136. bom_form.consumption = 'warning'
  1137. bom_form.subcontractor_ids.add(cls.subcontractor_partner1)
  1138. bom_form.product_tmpl_id = cls.finished_product.product_tmpl_id
  1139. with bom_form.bom_line_ids.new() as bom_line:
  1140. bom_line.product_id = cls.comp1_sn
  1141. bom_line.product_qty = 1
  1142. with bom_form.bom_line_ids.new() as bom_line:
  1143. bom_line.product_id = cls.comp2
  1144. bom_line.product_qty = 1
  1145. cls.bom_tracked = bom_form.save()
  1146. def test_flow_subcontracting_portal(self):
  1147. # Create a receipt picking from the subcontractor
  1148. picking_form = Form(self.env['stock.picking'])
  1149. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  1150. picking_form.partner_id = self.subcontractor_partner1
  1151. with picking_form.move_ids_without_package.new() as move:
  1152. move.product_id = self.finished_product
  1153. move.product_uom_qty = 2
  1154. picking_receipt = picking_form.save()
  1155. picking_receipt.action_confirm()
  1156. # Using the subcontractor (portal user)
  1157. lot1 = self.env['stock.lot'].with_user(self.portal_user).create({
  1158. 'name': 'lot1',
  1159. 'product_id': self.finished_product.id,
  1160. 'company_id': self.env.company.id,
  1161. })
  1162. lot2 = self.env['stock.lot'].with_user(self.portal_user).create({
  1163. 'name': 'lot2',
  1164. 'product_id': self.finished_product.id,
  1165. 'company_id': self.env.company.id,
  1166. })
  1167. serial1 = self.env['stock.lot'].with_user(self.portal_user).create({
  1168. 'name': 'lot1',
  1169. 'product_id': self.comp1_sn.id,
  1170. 'company_id': self.env.company.id,
  1171. })
  1172. serial2 = self.env['stock.lot'].with_user(self.portal_user).create({
  1173. 'name': 'lot2',
  1174. 'product_id': self.comp1_sn.id,
  1175. 'company_id': self.env.company.id,
  1176. })
  1177. serial3 = self.env['stock.lot'].with_user(self.portal_user).create({
  1178. 'name': 'lot3',
  1179. 'product_id': self.comp1_sn.id,
  1180. 'company_id': self.env.company.id,
  1181. })
  1182. action = picking_receipt.with_user(self.portal_user).with_context({'is_subcontracting_portal': 1}).move_ids.action_show_details()
  1183. mo = self.env['mrp.production'].with_user(self.portal_user).browse(action['res_id'])
  1184. mo_form = Form(mo.with_context(action['context']), view=action['view_id'])
  1185. # Registering components for the first manufactured product
  1186. mo_form.qty_producing = 1
  1187. mo_form.lot_producing_id = lot1
  1188. with mo_form.move_line_raw_ids.edit(0) as ml:
  1189. ml.lot_id = serial1
  1190. mo = mo_form.save()
  1191. mo.subcontracting_record_component()
  1192. # Continue record of components with new MO (backorder was when recording first MO)
  1193. action = picking_receipt.with_user(self.portal_user).with_context({'is_subcontracting_portal': 1}).move_ids.action_show_details()
  1194. mo = self.env['mrp.production'].with_user(self.portal_user).browse(action['res_id'])
  1195. mo_form = Form(mo.with_context(action['context']), view=action['view_id'])
  1196. # Registering components for the second manufactured product with over-consumption, which leads to a warning
  1197. mo_form.qty_producing = 1
  1198. mo_form.lot_producing_id = lot2
  1199. with mo_form.move_line_raw_ids.edit(0) as ml:
  1200. ml.lot_id = serial2
  1201. with mo_form.move_line_raw_ids.new() as ml:
  1202. ml.product_id = self.comp1_sn
  1203. ml.lot_id = serial3
  1204. with mo_form.move_line_raw_ids.edit(1) as ml:
  1205. ml.qty_done = 2
  1206. # The portal user should not be able to add a product not in the BoM
  1207. with self.assertRaises(AccessError):
  1208. with mo_form.move_line_raw_ids.new() as ml:
  1209. ml.product_id = self.product_not_in_bom
  1210. mo = mo_form.save()
  1211. action_warning = mo.subcontracting_record_component()
  1212. warning = Form(self.env['mrp.consumption.warning'].with_context(**action_warning['context']))
  1213. warning = warning.save()
  1214. warning.action_confirm()
  1215. # Attempt to validate from the portal user should give an error
  1216. with self.assertRaises(UserError):
  1217. picking_receipt.with_user(self.portal_user).button_validate()
  1218. # Validation from the backend user
  1219. picking_receipt.button_validate()
  1220. self.assertEqual(mo.state, 'done')
  1221. self.assertEqual(mo.move_line_raw_ids[0].qty_done, 1)
  1222. self.assertEqual(mo.move_line_raw_ids[0].lot_id, serial2)
  1223. self.assertEqual(mo.move_line_raw_ids[1].qty_done, 1)
  1224. self.assertEqual(mo.move_line_raw_ids[1].lot_id, serial3)
  1225. self.assertEqual(mo.move_line_raw_ids[2].qty_done, 2)
  1226. class TestSubcontractingSerialMassReceipt(TransactionCase):
  1227. def setUp(self):
  1228. super().setUp()
  1229. self.subcontractor = self.env['res.partner'].create({
  1230. 'name': 'Subcontractor',
  1231. })
  1232. self.resupply_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
  1233. self.raw_material = self.env['product.product'].create({
  1234. 'name': 'Component',
  1235. 'type': 'product',
  1236. 'route_ids': [Command.link(self.resupply_route.id)],
  1237. })
  1238. self.finished = self.env['product.product'].create({
  1239. 'name': 'Finished',
  1240. 'type': 'product',
  1241. 'tracking': 'serial'
  1242. })
  1243. self.bom = self.env['mrp.bom'].create({
  1244. 'product_id': self.finished.id,
  1245. 'product_tmpl_id': self.finished.product_tmpl_id.id,
  1246. 'product_qty': 1.0,
  1247. 'type': 'subcontract',
  1248. 'subcontractor_ids': [Command.link(self.subcontractor.id)],
  1249. 'consumption': 'strict',
  1250. 'bom_line_ids': [
  1251. Command.create({'product_id': self.raw_material.id, 'product_qty': 1}),
  1252. ]
  1253. })
  1254. def test_receive_after_resupply(self):
  1255. quantities = [5, 4, 1]
  1256. # Make needed component stock
  1257. self.env['stock.quant']._update_available_quantity(self.raw_material, self.env.ref('stock.stock_location_stock'), sum(quantities))
  1258. # Create a receipt picking from the subcontractor
  1259. picking_form = Form(self.env['stock.picking'])
  1260. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  1261. picking_form.partner_id = self.subcontractor
  1262. with picking_form.move_ids_without_package.new() as move:
  1263. move.product_id = self.finished
  1264. move.product_uom_qty = sum(quantities)
  1265. picking_receipt = picking_form.save()
  1266. picking_receipt.action_confirm()
  1267. # Process the delivery of the components
  1268. picking_deliver = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)]).picking_ids
  1269. picking_deliver.action_assign()
  1270. picking_deliver.button_validate()
  1271. wizard_data = picking_deliver.button_validate()
  1272. wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
  1273. wizard.process()
  1274. # Receive
  1275. for quantity in quantities:
  1276. # Receive <quantity> finished products
  1277. Form(self.env['stock.assign.serial'].with_context(
  1278. default_move_id=picking_receipt.move_ids[0].id,
  1279. 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',
  1280. default_next_serial_count=quantity,
  1281. )).save().generate_serial_numbers()
  1282. wizard_data = picking_receipt.button_validate()
  1283. if wizard_data is not True:
  1284. # Create backorder
  1285. wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
  1286. wizard.process()
  1287. self.assertEqual(picking_receipt.state, 'done')
  1288. picking_receipt = picking_receipt.backorder_ids[-1]
  1289. self.assertEqual(picking_receipt.state, 'assigned')
  1290. self.assertEqual(picking_receipt.state, 'done')
  1291. self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.env.ref('stock.stock_location_stock')), 0)
  1292. self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.subcontractor.property_stock_subcontractor), 0)
  1293. def test_receive_no_resupply(self):
  1294. quantity = 5
  1295. # Create a receipt picking from the subcontractor
  1296. picking_form = Form(self.env['stock.picking'])
  1297. picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
  1298. picking_form.partner_id = self.subcontractor
  1299. with picking_form.move_ids_without_package.new() as move:
  1300. move.product_id = self.finished
  1301. move.product_uom_qty = quantity
  1302. picking_receipt = picking_form.save()
  1303. picking_receipt.action_confirm()
  1304. # Receive finished products
  1305. Form(self.env['stock.assign.serial'].with_context(
  1306. default_move_id=picking_receipt.move_ids[0].id,
  1307. 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',
  1308. default_next_serial_count=quantity,
  1309. )).save().generate_serial_numbers()
  1310. picking_receipt.button_validate()
  1311. self.assertEqual(picking_receipt.state, 'done')
  1312. self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.env.ref('stock.stock_location_stock')), 0)
  1313. self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.subcontractor.property_stock_subcontractor, allow_negative=True), -quantity)