1# -*- coding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4from odoo.tests import Form
5from odoo.addons.mrp.tests.common import TestMrpCommon
6
7
8class TestMultistepManufacturingWarehouse(TestMrpCommon):
9
10    def setUp(self):
11        super(TestMultistepManufacturingWarehouse, self).setUp()
12        # Create warehouse
13        self.customer_location = self.env['ir.model.data'].xmlid_to_res_id('stock.stock_location_customers')
14        warehouse_form = Form(self.env['stock.warehouse'])
15        warehouse_form.name = 'Test Warehouse'
16        warehouse_form.code = 'TWH'
17        self.warehouse = warehouse_form.save()
18
19        self.uom_unit = self.env.ref('uom.product_uom_unit')
20
21        # Create manufactured product
22        product_form = Form(self.env['product.product'])
23        product_form.name = 'Stick'
24        product_form.uom_id = self.uom_unit
25        product_form.uom_po_id = self.uom_unit
26        product_form.type = 'product'
27        product_form.route_ids.clear()
28        product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
29        product_form.route_ids.add(self.warehouse.mto_pull_id.route_id)
30        self.finished_product = product_form.save()
31
32        # Create raw product for manufactured product
33        product_form = Form(self.env['product.product'])
34        product_form.name = 'Raw Stick'
35        product_form.type = 'product'
36        product_form.uom_id = self.uom_unit
37        product_form.uom_po_id = self.uom_unit
38        self.raw_product = product_form.save()
39
40        # Create bom for manufactured product
41        bom_product_form = Form(self.env['mrp.bom'])
42        bom_product_form.product_id = self.finished_product
43        bom_product_form.product_tmpl_id = self.finished_product.product_tmpl_id
44        bom_product_form.product_qty = 1.0
45        bom_product_form.type = 'normal'
46        with bom_product_form.bom_line_ids.new() as bom_line:
47            bom_line.product_id = self.raw_product
48            bom_line.product_qty = 2.0
49
50        self.bom = bom_product_form.save()
51
52    def _check_location_and_routes(self):
53        # Check manufacturing pull rule.
54        self.assertTrue(self.warehouse.manufacture_pull_id)
55        self.assertTrue(self.warehouse.manufacture_pull_id.active, self.warehouse.manufacture_to_resupply)
56        self.assertTrue(self.warehouse.manufacture_pull_id.route_id)
57        # Check new routes created or not.
58        self.assertTrue(self.warehouse.pbm_route_id)
59        # Check location should be created and linked to warehouse.
60        self.assertTrue(self.warehouse.pbm_loc_id)
61        self.assertEqual(self.warehouse.pbm_loc_id.active, self.warehouse.manufacture_steps != 'mrp_one_step', "Input location must be de-active for single step only.")
62        self.assertTrue(self.warehouse.manu_type_id.active)
63
64    def test_00_create_warehouse(self):
65        """ Warehouse testing for direct manufacturing """
66        with Form(self.warehouse) as warehouse:
67            warehouse.manufacture_steps = 'mrp_one_step'
68        self._check_location_and_routes()
69        # Check locations of existing pull rule
70        self.assertFalse(self.warehouse.pbm_route_id.rule_ids, 'only the update of global manufacture route should happen.')
71        self.assertEqual(self.warehouse.manufacture_pull_id.location_id.id, self.warehouse.lot_stock_id.id)
72
73    def test_01_warehouse_twostep_manufacturing(self):
74        """ Warehouse testing for picking before manufacturing """
75        with Form(self.warehouse) as warehouse:
76            warehouse.manufacture_steps = 'pbm'
77        self._check_location_and_routes()
78        self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 2)
79        self.assertEqual(self.warehouse.manufacture_pull_id.location_id.id, self.warehouse.lot_stock_id.id)
80
81    def test_02_warehouse_twostep_manufacturing(self):
82        """ Warehouse testing for picking ans store after manufacturing """
83        with Form(self.warehouse) as warehouse:
84            warehouse.manufacture_steps = 'pbm_sam'
85        self._check_location_and_routes()
86        self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 3)
87        self.assertEqual(self.warehouse.manufacture_pull_id.location_id.id, self.warehouse.sam_loc_id.id)
88
89    def test_manufacturing_3_steps(self):
90        """ Test MO/picking before manufacturing/picking after manufacturing
91        components and move_orig/move_dest. Ensure that everything is created
92        correctly.
93        """
94        with Form(self.warehouse) as warehouse:
95            warehouse.manufacture_steps = 'pbm_sam'
96
97        production_form = Form(self.env['mrp.production'])
98        production_form.product_id = self.finished_product
99        production_form.picking_type_id = self.warehouse.manu_type_id
100        production = production_form.save()
101        production.action_confirm()
102
103        move_raw_ids = production.move_raw_ids
104        self.assertEqual(len(move_raw_ids), 1)
105        self.assertEqual(move_raw_ids.product_id, self.raw_product)
106        self.assertEqual(move_raw_ids.picking_type_id, self.warehouse.manu_type_id)
107        pbm_move = move_raw_ids.move_orig_ids
108        self.assertEqual(len(pbm_move), 1)
109        self.assertEqual(pbm_move.location_id, self.warehouse.lot_stock_id)
110        self.assertEqual(pbm_move.location_dest_id, self.warehouse.pbm_loc_id)
111        self.assertEqual(pbm_move.picking_type_id, self.warehouse.pbm_type_id)
112        self.assertFalse(pbm_move.move_orig_ids)
113
114        move_finished_ids = production.move_finished_ids
115        self.assertEqual(len(move_finished_ids), 1)
116        self.assertEqual(move_finished_ids.product_id, self.finished_product)
117        self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id)
118        sam_move = move_finished_ids.move_dest_ids
119        self.assertEqual(len(sam_move), 1)
120        self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id)
121        self.assertEqual(sam_move.location_dest_id, self.warehouse.lot_stock_id)
122        self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id)
123        self.assertFalse(sam_move.move_dest_ids)
124
125    def test_manufacturing_flow(self):
126        """ Simulate a pick pack ship delivery combined with a picking before
127        manufacturing and store after manufacturing. Also ensure that the MO and
128        the moves to stock are created with the generic pull rules.
129        In order to trigger the rule we create a picking to the customer with
130        the 'make to order' procure method
131        """
132        with Form(self.warehouse) as warehouse:
133            warehouse.manufacture_steps = 'pbm_sam'
134            warehouse.delivery_steps = 'pick_pack_ship'
135        self.warehouse.flush()
136        self.env.ref('stock.route_warehouse0_mto').active = True
137        self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
138        picking_customer = self.env['stock.picking'].create({
139            'location_id': self.warehouse.wh_output_stock_loc_id.id,
140            'location_dest_id': self.customer_location,
141            'partner_id': self.env['ir.model.data'].xmlid_to_res_id('base.res_partner_4'),
142            'picking_type_id': self.warehouse.out_type_id.id,
143        })
144        self.env['stock.move'].create({
145            'name': self.finished_product.name,
146            'product_id': self.finished_product.id,
147            'product_uom_qty': 2,
148            'product_uom': self.uom_unit.id,
149            'picking_id': picking_customer.id,
150            'location_id': self.warehouse.wh_output_stock_loc_id.id,
151            'location_dest_id': self.customer_location,
152            'procure_method': 'make_to_order',
153            'origin': 'SOURCEDOCUMENT',
154            'state': 'draft',
155        })
156        picking_customer.action_confirm()
157        production_order = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
158        self.assertTrue(production_order)
159        self.assertEqual(production_order.origin, 'SOURCEDOCUMENT', 'The MO origin should be the SO name')
160        self.assertNotEqual(production_order.name, 'SOURCEDOCUMENT', 'The MO name should not be the origin of the move')
161
162        picking_stock_preprod = self.env['stock.move'].search([
163            ('product_id', '=', self.raw_product.id),
164            ('location_id', '=', self.warehouse.lot_stock_id.id),
165            ('location_dest_id', '=', self.warehouse.pbm_loc_id.id),
166            ('picking_type_id', '=', self.warehouse.pbm_type_id.id)
167        ]).picking_id
168        picking_stock_postprod = self.env['stock.move'].search([
169            ('product_id', '=', self.finished_product.id),
170            ('location_id', '=', self.warehouse.sam_loc_id.id),
171            ('location_dest_id', '=', self.warehouse.lot_stock_id.id),
172            ('picking_type_id', '=', self.warehouse.sam_type_id.id)
173        ]).picking_id
174
175        self.assertTrue(picking_stock_preprod)
176        self.assertTrue(picking_stock_postprod)
177        self.assertEqual(picking_stock_preprod.state, 'confirmed')
178        self.assertEqual(picking_stock_postprod.state, 'waiting')
179        self.assertEqual(picking_stock_preprod.origin, production_order.name, 'The pre-prod origin should be the MO name')
180        self.assertEqual(picking_stock_postprod.origin, 'SOURCEDOCUMENT', 'The post-prod origin should be the SO name')
181
182        picking_stock_preprod.action_assign()
183        picking_stock_preprod.move_line_ids.qty_done = 4
184        picking_stock_preprod._action_done()
185
186        self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.lot_stock_id).mapped('quantity')))
187        self.assertTrue(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id))
188
189        production_order.action_assign()
190        self.assertEqual(production_order.reservation_state, 'assigned')
191        self.assertEqual(picking_stock_postprod.state, 'waiting')
192
193        produce_form = Form(production_order)
194        produce_form.qty_producing = production_order.product_qty
195        production_order = produce_form.save()
196        production_order.button_mark_done()
197
198        self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id).mapped('quantity')))
199
200        self.assertEqual(picking_stock_postprod.state, 'assigned')
201
202        picking_stock_pick = self.env['stock.move'].search([
203            ('product_id', '=', self.finished_product.id),
204            ('location_id', '=', self.warehouse.lot_stock_id.id),
205            ('location_dest_id', '=', self.warehouse.wh_pack_stock_loc_id.id),
206            ('picking_type_id', '=', self.warehouse.pick_type_id.id)
207        ]).picking_id
208        self.assertEqual(picking_stock_pick.move_lines.move_orig_ids.picking_id, picking_stock_postprod)
209
210    def test_cancel_propagation(self):
211        """ Test cancelling moves in a 'picking before
212        manufacturing' and 'store after manufacturing' process. The propagation of
213        cancel depends on the default values on each rule of the chain.
214        """
215        self.warehouse.manufacture_steps = 'pbm_sam'
216        self.warehouse.flush()
217        self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
218        picking_customer = self.env['stock.picking'].create({
219            'location_id': self.warehouse.lot_stock_id.id,
220            'location_dest_id': self.customer_location,
221            'partner_id': self.env['ir.model.data'].xmlid_to_res_id('base.res_partner_4'),
222            'picking_type_id': self.warehouse.out_type_id.id,
223        })
224        self.env['stock.move'].create({
225            'name': self.finished_product.name,
226            'product_id': self.finished_product.id,
227            'product_uom_qty': 2,
228            'picking_id': picking_customer.id,
229            'product_uom': self.uom_unit.id,
230            'location_id': self.warehouse.lot_stock_id.id,
231            'location_dest_id': self.customer_location,
232            'procure_method': 'make_to_order',
233        })
234        picking_customer.action_confirm()
235        production_order = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
236        self.assertTrue(production_order)
237
238        move_stock_preprod = self.env['stock.move'].search([
239            ('product_id', '=', self.raw_product.id),
240            ('location_id', '=', self.warehouse.lot_stock_id.id),
241            ('location_dest_id', '=', self.warehouse.pbm_loc_id.id),
242            ('picking_type_id', '=', self.warehouse.pbm_type_id.id)
243        ])
244        move_stock_postprod = self.env['stock.move'].search([
245            ('product_id', '=', self.finished_product.id),
246            ('location_id', '=', self.warehouse.sam_loc_id.id),
247            ('location_dest_id', '=', self.warehouse.lot_stock_id.id),
248            ('picking_type_id', '=', self.warehouse.sam_type_id.id)
249        ])
250
251        self.assertTrue(move_stock_preprod)
252        self.assertTrue(move_stock_postprod)
253        self.assertEqual(move_stock_preprod.state, 'confirmed')
254        self.assertEqual(move_stock_postprod.state, 'waiting')
255
256        move_stock_preprod._action_cancel()
257        self.assertEqual(production_order.state, 'confirmed')
258        production_order.action_cancel()
259        self.assertTrue(move_stock_postprod.state, 'cancel')
260
261    def test_no_initial_demand(self):
262        """ Test MO/picking before manufacturing/picking after manufacturing
263        components and move_orig/move_dest. Ensure that everything is created
264        correctly.
265        """
266        with Form(self.warehouse) as warehouse:
267            warehouse.manufacture_steps = 'pbm_sam'
268        production_form = Form(self.env['mrp.production'])
269        production_form.product_id = self.finished_product
270        production_form.picking_type_id = self.warehouse.manu_type_id
271        production = production_form.save()
272        production.move_raw_ids.product_uom_qty = 0
273        production.action_confirm()
274        production.action_assign()
275        self.assertFalse(production.move_raw_ids.move_orig_ids)
276        self.assertEqual(production.state, 'confirmed')
277        self.assertEqual(production.reservation_state, 'assigned')
278
279    def test_manufacturing_3_steps_flexible(self):
280        """ Test MO/picking before manufacturing/picking after manufacturing
281        components and move_orig/move_dest. Ensure that additional moves are put
282        in picking before manufacturing too.
283        """
284        with Form(self.warehouse) as warehouse:
285            warehouse.manufacture_steps = 'pbm_sam'
286        bom = self.env['mrp.bom'].search([
287            ('product_id', '=', self.finished_product.id)
288        ])
289        new_product = self.env['product.product'].create({
290            'name': 'New product',
291            'type': 'product',
292        })
293        bom.consumption = 'flexible'
294        production_form = Form(self.env['mrp.production'])
295        production_form.product_id = self.finished_product
296        production_form.picking_type_id = self.warehouse.manu_type_id
297        production = production_form.save()
298
299        production.action_confirm()
300
301        production_form = Form(production)
302        with production_form.move_raw_ids.new() as move:
303            move.product_id = new_product
304            move.product_uom_qty = 2
305        production = production_form.save()
306        move_raw_ids = production.move_raw_ids
307        self.assertEqual(len(move_raw_ids), 2)
308        pbm_move = move_raw_ids.move_orig_ids
309        self.assertEqual(len(pbm_move), 2)
310        self.assertTrue(new_product in pbm_move.product_id)
311
312    def test_manufacturing_complex_product_3_steps(self):
313        """ Test MO/picking after manufacturing a complex product which uses
314        manufactured components. Ensure that everything is created and picked
315        correctly.
316        """
317
318        self.warehouse.mto_pull_id.route_id.active = True
319        # Creating complex product which trigger another manifacture
320
321        product_form = Form(self.env['product.product'])
322        product_form.name = 'Arrow'
323        product_form.type = 'product'
324        product_form.route_ids.clear()
325        product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
326        product_form.route_ids.add(self.warehouse.mto_pull_id.route_id)
327        self.complex_product = product_form.save()
328
329        ## Create raw product for manufactured product
330        product_form = Form(self.env['product.product'])
331        product_form.name = 'Raw Iron'
332        product_form.type = 'product'
333        product_form.uom_id = self.uom_unit
334        product_form.uom_po_id = self.uom_unit
335        self.raw_product_2 = product_form.save()
336
337        with Form(self.finished_product) as finished_product:
338            finished_product.route_ids.clear()
339            finished_product.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
340            finished_product.route_ids.add(self.warehouse.mto_pull_id.route_id)
341
342        ## Create bom for manufactured product
343        bom_product_form = Form(self.env['mrp.bom'])
344        bom_product_form.product_id = self.complex_product
345        bom_product_form.product_tmpl_id = self.complex_product.product_tmpl_id
346        with bom_product_form.bom_line_ids.new() as line:
347            line.product_id = self.finished_product
348            line.product_qty = 1.0
349        with bom_product_form.bom_line_ids.new() as line:
350            line.product_id = self.raw_product_2
351            line.product_qty = 1.0
352
353        self.complex_bom = bom_product_form.save()
354
355        with Form(self.warehouse) as warehouse:
356            warehouse.manufacture_steps = 'pbm_sam'
357
358        production_form = Form(self.env['mrp.production'])
359        production_form.product_id = self.complex_product
360        production_form.picking_type_id = self.warehouse.manu_type_id
361        production = production_form.save()
362        production.action_confirm()
363
364        move_raw_ids = production.move_raw_ids
365        self.assertEqual(len(move_raw_ids), 2)
366        sfp_move_raw_id, raw_move_raw_id = move_raw_ids
367        self.assertEqual(sfp_move_raw_id.product_id, self.finished_product)
368        self.assertEqual(raw_move_raw_id.product_id, self.raw_product_2)
369
370        for move_raw_id in move_raw_ids:
371            self.assertEqual(move_raw_id.picking_type_id, self.warehouse.manu_type_id)
372
373            pbm_move = move_raw_id.move_orig_ids
374            self.assertEqual(len(pbm_move), 1)
375            self.assertEqual(pbm_move.location_id, self.warehouse.lot_stock_id)
376            self.assertEqual(pbm_move.location_dest_id, self.warehouse.pbm_loc_id)
377            self.assertEqual(pbm_move.picking_type_id, self.warehouse.pbm_type_id)
378
379        # Check move locations
380        move_finished_ids = production.move_finished_ids
381        self.assertEqual(len(move_finished_ids), 1)
382        self.assertEqual(move_finished_ids.product_id, self.complex_product)
383        self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id)
384        sam_move = move_finished_ids.move_dest_ids
385        self.assertEqual(len(sam_move), 1)
386        self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id)
387        self.assertEqual(sam_move.location_dest_id, self.warehouse.lot_stock_id)
388        self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id)
389        self.assertFalse(sam_move.move_dest_ids)
390
391        subproduction = self.env['mrp.production'].browse(production.id+1)
392        sfp_pickings = subproduction.picking_ids.sorted('id')
393
394        # SFP Production: 2 pickings, 1 group
395        self.assertEqual(len(sfp_pickings), 2)
396        self.assertEqual(sfp_pickings.mapped('group_id'), subproduction.procurement_group_id)
397
398        ## Move Raw Stick - Stock -> Preprocessing
399        picking = sfp_pickings[0]
400        self.assertEqual(len(picking.move_lines), 1)
401        picking.move_lines[0].product_id = self.raw_product
402
403        ## Move SFP - PostProcessing -> Stock
404        picking = sfp_pickings[1]
405        self.assertEqual(len(picking.move_lines), 1)
406        picking.move_lines[0].product_id = self.finished_product
407
408        # Main production 2 pickings, 1 group
409        pickings = production.picking_ids.sorted('id')
410        self.assertEqual(len(pickings), 2)
411        self.assertEqual(pickings.mapped('group_id'), production.procurement_group_id)
412
413        ## Move 2 components Stock -> Preprocessing
414        picking = pickings[0]
415        self.assertEqual(len(picking.move_lines), 2)
416        picking.move_lines[0].product_id = self.finished_product
417        picking.move_lines[1].product_id = self.raw_product_2
418
419        ## Move FP PostProcessing -> Stock
420        picking = pickings[1]
421        self.assertEqual(len(picking.move_lines), 1)
422        picking.product_id = self.complex_product
423
424    def test_3_steps_and_byproduct(self):
425        """ Suppose a warehouse with Manufacture option set to '3 setps' and a product P01 with a reordering rule.
426        Suppose P01 has a BoM and this BoM mentions that when some P01 are produced, some P02 are produced too.
427        This test ensures that when a MO is generated thanks to the reordering rule, 2 pickings are also
428        generated:
429            - One to bring the components
430            - Another to return the P01 and P02 produced
431        """
432        warehouse = self.warehouse
433        warehouse.manufacture_steps = 'pbm_sam'
434        warehouse_stock_location = warehouse.lot_stock_id
435        pre_production_location = warehouse.pbm_loc_id
436        post_production_location = warehouse.sam_loc_id
437
438        one_unit_uom = self.env['ir.model.data'].xmlid_to_object('uom.product_uom_unit')
439        [two_units_uom, four_units_uom] = self.env['uom.uom'].create([{
440            'name': 'x%s' % i,
441            'category_id': self.ref('uom.product_uom_categ_unit'),
442            'uom_type': 'bigger',
443            'factor_inv': i,
444        } for i in [2, 4]])
445
446        finished_product = self.env['product.product'].create({
447            'name': 'Super Product',
448            'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))],
449            'type': 'product',
450        })
451        secondary_product = self.env['product.product'].create({
452            'name': 'Secondary',
453            'type': 'product',
454        })
455        component = self.env['product.product'].create({
456            'name': 'Component',
457            'type': 'consu',
458        })
459
460        bom = self.env['mrp.bom'].create({
461            'product_tmpl_id': finished_product.product_tmpl_id.id,
462            'product_qty': 1,
463            'product_uom_id': two_units_uom.id,
464            'bom_line_ids': [(0, 0, {
465                'product_id': component.id,
466                'product_qty': 1,
467                'product_uom_id': one_unit_uom.id,
468            })],
469            'byproduct_ids': [(0, 0, {
470                'product_id': secondary_product.id,
471                'product_qty': 1,
472                'product_uom_id': four_units_uom.id,
473            })],
474        })
475
476        orderpoint = self.env['stock.warehouse.orderpoint'].create({
477            'warehouse_id': warehouse.id,
478            'location_id': warehouse_stock_location.id,
479            'product_id': finished_product.id,
480            'product_min_qty': 2,
481            'product_max_qty': 2,
482        })
483
484        self.env['procurement.group'].run_scheduler()
485        mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)])
486        pickings = mo.picking_ids
487        self.assertEqual(len(pickings), 2)
488
489        preprod_picking = pickings[0] if pickings[0].location_id == warehouse_stock_location else pickings[1]
490        self.assertEqual(preprod_picking.location_id, warehouse_stock_location)
491        self.assertEqual(preprod_picking.location_dest_id, pre_production_location)
492
493        postprod_picking = pickings - preprod_picking
494        self.assertEqual(postprod_picking.location_id, post_production_location)
495        self.assertEqual(postprod_picking.location_dest_id, warehouse_stock_location)
496
497        postprod_SML = postprod_picking.move_lines
498        self.assertEqual(len(postprod_SML), 2)
499        self.assertEqual(postprod_SML.location_id, post_production_location)
500        self.assertEqual(postprod_SML.location_dest_id, warehouse_stock_location)
501
502        finished_product_SML = postprod_SML[0] if postprod_SML[0].product_id == finished_product else postprod_SML[1]
503        secondary_product_SML = postprod_SML - finished_product_SML
504        self.assertEqual(finished_product_SML.product_uom.id, one_unit_uom.id)
505        self.assertEqual(finished_product_SML.product_uom_qty, 2)
506        self.assertEqual(secondary_product_SML.product_uom.id, four_units_uom.id)
507        self.assertEqual(secondary_product_SML.product_uom_qty, 1)
508