test_generate_serial_numbers.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from odoo.exceptions import UserError, ValidationError
  4. from odoo.tests.common import Form, TransactionCase
  5. class StockGenerate(TransactionCase):
  6. @classmethod
  7. def setUpClass(cls):
  8. super(StockGenerate, cls).setUpClass()
  9. Product = cls.env['product.product']
  10. cls.product_serial = Product.create({
  11. 'name': 'Tracked by SN',
  12. 'type': 'product',
  13. 'tracking': 'serial',
  14. })
  15. cls.uom_unit = cls.env.ref('uom.product_uom_unit')
  16. cls.warehouse = cls.env['stock.warehouse'].create({
  17. 'name': 'Base Warehouse',
  18. 'reception_steps': 'one_step',
  19. 'delivery_steps': 'ship_only',
  20. 'code': 'BWH'
  21. })
  22. cls.location = cls.env['stock.location'].create({
  23. 'name': 'Room A',
  24. 'location_id': cls.warehouse.lot_stock_id.id,
  25. })
  26. cls.location_dest = cls.env['stock.location'].create({
  27. 'name': 'Room B',
  28. 'location_id': cls.warehouse.lot_stock_id.id,
  29. })
  30. cls.Wizard = cls.env['stock.assign.serial']
  31. def get_new_move(self, nbre_of_lines):
  32. move_lines_val = []
  33. for i in range(nbre_of_lines):
  34. move_lines_val.append({
  35. 'product_id': self.product_serial.id,
  36. 'product_uom_id': self.uom_unit.id,
  37. 'reserved_uom_qty': 1,
  38. 'location_id': self.location.id,
  39. 'location_dest_id': self.location_dest.id
  40. })
  41. return self.env['stock.move'].create({
  42. 'name': 'Move Test',
  43. 'product_id': self.product_serial.id,
  44. 'product_uom': self.uom_unit.id,
  45. 'location_id': self.location.id,
  46. 'location_dest_id': self.location_dest.id,
  47. 'move_line_ids': [(0, 0, line_vals) for line_vals in move_lines_val]
  48. })
  49. def test_generate_01_sn(self):
  50. """ Creates a move with 5 move lines, then asks for generates 5 Serial
  51. Numbers. Checks move has 5 new move lines with each a SN, and the 5
  52. original move lines are still unchanged.
  53. """
  54. nbre_of_lines = 5
  55. move = self.get_new_move(nbre_of_lines)
  56. form_wizard = Form(self.env['stock.assign.serial'].with_context(
  57. default_move_id=move.id,
  58. default_next_serial_number='001',
  59. default_next_serial_count=nbre_of_lines,
  60. ))
  61. wiz = form_wizard.save()
  62. self.assertEqual(len(move.move_line_ids), nbre_of_lines)
  63. wiz.generate_serial_numbers()
  64. # Checks new move lines have the right SN
  65. generated_numbers = ['001', '002', '003', '004', '005']
  66. self.assertEqual(len(move.move_line_ids), nbre_of_lines + len(generated_numbers))
  67. for move_line in move.move_line_nosuggest_ids:
  68. # For a product tracked by SN, the `qty_done` is set on 1 when
  69. # `lot_name` is set.
  70. self.assertEqual(move_line.qty_done, 1)
  71. self.assertEqual(move_line.lot_name, generated_numbers.pop(0))
  72. # Checks pre-generated move lines didn't change
  73. for move_line in (move.move_line_ids - move.move_line_nosuggest_ids):
  74. self.assertEqual(move_line.qty_done, 0)
  75. self.assertEqual(move_line.lot_name, False)
  76. def test_generate_02_prefix_suffix(self):
  77. """ Generates some Serial Numbers and checks the prefix and/or suffix
  78. are correctly used.
  79. """
  80. nbre_of_lines = 10
  81. # Case #1: Prefix, no suffix
  82. move = self.get_new_move(nbre_of_lines)
  83. form_wizard = Form(self.env['stock.assign.serial'].with_context(
  84. default_move_id=move.id,
  85. default_next_serial_number='bilou-87',
  86. default_next_serial_count=nbre_of_lines,
  87. ))
  88. wiz = form_wizard.save()
  89. wiz.generate_serial_numbers()
  90. # Checks all move lines have the right SN
  91. generated_numbers = [
  92. 'bilou-87', 'bilou-88', 'bilou-89', 'bilou-90', 'bilou-91',
  93. 'bilou-92', 'bilou-93', 'bilou-94', 'bilou-95', 'bilou-96'
  94. ]
  95. for move_line in move.move_line_nosuggest_ids:
  96. # For a product tracked by SN, the `qty_done` is set on 1 when
  97. # `lot_name` is set.
  98. self.assertEqual(move_line.qty_done, 1)
  99. self.assertEqual(
  100. move_line.lot_name,
  101. generated_numbers.pop(0)
  102. )
  103. # Case #2: No prefix, suffix
  104. move = self.get_new_move(nbre_of_lines)
  105. form_wizard = Form(self.env['stock.assign.serial'].with_context(
  106. default_move_id=move.id,
  107. default_next_serial_number='005-ccc',
  108. default_next_serial_count=nbre_of_lines,
  109. ))
  110. wiz = form_wizard.save()
  111. wiz.generate_serial_numbers()
  112. # Checks all move lines have the right SN
  113. generated_numbers = [
  114. '005-ccc', '006-ccc', '007-ccc', '008-ccc', '009-ccc',
  115. '010-ccc', '011-ccc', '012-ccc', '013-ccc', '014-ccc'
  116. ]
  117. for move_line in move.move_line_nosuggest_ids:
  118. # For a product tracked by SN, the `qty_done` is set on 1 when
  119. # `lot_name` is set.
  120. self.assertEqual(move_line.qty_done, 1)
  121. self.assertEqual(
  122. move_line.lot_name,
  123. generated_numbers.pop(0)
  124. )
  125. # Case #3: Prefix + suffix
  126. move = self.get_new_move(nbre_of_lines)
  127. form_wizard = Form(self.env['stock.assign.serial'].with_context(
  128. default_move_id=move.id,
  129. default_next_serial_number='alpha-012-345-beta',
  130. default_next_serial_count=nbre_of_lines,
  131. ))
  132. wiz = form_wizard.save()
  133. wiz.generate_serial_numbers()
  134. # Checks all move lines have the right SN
  135. generated_numbers = [
  136. 'alpha-012-345-beta', 'alpha-012-346-beta', 'alpha-012-347-beta',
  137. 'alpha-012-348-beta', 'alpha-012-349-beta', 'alpha-012-350-beta',
  138. 'alpha-012-351-beta', 'alpha-012-352-beta', 'alpha-012-353-beta',
  139. 'alpha-012-354-beta'
  140. ]
  141. for move_line in move.move_line_nosuggest_ids:
  142. # For a product tracked by SN, the `qty_done` is set on 1 when
  143. # `lot_name` is set.
  144. self.assertEqual(move_line.qty_done, 1)
  145. self.assertEqual(
  146. move_line.lot_name,
  147. generated_numbers.pop(0)
  148. )
  149. # Case #4: Prefix + suffix, identical number pattern
  150. move = self.get_new_move(nbre_of_lines)
  151. form_wizard = Form(self.env['stock.assign.serial'].with_context(
  152. default_move_id=move.id,
  153. default_next_serial_number='BAV023B00001S00001',
  154. default_next_serial_count=nbre_of_lines,
  155. ))
  156. wiz = form_wizard.save()
  157. wiz.generate_serial_numbers()
  158. # Checks all move lines have the right SN
  159. generated_numbers = [
  160. 'BAV023B00001S00001', 'BAV023B00001S00002', 'BAV023B00001S00003',
  161. 'BAV023B00001S00004', 'BAV023B00001S00005', 'BAV023B00001S00006',
  162. 'BAV023B00001S00007', 'BAV023B00001S00008', 'BAV023B00001S00009',
  163. 'BAV023B00001S00010'
  164. ]
  165. for move_line in move.move_line_nosuggest_ids:
  166. # For a product tracked by SN, the `qty_done` is set on 1 when
  167. # `lot_name` is set.
  168. self.assertEqual(move_line.qty_done, 1)
  169. self.assertEqual(
  170. move_line.lot_name,
  171. generated_numbers.pop(0)
  172. )
  173. def test_generate_03_raise_exception(self):
  174. """ Tries to generate some SN but with invalid initial number.
  175. """
  176. move = self.get_new_move(3)
  177. form_wizard = Form(self.env['stock.assign.serial'].with_context(
  178. default_move_id=move.id,
  179. default_next_serial_number='code-xxx',
  180. ))
  181. form_wizard.next_serial_count = 0
  182. # Must raise an exception because `next_serial_count` must be greater than 0.
  183. with self.assertRaises(ValidationError):
  184. form_wizard.save()
  185. form_wizard.next_serial_count = 3
  186. wiz = form_wizard.save()
  187. wiz.generate_serial_numbers()
  188. self.assertEqual(move.move_line_nosuggest_ids.mapped('lot_name'), ["code-xxx0", "code-xxx1", "code-xxx2"])
  189. def test_generate_04_generate_in_multiple_time(self):
  190. """ Generates a Serial Number for each move lines (except the last one)
  191. but with multiple assignments, and checks the generated Serial Numbers
  192. are what we expect.
  193. """
  194. nbre_of_lines = 10
  195. move = self.get_new_move(nbre_of_lines)
  196. form_wizard = Form(self.env['stock.assign.serial'].with_context(
  197. default_move_id=move.id,
  198. ))
  199. # First assignment
  200. form_wizard.next_serial_count = 3
  201. form_wizard.next_serial_number = '001'
  202. wiz = form_wizard.save()
  203. wiz.generate_serial_numbers()
  204. # Second assignment
  205. form_wizard.next_serial_count = 2
  206. form_wizard.next_serial_number = 'bilou-64'
  207. wiz = form_wizard.save()
  208. wiz.generate_serial_numbers()
  209. # Third assignment
  210. form_wizard.next_serial_count = 4
  211. form_wizard.next_serial_number = 'ro-1337-bot'
  212. wiz = form_wizard.save()
  213. wiz.generate_serial_numbers()
  214. # Checks all move lines have the right SN
  215. generated_numbers = [
  216. # Correspond to the first assignment
  217. '001', '002', '003',
  218. # Correspond to the second assignment
  219. 'bilou-64', 'bilou-65',
  220. # Correspond to the third assignment
  221. 'ro-1337-bot', 'ro-1338-bot', 'ro-1339-bot', 'ro-1340-bot',
  222. ]
  223. self.assertEqual(len(move.move_line_ids), nbre_of_lines + len(generated_numbers))
  224. self.assertEqual(len(move.move_line_nosuggest_ids), len(generated_numbers))
  225. for move_line in move.move_line_nosuggest_ids:
  226. self.assertEqual(move_line.qty_done, 1)
  227. self.assertEqual(move_line.lot_name, generated_numbers.pop(0))
  228. for move_line in (move.move_line_ids - move.move_line_nosuggest_ids):
  229. self.assertEqual(move_line.qty_done, 0)
  230. self.assertEqual(move_line.lot_name, False)
  231. def test_generate_with_putaway(self):
  232. """ Checks the `location_dest_id` of generated move lines is correclty
  233. set in fonction of defined putaway rules.
  234. """
  235. nbre_of_lines = 4
  236. shelf_location = self.env['stock.location'].create({
  237. 'name': 'shelf1',
  238. 'usage': 'internal',
  239. 'location_id': self.location_dest.id,
  240. })
  241. # Checks a first time without putaway...
  242. move = self.get_new_move(nbre_of_lines)
  243. form_wizard = Form(self.env['stock.assign.serial'].with_context(
  244. default_move_id=move.id,
  245. ))
  246. form_wizard.next_serial_count = nbre_of_lines
  247. form_wizard.next_serial_number = '001'
  248. wiz = form_wizard.save()
  249. wiz.generate_serial_numbers()
  250. for move_line in move.move_line_nosuggest_ids:
  251. self.assertEqual(move_line.qty_done, 1)
  252. # The location dest must be the default one.
  253. self.assertEqual(move_line.location_dest_id.id, self.location_dest.id)
  254. # We need to activate multi-locations to use putaway rules.
  255. grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
  256. self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]})
  257. # Creates a putaway rule
  258. putaway_product = self.env['stock.putaway.rule'].create({
  259. 'product_id': self.product_serial.id,
  260. 'location_in_id': self.location_dest.id,
  261. 'location_out_id': shelf_location.id,
  262. })
  263. # Checks now with putaway...
  264. move = self.get_new_move(nbre_of_lines)
  265. form_wizard = Form(self.env['stock.assign.serial'].with_context(
  266. default_move_id=move.id,
  267. ))
  268. form_wizard.next_serial_count = nbre_of_lines
  269. form_wizard.next_serial_number = '001'
  270. wiz = form_wizard.save()
  271. wiz.generate_serial_numbers()
  272. for move_line in move.move_line_nosuggest_ids:
  273. self.assertEqual(move_line.qty_done, 1)
  274. # The location dest must be now the one from the putaway.
  275. self.assertEqual(move_line.location_dest_id.id, shelf_location.id)
  276. def test_set_multiple_lot_name_01(self):
  277. """ Sets five SN in one time in stock move view form, then checks move
  278. has five new move lines with the right `lot_name`.
  279. """
  280. nbre_of_lines = 10
  281. picking_type = self.env['stock.picking.type'].search([
  282. ('use_create_lots', '=', True),
  283. ('warehouse_id', '=', self.warehouse.id)
  284. ])
  285. move = self.get_new_move(nbre_of_lines)
  286. move.picking_type_id = picking_type
  287. # We must begin with a move with 10 move lines.
  288. self.assertEqual(len(move.move_line_ids), nbre_of_lines)
  289. value_list = [
  290. 'abc-235',
  291. 'abc-237',
  292. 'abc-238',
  293. 'abc-282',
  294. 'abc-301',
  295. ]
  296. values = '\n'.join(value_list)
  297. move_form = Form(move, view='stock.view_stock_move_nosuggest_operations')
  298. with move_form.move_line_nosuggest_ids.new() as line:
  299. line.lot_name = values
  300. move = move_form.save()
  301. # After we set multiple SN, we must have now 15 move lines.
  302. self.assertEqual(len(move.move_line_ids), nbre_of_lines + len(value_list))
  303. # Then we look each SN name is correct.
  304. for move_line in move.move_line_nosuggest_ids:
  305. self.assertEqual(move_line.lot_name, value_list.pop(0))
  306. for move_line in (move.move_line_ids - move.move_line_nosuggest_ids):
  307. self.assertEqual(move_line.lot_name, False)
  308. def test_set_multiple_lot_name_02_empty_values(self):
  309. """ Sets multiple values with some empty lines in one time, then checks
  310. we haven't create useless move line and all move line's `lot_name` have
  311. been correctly set.
  312. """
  313. nbre_of_lines = 5
  314. picking_type = self.env['stock.picking.type'].search([
  315. ('use_create_lots', '=', True),
  316. ('warehouse_id', '=', self.warehouse.id)
  317. ])
  318. move = self.get_new_move(nbre_of_lines)
  319. move.picking_type_id = picking_type
  320. # We must begin with a move with five move lines.
  321. self.assertEqual(len(move.move_line_ids), nbre_of_lines)
  322. value_list = [
  323. '',
  324. 'abc-235',
  325. '',
  326. 'abc-237',
  327. '',
  328. '',
  329. 'abc-238',
  330. 'abc-282',
  331. 'abc-301',
  332. '',
  333. ]
  334. values = '\n'.join(value_list)
  335. # Checks we have more values than move lines.
  336. self.assertTrue(len(move.move_line_ids) < len(value_list))
  337. move_form = Form(move, view='stock.view_stock_move_nosuggest_operations')
  338. with move_form.move_line_nosuggest_ids.new() as line:
  339. line.lot_name = values
  340. move = move_form.save()
  341. filtered_value_list = list(filter(lambda line: len(line), value_list))
  342. # After we set multiple SN, we must have a line for each value.
  343. self.assertEqual(len(move.move_line_ids), nbre_of_lines + len(filtered_value_list))
  344. # Then we look each SN name is correct.
  345. for move_line in move.move_line_nosuggest_ids:
  346. self.assertEqual(move_line.lot_name, filtered_value_list.pop(0))
  347. for move_line in (move.move_line_ids - move.move_line_nosuggest_ids):
  348. self.assertEqual(move_line.lot_name, False)
  349. def test_generate_with_putaway_02(self):
  350. """
  351. Suppose a tracked-by-USN product P
  352. Sub locations in WH/Stock + Storage Category
  353. The Storage Category adds a capacity constraint (max 1 x P / Location)
  354. - Plan a receipt with 2 x P
  355. - Receive 4 x P
  356. -> The test ensures that the destination locations are correct
  357. """
  358. stock_location = self.warehouse.lot_stock_id
  359. self.env.user.write({'groups_id': [(4, self.env.ref('stock.group_stock_storage_categories').id)]})
  360. self.env.user.write({'groups_id': [(4, self.env.ref('stock.group_stock_multi_locations').id)]})
  361. # max 1 x product_serial
  362. stor_category = self.env['stock.storage.category'].create({
  363. 'name': 'Super Storage Category',
  364. 'product_capacity_ids': [(0, 0, {
  365. 'product_id': self.product_serial.id,
  366. 'quantity': 1,
  367. })]
  368. })
  369. # 5 sub locations with the storage category
  370. # (the last one should never be used)
  371. sub_loc_01, sub_loc_02, sub_loc_03, sub_loc_04, dummy = self.env['stock.location'].create([{
  372. 'name': 'Sub Location %s' % i,
  373. 'usage': 'internal',
  374. 'location_id': stock_location.id,
  375. 'storage_category_id': stor_category.id,
  376. } for i in [1, 2, 3, 4, 5]])
  377. self.env['stock.putaway.rule'].create({
  378. 'location_in_id': stock_location.id,
  379. 'location_out_id': stock_location.id,
  380. 'product_id': self.product_serial.id,
  381. 'storage_category_id': stor_category.id,
  382. })
  383. # Receive 1 x P
  384. receipt_picking = self.env['stock.picking'].create({
  385. 'picking_type_id': self.warehouse.in_type_id.id,
  386. 'location_id': self.env.ref('stock.stock_location_suppliers').id,
  387. 'location_dest_id': stock_location.id,
  388. })
  389. move = self.env['stock.move'].create({
  390. 'name': self.product_serial.name,
  391. 'product_id': self.product_serial.id,
  392. 'product_uom': self.product_serial.uom_id.id,
  393. 'product_uom_qty': 2.0,
  394. 'picking_id': receipt_picking.id,
  395. 'location_id': receipt_picking.location_id.id,
  396. 'location_dest_id': receipt_picking.location_dest_id.id,
  397. })
  398. receipt_picking.action_confirm()
  399. self.assertEqual(move.move_line_ids[0].location_dest_id, sub_loc_01)
  400. self.assertEqual(move.move_line_ids[1].location_dest_id, sub_loc_02)
  401. form_wizard = Form(self.env['stock.assign.serial'].with_context(
  402. default_move_id=move.id,
  403. default_next_serial_number='001',
  404. default_next_serial_count=4,
  405. ))
  406. wiz = form_wizard.save()
  407. wiz.generate_serial_numbers()
  408. self.assertRecordValues(move.move_line_ids, [
  409. {'qty_done': 1, 'lot_name': '001', 'location_dest_id': sub_loc_01.id},
  410. {'qty_done': 1, 'lot_name': '002', 'location_dest_id': sub_loc_02.id},
  411. {'qty_done': 1, 'lot_name': '003', 'location_dest_id': sub_loc_03.id},
  412. {'qty_done': 1, 'lot_name': '004', 'location_dest_id': sub_loc_04.id},
  413. ])