test_bom.py 68 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from odoo import exceptions, Command, fields
  4. from odoo.tests import Form
  5. from odoo.addons.mrp.tests.common import TestMrpCommon
  6. from odoo.tools import float_compare, float_round, float_repr
  7. from freezegun import freeze_time
  8. @freeze_time(fields.Date.today())
  9. class TestBoM(TestMrpCommon):
  10. def test_01_explode(self):
  11. boms, lines = self.bom_1.explode(self.product_4, 3)
  12. self.assertEqual(set([bom[0].id for bom in boms]), set(self.bom_1.ids))
  13. self.assertEqual(set([line[0].id for line in lines]), set(self.bom_1.bom_line_ids.ids))
  14. boms, lines = self.bom_3.explode(self.product_6, 3)
  15. self.assertEqual(set([bom[0].id for bom in boms]), set((self.bom_2 | self.bom_3).ids))
  16. self.assertEqual(
  17. set([line[0].id for line in lines]),
  18. set((self.bom_2 | self.bom_3).mapped('bom_line_ids').filtered(lambda line: not line.child_bom_id or line.child_bom_id.type != 'phantom').ids))
  19. def test_10_variants(self):
  20. test_bom = self.env['mrp.bom'].create({
  21. 'product_tmpl_id': self.product_7_template.id,
  22. 'product_uom_id': self.uom_unit.id,
  23. 'product_qty': 4.0,
  24. 'type': 'normal',
  25. 'operation_ids': [
  26. Command.create({
  27. 'name': 'Cutting Machine',
  28. 'workcenter_id': self.workcenter_1.id,
  29. 'time_cycle': 12,
  30. 'sequence': 1
  31. }),
  32. Command.create({
  33. 'name': 'Weld Machine',
  34. 'workcenter_id': self.workcenter_1.id,
  35. 'time_cycle': 18,
  36. 'sequence': 2,
  37. 'bom_product_template_attribute_value_ids': [Command.link(self.product_7_attr1_v1.id)]
  38. }),
  39. Command.create({
  40. 'name': 'Taking a coffee',
  41. 'workcenter_id': self.workcenter_1.id,
  42. 'time_cycle': 5,
  43. 'sequence': 3,
  44. 'bom_product_template_attribute_value_ids': [Command.link(self.product_7_attr1_v2.id)]
  45. })
  46. ],
  47. 'byproduct_ids': [
  48. Command.create({
  49. 'product_id': self.product_1.id,
  50. 'product_uom_id': self.product_1.uom_id.id,
  51. 'product_qty': 1,
  52. }),
  53. Command.create({
  54. 'product_id': self.product_2.id,
  55. 'product_uom_id': self.product_2.uom_id.id,
  56. 'product_qty': 1,
  57. 'bom_product_template_attribute_value_ids': [Command.link(self.product_7_attr1_v1.id)]
  58. }),
  59. Command.create({
  60. 'product_id': self.product_3.id,
  61. 'product_uom_id': self.product_3.uom_id.id,
  62. 'product_qty': 1,
  63. 'bom_product_template_attribute_value_ids': [Command.link(self.product_7_attr1_v2.id)]
  64. }),
  65. ],
  66. 'bom_line_ids': [
  67. Command.create({
  68. 'product_id': self.product_2.id,
  69. 'product_qty': 2,
  70. }),
  71. Command.create({
  72. 'product_id': self.product_3.id,
  73. 'product_qty': 2,
  74. 'bom_product_template_attribute_value_ids': [Command.link(self.product_7_attr1_v1.id)],
  75. }),
  76. Command.create({
  77. 'product_id': self.product_4.id,
  78. 'product_qty': 2,
  79. 'bom_product_template_attribute_value_ids': [Command.link(self.product_7_attr1_v2.id)],
  80. }),
  81. ]
  82. })
  83. test_bom_l1, test_bom_l2, test_bom_l3 = test_bom.bom_line_ids
  84. boms, lines = test_bom.explode(self.product_7_3, 4)
  85. self.assertIn(test_bom, [b[0]for b in boms])
  86. self.assertIn(test_bom_l1, [l[0] for l in lines])
  87. self.assertNotIn(test_bom_l2, [l[0] for l in lines])
  88. self.assertNotIn(test_bom_l3, [l[0] for l in lines])
  89. boms, lines = test_bom.explode(self.product_7_1, 4)
  90. self.assertIn(test_bom, [b[0]for b in boms])
  91. self.assertIn(test_bom_l1, [l[0] for l in lines])
  92. self.assertIn(test_bom_l2, [l[0] for l in lines])
  93. self.assertNotIn(test_bom_l3, [l[0] for l in lines])
  94. boms, lines = test_bom.explode(self.product_7_2, 4)
  95. self.assertIn(test_bom, [b[0]for b in boms])
  96. self.assertIn(test_bom_l1, [l[0] for l in lines])
  97. self.assertNotIn(test_bom_l2, [l[0] for l in lines])
  98. self.assertIn(test_bom_l3, [l[0] for l in lines])
  99. mrp_order_form = Form(self.env['mrp.production'])
  100. mrp_order_form.product_id = self.product_7_3
  101. mrp_order = mrp_order_form.save()
  102. self.assertEqual(mrp_order.bom_id, test_bom)
  103. self.assertEqual(len(mrp_order.workorder_ids), 1)
  104. self.assertEqual(mrp_order.workorder_ids.operation_id, test_bom.operation_ids[0])
  105. self.assertEqual(len(mrp_order.move_byproduct_ids), 1)
  106. self.assertEqual(mrp_order.move_byproduct_ids.product_id, self.product_1)
  107. mrp_order_form = Form(self.env['mrp.production'])
  108. mrp_order_form.product_id = self.product_7_1
  109. mrp_order_form.product_id = self.env['product.product'] # Check form
  110. mrp_order_form.product_id = self.product_7_1
  111. mrp_order_form.bom_id = self.env['mrp.bom'] # Check form
  112. mrp_order_form.bom_id = test_bom
  113. mrp_order = mrp_order_form.save()
  114. self.assertEqual(mrp_order.bom_id, test_bom)
  115. self.assertEqual(len(mrp_order.workorder_ids), 2)
  116. self.assertEqual(mrp_order.workorder_ids.operation_id, test_bom.operation_ids[:2])
  117. self.assertEqual(len(mrp_order.move_byproduct_ids), 2)
  118. self.assertEqual(mrp_order.move_byproduct_ids.product_id, self.product_1 | self.product_2)
  119. mrp_order_form = Form(self.env['mrp.production'])
  120. mrp_order_form.product_id = self.product_7_2
  121. mrp_order = mrp_order_form.save()
  122. self.assertEqual(mrp_order.bom_id, test_bom)
  123. self.assertEqual(len(mrp_order.workorder_ids), 2)
  124. self.assertEqual(mrp_order.workorder_ids.operation_id, test_bom.operation_ids[0] | test_bom.operation_ids[2])
  125. self.assertEqual(len(mrp_order.move_byproduct_ids), 2)
  126. self.assertEqual(mrp_order.move_byproduct_ids.product_id, self.product_1 | self.product_3)
  127. def test_11_multi_level_variants(self):
  128. tmp_picking_type = self.env['stock.picking.type'].create({
  129. 'name': 'Manufacturing',
  130. 'code': 'mrp_operation',
  131. 'sequence_code': 'TMP',
  132. 'sequence_id': self.env['ir.sequence'].create({
  133. 'code': 'mrp.production',
  134. 'name': 'tmp_production_sequence',
  135. }).id,
  136. })
  137. test_bom_1 = self.env['mrp.bom'].create({
  138. 'product_tmpl_id': self.product_5.product_tmpl_id.id,
  139. 'product_uom_id': self.product_5.uom_id.id,
  140. 'product_qty': 1.0,
  141. 'type': 'phantom'
  142. })
  143. test_bom_1.write({
  144. 'operation_ids': [
  145. (0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
  146. ],
  147. })
  148. test_bom_1.bom_line_ids = [(0, 0, {
  149. 'product_id': self.product_3.id,
  150. 'product_qty': 3,
  151. })]
  152. test_bom_2 = self.env['mrp.bom'].create({
  153. 'product_tmpl_id': self.product_7_template.id,
  154. 'product_uom_id': self.uom_unit.id,
  155. 'product_qty': 4.0,
  156. 'type': 'normal',
  157. })
  158. test_bom_2.write({
  159. 'operation_ids': [
  160. (0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
  161. (0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
  162. ]
  163. })
  164. test_bom_2.bom_line_ids = [(0, 0, {
  165. 'product_id': self.product_2.id,
  166. 'product_qty': 2,
  167. })]
  168. test_bom_2.bom_line_ids = [(0, 0, {
  169. 'product_id': self.product_5.id,
  170. 'product_qty': 2,
  171. 'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v1.id)],
  172. })]
  173. test_bom_2.bom_line_ids = [(0, 0, {
  174. 'product_id': self.product_5.id,
  175. 'product_qty': 2,
  176. 'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v2.id)],
  177. })]
  178. test_bom_2.bom_line_ids = [(0, 0, {
  179. 'product_id': self.product_4.id,
  180. 'product_qty': 2,
  181. })]
  182. test_bom_2_l1, _test_bom_2_l2, _test_bom_2_l3, test_bom_2_l4 = test_bom_2.bom_line_ids
  183. # check product > product_tmpl
  184. boms, lines = test_bom_2.explode(self.product_7_1, 4)
  185. self.assertEqual(set((test_bom_2 | self.bom_2).ids), set([b[0].id for b in boms]))
  186. self.assertEqual(set((test_bom_2_l1 | test_bom_2_l4 | self.bom_2.bom_line_ids).ids), set([l[0].id for l in lines]))
  187. # check sequence priority
  188. test_bom_1.write({'sequence': 1})
  189. boms, lines = test_bom_2.explode(self.product_7_1, 4)
  190. self.assertEqual(set((test_bom_2 | test_bom_1).ids), set([b[0].id for b in boms]))
  191. self.assertEqual(set((test_bom_2_l1 | test_bom_2_l4 | test_bom_1.bom_line_ids).ids), set([l[0].id for l in lines]))
  192. # check with another picking_type
  193. test_bom_1.write({'picking_type_id': self.warehouse_1.manu_type_id.id})
  194. self.bom_2.write({'picking_type_id': tmp_picking_type.id})
  195. test_bom_2.write({'picking_type_id': tmp_picking_type.id})
  196. boms, lines = test_bom_2.explode(self.product_7_1, 4)
  197. self.assertEqual(set((test_bom_2 | self.bom_2).ids), set([b[0].id for b in boms]))
  198. self.assertEqual(set((test_bom_2_l1 | test_bom_2_l4 | self.bom_2.bom_line_ids).ids), set([l[0].id for l in lines]))
  199. self.product_9, self.product_10 = self.env['product.product'].create([{
  200. 'name': 'Paper', # product_9
  201. }, {
  202. 'name': 'Stone', # product_10
  203. }])
  204. #check recursion
  205. test_bom_3 = self.env['mrp.bom'].create({
  206. 'product_id': self.product_9.id,
  207. 'product_tmpl_id': self.product_9.product_tmpl_id.id,
  208. 'product_uom_id': self.product_9.uom_id.id,
  209. 'product_qty': 1.0,
  210. 'consumption': 'flexible',
  211. 'type': 'normal'
  212. })
  213. test_bom_4 = self.env['mrp.bom'].create({
  214. 'product_id': self.product_10.id,
  215. 'product_tmpl_id': self.product_10.product_tmpl_id.id,
  216. 'product_uom_id': self.product_10.uom_id.id,
  217. 'product_qty': 1.0,
  218. 'consumption': 'flexible',
  219. 'type': 'phantom'
  220. })
  221. test_bom_3.bom_line_ids = [(0, 0, {
  222. 'product_id': self.product_10.id,
  223. 'product_qty': 1.0,
  224. })]
  225. with self.assertRaises(exceptions.UserError):
  226. test_bom_4.bom_line_ids = [(0, 0, {
  227. 'product_id': self.product_9.id,
  228. 'product_qty': 1.0,
  229. })]
  230. def test_12_multi_level_variants2(self):
  231. """Test skip bom line with same attribute values in bom lines."""
  232. Product = self.env['product.product']
  233. ProductAttribute = self.env['product.attribute']
  234. ProductAttributeValue = self.env['product.attribute.value']
  235. # Product Attribute
  236. att_color = ProductAttribute.create({'name': 'Color', 'sequence': 1})
  237. att_size = ProductAttribute.create({'name': 'size', 'sequence': 2})
  238. # Product Attribute color Value
  239. att_color_red = ProductAttributeValue.create({'name': 'red', 'attribute_id': att_color.id, 'sequence': 1})
  240. att_color_blue = ProductAttributeValue.create({'name': 'blue', 'attribute_id': att_color.id, 'sequence': 2})
  241. # Product Attribute size Value
  242. att_size_big = ProductAttributeValue.create({'name': 'big', 'attribute_id': att_size.id, 'sequence': 1})
  243. att_size_medium = ProductAttributeValue.create({'name': 'medium', 'attribute_id': att_size.id, 'sequence': 2})
  244. # Create Template Product
  245. product_template = self.env['product.template'].create({
  246. 'name': 'Sofa',
  247. 'attribute_line_ids': [
  248. (0, 0, {
  249. 'attribute_id': att_color.id,
  250. 'value_ids': [(6, 0, [att_color_red.id, att_color_blue.id])]
  251. }),
  252. (0, 0, {
  253. 'attribute_id': att_size.id,
  254. 'value_ids': [(6, 0, [att_size_big.id, att_size_medium.id])]
  255. })
  256. ]
  257. })
  258. sofa_red = product_template.attribute_line_ids[0].product_template_value_ids[0]
  259. sofa_blue = product_template.attribute_line_ids[0].product_template_value_ids[1]
  260. sofa_big = product_template.attribute_line_ids[1].product_template_value_ids[0]
  261. sofa_medium = product_template.attribute_line_ids[1].product_template_value_ids[1]
  262. # Create components Of BOM
  263. product_A = Product.create({
  264. 'name': 'Wood'})
  265. product_B = Product.create({
  266. 'name': 'Clothes'})
  267. # Create BOM
  268. self.env['mrp.bom'].create({
  269. 'product_tmpl_id': product_template.id,
  270. 'product_qty': 1.0,
  271. 'type': 'normal',
  272. 'bom_line_ids': [
  273. (0, 0, {
  274. 'product_id': product_A.id,
  275. 'product_qty': 1,
  276. 'bom_product_template_attribute_value_ids': [(4, sofa_red.id), (4, sofa_blue.id), (4, sofa_big.id)],
  277. }),
  278. (0, 0, {
  279. 'product_id': product_B.id,
  280. 'product_qty': 1,
  281. 'bom_product_template_attribute_value_ids': [(4, sofa_red.id), (4, sofa_blue.id)]
  282. })
  283. ]
  284. })
  285. dict_consumed_products = {
  286. sofa_red + sofa_big: product_A + product_B,
  287. sofa_red + sofa_medium: product_B,
  288. sofa_blue + sofa_big: product_A + product_B,
  289. sofa_blue + sofa_medium: product_B,
  290. }
  291. # Create production order for all variants.
  292. for combination, consumed_products in dict_consumed_products.items():
  293. product = product_template.product_variant_ids.filtered(lambda p: p.product_template_attribute_value_ids == combination)
  294. mrp_order_form = Form(self.env['mrp.production'])
  295. mrp_order_form.product_id = product
  296. mrp_order = mrp_order_form.save()
  297. # Check consumed materials in production order.
  298. self.assertEqual(mrp_order.move_raw_ids.product_id, consumed_products)
  299. def test_13_bom_kit_qty(self):
  300. self.env['mrp.bom'].create({
  301. 'product_id': self.product_7_3.id,
  302. 'product_tmpl_id': self.product_7_template.id,
  303. 'product_uom_id': self.uom_unit.id,
  304. 'product_qty': 4.0,
  305. 'type': 'phantom',
  306. 'bom_line_ids': [
  307. (0, 0, {
  308. 'product_id': self.product_2.id,
  309. 'product_qty': 2,
  310. }),
  311. (0, 0, {
  312. 'product_id': self.product_3.id,
  313. 'product_qty': 2,
  314. })
  315. ]
  316. })
  317. location = self.env.ref('stock.stock_location_stock')
  318. self.env['stock.quant']._update_available_quantity(self.product_2, location, 4.0)
  319. self.env['stock.quant']._update_available_quantity(self.product_3, location, 8.0)
  320. # Force the kit product available qty to be computed at the same time than its component quantities
  321. # Because `qty_available` of a bom kit "recurse" on `qty_available` of its component,
  322. # and this is a tricky thing for the ORM:
  323. # `qty_available` gets called for `product_7_3`, `product_2` and `product_3`
  324. # which then recurse on calling `qty_available` for `product_2` and `product_3` to compute the quantity of
  325. # the kit `product_7_3`. `product_2` and `product_3` gets protected at the first call of the compute method,
  326. # ending the recurse call to not call the compute method and just left the Falsy value `0.0`
  327. # for the components available qty.
  328. kit_product_qty, _, _ = (self.product_7_3 + self.product_2 + self.product_3).mapped("qty_available")
  329. self.assertEqual(kit_product_qty, 8)
  330. def test_14_bom_kit_qty_multi_uom(self):
  331. uom_dozens = self.env.ref('uom.product_uom_dozen')
  332. uom_unit = self.env.ref('uom.product_uom_unit')
  333. product_unit = self.env['product.product'].create({
  334. 'name': 'Test units',
  335. 'type': 'product',
  336. 'uom_id': uom_unit.id,
  337. })
  338. product_dozens = self.env['product.product'].create({
  339. 'name': 'Test dozens',
  340. 'type': 'product',
  341. 'uom_id': uom_dozens.id,
  342. })
  343. self.env['mrp.bom'].create({
  344. 'product_tmpl_id': product_unit.product_tmpl_id.id,
  345. 'product_uom_id': self.uom_unit.id,
  346. 'product_qty': 1.0,
  347. 'type': 'phantom',
  348. 'bom_line_ids': [
  349. (0, 0, {
  350. 'product_id': product_dozens.id,
  351. 'product_qty': 1,
  352. 'product_uom_id': uom_unit.id,
  353. })
  354. ]
  355. })
  356. location = self.env.ref('stock.stock_location_stock')
  357. self.env['stock.quant']._update_available_quantity(product_dozens, location, 1.0)
  358. self.assertEqual(product_unit.qty_available, 12.0)
  359. def test_13_negative_on_hand_qty(self):
  360. # We set the Product Unit of Measure digits to 5.
  361. # Because float_round(-384.0, 5) = -384.00000000000006
  362. # And float_round(-384.0, 2) = -384.0
  363. precision = self.env.ref('product.decimal_product_uom')
  364. precision.digits = 5
  365. # We set the Unit(s) rounding to 0.0001 (digit = 4)
  366. uom_unit = self.env.ref('uom.product_uom_unit')
  367. uom_unit.rounding = 0.0001
  368. _ = self.env['mrp.bom'].create({
  369. 'product_id': self.product_2.id,
  370. 'product_tmpl_id': self.product_2.product_tmpl_id.id,
  371. 'product_uom_id': uom_unit.id,
  372. 'product_qty': 1.00,
  373. 'type': 'phantom',
  374. 'bom_line_ids': [
  375. (0, 0, {
  376. 'product_id': self.product_3.id,
  377. 'product_qty': 1.000,
  378. }),
  379. ]
  380. })
  381. self.env['stock.quant']._update_available_quantity(self.product_3, self.env.ref('stock.stock_location_stock'), -384.0)
  382. kit_product_qty = self.product_2.qty_available # Without product_3 in the prefetch
  383. # Use the float_repr to remove extra small decimal (and represent the front-end behavior)
  384. self.assertEqual(float_repr(float_round(kit_product_qty, precision_digits=precision.digits), precision_digits=precision.digits), '-384.00000')
  385. self.product_2.invalidate_recordset(['qty_available'])
  386. kit_product_qty, _ = (self.product_2 + self.product_3).mapped("qty_available") # With product_3 in the prefetch
  387. self.assertEqual(float_repr(float_round(kit_product_qty, precision_digits=precision.digits), precision_digits=precision.digits), '-384.00000')
  388. def test_13_bom_kit_qty_multi_uom(self):
  389. uom_dozens = self.env.ref('uom.product_uom_dozen')
  390. uom_unit = self.env.ref('uom.product_uom_unit')
  391. product_unit = self.env['product.product'].create({
  392. 'name': 'Test units',
  393. 'type': 'product',
  394. 'uom_id': uom_unit.id,
  395. })
  396. product_dozens = self.env['product.product'].create({
  397. 'name': 'Test dozens',
  398. 'type': 'product',
  399. 'uom_id': uom_dozens.id,
  400. })
  401. self.env['mrp.bom'].create({
  402. 'product_tmpl_id': product_unit.product_tmpl_id.id,
  403. 'product_uom_id': self.uom_unit.id,
  404. 'product_qty': 1.0,
  405. 'type': 'phantom',
  406. 'bom_line_ids': [
  407. (0, 0, {
  408. 'product_id': product_dozens.id,
  409. 'product_qty': 1,
  410. 'product_uom_id': uom_unit.id,
  411. })
  412. ]
  413. })
  414. location = self.env.ref('stock.stock_location_stock')
  415. self.env['stock.quant']._update_available_quantity(product_dozens, location, 1.0)
  416. self.assertEqual(product_unit.qty_available, 12.0)
  417. def test_20_bom_report(self):
  418. """ Simulate a crumble receipt with mrp and open the bom structure
  419. report and check that data insde are correct.
  420. """
  421. uom_kg = self.env.ref('uom.product_uom_kgm')
  422. uom_litre = self.env.ref('uom.product_uom_litre')
  423. crumble = self.env['product.product'].create({
  424. 'name': 'Crumble',
  425. 'type': 'product',
  426. 'uom_id': uom_kg.id,
  427. 'uom_po_id': uom_kg.id,
  428. })
  429. butter = self.env['product.product'].create({
  430. 'name': 'Butter',
  431. 'type': 'product',
  432. 'uom_id': uom_kg.id,
  433. 'uom_po_id': uom_kg.id,
  434. 'standard_price': 7.01
  435. })
  436. biscuit = self.env['product.product'].create({
  437. 'name': 'Biscuit',
  438. 'type': 'product',
  439. 'uom_id': uom_kg.id,
  440. 'uom_po_id': uom_kg.id,
  441. 'standard_price': 1.5
  442. })
  443. bom_form_crumble = Form(self.env['mrp.bom'])
  444. bom_form_crumble.product_tmpl_id = crumble.product_tmpl_id
  445. bom_form_crumble.product_qty = 11
  446. bom_form_crumble.product_uom_id = uom_kg
  447. bom_crumble = bom_form_crumble.save()
  448. workcenter = self.env['mrp.workcenter'].create({
  449. 'costs_hour': 10,
  450. 'name': 'Deserts Table'
  451. })
  452. # Required to display `operation_ids` in the form view
  453. self.env.user.groups_id += self.env.ref("mrp.group_mrp_routings")
  454. with Form(bom_crumble) as bom:
  455. with bom.bom_line_ids.new() as line:
  456. line.product_id = butter
  457. line.product_uom_id = uom_kg
  458. line.product_qty = 5
  459. with bom.bom_line_ids.new() as line:
  460. line.product_id = biscuit
  461. line.product_uom_id = uom_kg
  462. line.product_qty = 6
  463. with bom.operation_ids.new() as operation:
  464. operation.workcenter_id = workcenter
  465. operation.name = 'Prepare biscuits'
  466. operation.time_cycle_manual = 5
  467. operation.bom_id = bom_crumble # Can't handle by the testing env
  468. with bom.operation_ids.new() as operation:
  469. operation.workcenter_id = workcenter
  470. operation.name = 'Prepare butter'
  471. operation.time_cycle_manual = 3
  472. operation.bom_id = bom_crumble
  473. with bom.operation_ids.new() as operation:
  474. operation.workcenter_id = workcenter
  475. operation.name = 'Mix manually'
  476. operation.time_cycle_manual = 5
  477. operation.bom_id = bom_crumble
  478. # TEST BOM STRUCTURE VALUE WITH BOM QUANTITY
  479. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=11, searchVariant=False)
  480. # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes for 1 biscuits so 13 * 11 = 143 minutes
  481. self.assertEqual(report_values['lines']['operations_time'], 143.0, 'Operation time should be the same for 1 unit or for the batch')
  482. # Operation cost is the sum of operation line.
  483. self.assertEqual(float_compare(report_values['lines']['operations_cost'], 23.84, precision_digits=2), 0, '143 minute for 10$/hours -> 23.84')
  484. for component_line in report_values['lines']['components']:
  485. # standard price * bom line quantity * current quantity / bom finished product quantity
  486. if component_line['product'].id == butter.id:
  487. # 5 kg of butter at 7.01$ for 11kg of crumble -> 35.05$
  488. self.assertEqual(float_compare(component_line['bom_cost'], (7.01 * 5), precision_digits=2), 0)
  489. if component_line['product'].id == biscuit.id:
  490. # 6 kg of biscuits at 1.50$ for 11kg of crumble -> 9$
  491. self.assertEqual(float_compare(component_line['bom_cost'], (1.5 * 6), precision_digits=2), 0)
  492. # total price = 35.05 + 9 + operation_cost(23.84) = 67.89
  493. self.assertEqual(float_compare(report_values['lines']['bom_cost'], 67.89, precision_digits=2), 0, 'Product Bom Price is not correct')
  494. self.assertEqual(float_compare(report_values['lines']['bom_cost'] / 11.0, 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
  495. # TEST BOM STRUCTURE VALUE BY UNIT
  496. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=1, searchVariant=False)
  497. # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
  498. self.assertEqual(report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch')
  499. # Operation cost is the sum of operation line.
  500. operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round(3 / 60 * 10, precision_digits=2)
  501. self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16')
  502. for component_line in report_values['lines']['components']:
  503. # standard price * bom line quantity * current quantity / bom finished product quantity
  504. if component_line['product'].id == butter.id:
  505. # 5 kg of butter at 7.01$ for 11kg of crumble -> / 11 for price per unit (3.19)
  506. self.assertEqual(float_compare(component_line['bom_cost'], (7.01 * 5) * (1 / 11), precision_digits=2), 0)
  507. if component_line['product'].id == biscuit.id:
  508. # 6 kg of biscuits at 1.50$ for 11kg of crumble -> / 11 for price per unit (0.82)
  509. self.assertEqual(float_compare(component_line['bom_cost'], (1.5 * 6) * (1 / 11), precision_digits=2), 0)
  510. # total price = 3.19 + 0.82 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 6,17
  511. self.assertEqual(float_compare(report_values['lines']['bom_cost'], 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
  512. # TEST OPERATION COST WHEN PRODUCED QTY > BOM QUANTITY
  513. report_values_12 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=12, searchVariant=False)
  514. report_values_22 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=22, searchVariant=False)
  515. #Operation cost = 47.66 € = 256 (min) * 10€/h
  516. self.assertEqual(float_compare(report_values_22['lines']['operations_cost'], 47.66, precision_digits=2), 0, 'Operation cost is not correct')
  517. # Create a more complex BoM with a sub product
  518. cheese_cake = self.env['product.product'].create({
  519. 'name': 'Cheese Cake 300g',
  520. 'type': 'product',
  521. })
  522. cream = self.env['product.product'].create({
  523. 'name': 'cream',
  524. 'type': 'product',
  525. 'uom_id': uom_litre.id,
  526. 'uom_po_id': uom_litre.id,
  527. 'standard_price': 5.17,
  528. })
  529. bom_form_cheese_cake = Form(self.env['mrp.bom'])
  530. bom_form_cheese_cake.product_tmpl_id = cheese_cake.product_tmpl_id
  531. bom_form_cheese_cake.product_qty = 60
  532. bom_form_cheese_cake.product_uom_id = self.uom_unit
  533. bom_cheese_cake = bom_form_cheese_cake.save()
  534. workcenter_2 = self.env['mrp.workcenter'].create({
  535. 'name': 'cake mounting',
  536. 'costs_hour': 20,
  537. 'time_start': 10,
  538. 'time_stop': 15
  539. })
  540. self.env['mrp.workcenter.capacity'].create({
  541. 'product_id': cheese_cake.id,
  542. 'workcenter_id': workcenter_2.id,
  543. 'time_start': 2,
  544. 'time_stop': 1,
  545. })
  546. with Form(bom_cheese_cake) as bom:
  547. with bom.bom_line_ids.new() as line:
  548. line.product_id = cream
  549. line.product_uom_id = uom_litre
  550. line.product_qty = 3
  551. with bom.bom_line_ids.new() as line:
  552. line.product_id = crumble
  553. line.product_uom_id = uom_kg
  554. line.product_qty = 5.4
  555. with bom.operation_ids.new() as operation:
  556. operation.workcenter_id = workcenter
  557. operation.name = 'Mix cheese and crumble'
  558. operation.time_cycle_manual = 10
  559. operation.bom_id = bom_cheese_cake
  560. with bom.operation_ids.new() as operation:
  561. operation.workcenter_id = workcenter_2
  562. operation.name = 'Cake mounting'
  563. operation.time_cycle_manual = 5
  564. operation.bom_id = bom_cheese_cake
  565. # TEST CHEESE BOM STRUCTURE VALUE WITH BOM QUANTITY
  566. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_cheese_cake.id, searchQty=60, searchVariant=False)
  567. #Operation time = 15 min * 60 + time_start + time_stop + capacity_time_start + capacity_time_stop= 928
  568. self.assertEqual(report_values['lines']['operations_time'], 928.0, 'Operation time should be the same for 1 unit or for the batch')
  569. # Operation cost is the sum of operation line : (60 * 10)/60 * 10€ + (10 + 15 + 60 * 5)/60 * 20€ + (1 + 2)/60 * 20€ = 209,33€
  570. self.assertEqual(float_compare(report_values['lines']['operations_cost'], 209.33, precision_digits=2), 0)
  571. for component_line in report_values['lines']['components']:
  572. # standard price * bom line quantity * current quantity / bom finished product quantity
  573. if component_line['product'].id == cream.id:
  574. # 3 liter of cream at 5.17$ for 60 unit of cheese cake -> 15.51$
  575. self.assertEqual(float_compare(component_line['bom_cost'], (3 * 5.17), precision_digits=2), 0)
  576. if component_line['product'].id == crumble.id:
  577. # 5.4 kg of crumble at the cost of a batch.
  578. crumble_cost = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=5.4, searchVariant=False)['lines']['bom_cost']
  579. self.assertEqual(float_compare(component_line['bom_cost'], crumble_cost, precision_digits=2), 0)
  580. # total price = Cream (15.51€) + crumble_cost (34.63 €) + operation_cost(209,33) = 259.47€
  581. self.assertEqual(float_compare(report_values['lines']['bom_cost'], 259.47, precision_digits=2), 0, 'Product Bom Price is not correct')
  582. def test_bom_report_dozens(self):
  583. """ Simulate a drawer bom with dozens as bom units
  584. """
  585. uom_dozen = self.env.ref('uom.product_uom_dozen')
  586. uom_unit = self.env.ref('uom.product_uom_unit')
  587. drawer = self.env['product.product'].create({
  588. 'name': 'drawer',
  589. 'type': 'product',
  590. 'uom_id': uom_unit.id,
  591. 'uom_po_id': uom_unit.id,
  592. })
  593. screw = self.env['product.product'].create({
  594. 'name': 'screw',
  595. 'type': 'product',
  596. 'uom_id': uom_unit.id,
  597. 'uom_po_id': uom_unit.id,
  598. 'standard_price': 7.01
  599. })
  600. bom_form_drawer = Form(self.env['mrp.bom'])
  601. bom_form_drawer.product_tmpl_id = drawer.product_tmpl_id
  602. bom_form_drawer.product_qty = 11
  603. bom_form_drawer.product_uom_id = uom_dozen
  604. bom_drawer = bom_form_drawer.save()
  605. workcenter = self.env['mrp.workcenter'].create({
  606. 'costs_hour': 10,
  607. 'name': 'Deserts Table'
  608. })
  609. # Required to display `operation_ids` in the form view
  610. self.env.user.groups_id += self.env.ref("mrp.group_mrp_routings")
  611. with Form(bom_drawer) as bom:
  612. with bom.bom_line_ids.new() as line:
  613. line.product_id = screw
  614. line.product_uom_id = uom_unit
  615. line.product_qty = 5
  616. with bom.operation_ids.new() as operation:
  617. operation.workcenter_id = workcenter
  618. operation.name = 'Screw drawer'
  619. operation.time_cycle_manual = 5
  620. operation.bom_id = bom_drawer
  621. # TEST BOM STRUCTURE VALUE WITH BOM QUANTITY
  622. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_drawer.id, searchQty=11, searchVariant=False)
  623. # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
  624. self.assertEqual(report_values['lines']['operations_time'], 660.0, 'Operation time should be the same for 1 unit or for the batch')
  625. def test_21_bom_report_variant(self):
  626. """ Test a sub BoM process with multiple variants.
  627. BOM 1:
  628. product template = car
  629. quantity = 5 units
  630. - red paint 50l -> red car (product.product)
  631. - blue paint 50l -> blue car
  632. - red dashboard with gps -> red car with GPS
  633. - red dashboard w/h gps -> red w/h GPS
  634. - blue dashboard with gps -> blue car with GPS
  635. - blue dashboard w/h gps -> blue w/h GPS
  636. BOM 2:
  637. product_tmpl = dashboard
  638. quantity = 2
  639. - red paint 1l -> red dashboard (product.product)
  640. - blue paint 1l -> blue dashboard
  641. - gps -> dashboard with gps
  642. Check the Price for a Blue Car with GPS -> 910$:
  643. 10l of blue paint -> 200$
  644. 1 blue dashboard GPS -> 710$:
  645. - 0.5l of blue paint -> 10$
  646. - GPS -> 700$
  647. Check the price for a red car -> 10.5l of red paint -> 210$
  648. """
  649. # Create a product template car with attributes gps(yes, no), color(red, blue)
  650. self.car = self.env['product.template'].create({
  651. 'name': 'Car',
  652. })
  653. self.gps_attribute = self.env['product.attribute'].create({'name': 'GPS', 'sequence': 1})
  654. self.gps_yes = self.env['product.attribute.value'].create({
  655. 'name': 'Yes',
  656. 'attribute_id': self.gps_attribute.id,
  657. 'sequence': 1,
  658. })
  659. self.gps_no = self.env['product.attribute.value'].create({
  660. 'name': 'No',
  661. 'attribute_id': self.gps_attribute.id,
  662. 'sequence': 2,
  663. })
  664. self.car_gps_attribute_line = self.env['product.template.attribute.line'].create({
  665. 'product_tmpl_id': self.car.id,
  666. 'attribute_id': self.gps_attribute.id,
  667. 'value_ids': [(6, 0, [self.gps_yes.id, self.gps_no.id])],
  668. })
  669. self.car_gps_yes = self.car_gps_attribute_line.product_template_value_ids[0]
  670. self.car_gps_no = self.car_gps_attribute_line.product_template_value_ids[1]
  671. self.color_attribute = self.env['product.attribute'].create({'name': 'Color', 'sequence': 1})
  672. self.color_red = self.env['product.attribute.value'].create({
  673. 'name': 'Red',
  674. 'attribute_id': self.color_attribute.id,
  675. 'sequence': 1,
  676. })
  677. self.color_blue = self.env['product.attribute.value'].create({
  678. 'name': 'Blue',
  679. 'attribute_id': self.color_attribute.id,
  680. 'sequence': 2,
  681. })
  682. self.car_color_attribute_line = self.env['product.template.attribute.line'].create({
  683. 'product_tmpl_id': self.car.id,
  684. 'attribute_id': self.color_attribute.id,
  685. 'value_ids': [(6, 0, [self.color_red.id, self.color_blue.id])],
  686. })
  687. self.car_color_red = self.car_color_attribute_line.product_template_value_ids[0]
  688. self.car_color_blue = self.car_color_attribute_line.product_template_value_ids[1]
  689. # Blue and red paint
  690. uom_litre = self.env.ref('uom.product_uom_litre')
  691. self.paint = self.env['product.template'].create({
  692. 'name': 'Paint',
  693. 'uom_id': uom_litre.id,
  694. 'uom_po_id': uom_litre.id
  695. })
  696. self.paint_color_attribute_line = self.env['product.template.attribute.line'].create({
  697. 'product_tmpl_id': self.paint.id,
  698. 'attribute_id': self.color_attribute.id,
  699. 'value_ids': [(6, 0, [self.color_red.id, self.color_blue.id])],
  700. })
  701. self.paint_color_red = self.paint_color_attribute_line.product_template_value_ids[0]
  702. self.paint_color_blue = self.paint_color_attribute_line.product_template_value_ids[1]
  703. self.paint.product_variant_ids.write({'standard_price': 20})
  704. self.dashboard = self.env['product.template'].create({
  705. 'name': 'Dashboard',
  706. 'standard_price': 1000,
  707. })
  708. self.dashboard_gps_attribute_line = self.env['product.template.attribute.line'].create({
  709. 'product_tmpl_id': self.dashboard.id,
  710. 'attribute_id': self.gps_attribute.id,
  711. 'value_ids': [(6, 0, [self.gps_yes.id, self.gps_no.id])],
  712. })
  713. self.dashboard_gps_yes = self.dashboard_gps_attribute_line.product_template_value_ids[0]
  714. self.dashboard_gps_no = self.dashboard_gps_attribute_line.product_template_value_ids[1]
  715. self.dashboard_color_attribute_line = self.env['product.template.attribute.line'].create({
  716. 'product_tmpl_id': self.dashboard.id,
  717. 'attribute_id': self.color_attribute.id,
  718. 'value_ids': [(6, 0, [self.color_red.id, self.color_blue.id])],
  719. })
  720. self.dashboard_color_red = self.dashboard_color_attribute_line.product_template_value_ids[0]
  721. self.dashboard_color_blue = self.dashboard_color_attribute_line.product_template_value_ids[1]
  722. self.gps = self.env['product.product'].create({
  723. 'name': 'GPS',
  724. 'standard_price': 700,
  725. })
  726. bom_form_car = Form(self.env['mrp.bom'])
  727. bom_form_car.product_tmpl_id = self.car
  728. bom_form_car.product_qty = 5
  729. with bom_form_car.bom_line_ids.new() as line:
  730. line.product_id = self.paint._get_variant_for_combination(self.paint_color_red)
  731. line.product_uom_id = uom_litre
  732. line.product_qty = 50
  733. line.bom_product_template_attribute_value_ids.add(self.car_color_red)
  734. with bom_form_car.bom_line_ids.new() as line:
  735. line.product_id = self.paint._get_variant_for_combination(self.paint_color_blue)
  736. line.product_uom_id = uom_litre
  737. line.product_qty = 50
  738. line.bom_product_template_attribute_value_ids.add(self.car_color_blue)
  739. with bom_form_car.bom_line_ids.new() as line:
  740. line.product_id = self.dashboard._get_variant_for_combination(self.dashboard_gps_yes + self.dashboard_color_red)
  741. line.product_qty = 5
  742. line.bom_product_template_attribute_value_ids.add(self.car_gps_yes)
  743. line.bom_product_template_attribute_value_ids.add(self.car_color_red)
  744. with bom_form_car.bom_line_ids.new() as line:
  745. line.product_id = self.dashboard._get_variant_for_combination(self.dashboard_gps_yes + self.dashboard_color_blue)
  746. line.product_qty = 5
  747. line.bom_product_template_attribute_value_ids.add(self.car_gps_yes)
  748. line.bom_product_template_attribute_value_ids.add(self.car_color_blue)
  749. with bom_form_car.bom_line_ids.new() as line:
  750. line.product_id = self.dashboard._get_variant_for_combination(self.dashboard_gps_no + self.dashboard_color_red)
  751. line.product_qty = 5
  752. line.bom_product_template_attribute_value_ids.add(self.car_gps_no)
  753. line.bom_product_template_attribute_value_ids.add(self.car_color_red)
  754. with bom_form_car.bom_line_ids.new() as line:
  755. line.product_id = self.dashboard._get_variant_for_combination(self.dashboard_gps_no + self.dashboard_color_blue)
  756. line.product_qty = 5
  757. line.bom_product_template_attribute_value_ids.add(self.car_gps_no)
  758. line.bom_product_template_attribute_value_ids.add(self.car_color_blue)
  759. bom_car = bom_form_car.save()
  760. bom_dashboard = Form(self.env['mrp.bom'])
  761. bom_dashboard.product_tmpl_id = self.dashboard
  762. bom_dashboard.product_qty = 2
  763. with bom_dashboard.bom_line_ids.new() as line:
  764. line.product_id = self.paint._get_variant_for_combination(self.paint_color_red)
  765. line.product_uom_id = uom_litre
  766. line.product_qty = 1
  767. line.bom_product_template_attribute_value_ids.add(self.dashboard_color_red)
  768. with bom_dashboard.bom_line_ids.new() as line:
  769. line.product_id = self.paint._get_variant_for_combination(self.paint_color_blue)
  770. line.product_uom_id = uom_litre
  771. line.product_qty = 1
  772. line.bom_product_template_attribute_value_ids.add(self.dashboard_color_blue)
  773. with bom_dashboard.bom_line_ids.new() as line:
  774. line.product_id = self.gps
  775. line.product_qty = 2
  776. line.bom_product_template_attribute_value_ids.add(self.dashboard_gps_yes)
  777. bom_dashboard = bom_dashboard.save()
  778. blue_car_with_gps = self.car._get_variant_for_combination(self.car_color_blue + self.car_gps_yes)
  779. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_car.id, searchQty=1, searchVariant=blue_car_with_gps.id)
  780. # Two lines. blue dashboard with gps and blue paint.
  781. self.assertEqual(len(report_values['lines']['components']), 2)
  782. # 10l of blue paint
  783. blue_paint = self.paint._get_variant_for_combination(self.paint_color_blue)
  784. self.assertEqual(blue_paint.id, report_values['lines']['components'][0]['product'].id)
  785. self.assertEqual(report_values['lines']['components'][0]['quantity'], 10)
  786. # 1 blue dashboard with GPS
  787. blue_dashboard_gps = self.dashboard._get_variant_for_combination(self.dashboard_color_blue + self.dashboard_gps_yes)
  788. self.assertEqual(blue_dashboard_gps.id, report_values['lines']['components'][1]['product'].id)
  789. self.assertEqual(report_values['lines']['components'][1]['quantity'], 1)
  790. report_values_dashboad = report_values['lines']['components'][1]
  791. self.assertEqual(len(report_values_dashboad['components']), 2)
  792. self.assertEqual(blue_paint.id, report_values_dashboad['components'][0]['product'].id)
  793. self.assertEqual(self.gps.id, report_values_dashboad['components'][1]['product'].id)
  794. # 0.5l of paint at price of 20$/litre -> 10$
  795. self.assertEqual(report_values_dashboad['components'][0]['bom_cost'], 10)
  796. # GPS 700$
  797. self.assertEqual(report_values_dashboad['components'][1]['bom_cost'], 700)
  798. # Dashboard blue with GPS should have a BoM cost of 710$
  799. self.assertEqual(report_values['lines']['components'][1]['bom_cost'], 710)
  800. # 10l of paint at price of 20$/litre -> 200$
  801. self.assertEqual(report_values['lines']['components'][0]['bom_cost'], 200)
  802. # Total cost of blue car with GPS: 10 + 700 + 200 = 910
  803. self.assertEqual(report_values['lines']['bom_cost'], 910)
  804. red_car_without_gps = self.car._get_variant_for_combination(self.car_color_red + self.car_gps_no)
  805. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_car.id, searchQty=1, searchVariant=red_car_without_gps.id)
  806. # Same math than before but without GPS
  807. self.assertEqual(report_values['lines']['bom_cost'], 210)
  808. def test_22_bom_report_recursive_bom(self):
  809. """ Test report with recursive BoM and different quantities.
  810. BoM 1:
  811. product = Finished (units)
  812. quantity = 100 units
  813. - Semi-Finished 5 kg
  814. BoM 2:
  815. product = Semi-Finished (kg)
  816. quantity = 11 kg
  817. - Assembly 2 dozens
  818. BoM 3:
  819. product = Assembly (dozens)
  820. quantity = 5 dozens
  821. - Raw Material 4 litres (product.product 5$/litre)
  822. Check the Price for 80 units of Finished -> 2.92$:
  823. """
  824. # Create a products templates
  825. uom_unit = self.env.ref('uom.product_uom_unit')
  826. uom_kg = self.env.ref('uom.product_uom_kgm')
  827. uom_dozen = self.env.ref('uom.product_uom_dozen')
  828. uom_litre = self.env.ref('uom.product_uom_litre')
  829. finished = self.env['product.product'].create({
  830. 'name': 'Finished',
  831. 'type': 'product',
  832. 'uom_id': uom_unit.id,
  833. 'uom_po_id': uom_unit.id,
  834. })
  835. semi_finished = self.env['product.product'].create({
  836. 'name': 'Semi-Finished',
  837. 'type': 'product',
  838. 'uom_id': uom_kg.id,
  839. 'uom_po_id': uom_kg.id,
  840. })
  841. assembly = self.env['product.product'].create({
  842. 'name': 'Assembly',
  843. 'type': 'product',
  844. 'uom_id': uom_dozen.id,
  845. 'uom_po_id': uom_dozen.id,
  846. })
  847. raw_material = self.env['product.product'].create({
  848. 'name': 'Raw Material',
  849. 'type': 'product',
  850. 'uom_id': uom_litre.id,
  851. 'uom_po_id': uom_litre.id,
  852. 'standard_price': 5,
  853. })
  854. #Create bom
  855. bom_finished = Form(self.env['mrp.bom'])
  856. bom_finished.product_tmpl_id = finished.product_tmpl_id
  857. bom_finished.product_qty = 100
  858. with bom_finished.bom_line_ids.new() as line:
  859. line.product_id = semi_finished
  860. line.product_uom_id = uom_kg
  861. line.product_qty = 5
  862. bom_finished = bom_finished.save()
  863. bom_semi_finished = Form(self.env['mrp.bom'])
  864. bom_semi_finished.product_tmpl_id = semi_finished.product_tmpl_id
  865. bom_semi_finished.product_qty = 11
  866. with bom_semi_finished.bom_line_ids.new() as line:
  867. line.product_id = assembly
  868. line.product_uom_id = uom_dozen
  869. line.product_qty = 2
  870. bom_semi_finished = bom_semi_finished.save()
  871. bom_assembly = Form(self.env['mrp.bom'])
  872. bom_assembly.product_tmpl_id = assembly.product_tmpl_id
  873. bom_assembly.product_qty = 5
  874. with bom_assembly.bom_line_ids.new() as line:
  875. line.product_id = raw_material
  876. line.product_uom_id = uom_litre
  877. line.product_qty = 4
  878. bom_assembly = bom_assembly.save()
  879. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_finished.id, searchQty=80)
  880. self.assertAlmostEqual(report_values['lines']['bom_cost'], 2.92)
  881. def test_bom_report_capacity_with_quantity_of_0(self):
  882. uom_unit = self.env.ref('uom.product_uom_unit')
  883. location = self.env.ref('stock.stock_location_stock')
  884. target = self.env['product.product'].create({
  885. 'name': 'Target',
  886. 'type': 'product',
  887. })
  888. product_one = self.env['product.product'].create({
  889. 'name': 'Component one',
  890. 'type': 'product',
  891. })
  892. self.env['stock.quant']._update_available_quantity(product_one, location, 3.0)
  893. product_two = self.env['product.product'].create({
  894. 'name': 'Component two',
  895. 'type': 'product',
  896. })
  897. self.env['stock.quant']._update_available_quantity(product_two, location, 4.0)
  898. bom = self.env['mrp.bom'].create({
  899. 'product_tmpl_id': target.product_tmpl_id.id,
  900. 'product_uom_id': self.uom_unit.id,
  901. 'product_qty': 1.0,
  902. 'type': 'phantom',
  903. 'bom_line_ids': [
  904. Command.create({
  905. 'product_id': product_one.id,
  906. 'product_qty': 0,
  907. 'product_uom_id': uom_unit.id,
  908. }),
  909. Command.create({
  910. 'product_id': product_two.id,
  911. 'product_qty': 1,
  912. 'product_uom_id': uom_unit.id,
  913. })
  914. ]
  915. })
  916. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom.id)
  917. # The first product shouldn't affect the producible quantity because the target needs none of it
  918. # So with 4 of the second product available, we can produce 4 items
  919. self.assertEqual(report_values["lines"]["producible_qty"], 4)
  920. def test_bom_report_capacity_with_duplicate_components(self):
  921. location = self.env.ref('stock.stock_location_stock')
  922. self.env['stock.quant']._update_available_quantity(self.product_2, location, 2.0)
  923. bom = self.env['mrp.bom'].create({
  924. 'product_tmpl_id': self.product_3.product_tmpl_id.id,
  925. 'product_qty': 1,
  926. 'bom_line_ids': [
  927. Command.create({
  928. 'product_id': self.product_2.id,
  929. 'product_qty': 2,
  930. }),
  931. Command.create({
  932. 'product_id': self.product_2.id,
  933. 'product_qty': 2,
  934. })
  935. ]
  936. })
  937. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom.id)
  938. # Total quantity of components is 4, so shouldn't be able to produce a single one.
  939. self.assertEqual(report_values['lines']['producible_qty'], 0)
  940. def test_bom_report_same_component(self):
  941. """ Test report bom structure with duplicated components.
  942. """
  943. location = self.env.ref('stock.stock_location_stock')
  944. uom_unit = self.env.ref('uom.product_uom_unit')
  945. final_product_tmpl = self.env['product.template'].create({'name': 'Final Product', 'type': 'product'})
  946. component_product = self.env['product.product'].create({'name': 'Compo 1', 'type': 'product'})
  947. self.env['stock.quant']._update_available_quantity(component_product, location, 3.0)
  948. bom = self.env['mrp.bom'].create({
  949. 'product_tmpl_id': final_product_tmpl.id,
  950. 'product_uom_id': self.uom_unit.id,
  951. 'product_qty': 1.0,
  952. 'type': 'normal',
  953. 'bom_line_ids': [
  954. Command.create({
  955. 'product_id': component_product.id,
  956. 'product_qty': 3,
  957. 'product_uom_id': uom_unit.id,
  958. }),
  959. Command.create({
  960. 'product_id': component_product.id,
  961. 'product_qty': 3,
  962. 'product_uom_id': uom_unit.id,
  963. })
  964. ]
  965. })
  966. report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom.id)
  967. line1_values = report_values['lines']['components'][0]
  968. line2_values = report_values['lines']['components'][1]
  969. self.assertEqual(line1_values['availability_state'], 'available', 'The first component should be available.')
  970. self.assertEqual(line2_values['availability_state'], 'unavailable', 'The second component should be marked as unavailable')
  971. def test_validate_no_bom_line_with_same_product(self):
  972. """
  973. Cannot set a BOM line on a BOM with the same product as the BOM itself
  974. """
  975. uom_unit = self.env.ref('uom.product_uom_unit')
  976. finished = self.env['product.product'].create({
  977. 'name': 'Finished',
  978. 'type': 'product',
  979. 'uom_id': uom_unit.id,
  980. 'uom_po_id': uom_unit.id,
  981. })
  982. bom_finished = Form(self.env['mrp.bom'])
  983. bom_finished.product_tmpl_id = finished.product_tmpl_id
  984. bom_finished.product_qty = 100
  985. with bom_finished.bom_line_ids.new() as line:
  986. line.product_id = finished
  987. line.product_uom_id = uom_unit
  988. line.product_qty = 5
  989. with self.assertRaises(exceptions.ValidationError), self.cr.savepoint():
  990. bom_finished = bom_finished.save()
  991. def test_validate_no_bom_line_with_same_product_variant(self):
  992. """
  993. Cannot set a BOM line on a BOM with the same product variant as the BOM itself
  994. """
  995. uom_unit = self.env.ref('uom.product_uom_unit')
  996. bom_finished = Form(self.env['mrp.bom'])
  997. bom_finished.product_tmpl_id = self.product_7_template
  998. bom_finished.product_id = self.product_7_3
  999. bom_finished.product_qty = 100
  1000. with bom_finished.bom_line_ids.new() as line:
  1001. line.product_id = self.product_7_3
  1002. line.product_uom_id = uom_unit
  1003. line.product_qty = 5
  1004. with self.assertRaises(exceptions.ValidationError), self.cr.savepoint():
  1005. bom_finished = bom_finished.save()
  1006. def test_validate_bom_line_with_different_product_variant(self):
  1007. """
  1008. Can set a BOM line on a BOM with a different product variant as the BOM itself (same product)
  1009. Usecase for example A black T-shirt made from a white T-shirt and
  1010. black color.
  1011. """
  1012. uom_unit = self.env.ref('uom.product_uom_unit')
  1013. bom_finished = Form(self.env['mrp.bom'])
  1014. bom_finished.product_tmpl_id = self.product_7_template
  1015. bom_finished.product_id = self.product_7_3
  1016. bom_finished.product_qty = 100
  1017. with bom_finished.bom_line_ids.new() as line:
  1018. line.product_id = self.product_7_2
  1019. line.product_uom_id = uom_unit
  1020. line.product_qty = 5
  1021. bom_finished = bom_finished.save()
  1022. def test_validate_bom_line_with_variant_of_bom_product(self):
  1023. """
  1024. Can set a BOM line on a BOM with a product variant when the BOM has no variant selected
  1025. """
  1026. uom_unit = self.env.ref('uom.product_uom_unit')
  1027. bom_finished = Form(self.env['mrp.bom'])
  1028. bom_finished.product_tmpl_id = self.product_6.product_tmpl_id
  1029. # no product_id
  1030. bom_finished.product_qty = 100
  1031. with bom_finished.bom_line_ids.new() as line:
  1032. line.product_id = self.product_7_2
  1033. line.product_uom_id = uom_unit
  1034. line.product_qty = 5
  1035. bom_finished = bom_finished.save()
  1036. def test_replenishment(self):
  1037. """ Tests the auto generation of manual orderpoints.
  1038. The multiple quantity of the orderpoint should be the
  1039. quantity of the BoM in the UoM of the product.
  1040. """
  1041. uom_kg = self.env.ref('uom.product_uom_kgm')
  1042. uom_gram = self.env.ref('uom.product_uom_gram')
  1043. product_gram = self.env['product.product'].create({
  1044. 'name': 'Product sold in grams',
  1045. 'type': 'product',
  1046. 'uom_id': uom_gram.id,
  1047. 'uom_po_id': uom_gram.id,
  1048. })
  1049. # We create a BoM that manufactures 2kg of product
  1050. self.env['mrp.bom'].create({
  1051. 'product_id': product_gram.id,
  1052. 'product_tmpl_id': product_gram.product_tmpl_id.id,
  1053. 'product_uom_id': uom_kg.id,
  1054. 'product_qty': 2.0,
  1055. 'type': 'normal',
  1056. })
  1057. # We create a delivery order of 2300 grams
  1058. picking_form = Form(self.env['stock.picking'])
  1059. picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
  1060. with picking_form.move_ids_without_package.new() as move:
  1061. move.product_id = product_gram
  1062. move.product_uom_qty = 2300.0
  1063. customer_picking = picking_form.save()
  1064. customer_picking.action_confirm()
  1065. # We check the created orderpoint
  1066. self.env.flush_all()
  1067. self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
  1068. orderpoint = self.env['stock.warehouse.orderpoint'].search([('product_id', '=', product_gram.id)])
  1069. manufacturing_route_id = self.ref('mrp.route_warehouse0_manufacture')
  1070. self.assertEqual(orderpoint.route_id.id, manufacturing_route_id)
  1071. self.assertEqual(orderpoint.qty_multiple, 2000.0)
  1072. self.assertEqual(orderpoint.qty_to_order, 4000.0)
  1073. def test_bom_kit_with_sub_kit(self):
  1074. p1, p2, p3, p4 = self.make_prods(4)
  1075. self.make_bom(p1, p2, p3)
  1076. self.make_bom(p2, p3, p4)
  1077. loc = self.env.ref("stock.stock_location_stock")
  1078. self.env["stock.quant"]._update_available_quantity(p3, loc, 10)
  1079. self.env["stock.quant"]._update_available_quantity(p4, loc, 10)
  1080. self.assertEqual(p1.qty_available, 5.0)
  1081. self.assertEqual(p2.qty_available, 10.0)
  1082. self.assertEqual(p3.qty_available, 10.0)
  1083. def test_operation_blocked_by_another_operation(self):
  1084. """ Test that an operation is not blocked by another operation if the variant is different
  1085. Product with 4 variants (red big, red medium, blue big, blue medium)
  1086. BoM:
  1087. - OP1 (apply on Red)
  1088. - OP2 (blocked by OP1)
  1089. Create a MO for Red big, OP1 is started, OP2 should be blocked
  1090. Create a Mo for Blue big, OP1 is not applied, OP2 should not be blocked
  1091. """
  1092. ProductAttribute = self.env['product.attribute']
  1093. ProductAttributeValue = self.env['product.attribute.value']
  1094. # Product Attribute
  1095. att_color = ProductAttribute.create({'name': 'Color', 'sequence': 1})
  1096. att_size = ProductAttribute.create({'name': 'size', 'sequence': 2})
  1097. # Product Attribute color Value
  1098. att_color_red = ProductAttributeValue.create({'name': 'red', 'attribute_id': att_color.id, 'sequence': 1})
  1099. att_color_blue = ProductAttributeValue.create({'name': 'blue', 'attribute_id': att_color.id, 'sequence': 2})
  1100. # Product Attribute size Value
  1101. att_size_big = ProductAttributeValue.create({'name': 'big', 'attribute_id': att_size.id, 'sequence': 1})
  1102. att_size_medium = ProductAttributeValue.create({'name': 'medium', 'attribute_id': att_size.id, 'sequence': 2})
  1103. # Create create a product with 4 variants
  1104. product_template = self.env['product.template'].create({
  1105. 'name': 'Sofa',
  1106. 'attribute_line_ids': [
  1107. (0, 0, {
  1108. 'attribute_id': att_color.id,
  1109. 'value_ids': [(6, 0, [att_color_red.id, att_color_blue.id])]
  1110. }),
  1111. (0, 0, {
  1112. 'attribute_id': att_size.id,
  1113. 'value_ids': [(6, 0, [att_size_big.id, att_size_medium.id])]
  1114. })
  1115. ]
  1116. })
  1117. bom = self.env['mrp.bom'].create({
  1118. 'product_tmpl_id': product_template.id,
  1119. 'product_uom_id': self.uom_unit.id,
  1120. 'product_qty': 1.0,
  1121. 'allow_operation_dependencies': True,
  1122. 'operation_ids': [(0, 0, {'name': 'op1', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 1.0, 'bom_product_template_attribute_value_ids': [(4, att_color_blue.pav_attribute_line_ids.product_template_value_ids[0].id)]}),
  1123. (0, 0, {'name': 'op2', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 1.0})],
  1124. })
  1125. # Make 1st workorder depend on 2nd
  1126. bom.operation_ids[1].blocked_by_operation_ids = [Command.link(bom.operation_ids[0].id)]
  1127. # Make MO for red big
  1128. mo_form = Form(self.env['mrp.production'])
  1129. mo_form.product_id = product_template.product_variant_ids[0]
  1130. mo_form.bom_id = bom
  1131. mo_form.product_qty = 1.0
  1132. mo = mo_form.save()
  1133. mo.action_confirm()
  1134. self.assertEqual(mo.state, 'confirmed')
  1135. # Make MO for blue big
  1136. mo_form = Form(self.env['mrp.production'])
  1137. mo_form.product_id = product_template.product_variant_ids[2]
  1138. mo_form.bom_id = bom
  1139. mo_form.product_qty = 1.0
  1140. mo = mo_form.save()
  1141. mo.action_confirm()
  1142. self.assertEqual(mo.state, 'confirmed')
  1143. mo.qty_producing = 1.0
  1144. mo.action_assign()
  1145. mo.button_plan()
  1146. mo.button_mark_done()
  1147. self.assertEqual(mo.state, 'done')
  1148. def test_cycle_on_line_creation(self):
  1149. bom_1_finished_product = self.bom_1.product_id
  1150. bom_2_finished_product = self.bom_2.product_id
  1151. with self.assertRaises(exceptions.ValidationError):
  1152. # finished product is one of the components:
  1153. self.bom_1.bom_line_ids = [(0, 0, {'product_id': bom_1_finished_product.id, 'product_qty': 1.0},)]
  1154. with self.assertRaises(exceptions.ValidationError):
  1155. # cycle:
  1156. self.bom_1.bom_line_ids = [(0, 0, {'product_id': bom_2_finished_product.id, 'product_qty': 1.0},)]
  1157. def test_cycle_on_line_update(self):
  1158. lines = self.bom_1.bom_line_ids
  1159. bom_2_finished_product = self.bom_2.product_id
  1160. with self.assertRaises(exceptions.ValidationError):
  1161. self.bom_1.bom_line_ids = [(1, lines[0].id, {'product_id': bom_2_finished_product.id})]
  1162. def test_cycle_on_bom_unarchive(self):
  1163. finished_product = self.bom_1.product_id
  1164. component = self.bom_1.bom_line_ids.product_id[0]
  1165. self.bom_1.active = False
  1166. self.env['mrp.bom'].create({
  1167. 'product_id': component.id,
  1168. 'product_tmpl_id': component.product_tmpl_id.id,
  1169. 'product_uom_id': component.uom_id.id,
  1170. 'product_qty': 1.0,
  1171. 'type': 'normal',
  1172. 'bom_line_ids': [
  1173. (0, 0, {'product_id': finished_product.id, 'product_qty': 1.0}),
  1174. ],
  1175. })
  1176. with self.assertRaises(exceptions.ValidationError):
  1177. self.bom_1.active = True
  1178. def test_cycle_on_bom_creation(self):
  1179. finished_product = self.bom_4.product_id
  1180. component = self.bom_4.bom_line_ids.product_id
  1181. with self.assertRaises(exceptions.ValidationError):
  1182. self.env['mrp.bom'].create({
  1183. 'product_id': component.id,
  1184. 'product_tmpl_id': component.product_tmpl_id.id,
  1185. 'product_uom_id': component.uom_id.id,
  1186. 'product_qty': 1.0,
  1187. 'type': 'normal',
  1188. 'bom_line_ids': [
  1189. (0, 0, {'product_id': finished_product.id, 'product_qty': 1.0}),
  1190. ],
  1191. })
  1192. def test_indirect_cycle_on_bom_creation(self):
  1193. """
  1194. Three BoMs:
  1195. A -> D
  1196. A -> B
  1197. B -> C
  1198. Create a new BoM C -> A. At first glance, this new BoM is ok because it
  1199. does nat have a cycle (C -> A -> D). But there is an indirect cycle:
  1200. A -> B -> C -> A
  1201. Hence this new BoM should raise an error.
  1202. """
  1203. product_A, product_B, product_C, product_D = self.env['product.product'].create([{
  1204. 'name': '%s' % i
  1205. } for i in range(4)])
  1206. self.env['mrp.bom'].create([{
  1207. 'product_id': finished.id,
  1208. 'product_tmpl_id': finished.product_tmpl_id.id,
  1209. 'product_uom_id': finished.uom_id.id,
  1210. 'product_qty': 1.0,
  1211. 'type': 'normal',
  1212. 'bom_line_ids': [
  1213. (0, 0, {'product_id': compo.id, 'product_qty': 1.0}),
  1214. ],
  1215. } for finished, compo, in [
  1216. (product_A, product_D),
  1217. (product_A, product_B),
  1218. (product_B, product_C),
  1219. ]])
  1220. with self.assertRaises(exceptions.ValidationError):
  1221. self.env['mrp.bom'].create({
  1222. 'product_id': product_C.id,
  1223. 'product_tmpl_id': product_C.product_tmpl_id.id,
  1224. 'product_uom_id': product_C.uom_id.id,
  1225. 'product_qty': 1.0,
  1226. 'type': 'normal',
  1227. 'bom_line_ids': [
  1228. (0, 0, {'product_id': product_A.id, 'product_qty': 1.0}),
  1229. ],
  1230. })
  1231. def test_cycle_on_bom_sequencing(self):
  1232. """
  1233. Six BoMs:
  1234. A -> D
  1235. A -> B
  1236. C -> D
  1237. C -> E
  1238. B -> C
  1239. C -> A
  1240. First new sequence: we reverse C->D and C->E, this is ok as it does not
  1241. create any cycle. Change the sequence again and set C->A before C->D: it
  1242. should raise an error because C->A becomes the main BoM of C, and this
  1243. will create a cycle: A -> B -> C -> A
  1244. """
  1245. product_A, product_B, product_C, product_D, product_E = self.env['product.product'].create([{
  1246. 'name': '%s' % i
  1247. } for i in range(5)])
  1248. boms = self.env['mrp.bom'].create([{
  1249. 'product_id': finished.id,
  1250. 'product_tmpl_id': finished.product_tmpl_id.id,
  1251. 'product_uom_id': finished.uom_id.id,
  1252. 'product_qty': 1.0,
  1253. 'type': 'normal',
  1254. 'bom_line_ids': [
  1255. (0, 0, {'product_id': compo.id, 'product_qty': 1.0}),
  1256. ],
  1257. } for finished, compo, in [
  1258. (product_A, product_D),
  1259. (product_A, product_B),
  1260. (product_C, product_D),
  1261. (product_C, product_E),
  1262. (product_B, product_C),
  1263. (product_C, product_A),
  1264. ]])
  1265. # simulate resequence from UI (reverse C->D and C->E)
  1266. # (see odoo/addons/web/controllers/main.py:1352)
  1267. boms.invalidate_recordset()
  1268. for i, record in enumerate(boms[0] | boms[1] | boms[3] | boms[2] | boms[4] | boms[5]):
  1269. record.write({'sequence': i})
  1270. # simulate a second resequencing (set C->A before C->D)
  1271. with self.assertRaises(exceptions.ValidationError):
  1272. for i, record in enumerate(boms[0] | boms[1] | boms[5] | boms[3] | boms[2] | boms[4]):
  1273. record.write({'sequence': i})
  1274. def test_cycle_on_legit_apply_variants(self):
  1275. """ Should not raise anything """
  1276. self.env['mrp.bom'].create({
  1277. 'product_tmpl_id': self.product_7_template.id,
  1278. 'product_uom_id': self.product_7_template.uom_id.id,
  1279. 'product_qty': 1.0,
  1280. 'type': 'normal',
  1281. 'bom_line_ids': [
  1282. (0, 0, {
  1283. 'product_id': self.product_1.id,
  1284. 'product_qty': 1.0
  1285. }),
  1286. (0, 0, {
  1287. 'product_id': self.product_2.id,
  1288. 'product_qty': 1.0,
  1289. 'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v2.id)]
  1290. }),
  1291. ],
  1292. })
  1293. self.env['mrp.bom'].create({
  1294. 'product_tmpl_id': self.product_2.product_tmpl_id.id,
  1295. 'product_uom_id': self.product_2.uom_id.id,
  1296. 'product_qty': 1.0,
  1297. 'type': 'normal',
  1298. 'bom_line_ids': [
  1299. (0, 0, {'product_id': self.product_7_1.id, 'product_qty': 1.0}),
  1300. ],
  1301. })
  1302. def test_component_when_bom_change(self):
  1303. """
  1304. Checks that the component of the previous BoM is removed when another BoM is set on the MO:
  1305. - Create a product with 2 BoMs:
  1306. BoM 1: compoennt 1
  1307. BoM 2: component 2
  1308. - Create a MO for the product with BoM 1
  1309. - check that the component 1 is set
  1310. - change the BoM on the MO to BoM 2
  1311. - come back to BoM 1
  1312. - check that the component 2 is removed and replaced by the component 1
  1313. """
  1314. # Create BoM 1 with component 1
  1315. bom_1 = self.env['mrp.bom'].create({
  1316. 'product_tmpl_id': self.product_7_template.id,
  1317. 'product_uom_id': self.product_7_template.uom_id.id,
  1318. 'product_qty': 1.0,
  1319. 'type': 'normal',
  1320. 'bom_line_ids': [Command.create({
  1321. 'product_id': self.product_1.id,
  1322. 'product_qty': 1.0,
  1323. })],
  1324. })
  1325. # Create BoM 2 with component 2
  1326. bom_2 = self.env['mrp.bom'].create({
  1327. 'product_tmpl_id': self.product_7_template.id,
  1328. 'product_uom_id': self.product_7_template.uom_id.id,
  1329. 'product_qty': 1.0,
  1330. 'type': 'normal',
  1331. 'bom_line_ids': [Command.create({
  1332. 'product_id': self.product_2.id,
  1333. 'product_qty': 1.0,
  1334. })],
  1335. })
  1336. # Create a MO with BoM 1
  1337. mo = self.env['mrp.production'].create({
  1338. 'product_qty': 1.0,
  1339. 'bom_id': bom_1.id,
  1340. })
  1341. # Check that component 1 is set
  1342. self.assertEqual(mo.move_raw_ids.product_id, self.product_1)
  1343. # Change BoM in the MO to BoM 2
  1344. mo_form = Form(mo)
  1345. mo_form.bom_id = bom_2
  1346. # Check that component 2 is set
  1347. self.assertEqual(mo_form.move_raw_ids._records[0]['product_id'], self.product_2.id)
  1348. self.assertEqual(len(mo_form.move_raw_ids._records), 1)
  1349. # Revert back to BoM 1
  1350. mo_form.bom_id = bom_1
  1351. # Check that component 1 is set again and component 2 is removed
  1352. self.assertEqual(mo_form.move_raw_ids._records[0]['product_id'], self.product_1.id)
  1353. self.assertEqual(len(mo_form.move_raw_ids._records), 1)