# Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import Command from odoo.exceptions import UserError from .test_common import TestQualityCommon from odoo.tests import Form, tagged @tagged('-at_install', 'post_install') class TestQualityCheck(TestQualityCommon): @classmethod def setUpClass(cls): super().setUpClass() cls.receipt = cls.env['stock.picking'].create({ 'picking_type_id': cls.picking_type_id, 'location_id': cls.location_id, 'location_dest_id': cls.location_dest_id, }) cls.product_move, cls.product2_move = cls.env['stock.move'].create([ { 'product_id': cls.product.id, 'product_uom_qty': 2, 'picking_id': cls.receipt.id, 'location_id': cls.receipt.location_id.id, 'location_dest_id': cls.receipt.location_dest_id.id, }, { 'product_id': cls.product_2.id, 'product_uom_qty': 2, 'picking_id': cls.receipt.id, 'location_id': cls.receipt.location_id.id, 'location_dest_id': cls.receipt.location_dest_id.id, } ]) def test_00_picking_quality_check(self): """Test quality check on incoming shipment.""" # Create Quality Point for incoming shipment. self.qality_point_test = self.env['quality.point'].create({ 'product_ids': [(4, self.product.id)], 'picking_type_ids': [(4, self.picking_type_id)], }) # Check that quality point created. self.assertTrue(self.qality_point_test, "First Quality Point not created for Laptop Customized.") # Create incoming shipment. self.picking_in = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) self.env['stock.move'].create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id}) # Check that incoming shipment is created. self.assertTrue(self.picking_in, "Incoming shipment not created.") # Confirm incoming shipment. self.picking_in.action_confirm() # Check Quality Check for incoming shipment is created and check it's state is 'none'. self.assertEqual(len(self.picking_in.check_ids), 1) self.assertEqual(self.picking_in.check_ids.quality_state, 'none') # 'Pass' Quality Checks of incoming shipment. self.picking_in.check_ids.do_pass() # Validate incoming shipment. self.picking_in.button_validate() # Now check state of quality check. self.assertEqual(self.picking_in.check_ids.quality_state, 'pass') def test_01_picking_quality_check_type_text(self): """ Test a Quality Check on a picking with 'Instruction' as test type. """ # Create Quality Point for incoming shipment with 'Instructions' as test type self.qality_point_test = self.env['quality.point'].create({ 'product_ids': [(4, self.product.id)], 'picking_type_ids': [(4, self.picking_type_id)], 'test_type_id': self.env.ref('quality.test_type_instructions').id }) # Check that quality point created. self.assertTrue(self.qality_point_test, "Quality Point not created") # Create incoming shipment. self.picking_in = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) self.env['stock.move'].create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id}) # Check that incoming shipment is created. self.assertTrue(self.picking_in, "Incoming shipment not created.") # Confirm incoming shipment. self.picking_in.action_confirm() # Check Quality Check for incoming shipment is created and check it's state is 'none'. self.assertEqual(len(self.picking_in.check_ids), 1) self.assertEqual(self.picking_in.check_ids.quality_state, 'none') # Check that the Quality Check on the picking has 'instruction' as test_type self.assertEqual(self.picking_in.check_ids[0].test_type, 'instructions') def test_02_picking_quality_check_type_picture(self): """ Test a Quality Check on a picking with 'Take Picture' as test type. """ # Create Quality Point for incoming shipment with 'Take Picture' as test type self.qality_point_test = self.env['quality.point'].create({ 'product_ids': [(4, self.product.id)], 'picking_type_ids': [(4, self.picking_type_id)], 'test_type_id': self.env.ref('quality.test_type_picture').id }) # Check that quality point created. self.assertTrue(self.qality_point_test, "Quality Point not created") # Create incoming shipment. self.picking_in = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) self.env['stock.move'].create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id}) # Check that incoming shipment is created. self.assertTrue(self.picking_in, "Incoming shipment not created.") # Confirm incoming shipment. self.picking_in.action_confirm() # Check Quality Check for incoming shipment is created and check it's state is 'none'. self.assertEqual(len(self.picking_in.check_ids), 1) self.assertEqual(self.picking_in.check_ids.quality_state, 'none') # Check that the Quality Check on the picking has 'picture' as test_type self.assertEqual(self.picking_in.check_ids[0].test_type, 'picture') def test_03_lot_quality_check(self): """ Test a Quality Check at the lot level. """ product_tracked_by_lot = self.env['product.product'].create({ 'name': 'Product tracked by lot', 'tracking': 'lot', }) product = self.env['product.product'].create({ 'name': 'Product', 'tracking': 'none', }) # Create Quality Point for incoming shipment on lots with 'Measure' as test type self.quality_point_test1 = self.env['quality.point'].create({ 'product_ids': [product_tracked_by_lot.id], 'picking_type_ids': [self.picking_type_id], 'test_type_id': self.env.ref('quality_control.test_type_measure').id, 'testing_percentage_within_lot': 10.02, 'measure_on': 'move_line', 'norm': 5., 'tolerance_min': 4., 'tolerance_max': 6., }) # Create Quality Point for incoming shipment on lots for all products self.quality_point_test2 = self.env['quality.point'].create({ 'picking_type_ids': [self.picking_type_id], 'test_type_id': self.env.ref('quality_control.test_type_measure').id, 'measure_on': 'move_line', }) # Create Quality Point for product without tracking self.quality_point_test3 = self.env['quality.point'].create({ 'product_ids': [product.id], 'picking_type_ids': [self.picking_type_id], 'test_type_id': self.env.ref('quality_control.test_type_measure').id, 'measure_on': 'move_line', }) # Check that the quality points are created self.assertTrue(self.quality_point_test1, "Quality Point not created") self.assertTrue(self.quality_point_test2, "Quality Point not created") self.assertTrue(self.quality_point_test3, "Quality Point not created") # Create incoming shipment self.picking_in = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) move = self.env['stock.move'].create({ 'product_id': product_tracked_by_lot.id, 'product_uom_qty': 11, 'product_uom': product_tracked_by_lot.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id}) # Check that incoming shipment is created self.assertTrue(self.picking_in, "Incoming shipment not created.") # Creating move lines with the serial number widget move._generate_serial_numbers("1", next_serial_count=10) self.assertTrue(len(move.move_line_ids) == 10, "Not all move lines are created with _generate_serial_number") # Check that quality checks were created self.assertTrue(len(move.move_line_ids.check_ids) == 20, "Wrong number of Quality Checks created on the move lines") # Create move line without qty_done and setting it after move_line_vals = move._prepare_move_line_vals() move_line = self.env['stock.move.line'].create(move_line_vals) move_line.quantity = 1. self.assertTrue(len(move.move_line_ids.check_ids) == 22, "Wrong number of Quality Checks created on the move lines") # Updating quantity of one move line move.move_line_ids[0].quantity = 2 check_line1 = move.move_line_ids[0].check_ids[0] check_line2 = move.move_line_ids[1].check_ids[0] # Check that the percentage of the lot to check is correct self.assertTrue(check_line1.qty_to_test == 0.21, "Quantity to test within lot not well calculated (check rounding)") self.assertTrue(check_line2.qty_to_test == 0.11, "Quantity to test within lot not well calculated (check rounding)") # Check that tests are failing and succeeding properly check_line1.measure = 3. check_line2.measure = 4.5 check_line1.do_measure() check_line2.do_measure() self.assertTrue(check_line1.quality_state == 'fail', "Quality Check of type 'measure' not failing on move line") self.assertTrue(check_line2.quality_state == 'pass', "Quality Check of type 'measure' not passing on move line") # Create move with a product without tracking with done quantity move_without_tracking1 = self.env['stock.move'].create({ 'product_id': product.id, 'product_uom_qty': 1, 'quantity': 1, 'product_uom': product.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) self.assertTrue(len(move_without_tracking1.move_line_ids.check_ids) == 2, "Wrong number of Quality Checks created on the move lines") # Create move with a product without tracking without done quantity and changing done quantity after move_without_tracking2 = self.env['stock.move'].create({ 'product_id': product.id, 'product_uom_qty': 1, 'quantity': 0, 'product_uom': product.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) move_without_tracking2.quantity = 1 self.assertTrue(len(move_without_tracking2.move_line_ids.check_ids) == 2, "Wrong number of Quality Checks created on the move lines") def test_04_picking_quality_check_creation_no_products_no_categories(self): """ Test Quality Check creation on incoming shipment from a Quality Point with no products and no product_categories set """ # Create Quality Point for incoming shipment with no product or product_category set. self.quality_point_test = self.env['quality.point'].create({ 'picking_type_ids': [(4, self.picking_type_id)], 'test_type_id': self.env.ref('quality_control.test_type_passfail').id }) # Check that Quality Point has been created. self.assertTrue(self.quality_point_test, "Quality Point not created.") # Create incoming shipment. self.picking_in = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) self.env['stock.move'].create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Check that incoming shipment has been created. self.assertTrue(self.picking_in, "Incoming shipment not created.") # Confirm incoming shipment. self.picking_in.action_confirm() # Check that Quality Check for incoming shipment has been created. self.assertEqual(len(self.picking_in.check_ids), 1) def test_05_picking_quality_check_creation_with_product_no_categories(self): """ Test Quality Check creation on incoming shipment from a Quality Point with products and no product_categories set """ # Create Quality Point for incoming shipment with only a product set. self.quality_point_test = self.env['quality.point'].create({ 'product_ids': [(4, self.product.id)], 'picking_type_ids': [(4, self.picking_type_id)], 'test_type_id': self.env.ref('quality_control.test_type_passfail').id }) # Check that Quality Point has been created. self.assertTrue(self.quality_point_test, "Quality Point not created.") # Create incoming shipment. self.picking_in = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Create move with right product. self.env['stock.move'].create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Create move with wrong product. self.env['stock.move'].create({ 'product_id': self.product_2.id, 'product_uom_qty': 2, 'product_uom': self.product_2.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Check that incoming shipment has been created. self.assertTrue(self.picking_in, "Incoming shipment not created.") # Confirm incoming shipment. self.picking_in.action_confirm() # Check that only one Quality Check for incoming shipment has been created for the right product. self.assertEqual(len(self.picking_in.check_ids), 1) self.assertEqual(len(self.picking_in.check_ids.filtered(lambda c: c.product_id.id == self.product.id)), 1) self.assertEqual(len(self.picking_in.check_ids.filtered(lambda c: c.product_id.id == self.product_2.id)), 0) def test_06_picking_quality_check_creation_no_product_with_categories(self): """ Test Quality Check creation on incoming shipment from a Quality Point with no products and product_categories set """ # Create Quality Point for incoming shipment with only a product_category set. self.quality_point_test = self.env['quality.point'].create({ 'product_category_ids': [(4, self.product_category_base.id)], 'picking_type_ids': [(4, self.picking_type_id)], 'test_type_id': self.env.ref('quality_control.test_type_passfail').id }) # Check that Quality Point has been created. self.assertTrue(self.quality_point_test, "Quality Point not created.") # Create incoming shipment. self.picking_in = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Create move with product having right category (child of Quality Point set category). self.env['stock.move'].create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Create move with product having wrong category (parent of Quality Point set category). self.env['stock.move'].create({ 'product_id': self.product_2.id, 'product_uom_qty': 2, 'product_uom': self.product_2.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Check that incoming shipment has been created. self.assertTrue(self.picking_in, "Incoming shipment not created.") # Confirm incoming shipment. self.picking_in.action_confirm() # Check that only one Quality Check for incoming shipment has been created for the right category. self.assertEqual(len(self.picking_in.check_ids), 1) self.assertEqual(len(self.picking_in.check_ids.filtered(lambda c: c.product_id.id == self.product.id)), 1) self.assertEqual(len(self.picking_in.check_ids.filtered(lambda c: c.product_id.id == self.product_2.id)), 0) def test_07_picking_quality_check_creation_with_product_and_categories(self): """ Test Quality Check creation on incoming shipment from a Quality Point with both products and product_categories set """ # Create Quality Point for incoming shipment with only a product_category set. self.quality_point_test = self.env['quality.point'].create({ 'product_ids': [(4, self.product_2.id), (4, self.product_4.id)], 'product_category_ids': [(4, self.product_category_base.id)], 'picking_type_ids': [(4, self.picking_type_id)], 'test_type_id': self.env.ref('quality_control.test_type_passfail').id }) # Check that Quality Point has been created. self.assertTrue(self.quality_point_test, "Quality Point not created.") # Create incoming shipment. self.picking_in = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Create move with wrong product but having right category (child of Quality Point set category. self.env['stock.move'].create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Create move with right product but having wrong category (parent of Quality Point set category). self.env['stock.move'].create({ 'product_id': self.product_2.id, 'product_uom_qty': 2, 'product_uom': self.product_2.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Create move with wrong product and having wrong category (parent of Quality Point set category). self.env['stock.move'].create({ 'product_id': self.product_3.id, 'product_uom_qty': 2, 'product_uom': self.product_3.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Create move with right product having right category self.env['stock.move'].create({ 'product_id': self.product_4.id, 'product_uom_qty': 2, 'product_uom': self.product_4.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Check that incoming shipment has been created. self.assertTrue(self.picking_in, "Incoming shipment not created.") # Confirm incoming shipment. self.picking_in.action_confirm() # Check that Quality Check for incoming shipment have been created only for the right product / category. self.assertEqual(len(self.picking_in.check_ids), 3) self.assertEqual(len(self.picking_in.check_ids.filtered(lambda c: c.product_id.id == self.product.id)), 1) self.assertEqual(len(self.picking_in.check_ids.filtered(lambda c: c.product_id.id == self.product_2.id)), 1) self.assertEqual(len(self.picking_in.check_ids.filtered(lambda c: c.product_id.id == self.product_3.id)), 0) self.assertEqual(len(self.picking_in.check_ids.filtered(lambda c: c.product_id.id == self.product_4.id)), 1) def test_08_on_product_creation_with_product_and_categories(self): """ Test Quality Check creation on incoming shipment from a Quality Point with both products and product_categories set """ # Create Quality Point for incoming shipment with only a product_category set. self.quality_point_test = self.env['quality.point'].create({ 'product_ids': [(4, self.product_2.id), (4, self.product_4.id)], 'product_category_ids': [(4, self.product_category_base.id)], 'picking_type_ids': [(4, self.picking_type_id)], 'measure_on': 'move_line', 'test_type_id': self.env.ref('quality_control.test_type_passfail').id }) # Create incoming shipment. self.picking_in = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Create move with wrong product but having right category (child of Quality Point set category. self.env['stock.move'].create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Create move with right product but having wrong category (parent of Quality Point set category). self.env['stock.move'].create({ 'product_id': self.product_2.id, 'product_uom_qty': 2, 'product_uom': self.product_2.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Create move with wrong product and having wrong category (parent of Quality Point set category). self.env['stock.move'].create({ 'product_id': self.product_3.id, 'product_uom_qty': 2, 'product_uom': self.product_3.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Create move with right product having right category self.env['stock.move'].create({ 'product_id': self.product_4.id, 'product_uom_qty': 2, 'product_uom': self.product_4.uom_id.id, 'picking_id': self.picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id }) # Confirm incoming shipment. self.picking_in.action_confirm() self.picking_in.move_ids.picked = True # Check that Quality Check for incoming shipment have been created for all the good move lines self.assertEqual(len(self.picking_in.check_ids), 3) self.assertEqual(len(self.picking_in.check_ids.filtered(lambda c: c.product_id.id == self.product.id)), 1) self.assertEqual(len(self.picking_in.check_ids.filtered(lambda c: c.product_id.id == self.product_2.id)), 1) self.assertEqual(len(self.picking_in.check_ids.filtered(lambda c: c.product_id.id == self.product_3.id)), 0) self.assertEqual(len(self.picking_in.check_ids.filtered(lambda c: c.product_id.id == self.product_4.id)), 1) def test_09_quality_check_on_operations(self): """ Test Quality Check creation of 'operation' type, meaning only one QC will be created per picking. """ # Create Quality Point for incoming shipment with only a product_category set. quality_point_operation_type = self.env['quality.point'].create({ 'picking_type_ids': [(4, self.picking_type_id)], 'measure_on': 'operation', 'test_type_id': self.env.ref('quality_control.test_type_passfail').id }) receipt = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) self.env['stock.move'].create([{ 'product_id': product.id, 'product_uom_qty': 1, 'product_uom': product.uom_id.id, 'picking_id': receipt.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, } for product in (self.product, self.product_2)]) receipt.action_confirm() receipt.with_context(default_picking_id=receipt.id).write({ 'move_ids': [Command.create({ 'product_id': self.product_3.id, 'product_uom_qty': 1, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, })], }) self.assertEqual(len(receipt.check_ids), 1) self.assertEqual(receipt.check_ids.point_id, quality_point_operation_type) self.assertEqual(receipt.check_ids.picking_id, receipt) with self.assertRaises(UserError): receipt._action_done() receipt.check_ids.do_pass() receipt._action_done() def test_check_no_serial(self): """ The tracked product without set lot should raise a user error stating that a lot should be set. """ self.product.write({ 'tracking': 'serial', 'is_storable': True, }) picking_type_without_lot = self.env['stock.picking.type'].browse(self.picking_type_id).copy({ 'use_create_lots': False, 'use_existing_lots': False, }) self.env['quality.point'].create({ 'picking_type_ids': [Command.link(self.picking_type_id), Command.link(picking_type_without_lot.id)], 'measure_on': 'move_line', 'test_type_id': self.env.ref('quality_control.test_type_passfail').id }) receipts = self.env['stock.picking'].create([ { 'picking_type_id': picking_type, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, 'move_ids': [Command.create({ 'product_id': self.product.id, 'product_uom_qty': 5, 'product_uom': self.product.uom_id.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, })], } for picking_type in (self.picking_type_id, picking_type_without_lot.id) ]) receipts.action_confirm() receipt, receipt_wihtout_lot = receipts # Use case 1: lot is necessary move = receipt.move_ids self.assertFalse(move.move_line_ids.lot_id) self.assertEqual(move.move_line_ids.mapped('lot_name'), [False] * 5) # check that a user error is raised if we try to check the quality without setting a lot with self.assertRaises(UserError, msg="A UserError should be raised as lot/sn is needed."): receipt.check_quality() move.move_line_ids[0].lot_name = "test_sn1" move.move_line_ids[0].picked = True qc_wizard = Form.from_action(self.env, receipt.check_quality()) # no quality check created yet quality_check = qc_wizard.save() # there is only one check created for the picking self.assertTrue(quality_check.is_last_check) # Use case 2: lot is not necessary self.assertRecordValues(receipt_wihtout_lot.move_line_ids, [ {'lot_id': False, 'lot_name': False}, ]) qc_wizard = Form.from_action(self.env, receipt_wihtout_lot.check_quality()) # no quality check created yet quality_check = qc_wizard.save() # there is only one check created for the picking self.assertTrue(quality_check.is_last_check) def test_checks_removal_on_SM_cancellation(self): """ Configuration: - 2 storable products P1 and P2 - Receipt in 2 steps - QCP for internal pickings Process a first receipt with P1 and P2 (an internal picking and two quality checks are created) Process a second receipt with P1. The SM input->stock should be merged into the existing one and the quality checks should still exist """ warehouse = self.env['stock.warehouse'].search([], limit=1) warehouse.reception_steps = 'two_steps' p01, p02 = self.env['product.product'].create([{ 'name': name, 'is_storable': True, } for name in ('SuperProduct01', 'SuperProduct02')]) self.env['quality.point'].create([{ 'product_ids': [(4, product.id)], 'picking_type_ids': [(4, warehouse.store_type_id.id)], } for product in (p01, p02)]) # stock reference to simulate a common source document (e.g. PO) reference = self.env['stock.reference'].create({'name': 'reference'}) receipt = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': warehouse.wh_input_stock_loc_id.id, }) self.env['stock.move'].create([{ 'product_id': product.id, 'product_uom_qty': 1, 'product_uom': product.uom_id.id, 'picking_id': receipt.id, 'location_id': self.location_id, 'location_dest_id': warehouse.wh_input_stock_loc_id.id, 'reference_ids': [Command.link(reference.id)], } for product in (p01, p02)]) receipt.action_confirm() receipt.move_ids.quantity = 1 receipt.button_validate() storage_transfer = self.env['stock.picking'].search( [('location_id', '=', warehouse.wh_input_stock_loc_id.id), ('picking_type_id', '=', warehouse.store_type_id.id)], order='id desc', limit=1) self.assertEqual(storage_transfer.check_ids.product_id, p01 + p02) receipt = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': warehouse.wh_input_stock_loc_id.id, }) self.env['stock.move'].create({ 'product_id': p01.id, 'product_uom_qty': 1, 'product_uom': p01.uom_id.id, 'picking_id': receipt.id, 'location_id': self.location_id, 'location_dest_id': warehouse.wh_input_stock_loc_id.id, 'reference_ids': [Command.link(reference.id)], }) receipt.action_confirm() receipt.move_ids.quantity = 1 receipt.button_validate() self.assertRecordValues(storage_transfer.move_ids, [ {'product_id': p01.id, 'product_uom_qty': 2}, {'product_id': p02.id, 'product_uom_qty': 1}, ]) self.assertEqual(storage_transfer.check_ids.product_id, p01 + p02) def test_propagate_sml_lot_name(self): self.env['quality.point'].create({ 'picking_type_ids': [(4, self.picking_type_id)], 'measure_on': 'move_line', 'test_type_id': self.env.ref('quality_control.test_type_passfail').id }) self.product.write({ 'is_storable': True, 'tracking': 'serial', }) receipt = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) move = self.env['stock.move'].create({ 'product_id': self.product.id, 'product_uom_qty': 1, 'product_uom': self.product.uom_id.id, 'picking_id': receipt.id, 'location_id': receipt.location_id.id, 'location_dest_id': receipt.location_dest_id.id, }) receipt.action_confirm() ml = move.move_line_ids ml.write({ 'quantity': 1, 'lot_name': '1457', }) self.assertEqual(ml.check_ids.lot_name, '1457') ml.lot_name = '1458' self.assertEqual(ml.check_ids.lot_name, '1458') # after validation check if lot_id is also propagated ml.check_ids.do_pass() receipt.button_validate() self.assertEqual(ml.check_ids.lot_line_id, ml.lot_id) self.assertEqual(ml.check_ids.lot_ids, ml.lot_id) # Get lot from lot name self.assertEqual(ml.check_ids._get_check_action_name(), 'Quality Check : Office Chair - 1.0 Units - 1458') ml.lot_name = False # Get lot from lot id self.assertEqual(ml.check_ids._get_check_action_name(), 'Quality Check : Office Chair - 1.0 Units - 1458') def test_update_sml_done_qty(self): """ When changing the done quantity of a SML, the related QC should be updated too """ self.env['quality.point'].create({ 'product_ids': [(4, self.product.id)], 'picking_type_ids': [(4, self.picking_type_id)], 'measure_on': 'move_line', }) picking = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) move = self.env['stock.move'].create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'picking_id': picking.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) picking.action_confirm() move.quantity = 1.0 self.assertEqual(picking.check_ids.qty_line, 1) move.quantity = 0.0 self.assertEqual(picking.check_ids.qty_line, 0) move.quantity = 2.0 self.assertEqual(picking.check_ids.qty_line, 2) def test_quality_check_with_backorder(self): """Test that a user without quality manager access rights can create a backorder""" # Create a user wtih stock and quality user rights user = self.env['res.users'].create({ 'name': 'Inventory Manager', 'login': 'test', 'email': 'test@test.com', 'group_ids': [(6, 0, [self.env.ref('stock.group_stock_user').id, self.env.ref('quality.group_quality_user').id])] }) self.env['quality.point'].create([{ 'product_ids': [(4, self.product.id)], 'picking_type_ids': [(4, self.picking_type_id)], 'measure_on': 'operation', }, { 'picking_type_ids': [(4, self.picking_type_id)], 'measure_on': 'move_line', }]) picking = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) move = self.env['stock.move'].create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'picking_id': picking.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) picking.action_confirm() move.quantity = 1.0 self.assertEqual(len(picking.check_ids), 2) # 'Pass' Quality Checks of shipment. for check in picking.check_ids: check.do_pass() # Validate the picking and create a backorder Form.from_action(self.env, picking.button_validate()).save()\ .with_user(user).process() # Check that the backorder is created and in assigned state self.assertEqual(picking.state, 'done') backorder = picking.backorder_ids self.assertEqual(backorder.state, 'assigned') self.assertEqual(len(backorder.check_ids), 2) # 'Pass' Quality Checks of backorder. for check in backorder.check_ids: check.do_pass() # Validate the backorder backorder.move_ids.quantity = 1.0 backorder.with_user(user).button_validate() self.assertEqual(backorder.state, 'done') def test_failure_location_move_line(self): """ Quality point per quantity with failure locations list, a picking with 2 products / moves, fail one move with qty less than total move qty, a new move with the failing quantity is created, moving it to the failure location chosen """ self.env['quality.point'].create({ 'picking_type_ids': [Command.link(self.picking_type_id)], 'measure_on': 'move_line', 'test_type_id': self.env.ref('quality_control.test_type_passfail').id, 'failure_location_ids': [Command.link(self.failure_location.id)], }) self.receipt.action_confirm() self.assertEqual(len(self.receipt.check_ids), 2) # open the wizard to do the checks action = self.receipt.check_ids.action_open_quality_check_wizard() wizard = self.env[action['res_model']].with_context(action['context']).create({}) self.assertEqual(len(wizard.check_ids), 2) self.assertEqual(wizard.current_check_id.move_line_id, self.product_move.move_line_ids) # pass the first quantity action = wizard.do_pass() wizard = self.env[action['res_model']].with_context(action['context']).create({}) self.assertEqual(wizard.current_check_id.move_line_id, self.product2_move.move_line_ids) action = wizard.do_fail() wizard = self.env[action['res_model']].with_context(action['context']).browse(action['res_id']) self.assertEqual(wizard.qty_failed, 2) # only fail one qty of the two wizard.qty_failed = 1 wizard.failure_location_id = self.failure_location.id wizard.confirm_fail() self.assertEqual(len(self.receipt.check_ids), 3) # there should be 3 moves and 3 checks self.assertEqual(len(self.receipt.move_ids), 3) self.assertRecordValues(self.receipt.check_ids, [ {'quality_state': 'pass', 'product_id': self.product.id, 'qty_line': 2, 'failure_location_id': False}, {'quality_state': 'fail', 'product_id': self.product_2.id, 'qty_line': 1, 'failure_location_id': self.failure_location.id}, {'quality_state': 'pass', 'product_id': self.product_2.id, 'qty_line': 1, 'failure_location_id': False}, ]) def test_failure_location_product(self): """ Quality point per quantity with failure locations list, a picking with 2 products / moves, fail one move with qty less than total move qty, a new move with the failing quantity is created, moving it to the failure location chosen """ self.env['quality.point'].create({ 'picking_type_ids': [Command.link(self.picking_type_id)], 'measure_on': 'product', 'product_ids': [Command.link(self.product_2.id)], 'test_type_id': self.env.ref('quality_control.test_type_passfail').id, 'failure_location_ids': [Command.link(self.failure_location.id)], }) self.receipt.action_confirm() self.assertEqual(len(self.receipt.check_ids), 1) # open the wizard to do the checks action = self.receipt.check_ids.action_open_quality_check_wizard() wizard = self.env[action['res_model']].with_context(action['context']).create({}) self.assertEqual(len(wizard.check_ids), 1) self.assertEqual(wizard.check_ids.product_id, self.product_2) action = wizard.do_fail() wizard = self.env[action['res_model']].with_context(action['context']).browse(action['res_id']) wizard.failure_location_id = self.failure_location.id wizard.confirm_fail() # there should be 3 moves and 3 checks self.assertRecordValues(self.receipt.check_ids, [ {'quality_state': 'fail', 'product_id': self.product_2.id, 'failure_location_id': self.failure_location.id}, ]) self.assertRecordValues(self.receipt.move_ids, [ {'product_id': self.product.id, 'location_dest_id': self.receipt.location_dest_id.id}, {'product_id': self.product_2.id, 'location_dest_id': self.failure_location.id}, ]) def test_failure_location_operation(self): """ Quality point per quantity with failure locations list, a picking with 2 products / moves, fail one move with qty less than total move qty, a new move with the failing quantity is created, moving it to the failure location chosen """ self.env['quality.point'].create({ 'picking_type_ids': [Command.link(self.picking_type_id)], 'measure_on': 'operation', 'test_type_id': self.env.ref('quality_control.test_type_passfail').id, 'failure_location_ids': [Command.link(self.failure_location.id)], }) self.receipt.action_confirm() self.assertEqual(len(self.receipt.check_ids), 1) # open the wizard to do the checks action = self.receipt.check_ids.action_open_quality_check_wizard() wizard = self.env[action['res_model']].with_context(action['context']).create({}) self.assertEqual(len(wizard.check_ids), 1) action = wizard.do_fail() wizard = self.env[action['res_model']].with_context(action['context']).browse(action['res_id']) # only fail one qty of the two wizard.failure_location_id = self.failure_location.id wizard.confirm_fail() self.assertEqual(len(self.receipt.move_ids), 2) self.assertEqual(self.receipt.location_dest_id.id, self.location_dest_id) self.assertEqual(self.receipt.move_ids.location_dest_id, self.failure_location) self.assertRecordValues(self.receipt.check_ids, [ {'quality_state': 'fail', 'product_id': False, 'picking_id': self.receipt.id, 'failure_location_id': self.failure_location.id}, ]) def test_failure_location_lot(self): """ Quality point per quantity with failure locations list, a picking with 2 products / moves, fail one move with qty less than total move qty, a new move with the failing quantity is created, moving it to the chosen failure location. """ product_lot = self.env['product.product'].create({ 'name': 'product lot', 'is_storable': True, 'tracking': 'lot', }) self.env['quality.point'].create({ 'picking_type_ids': [Command.link(self.picking_type_id)], 'measure_on': 'move_line', 'product_ids': [Command.link(product_lot.id)], 'test_type_id': self.env.ref('quality_control.test_type_passfail').id, 'failure_location_ids': [Command.link(self.failure_location.id)], }) receipt = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) move = self.env['stock.move'].create({ 'product_id': product_lot.id, 'product_uom_qty': 4, 'picking_id': receipt.id, 'location_id': receipt.location_id.id, 'location_dest_id': receipt.location_dest_id.id, }) receipt.action_confirm() self.assertEqual(len(receipt.check_ids), 1) move.quantity = 0 move.move_line_ids = [Command.create({ 'product_id': product_lot.id, 'product_uom_id': product_lot.uom_id.id, 'quantity': 2, 'picking_id': receipt.id, 'lot_name': 'lot1', }), Command.create({ 'product_id': product_lot.id, 'product_uom_id': product_lot.uom_id.id, 'quantity': 2, 'picking_id': receipt.id, 'lot_name': 'lot2', })] # open the wizard to do the checks self.assertEqual(len(receipt.check_ids), 2) action = receipt.check_ids.action_open_quality_check_wizard() wizard = Form.from_action(self.env, receipt.check_ids.action_open_quality_check_wizard()).save() self.assertEqual(len(wizard.check_ids), 2) self.assertEqual(wizard.current_check_id.move_line_id, move.move_line_ids[0]) # pass the first quantity action = wizard.do_pass() wizard = self.env[action['res_model']].with_context(action['context']).create({}) self.assertEqual(wizard.current_check_id.move_line_id, move.move_line_ids[1]) action = wizard.do_fail() wizard = self.env[action['res_model']].with_context(action['context']).browse(action['res_id']) self.assertEqual(wizard.qty_failed, 2) wizard.failure_location_id = self.failure_location.id wizard.confirm_fail() self.assertEqual(len(receipt.check_ids), 2) # there should be a move for the passed quantity and a move for the failed quantity self.assertEqual(len(receipt.move_ids), 2) self.assertRecordValues(receipt.move_ids, [ {'product_id': product_lot.id, 'product_uom_qty': 2, 'quantity': 2, 'location_dest_id': receipt.location_dest_id.id}, {'product_id': product_lot.id, 'product_uom_qty': 2, 'quantity': 2, 'location_dest_id': self.failure_location.id}, ]) self.assertRecordValues(receipt.check_ids, [ {'quality_state': 'pass', 'product_id': product_lot.id, 'qty_line': 2, 'failure_location_id': False}, {'quality_state': 'fail', 'product_id': product_lot.id, 'qty_line': 2, 'failure_location_id': self.failure_location.id}, ]) self.assertRecordValues(receipt.move_ids, [ {'picked': False, 'product_id': product_lot.id, 'quantity': 2, 'location_dest_id': receipt.location_dest_id.id}, {'picked': False, 'product_id': product_lot.id, 'quantity': 2, 'location_dest_id': self.failure_location.id}, ]) receipt.button_validate() self.assertEqual(receipt.state, 'done') self.assertEqual(receipt.move_ids.mapped('state'), ['done', 'done']) def test_qp_with_product_ctg(self): """ Test that the quality check is created based on the product category of product and quality point. """ product_cat_2 = self.product_category_base.copy({'name': 'cat2'}) product_a = self.env['product.product'].create({ 'name': 'Product A', 'is_storable': True, 'categ_id': product_cat_2.id, }) product_b = self.env['product.product'].create({ 'name': 'Product B', 'is_storable': True, 'categ_id': self.product_category_base.id, }) self.env['quality.point'].create({ 'title': 'QP1', 'product_category_ids': [(4, product_cat_2.id), (4, self.product_category_base.id)], 'picking_type_ids': [(4, self.picking_type_id)], 'measure_on': 'move_line', }) self.env['quality.point'].create({ 'title': 'QP2', 'product_category_ids': [(4, self.product_category_base.id)], 'picking_type_ids': [(4, self.picking_type_id)], 'measure_on': 'move_line', }) picking = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) self.env['stock.move'].create({ 'product_id': product_a.id, 'product_uom_qty': 1, 'picking_id': picking.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) self.env['stock.move'].create({ 'product_id': product_b.id, 'product_uom_qty': 1, 'picking_id': picking.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) picking.action_confirm() self.assertEqual(len(picking.check_ids), 3) def test_qc_with_partial_reception(self): """ Test that the quality check is required only for move lines with quantity set. """ self.env['quality.point'].create({ 'picking_type_ids': [self.picking_type_id], 'test_type_id': self.env.ref('quality_control.test_type_measure').id, 'measure_on': 'move_line', }) (self.product_2 | self.product_3 | self.product_4).is_storable = True (self.product_2 | self.product_4).tracking = 'serial' # Create incoming shipment. picking_in = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) move_tracked_product_a = self.env['stock.move'].create({ 'product_id': self.product_2.id, 'product_uom_qty': 1, 'product_uom': self.product_2.uom_id.id, 'picking_id': picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id}) move_untracked = self.env['stock.move'].create({ 'product_id': self.product_3.id, 'product_uom_qty': 1, 'product_uom': self.product_3.uom_id.id, 'picking_id': picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id}) move_tracked_product_b = self.env['stock.move'].create({ 'product_id': self.product_4.id, 'product_uom_qty': 2, 'product_uom': self.product_4.uom_id.id, 'picking_id': picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) # Confirm incoming shipment. picking_in.action_confirm() # Check Quality Check for incoming shipment is created self.assertEqual(len(picking_in.check_ids), 4) self.assertTrue(picking_in.quality_check_todo) # Set the quantity for the untracked product and complete its quality check. move_untracked.quantity = 1 move_untracked.picked = True untracked_check_ids = picking_in.check_ids.filtered(lambda qc: qc.product_id == self.product_3) untracked_check_ids.do_pass() self.assertEqual(untracked_check_ids.quality_state, 'pass') # Register a quantity of 2 units for your product_b and none for product_a move_tracked_product_a.quantity = 0 move_tracked_product_b.quantity = 2 move_tracked_product_b.picked = True move_tracked_product_b._generate_serial_numbers("1", next_serial_count=2) tracked_check_ids_to_do = picking_in.check_ids.filtered(lambda qc: qc.product_id == self.product_4) self.env.invalidate_all() # Check that clicking on the Quality Check button shows you the QC's related to product_b qc_wizard = Form.from_action(self.env, picking_in.check_quality()).save() self.assertEqual(qc_wizard.check_ids, tracked_check_ids_to_do) # process one of the 2 QC's and keep the second one for validation tracked_check_ids_to_do[0].do_pass() self.assertEqual(tracked_check_ids_to_do[0].quality_state, 'pass') tracked_check_ids_to_do = tracked_check_ids_to_do.filtered(lambda qc: qc.quality_state == 'none') qc_wizard = Form.from_action(self.env, picking_in.check_quality()).save() self.assertEqual(qc_wizard.check_ids, tracked_check_ids_to_do) # Checks related to picked products appear regardless of checking while Validating the picking # or just doing the Quality Checks without validating the picking. move_tracked_product_a.quantity = 1 move_tracked_product_a._generate_serial_numbers("1", next_serial_count=1) self.assertFalse(move_tracked_product_a.picked) qc_wizard = Form.from_action(self.env, picking_in.check_quality()).save() self.assertEqual(qc_wizard.check_ids.lot_name, move_tracked_product_b.move_line_ids[1].lot_name) # Validate incoming shipment, the same QCs should appear. wizard = Form.from_action(self.env, picking_in.button_validate()).save() qc_wizard = Form.from_action(self.env, wizard.process()).save() self.assertEqual(qc_wizard.check_ids, tracked_check_ids_to_do) qc_wizard.do_pass() backorder = picking_in.backorder_ids self.assertEqual(picking_in.state, 'done') self.assertEqual(len(backorder.check_ids), 1) backorder.check_ids.do_pass() self.assertTrue(backorder.check_ids.quality_state, 'pass') def test_quality_check_with_scrapped_moves(self): """ Test that a quality check is not created for scrapped moves. """ self.env['quality.point'].create({ 'picking_type_ids': [Command.link(self.picking_type_id)], 'test_type_id': self.env.ref('quality_control.test_type_measure').id, 'measure_on': 'operation', }) self.product_3.is_storable = True # Create incoming shipment. picking_in = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) self.env['stock.move'].create({ 'product_id': self.product_3.id, 'product_uom_qty': 2, 'product_uom': self.product_3.uom_id.id, 'picking_id': picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) # Confirm incoming shipment. picking_in.action_confirm() # Check Quality Check for incoming shipment is created self.assertEqual(len(picking_in.check_ids), 1) # open the wizard to do the checks action = picking_in.check_ids.action_open_quality_check_wizard() wizard = Form(self.env[action['res_model']].with_context(action['context'])).save() self.assertEqual(len(wizard.check_ids), 1) wizard.do_fail() self.assertEqual(picking_in.check_ids.quality_state, 'fail') scrap = self.env['stock.scrap'].create({ 'picking_id': picking_in.id, 'product_id': self.product_3.id, 'product_uom_id': self.product_3.uom_id.id, 'scrap_qty': 5.0, }) scrap.do_scrap() self.assertEqual(len(picking_in.move_ids), 2) self.assertEqual(len(picking_in.check_ids), 1) def test_qc_by_product_with_partial_reception(self): """ Test that a new quality check is created for the backorder. If splitting before validating, test that the old QC remains. """ self.env['quality.point'].create({ 'picking_type_ids': [self.picking_type_id], 'test_type_id': self.env.ref('quality_control.test_type_measure').id, 'measure_on': 'product', }) picking_in = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'partner_id': self.partner_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) move = self.env['stock.move'].create({ 'product_id': self.product_2.id, 'product_uom_qty': 10, 'product_uom': self.product_2.uom_id.id, 'picking_id': picking_in.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id}) picking_in.action_confirm() self.assertEqual(len(picking_in.check_ids), 1) self.assertTrue(picking_in.quality_check_todo) move.quantity = 5 move.picked = True # validate the incoming picking and create a backorder action_quality_check = Form.from_action(self.env, picking_in.button_validate()).save().process() # Confirm the quality check wizard Form.from_action(self.env, action_quality_check).save().do_pass() # Check that the first quality check is still linked to the first picking self.assertEqual(len(picking_in.check_ids), 1) self.assertEqual(picking_in.check_ids.quality_state, 'pass') # Make sure that the backorder is correctly created and that it has a quality check backorder = picking_in.backorder_ids self.assertEqual(len(backorder.check_ids), 1) # Check that splitting the backorder doesn't remove the check backorder.move_ids.quantity = 3 backorder.move_ids.picked = True backorder.action_split_transfer() self.assertEqual(len(backorder.check_ids), 1) backorder.check_ids.do_pass() self.assertEqual(backorder.check_ids.quality_state, 'pass') # Check that the new-new backorder has its own quality check backorder_2 = backorder.backorder_ids self.assertEqual(len(backorder_2.check_ids), 1) self.assertNotIn(backorder_2.check_ids, (picking_in + backorder).check_ids) def test_quality_check_on_receipt_with_additional_move_lines(self): """ Check that adding a move line for a new product to an already assigned receipt will generate the associated quality check. """ # Create Quality Point for incoming shipments. self.env['quality.point'].create([ { 'title': "Check product 2", 'measure_on': "operation", 'product_ids': [Command.link(self.product_2.id)], 'picking_type_ids': [Command.link(self.picking_type_id)], }, ]) receipt = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, 'move_ids': [Command.create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, })], }) receipt.action_confirm() self.assertRecordValues(receipt, [{ 'state': 'assigned', 'quality_check_todo': False, }]) # create a move line as you would from the detailed operation self.env['stock.move.line'].create({ 'product_id': self.product_2.id, 'picking_id': receipt.id, 'quantity': 3, }) # try to validate the receipt to be warned that you still need to process some quality checks self.assertRecordValues(receipt.move_ids.sorted('quantity'), [ {'product_id': self.product.id, 'quantity': 2.0, 'state': 'assigned'}, {'product_id': self.product_2.id, 'quantity': 3.0, 'state': 'assigned'}, ]) receipt.invalidate_recordset() self.assertRecordValues(receipt, [{ 'state': 'assigned', 'quality_check_todo': True, }]) self.assertEqual(len(receipt.check_ids), 1) receipt.check_ids.do_pass() receipt.button_validate() self.assertEqual(len(receipt.check_ids), 1) def test_quality_check_on_delivery_with_additional_move_lines(self): """ Check that adding a move line for a new product to an already assigned delivery will generate the associated quality check. """ out_type_id = self.env.ref('stock.warehouse0').out_type_id # Create Quality Point for outgoing shipments. self.env['quality.point'].create([ { 'title': "Check product 2", 'measure_on': "operation", 'product_ids': [Command.link(self.product_2.id)], 'picking_type_ids': [Command.link(out_type_id.id)], }, ]) delivery = self.env['stock.picking'].create({ 'picking_type_id': out_type_id.id, 'location_id': self.location_dest_id, 'location_dest_id': self.ref('stock.stock_location_customers'), 'move_ids': [Command.create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'location_id': self.location_dest_id, 'location_dest_id': self.ref('stock.stock_location_customers'), })], }) delivery.action_confirm() self.assertRecordValues(delivery, [{ 'state': 'assigned', 'quality_check_todo': False, }]) # create a move line as you would from the detailed operation self.env['stock.move.line'].create({ 'product_id': self.product_2.id, 'picking_id': delivery.id, 'quantity': 3, }) # try to validate the delivery to be warned that you still need to process some quality checks self.assertRecordValues(delivery.move_ids.sorted('quantity'), [ {'product_id': self.product.id, 'quantity': 2.0, 'state': 'assigned'}, {'product_id': self.product_2.id, 'quantity': 3.0, 'state': 'assigned'}, ]) delivery.invalidate_recordset() self.assertRecordValues(delivery, [{ 'state': 'assigned', 'quality_check_todo': True, }]) self.assertEqual(len(delivery.check_ids), 1) delivery.check_ids.do_pass() delivery.button_validate() self.assertEqual(len(delivery.check_ids), 1) def test_zero_qty_failed_quantity_check(self): """ Quality point per quantity, a picking with 2 products / moves, fail one move with zero qty, and it pass all the qty. """ self.env['quality.point'].create({ 'picking_type_ids': [Command.link(self.picking_type_id)], 'product_ids': [Command.link(self.product.id)], 'measure_on': 'move_line', 'test_type_id': self.env.ref('quality_control.test_type_passfail').id, }) self.receipt.action_confirm() self.assertEqual(len(self.receipt.check_ids), 1) # open the wizard to do the checks action = self.receipt.check_ids.action_open_quality_check_wizard() wizard = self.env[action['res_model']].with_context(action['context']).create({}) self.assertEqual(wizard.current_check_id.move_line_id, self.product_move.move_line_ids) action = wizard.do_fail() wizard = self.env[action['res_model']].with_context(action['context']).browse(action['res_id']) self.assertEqual(wizard.qty_failed, 2) # zero qty fail wizard.qty_failed = 0 wizard.confirm_fail() # there should be 2 moves and 1 checks self.assertEqual(len(self.receipt.check_ids), 1) self.assertEqual(len(self.receipt.move_ids), 2) self.assertRecordValues(self.receipt.check_ids, [ {'quality_state': 'pass', 'product_id': self.product.id, 'qty_line': 2}, ]) def test_quality_check_creation_multi_company(self): """ Test confirming a new delivery when the picking and environment company differ. Test that the quality check can be created by using the picking's company. """ out_picking_type = self.env['stock.picking.type'].search([('company_id', '=', self.env.company.id), ('code', '=', 'outgoing')], limit=1) self.env['quality.point'].create([ { 'title': "Delivery QCP", 'picking_type_ids': out_picking_type.ids, }, { 'measure_on': 'move_line', 'title': "Delivery QCP", 'picking_type_ids': out_picking_type.ids, }, ]) customer_location = self.env['stock.location'].search([('usage', '=', 'customer')], limit=1) picking_out = self.env['stock.picking'].create({ 'picking_type_id': out_picking_type.id, 'partner_id': self.partner_id, 'location_id': out_picking_type.default_location_src_id.id, 'location_dest_id': customer_location.id, 'move_ids': [Command.create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'product_uom': self.product.uom_id.id, 'location_id': out_picking_type.default_location_src_id.id, 'location_dest_id': customer_location.id, })], }) # Force env.company to be different from the picking's company new_company = self.env['res.company'].create({ 'name': "New Company", }) picking_out.with_company(new_company).action_confirm() self.assertRecordValues(picking_out, [{ 'state': 'assigned', 'quality_check_todo': True, }]) self.assertTrue(picking_out.check_ids) def test_receipt_validation_triggers_serial_number_label_print(self): """ Ensure that the 'do_multi_print' action is trigger after quality check wizard validation when the operation's auto_print_lot_labels is activate and Serial Number is set on the product """ self.env.user.group_ids |= self.env.ref('stock.group_production_lot') picking_type = self.env['stock.picking.type'].browse(self.picking_type_id) picking_type.auto_print_lot_labels = True self.product.write({ 'is_storable': True, 'tracking': 'serial', }) self.env['quality.point'].create({ 'picking_type_ids': [Command.link(picking_type.id)], 'product_ids': [Command.link(self.product.id)], 'measure_on': 'product', 'test_type_id': self.ref('quality_control.test_type_passfail') }) receipts = self.env['stock.picking'].create([{ 'picking_type_id': picking_type.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, 'move_ids': [Command.create({ 'product_id': product.id, 'product_uom_qty': 1, 'product_uom': product.uom_id.id, })] } for product in [self.product, self.product_2]]) receipts[0].action_confirm() ml = receipts[0].move_ids.move_line_ids ml.write({ 'quantity': 1, 'lot_name': '1457', }) self.assertEqual(ml.lot_name, '1457') action_quality_check = Form.from_action(self.env, receipts.button_validate()).save() validate_res = action_quality_check.do_pass() self.assertEqual(validate_res.get('type'), 'ir.actions.client') self.assertEqual(validate_res.get('tag', False), 'do_multi_print') self.assertRecordValues(receipts, [ {'state': 'done'}, {'state': 'done'} ]) def test_product_quality_point_smart_button_count(self): """ Archived QCP should not be included in the product smart button count """ quality_point = self.env['quality.point'].create({ 'picking_type_ids': [Command.link(self.picking_type_id)], }) self.assertEqual(self.product.quality_control_point_qty, 1) quality_point.active = False self.product.invalidate_recordset(fnames=['quality_control_point_qty']) self.assertEqual(self.product.quality_control_point_qty, 0) def test_quality_point_stat_button_from_product_view(self): """ Ensure that the smart button on the product form view opens the quality points action with the some views configured. """ quality_point = self.env['quality.point'].create({ 'product_ids': [Command.link(self.product.id)], 'measure_on': 'product', 'product_category_ids': [Command.link(self.product_category_base.id)] }) action = self.product.action_see_quality_control_points() self.assertTrue(action['views'][0][0]) result = self.env['quality.point'].search(action['domain']) self.assertEqual(result, quality_point) def test_fail_quality_check_per_qty_for_serial_product(self): self.product.write({ 'is_storable': True, 'tracking': 'serial', }) self.env['quality.point'].create({ 'picking_type_ids': [Command.link(self.picking_type_id)], 'product_ids': self.product.ids, 'measure_on': 'move_line', 'test_type_id': self.ref('quality_control.test_type_passfail') }) self.receipt.action_confirm() # There are two check since the product are tracked (one per product) self.assertEqual(len(self.receipt.check_ids), 2) wizard = self.receipt.check_ids.action_open_quality_check_wizard() quality_check = Form.from_action(self.env, wizard).save() next_wizard = quality_check.do_fail() next_quality_check = Form.from_action(self.env, next_wizard).save() next_quality_check.do_fail() self.assertRecordValues(self.receipt.check_ids, [ {'quality_state': 'fail', 'product_id': self.product.id, 'qty_failed': 1.0}, {'quality_state': 'fail', 'product_id': self.product.id, 'qty_failed': 1.0}, ]) def test_no_error_for_tracked_product_quality_check(self): """Test that Quality Checks does not raise a UserError in 2 cases: - A tracked product's lot/serial is not set but its quality point is not by quantity (measure_on != 'move_line') - A quality point (measure_on='move_line') is configured for a non-tracked product with a tracked product whose lot is not set in the same picking """ self.product.write({ 'tracking': 'serial', 'is_storable': True, }) self.env['quality.point'].create([{ 'picking_type_ids': [Command.link(self.picking_type_id)], 'measure_on': 'product', 'test_type_id': self.env.ref('quality_control.test_type_passfail').id, }, { 'product_ids': [Command.link(self.product_2.id)], 'picking_type_ids': [Command.link(self.picking_type_id)], 'measure_on': 'move_line', 'test_type_id': self.env.ref('quality_control.test_type_passfail').id, }]) picking = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, 'move_ids': [Command.create({ 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, }) for p in [self.product, self.product_2]], }) picking.action_confirm() qc_wizard = Form.from_action(self.env, picking.check_quality()) quality_check = qc_wizard.save() self.assertEqual(len(quality_check.check_ids), 3) def test_partial_quantity_failure_split(self): """Test quantity failure splits move quantities correctly. CASE-1: With total qty = 5 and failed qty = 3: - Passed move: product_uom_qty = 2, move_line quantity = 2 - Failed move: product_uom_qty = 3, move_line quantity = 3 CASE-2: Failed qty > total qty (failed = 6): - Passed move: product_uom_qty = 0, move_line quantity = 0 - Failed move: product_uom_qty = 5, move_line quantity = 6 """ self.env['quality.point'].create({ 'picking_type_ids': [Command.link(self.picking_type_id)], 'measure_on': 'move_line', 'failure_location_ids': [Command.link(self.failure_location.id)], }) self.product.is_storable = True receipt1, receipt2 = self.env['stock.picking'].create([ { 'picking_type_id': self.picking_type_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, 'move_ids': [Command.create({ 'product_id': self.product.id, 'product_uom_qty': 5, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, })], } for _ in range(2) ]) for picking, failed_qty in [(receipt1, 3), (receipt2, 6)]: picking.action_confirm() wizard_action = picking.check_ids.action_open_quality_check_wizard() wizard = self.env[wizard_action['res_model']].with_context(wizard_action['context']).create({}) fail_action = wizard.do_fail() fail_wizard = self.env[fail_action['res_model']].with_context(fail_action['context']).browse(fail_action['res_id']) fail_wizard.qty_failed = failed_qty fail_wizard.confirm_fail() self.assertRecordValues(receipt1.move_ids, [ {'product_id': self.product.id, 'product_uom_qty': 2, 'quantity': 2}, {'product_id': self.product.id, 'product_uom_qty': 3, 'quantity': 3}, ]) self.assertRecordValues(receipt2.move_ids, [ {'product_id': self.product.id, 'product_uom_qty': 0, 'quantity': 0}, {'product_id': self.product.id, 'product_uom_qty': 5, 'quantity': 6}, ]) def test_quality_point_quantity_fail_pass_flow(self): """ Test failed quantities move to the failure location and passed quantities move to the original destination. Case 1: Fail 1 unit (goes to failure location), then pass 1 unit (goes to original destination) Case 2: Pass 1 unit (goes to original destination), then fail 2 units (goes to failure location) """ self.env['quality.point'].create({ 'picking_type_ids': [Command.link(self.picking_type_id)], 'measure_on': 'move_line', 'test_type_id': self.env.ref('quality_control.test_type_passfail').id, 'failure_location_ids': [Command.link(self.failure_location.id)], }) receipt = self.env['stock.picking'].create({ 'picking_type_id': self.picking_type_id, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, 'move_ids': [Command.create({ 'product_id': self.product.id, 'product_uom_qty': 2, 'location_id': self.location_id, 'location_dest_id': self.location_dest_id, })], }) receipt2 = receipt.copy() (receipt | receipt2).action_confirm() receipt2.move_ids.quantity = 1 receipt2.check_ids.do_pass() receipt2.move_ids.quantity = 3 receipt.move_ids.quantity = 1 for incoming_picking in (receipt, receipt2): action = incoming_picking.check_ids.filtered(lambda c: c.quality_state == 'none').action_open_quality_check_wizard() fail_wizard = self.env[action['res_model']].with_context(action['context']).create({}) fail_action = fail_wizard.do_fail() self.env[fail_action['res_model']].with_context(fail_action['context']).browse(fail_action['res_id']).confirm_fail() receipt.move_ids.quantity = 2 receipt.check_ids.filtered(lambda c: c.quality_state == 'none').do_pass() self.assertRecordValues(receipt.move_ids.move_line_ids, [ {'quantity': 1, 'location_dest_id': self.failure_location.id}, {'quantity': 1, 'location_dest_id': self.location_dest_id}, ]) self.assertRecordValues(receipt2.move_ids.move_line_ids, [ {'quantity': 1, 'location_dest_id': self.location_dest_id}, {'quantity': 2, 'location_dest_id': self.failure_location.id}, ])