common_consume_tracked_component.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import copy
  2. from odoo.exceptions import UserError
  3. from odoo.tests import common, Form
  4. from odoo.tools import float_is_zero
  5. class TestConsumeTrackedComponentCommon(common.TransactionCase):
  6. @classmethod
  7. def setUpClass(cls):
  8. """
  9. The following variables are used in each test to define the number of MO to generate.
  10. They're also used as a verification in the executeConsumptionTriggers() to see if enough MO were passed to it
  11. in order to test all the triggers.
  12. SERIAL : MO's product_tracking is 'serial'
  13. DEFAULT : MO's product_tracking is 'none' or 'lot'
  14. AVAILABLE : MO'S raw components are fully available
  15. """
  16. super().setUpClass()
  17. cls.SERIAL_AVAILABLE_TRIGGERS_COUNT = 3
  18. cls.DEFAULT_AVAILABLE_TRIGGERS_COUNT = 2
  19. cls.SERIAL_TRIGGERS_COUNT = 2
  20. cls.DEFAULT_TRIGGERS_COUNT = 1
  21. cls.manufacture_route = cls.env.ref('mrp.route_warehouse0_manufacture')
  22. cls.stock_id = cls.env.ref('stock.stock_location_stock').id
  23. cls.picking_type = cls.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0]
  24. cls.picking_type.use_create_components_lots = True
  25. cls.picking_type.use_auto_consume_components_lots = True
  26. #Create Products & Components
  27. cls.produced_lot = cls.env['product.product'].create({
  28. 'name': 'Produced Lot',
  29. 'type': 'product',
  30. 'categ_id': cls.env.ref('product.product_category_all').id,
  31. 'tracking' : 'lot',
  32. 'route_ids': [(4, cls.manufacture_route.id, 0)],
  33. })
  34. cls.produced_serial = cls.env['product.product'].create({
  35. 'name': 'Produced Serial',
  36. 'type': 'product',
  37. 'categ_id': cls.env.ref('product.product_category_all').id,
  38. 'tracking' : 'serial',
  39. 'route_ids': [(4, cls.manufacture_route.id, 0)],
  40. })
  41. cls.produced_none = cls.env['product.product'].create({
  42. 'name': 'Produced None',
  43. 'type': 'product',
  44. 'categ_id': cls.env.ref('product.product_category_all').id,
  45. 'tracking' : 'none',
  46. 'route_ids': [(4, cls.manufacture_route.id, 0)],
  47. })
  48. cls.raw_lot = cls.env['product.product'].create({
  49. 'name': 'Raw Lot',
  50. 'type': 'product',
  51. 'categ_id': cls.env.ref('product.product_category_all').id,
  52. 'tracking' : 'lot',
  53. })
  54. cls.raw_serial = cls.env['product.product'].create({
  55. 'name': 'Raw Serial',
  56. 'type': 'product',
  57. 'categ_id': cls.env.ref('product.product_category_all').id,
  58. 'tracking' : 'serial',
  59. })
  60. cls.raw_none = cls.env['product.product'].create({
  61. 'name': 'Raw None',
  62. 'type': 'product',
  63. 'categ_id': cls.env.ref('product.product_category_all').id,
  64. 'tracking' : 'none',
  65. })
  66. cls.raws = [cls.raw_none, cls.raw_lot, cls.raw_serial]
  67. #Workcenter
  68. cls.workcenter = cls.env['mrp.workcenter'].create({
  69. 'name' : 'Assembly Line',
  70. })
  71. #BoMs
  72. cls.bom_none = cls.env['mrp.bom'].create({
  73. 'product_tmpl_id' : cls.produced_none.product_tmpl_id.id,
  74. 'product_uom_id' : cls.produced_none.uom_id.id,
  75. 'consumption' : 'flexible',
  76. 'sequence' : 1
  77. })
  78. cls.bom_none_lines = cls.create_bom_lines(cls.bom_none, cls.raws, [3, 2, 1])
  79. cls.bom_lot = cls.env['mrp.bom'].create({
  80. 'product_tmpl_id' : cls.produced_lot.product_tmpl_id.id,
  81. 'product_uom_id' : cls.produced_lot.uom_id.id,
  82. 'consumption' : 'flexible',
  83. 'sequence' : 2
  84. })
  85. cls.bom_lot_lines = cls.create_bom_lines(cls.bom_lot, cls.raws, [3, 2, 1])
  86. cls.bom_serial = cls.env['mrp.bom'].create({
  87. 'product_tmpl_id' : cls.produced_serial.product_tmpl_id.id,
  88. 'product_uom_id' : cls.produced_serial.uom_id.id,
  89. 'consumption' : 'flexible',
  90. 'sequence' : 1
  91. })
  92. cls.bom_serial_lines = cls.create_bom_lines(cls.bom_serial, cls.raws, [3, 2, 1])
  93. #Manufacturing Orders
  94. cls.mo_none_tmpl = {
  95. 'product_id' : cls.produced_none.id,
  96. 'product_uom_id' : cls.produced_none.uom_id.id,
  97. 'product_qty' : 1,
  98. 'bom_id' : cls.bom_none.id
  99. }
  100. cls.mo_lot_tmpl = {
  101. 'product_id' : cls.produced_lot.id,
  102. 'product_uom_id' : cls.produced_lot.uom_id.id,
  103. 'product_qty' : 1,
  104. 'bom_id' : cls.bom_lot.id
  105. }
  106. cls.mo_serial_tmpl = {
  107. 'product_id' : cls.produced_serial.id,
  108. 'product_uom_id' : cls.produced_serial.uom_id.id,
  109. 'product_qty' : 1,
  110. 'bom_id' : cls.bom_serial.id
  111. }
  112. @classmethod
  113. def create_quant(cls, product, qty, offset=0, name="L"):
  114. i = 1
  115. if product.tracking == 'serial':
  116. i, qty = qty, 1
  117. if name == "L":
  118. name = "S"
  119. vals = []
  120. for x in range(1, i+1):
  121. qDict = {
  122. 'location_id': cls.stock_id,
  123. 'product_id': product.id,
  124. 'inventory_quantity': qty,
  125. }
  126. if product.tracking != 'none':
  127. qDict['lot_id'] = cls.env['stock.lot'].create({
  128. 'name': name + str(offset + x),
  129. 'product_id': product.id,
  130. 'company_id': cls.env.company.id
  131. }).id
  132. vals.append(qDict)
  133. return cls.env['stock.quant'].create(vals)
  134. @classmethod
  135. def create_bom_lines(cls, bom, products, quantities=None):
  136. if quantities is None:
  137. quantities = [1 for i in range(len(products))]
  138. vals = []
  139. for product, seq in zip(products, range(len(products))):
  140. vals.append({
  141. 'product_id' : product.id,
  142. 'product_qty' : quantities[seq],
  143. 'product_uom_id' : product.uom_id.id,
  144. 'sequence' : seq,
  145. 'bom_id' : bom.id,
  146. })
  147. return cls.env['mrp.bom.line'].create(vals)
  148. @classmethod
  149. def create_mo(cls, template, count):
  150. vals = []
  151. for _ in range(count):
  152. vals.append(copy.deepcopy(template))
  153. return cls.env['mrp.production'].create(vals)
  154. def executeConsumptionTriggers(self, mrp_productions):
  155. """
  156. There's 3 different triggers to test : _onchange_producing(), action_generate_serial(), button_mark_done().
  157. Depending on the tracking of the final product and the availability of the components,
  158. only a part of these 3 triggers is available or intended to work.
  159. This function automatically call and process the appropriate triggers.
  160. """
  161. tracking = mrp_productions[0].product_tracking
  162. sameTracking = True
  163. for mo in mrp_productions:
  164. sameTracking = sameTracking and mo.product_tracking == tracking
  165. self.assertTrue(sameTracking, "MOs passed to the executeConsumptionTriggers method shall have the same product_tracking")
  166. isSerial = tracking == 'serial'
  167. isAvailable = all(move.state == 'assigned' for move in mrp_productions.move_raw_ids)
  168. countOk = True
  169. length = len(mrp_productions)
  170. if isSerial:
  171. if isAvailable:
  172. countOk = length == self.SERIAL_AVAILABLE_TRIGGERS_COUNT
  173. else:
  174. countOk = length == self.SERIAL_TRIGGERS_COUNT
  175. else:
  176. if isAvailable:
  177. countOk = length == self.DEFAULT_AVAILABLE_TRIGGERS_COUNT
  178. else:
  179. countOk = length == self.DEFAULT_TRIGGERS_COUNT
  180. self.assertTrue(countOk, "The number of MOs passed to the executeConsumptionTriggers method does not match the associated TRIGGERS_COUNT")
  181. mrp_productions[0].qty_producing = mrp_productions[0].product_qty
  182. mrp_productions[0]._onchange_producing()
  183. i = 1
  184. if isSerial:
  185. mrp_productions[i].action_generate_serial()
  186. i += 1
  187. if isAvailable:
  188. mark_done_action = mrp_productions[i].button_mark_done()
  189. immediate_production_wizard = Form(
  190. self.env['mrp.immediate.production']
  191. .with_context(**mark_done_action['context'])
  192. ).save()
  193. error = False
  194. has_zero_tracked_component = not mrp_productions[i].picking_type_id.use_auto_consume_components_lots and \
  195. any(m.state not in ['done', 'cancel'] and m.has_tracking != 'none' and float_is_zero(m.quantity_done, m.product_uom.rounding) for m in mrp_productions[i].move_raw_ids)
  196. try:
  197. immediate_production_wizard.process()
  198. except UserError:
  199. error = True
  200. if has_zero_tracked_component:
  201. self.assertTrue(error, "Immediate Production Wizard shall raise an error.")
  202. else:
  203. self.assertFalse(error, "Immediate Production Wizard shall not raise an error.")