test_traceability.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from odoo.tests import Form
  4. from odoo.addons.mrp.tests.common import TestMrpCommon
  5. import logging
  6. _logger = logging.getLogger(__name__)
  7. class TestTraceability(TestMrpCommon):
  8. TRACKING_TYPES = ['none', 'serial', 'lot']
  9. def _create_product(self, tracking):
  10. return self.env['product.product'].create({
  11. 'name': 'Product %s' % tracking,
  12. 'type': 'product',
  13. 'tracking': tracking,
  14. 'categ_id': self.env.ref('product.product_category_all').id,
  15. })
  16. def test_tracking_types_on_mo(self):
  17. finished_no_track = self._create_product('none')
  18. finished_lot = self._create_product('lot')
  19. finished_serial = self._create_product('serial')
  20. consumed_no_track = self._create_product('none')
  21. consumed_lot = self._create_product('lot')
  22. consumed_serial = self._create_product('serial')
  23. stock_id = self.env.ref('stock.stock_location_stock').id
  24. Lot = self.env['stock.lot']
  25. # create inventory
  26. quants = self.env['stock.quant'].create({
  27. 'location_id': stock_id,
  28. 'product_id': consumed_no_track.id,
  29. 'inventory_quantity': 3
  30. })
  31. quants |= self.env['stock.quant'].create({
  32. 'location_id': stock_id,
  33. 'product_id': consumed_lot.id,
  34. 'inventory_quantity': 3,
  35. 'lot_id': Lot.create({'name': 'L1', 'product_id': consumed_lot.id, 'company_id': self.env.company.id}).id
  36. })
  37. quants |= self.env['stock.quant'].create({
  38. 'location_id': stock_id,
  39. 'product_id': consumed_serial.id,
  40. 'inventory_quantity': 1,
  41. 'lot_id': Lot.create({'name': 'S1', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
  42. })
  43. quants |= self.env['stock.quant'].create({
  44. 'location_id': stock_id,
  45. 'product_id': consumed_serial.id,
  46. 'inventory_quantity': 1,
  47. 'lot_id': Lot.create({'name': 'S2', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
  48. })
  49. quants |= self.env['stock.quant'].create({
  50. 'location_id': stock_id,
  51. 'product_id': consumed_serial.id,
  52. 'inventory_quantity': 1,
  53. 'lot_id': Lot.create({'name': 'S3', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
  54. })
  55. quants.action_apply_inventory()
  56. for finished_product in [finished_no_track, finished_lot, finished_serial]:
  57. bom = self.env['mrp.bom'].create({
  58. 'product_id': finished_product.id,
  59. 'product_tmpl_id': finished_product.product_tmpl_id.id,
  60. 'product_uom_id': self.env.ref('uom.product_uom_unit').id,
  61. 'product_qty': 1.0,
  62. 'type': 'normal',
  63. 'bom_line_ids': [
  64. (0, 0, {'product_id': consumed_no_track.id, 'product_qty': 1}),
  65. (0, 0, {'product_id': consumed_lot.id, 'product_qty': 1}),
  66. (0, 0, {'product_id': consumed_serial.id, 'product_qty': 1}),
  67. ],
  68. })
  69. mo_form = Form(self.env['mrp.production'])
  70. mo_form.product_id = finished_product
  71. mo_form.bom_id = bom
  72. mo_form.product_uom_id = self.env.ref('uom.product_uom_unit')
  73. mo_form.product_qty = 1
  74. mo = mo_form.save()
  75. mo.action_confirm()
  76. mo.action_assign()
  77. # Start MO production
  78. mo_form = Form(mo)
  79. mo_form.qty_producing = 1
  80. if finished_product.tracking != 'none':
  81. mo_form.lot_producing_id = self.env['stock.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id, 'company_id': self.env.company.id})
  82. mo = mo_form.save()
  83. details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
  84. with details_operation_form.move_line_ids.edit(0) as ml:
  85. ml.qty_done = 1
  86. details_operation_form.save()
  87. details_operation_form = Form(mo.move_raw_ids[2], view=self.env.ref('stock.view_stock_move_operations'))
  88. with details_operation_form.move_line_ids.edit(0) as ml:
  89. ml.qty_done = 1
  90. details_operation_form.save()
  91. mo.button_mark_done()
  92. self.assertEqual(mo.state, 'done', "Production order should be in done state.")
  93. # Check results of traceability
  94. context = ({
  95. 'active_id': mo.id,
  96. 'model': 'mrp.production',
  97. })
  98. lines = self.env['stock.traceability.report'].with_context(context).get_lines()
  99. self.assertEqual(len(lines), 1, "Should always return 1 line : the final product")
  100. final_product = lines[0]
  101. self.assertEqual(final_product['unfoldable'], True, "Final product should always be unfoldable")
  102. # Find parts of the final products
  103. lines = self.env['stock.traceability.report'].get_lines(final_product['id'], **{
  104. 'level': final_product['level'],
  105. 'model_id': final_product['model_id'],
  106. 'model_name': final_product['model'],
  107. })
  108. self.assertEqual(len(lines), 3, "There should be 3 lines. 1 for untracked, 1 for lot, and 1 for serial")
  109. for line in lines:
  110. tracking = line['columns'][1].split(' ')[1]
  111. self.assertEqual(
  112. line['columns'][-1], "1.00 Units", 'Part with tracking type "%s", should have quantity = 1' % (tracking)
  113. )
  114. unfoldable = False if tracking == 'none' else True
  115. self.assertEqual(
  116. line['unfoldable'],
  117. unfoldable,
  118. 'Parts with tracking type "%s", should have be unfoldable : %s' % (tracking, unfoldable)
  119. )
  120. def test_tracking_on_byproducts(self):
  121. product_final = self.env['product.product'].create({
  122. 'name': 'Finished Product',
  123. 'type': 'product',
  124. 'tracking': 'serial',
  125. })
  126. product_1 = self.env['product.product'].create({
  127. 'name': 'Raw 1',
  128. 'type': 'product',
  129. 'tracking': 'serial',
  130. })
  131. product_2 = self.env['product.product'].create({
  132. 'name': 'Raw 2',
  133. 'type': 'product',
  134. 'tracking': 'serial',
  135. })
  136. byproduct_1 = self.env['product.product'].create({
  137. 'name': 'Byproduct 1',
  138. 'type': 'product',
  139. 'tracking': 'serial',
  140. })
  141. byproduct_2 = self.env['product.product'].create({
  142. 'name': 'Byproduct 2',
  143. 'type': 'product',
  144. 'tracking': 'serial',
  145. })
  146. bom_1 = self.env['mrp.bom'].create({
  147. 'product_id': product_final.id,
  148. 'product_tmpl_id': product_final.product_tmpl_id.id,
  149. 'product_uom_id': self.uom_unit.id,
  150. 'product_qty': 1.0,
  151. 'consumption': 'flexible',
  152. 'type': 'normal',
  153. 'bom_line_ids': [
  154. (0, 0, {'product_id': product_1.id, 'product_qty': 1}),
  155. (0, 0, {'product_id': product_2.id, 'product_qty': 1})
  156. ],
  157. 'byproduct_ids': [
  158. (0, 0, {'product_id': byproduct_1.id, 'product_qty': 1, 'product_uom_id': byproduct_1.uom_id.id}),
  159. (0, 0, {'product_id': byproduct_2.id, 'product_qty': 1, 'product_uom_id': byproduct_2.uom_id.id})
  160. ]})
  161. mo_form = Form(self.env['mrp.production'])
  162. mo_form.product_id = product_final
  163. mo_form.bom_id = bom_1
  164. mo_form.product_qty = 2
  165. mo = mo_form.save()
  166. mo.action_confirm()
  167. mo_form = Form(mo)
  168. mo_form.lot_producing_id = self.env['stock.lot'].create({
  169. 'product_id': product_final.id,
  170. 'name': 'Final_lot_1',
  171. 'company_id': self.env.company.id,
  172. })
  173. mo = mo_form.save()
  174. details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
  175. with details_operation_form.move_line_ids.new() as ml:
  176. ml.lot_id = self.env['stock.lot'].create({
  177. 'product_id': product_1.id,
  178. 'name': 'Raw_1_lot_1',
  179. 'company_id': self.env.company.id,
  180. })
  181. ml.qty_done = 1
  182. details_operation_form.save()
  183. details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
  184. with details_operation_form.move_line_ids.new() as ml:
  185. ml.lot_id = self.env['stock.lot'].create({
  186. 'product_id': product_2.id,
  187. 'name': 'Raw_2_lot_1',
  188. 'company_id': self.env.company.id,
  189. })
  190. ml.qty_done = 1
  191. details_operation_form.save()
  192. details_operation_form = Form(
  193. mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1),
  194. view=self.env.ref('stock.view_stock_move_operations')
  195. )
  196. with details_operation_form.move_line_ids.new() as ml:
  197. ml.lot_id = self.env['stock.lot'].create({
  198. 'product_id': byproduct_1.id,
  199. 'name': 'Byproduct_1_lot_1',
  200. 'company_id': self.env.company.id,
  201. })
  202. ml.qty_done = 1
  203. details_operation_form.save()
  204. details_operation_form = Form(
  205. mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2),
  206. view=self.env.ref('stock.view_stock_move_operations')
  207. )
  208. with details_operation_form.move_line_ids.new() as ml:
  209. ml.lot_id = self.env['stock.lot'].create({
  210. 'product_id': byproduct_2.id,
  211. 'name': 'Byproduct_2_lot_1',
  212. 'company_id': self.env.company.id,
  213. })
  214. ml.qty_done = 1
  215. details_operation_form.save()
  216. action = mo.button_mark_done()
  217. backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
  218. backorder.save().action_backorder()
  219. mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
  220. mo_form = Form(mo_backorder)
  221. mo_form.lot_producing_id = self.env['stock.lot'].create({
  222. 'product_id': product_final.id,
  223. 'name': 'Final_lot_2',
  224. 'company_id': self.env.company.id,
  225. })
  226. mo_form.qty_producing = 1
  227. mo_backorder = mo_form.save()
  228. details_operation_form = Form(
  229. mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_1),
  230. view=self.env.ref('stock.view_stock_move_operations')
  231. )
  232. with details_operation_form.move_line_ids.new() as ml:
  233. ml.lot_id = self.env['stock.lot'].create({
  234. 'product_id': product_1.id,
  235. 'name': 'Raw_1_lot_2',
  236. 'company_id': self.env.company.id,
  237. })
  238. ml.qty_done = 1
  239. details_operation_form.save()
  240. details_operation_form = Form(
  241. mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_2),
  242. view=self.env.ref('stock.view_stock_move_operations')
  243. )
  244. with details_operation_form.move_line_ids.new() as ml:
  245. ml.lot_id = self.env['stock.lot'].create({
  246. 'product_id': product_2.id,
  247. 'name': 'Raw_2_lot_2',
  248. 'company_id': self.env.company.id,
  249. })
  250. ml.qty_done = 1
  251. details_operation_form.save()
  252. details_operation_form = Form(
  253. mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1),
  254. view=self.env.ref('stock.view_stock_move_operations')
  255. )
  256. with details_operation_form.move_line_ids.new() as ml:
  257. ml.lot_id = self.env['stock.lot'].create({
  258. 'product_id': byproduct_1.id,
  259. 'name': 'Byproduct_1_lot_2',
  260. 'company_id': self.env.company.id,
  261. })
  262. ml.qty_done = 1
  263. details_operation_form.save()
  264. details_operation_form = Form(
  265. mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2),
  266. view=self.env.ref('stock.view_stock_move_operations')
  267. )
  268. with details_operation_form.move_line_ids.new() as ml:
  269. ml.lot_id = self.env['stock.lot'].create({
  270. 'product_id': byproduct_2.id,
  271. 'name': 'Byproduct_2_lot_2',
  272. 'company_id': self.env.company.id,
  273. })
  274. ml.qty_done = 1
  275. details_operation_form.save()
  276. mo_backorder.button_mark_done()
  277. # self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 4)
  278. # self.assertEqual(len(mo.move_finished_ids.mapped('move_line_ids')), 6)
  279. mo = mo | mo_backorder
  280. raw_move_lines = mo.move_raw_ids.mapped('move_line_ids')
  281. raw_line_raw_1_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_1')
  282. self.assertEqual(set(raw_line_raw_1_lot_1.produce_line_ids.lot_id.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
  283. raw_line_raw_2_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_1')
  284. self.assertEqual(set(raw_line_raw_2_lot_1.produce_line_ids.lot_id.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
  285. finished_move_lines = mo.move_finished_ids.mapped('move_line_ids')
  286. finished_move_line_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_1')
  287. self.assertEqual(finished_move_line_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
  288. finished_move_line_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_2')
  289. raw_line_raw_1_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_2')
  290. raw_line_raw_2_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_2')
  291. self.assertEqual(finished_move_line_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
  292. byproduct_move_line_1_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_1')
  293. self.assertEqual(byproduct_move_line_1_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
  294. byproduct_move_line_1_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_2')
  295. self.assertEqual(byproduct_move_line_1_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
  296. byproduct_move_line_2_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_1')
  297. self.assertEqual(byproduct_move_line_2_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
  298. byproduct_move_line_2_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_2')
  299. self.assertEqual(byproduct_move_line_2_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
  300. def test_reuse_unbuilt_usn(self):
  301. """
  302. Produce a SN product
  303. Unbuilt it
  304. Produce a new SN product with same lot
  305. """
  306. mo, bom, p_final, p1, p2 = self.generate_mo(qty_base_1=1, qty_base_2=1, qty_final=1, tracking_final='serial')
  307. stock_location = self.env.ref('stock.stock_location_stock')
  308. self.env['stock.quant']._update_available_quantity(p1, stock_location, 1)
  309. self.env['stock.quant']._update_available_quantity(p2, stock_location, 1)
  310. mo.action_assign()
  311. lot = self.env['stock.lot'].create({
  312. 'name': 'lot1',
  313. 'product_id': p_final.id,
  314. 'company_id': self.env.company.id,
  315. })
  316. mo_form = Form(mo)
  317. mo_form.qty_producing = 1.0
  318. mo_form.lot_producing_id = lot
  319. mo = mo_form.save()
  320. mo.button_mark_done()
  321. unbuild_form = Form(self.env['mrp.unbuild'])
  322. unbuild_form.mo_id = mo
  323. unbuild_form.save().action_unbuild()
  324. mo_form = Form(self.env['mrp.production'])
  325. mo_form.bom_id = bom
  326. mo = mo_form.save()
  327. mo.action_confirm()
  328. with self.assertLogs(level="WARNING") as log_catcher:
  329. mo_form = Form(mo)
  330. mo_form.qty_producing = 1.0
  331. mo_form.lot_producing_id = lot
  332. mo = mo_form.save()
  333. _logger.warning('Dummy')
  334. self.assertEqual(len(log_catcher.output), 1, "Useless warnings: \n%s" % "\n".join(log_catcher.output[:-1]))
  335. mo.button_mark_done()
  336. self.assertEqual(mo.state, 'done')
  337. def test_tracked_and_manufactured_component(self):
  338. """
  339. Suppose this structure:
  340. productA --|- 1 x productB --|- 1 x productC
  341. with productB tracked by lot
  342. Ensure that, when we already have some qty of productB (with different lots),
  343. the user can produce several productA and can then produce some productB again
  344. """
  345. stock_location = self.env.ref('stock.stock_location_stock')
  346. picking_type = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0]
  347. picking_type.use_auto_consume_components_lots = True
  348. productA, productB, productC = self.env['product.product'].create([{
  349. 'name': 'Product A',
  350. 'type': 'product',
  351. }, {
  352. 'name': 'Product B',
  353. 'type': 'product',
  354. 'tracking': 'lot',
  355. }, {
  356. 'name': 'Product C',
  357. 'type': 'consu',
  358. }])
  359. lot_B01, lot_B02, lot_B03 = self.env['stock.lot'].create([{
  360. 'name': 'lot %s' % i,
  361. 'product_id': productB.id,
  362. 'company_id': self.env.company.id,
  363. } for i in [1, 2, 3]])
  364. self.env['mrp.bom'].create([{
  365. 'product_id': finished.id,
  366. 'product_tmpl_id': finished.product_tmpl_id.id,
  367. 'product_uom_id': self.uom_unit.id,
  368. 'product_qty': 1.0,
  369. 'type': 'normal',
  370. 'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1})],
  371. } for finished, component in [(productA, productB), (productB, productC)]])
  372. self.env['stock.quant']._update_available_quantity(productB, stock_location, 10, lot_id=lot_B01)
  373. self.env['stock.quant']._update_available_quantity(productB, stock_location, 5, lot_id=lot_B02)
  374. # Produce 15 x productA
  375. mo_form = Form(self.env['mrp.production'])
  376. mo_form.product_id = productA
  377. mo_form.product_qty = 15
  378. mo = mo_form.save()
  379. mo.action_confirm()
  380. action = mo.button_mark_done()
  381. wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
  382. wizard.process()
  383. # Produce 15 x productB
  384. mo_form = Form(self.env['mrp.production'])
  385. mo_form.product_id = productB
  386. mo_form.product_qty = 15
  387. mo = mo_form.save()
  388. mo.action_confirm()
  389. mo_form = Form(mo)
  390. mo_form.qty_producing = 15
  391. mo_form.lot_producing_id = lot_B03
  392. mo = mo_form.save()
  393. mo.button_mark_done()
  394. self.assertEqual(lot_B01.product_qty, 0)
  395. self.assertEqual(lot_B02.product_qty, 0)
  396. self.assertEqual(lot_B03.product_qty, 15)
  397. self.assertEqual(productA.qty_available, 15)
  398. def test_last_delivery_traceability(self):
  399. """
  400. Suppose this structure (-> means 'produces')
  401. 1 x Subcomponent A -> 1 x Component A -> 1 x EndProduct A
  402. All three tracked by lots. Ensure that after validating Picking A (out)
  403. for EndProduct A, all three lots' delivery_ids are set to
  404. Picking A.
  405. """
  406. stock_location = self.env.ref('stock.stock_location_stock')
  407. customer_location = self.env.ref('stock.stock_location_customers')
  408. # Create the three lot-tracked products.
  409. subcomponentA = self._create_product('lot')
  410. componentA = self._create_product('lot')
  411. endproductA = self._create_product('lot')
  412. # Create production lots.
  413. lot_subcomponentA, lot_componentA, lot_endProductA = self.env['stock.lot'].create([{
  414. 'name': 'lot %s' % product,
  415. 'product_id': product.id,
  416. 'company_id': self.env.company.id,
  417. } for product in (subcomponentA, componentA, endproductA)])
  418. # Create two boms, one for Component A and one for EndProduct A
  419. self.env['mrp.bom'].create([{
  420. 'product_id': finished.id,
  421. 'product_tmpl_id': finished.product_tmpl_id.id,
  422. 'product_uom_id': self.uom_unit.id,
  423. 'product_qty': 1.0,
  424. 'type': 'normal',
  425. 'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1})],
  426. } for finished, component in [(endproductA, componentA), (componentA, subcomponentA)]])
  427. self.env['stock.quant']._update_available_quantity(subcomponentA, stock_location, 1, lot_id=lot_subcomponentA)
  428. # Produce 1 component A
  429. mo_form = Form(self.env['mrp.production'])
  430. mo_form.product_id = componentA
  431. mo_form.product_qty = 1
  432. mo = mo_form.save()
  433. mo.action_confirm()
  434. mo_form = Form(mo)
  435. mo_form.qty_producing = 1
  436. mo_form.lot_producing_id = lot_componentA
  437. mo = mo_form.save()
  438. mo.move_raw_ids[0].quantity_done = 1.0
  439. mo.button_mark_done()
  440. # Produce 1 endProduct A
  441. mo_form = Form(self.env['mrp.production'])
  442. mo_form.product_id = endproductA
  443. mo_form.product_qty = 1
  444. mo = mo_form.save()
  445. mo.action_confirm()
  446. mo_form = Form(mo)
  447. mo_form.qty_producing = 1
  448. mo_form.lot_producing_id = lot_endProductA
  449. mo = mo_form.save()
  450. mo.move_raw_ids[0].quantity_done = 1.0
  451. mo.button_mark_done()
  452. # Create out picking for EndProduct A
  453. pickingA_out = self.env['stock.picking'].create({
  454. 'picking_type_id': self.env.ref('stock.picking_type_out').id,
  455. 'location_id': stock_location.id,
  456. 'location_dest_id': customer_location.id})
  457. moveA = self.env['stock.move'].create({
  458. 'name': 'Picking A move',
  459. 'product_id': endproductA.id,
  460. 'product_uom_qty': 1,
  461. 'product_uom': endproductA.uom_id.id,
  462. 'picking_id': pickingA_out.id,
  463. 'location_id': stock_location.id,
  464. 'location_dest_id': customer_location.id})
  465. # Confirm and assign pickingA
  466. pickingA_out.action_confirm()
  467. pickingA_out.action_assign()
  468. # Set move_line lot_id to the mrp.production lot_producing_id
  469. moveA.move_line_ids[0].write({
  470. 'qty_done': 1.0,
  471. 'lot_id': lot_endProductA.id,
  472. })
  473. # Transfer picking
  474. pickingA_out._action_done()
  475. # Use concat so that delivery_ids is computed in batch.
  476. for lot in lot_subcomponentA.concat(lot_componentA, lot_endProductA):
  477. self.assertEqual(lot.delivery_ids.ids, pickingA_out.ids)
  478. def test_unbuild_scrap_and_unscrap_tracked_component(self):
  479. """
  480. Suppose a tracked-by-SN component C. There is one C in stock with SN01.
  481. Build a product P that uses C with SN, unbuild P, scrap SN, unscrap SN
  482. and rebuild a product with SN in the components
  483. """
  484. warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
  485. stock_location = warehouse.lot_stock_id
  486. component = self.bom_4.bom_line_ids.product_id
  487. component.write({
  488. 'type': 'product',
  489. 'tracking': 'serial',
  490. })
  491. serial_number = self.env['stock.lot'].create({
  492. 'product_id': component.id,
  493. 'name': 'Super Serial',
  494. 'company_id': self.env.company.id,
  495. })
  496. self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=serial_number)
  497. # produce 1
  498. mo_form = Form(self.env['mrp.production'])
  499. mo_form.bom_id = self.bom_4
  500. mo = mo_form.save()
  501. mo.action_confirm()
  502. mo.action_assign()
  503. self.assertEqual(mo.move_raw_ids.move_line_ids.lot_id, serial_number)
  504. with Form(mo) as mo_form:
  505. mo_form.qty_producing = 1
  506. mo.move_raw_ids.move_line_ids.qty_done = 1
  507. mo.button_mark_done()
  508. # unbuild
  509. action = mo.button_unbuild()
  510. wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
  511. wizard.action_validate()
  512. # scrap the component
  513. scrap = self.env['stock.scrap'].create({
  514. 'product_id': component.id,
  515. 'product_uom_id': component.uom_id.id,
  516. 'scrap_qty': 1,
  517. 'lot_id': serial_number.id,
  518. })
  519. scrap_location = scrap.scrap_location_id
  520. scrap.do_scrap()
  521. # unscrap the component
  522. internal_move = self.env['stock.move'].create({
  523. 'name': component.name,
  524. 'location_id': scrap_location.id,
  525. 'location_dest_id': stock_location.id,
  526. 'product_id': component.id,
  527. 'product_uom': component.uom_id.id,
  528. 'product_uom_qty': 1.0,
  529. 'move_line_ids': [(0, 0, {
  530. 'product_id': component.id,
  531. 'location_id': scrap_location.id,
  532. 'location_dest_id': stock_location.id,
  533. 'product_uom_id': component.uom_id.id,
  534. 'qty_done': 1.0,
  535. 'lot_id': serial_number.id,
  536. })],
  537. })
  538. internal_move._action_confirm()
  539. internal_move._action_done()
  540. # produce one with the unscrapped component
  541. mo_form = Form(self.env['mrp.production'])
  542. mo_form.bom_id = self.bom_4
  543. mo = mo_form.save()
  544. mo.action_confirm()
  545. mo.action_assign()
  546. self.assertEqual(mo.move_raw_ids.move_line_ids.lot_id, serial_number)
  547. with Form(mo) as mo_form:
  548. mo_form.qty_producing = 1
  549. mo.move_raw_ids.move_line_ids.qty_done = 1
  550. mo.button_mark_done()
  551. self.assertRecordValues((mo.move_finished_ids + mo.move_raw_ids).move_line_ids, [
  552. {'product_id': self.bom_4.product_id.id, 'lot_id': False, 'qty_done': 1},
  553. {'product_id': component.id, 'lot_id': serial_number.id, 'qty_done': 1},
  554. ])
  555. def test_generate_serial_button(self):
  556. """Test if lot in form "00000dd" is manually created, the generate serial
  557. button can skip it and create the next one.
  558. """
  559. mo, _bom, p_final, _p1, _p2 = self.generate_mo(qty_base_1=1, qty_base_2=1, qty_final=1, tracking_final='lot')
  560. # generate lot lot_0 on MO
  561. mo.action_generate_serial()
  562. lot_0 = mo.lot_producing_id.name
  563. # manually create lot_1 (lot_0 + 1)
  564. lot_1 = self.env['stock.lot'].create({
  565. 'name': str(int(lot_0) + 1).zfill(7),
  566. 'product_id': p_final.id,
  567. 'company_id': self.env.company.id,
  568. }).name
  569. # generate lot lot_2 on a new MO
  570. mo = mo.copy()
  571. mo.action_confirm()
  572. mo.action_generate_serial()
  573. lot_2 = mo.lot_producing_id.name
  574. self.assertEqual(lot_2, str(int(lot_1) + 1).zfill(7))