# Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 import logging from odoo import Command, fields, http from odoo.tests import patch, tagged from odoo.tools.misc import file_open from odoo.addons.stock_barcode.tests.test_barcode_client_action import TestBarcodeClientAction from odoo.addons.stock_barcode.controllers.stock_barcode import StockBarcodeController _logger = logging.getLogger(__name__) @tagged('post_install', '-at_install') class TestInventoryAdjustmentBarcodeClientAction(TestBarcodeClientAction): # === HELPER METHODS ===# def create_serial_numbers(self, product, start_index, quantity=1, prefix='sn'): return self.env['stock.lot'].create([{ 'name': f'{prefix}{start_index + n}', 'product_id': product.id, 'company_id': self.env.company.id, } for n in range(quantity)]) # === TESTS ===# def test_inventory_adjustment(self): """ Simulate the following actions: - Open the inventory from the barcode app. - Scan twice the product 1. - Edit the line. - Add a product with the form view. - Validate """ self.start_tour("/odoo/barcode", 'test_inventory_adjustment', login='admin', timeout=180) inventory_moves = self.env['stock.move'].search([('product_id', 'in', [self.product1.id, self.product2.id]), ('is_inventory', '=', True)]) self.assertEqual(len(inventory_moves), 2) self.assertEqual(inventory_moves.mapped('quantity'), [2.0, 2.0]) self.assertEqual(inventory_moves.mapped('state'), ['done', 'done']) quants = self.env['stock.quant'].search([('product_id', 'in', [self.product1.id, self.product2.id]), ('location_id.usage', '=', 'internal')]) self.assertEqual(quants.mapped('quantity'), [2.0, 2.0]) self.assertEqual(quants.mapped('inventory_quantity'), [0, 0]) self.assertEqual(quants.mapped('inventory_diff_quantity'), [0, 0]) def test_inventory_adjustment_dont_update_location(self): """ Ensures the existing quants location cannot be update.""" self.env.user.write({'group_ids': [Command.link(self.env.ref('stock.group_stock_multi_locations').id)]}) self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'True') # Adds some quants and request a count. # Adds quants for the same product in two locations. self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 5) self.env['stock.quant']._update_available_quantity(self.product1, self.shelf2, 5) # Marks them as to count by the user. quants = self.env['stock.quant'].search([('product_id', '=', self.product1.id)]) quants.user_id = self.env.user quants.inventory_quantity_set = True quants.inventory_date = fields.Date.today() self.start_tour("/odoo/barcode", 'test_inventory_adjustment_dont_update_location', login='admin', timeout=180) quants = self.env['stock.quant'].search([ ('product_id', '=', self.product1.id), ('location_id.usage', '=', 'internal'), ]) self.assertRecordValues(quants, [ {'location_id': self.stock_location.id, 'quantity': 1}, {'location_id': self.shelf2.id, 'quantity': 0}, {'location_id': self.shelf1.id, 'quantity': 1}, ]) def test_inventory_adjustment_multi_company(self): """ When doing an Inventory Adjustment in Barcode: - ensures only products belonging to the current company or to no company can be scanned; - ensures that only request counts of the current company are displayed in the Barcode main menu. """ # Creates two companies and assign them to the user. company_a = self.env['res.company'].create({'name': 'Comp A - F2 FTW'}) company_b = self.env['res.company'].create({'name': 'Comp B - F3 Wee-Wee Pool'}) self.env.user.write({ 'company_ids': [(4, company_a.id), (4, company_b.id)], 'company_id': company_a.id, }) # Changes the company of some products. self.product1.company_id = company_a self.product2.company_id = company_b product_no_company = self.env['product.product'].create({ 'name': 'Company-less Product', 'is_storable': True, 'barcode': 'product_no_company', }) # Create a quant, then request_count in the company A. company_a_location = self.env['stock.location'].search([('company_id', '=', company_a.id), ('usage', '=', 'internal')], limit=1) self.env['stock.quant']._update_available_quantity(product_no_company, company_a_location, 2) wizard_request_count = self.env['stock.request.count'].create({ 'user_id': self.env.user.id, 'quant_ids': product_no_company.stock_quant_ids.ids, 'show_expected_quantity': True, }) wizard_request_count.action_request_count() self.start_tour("/odoo", 'test_inventory_adjustment_multi_company', login='admin', timeout=180) # Checks an inventory adjustment was correctly validated for each company. inventory_moves = self.env['stock.move'].search([ ('is_inventory', '=', True), ('product_id', 'in', (self.product1 | self.product2 | product_no_company).ids), ]) self.assertRecordValues(inventory_moves.sorted(lambda mv: (mv.product_id.id, mv.id)), [ {'product_id': self.product1.id, 'quantity': 1, 'company_id': company_a.id}, {'product_id': self.product2.id, 'quantity': 1, 'company_id': company_b.id}, {'product_id': product_no_company.id, 'quantity': 1, 'company_id': company_a.id}, {'product_id': product_no_company.id, 'quantity': 1, 'company_id': company_b.id}, ]) def test_inventory_adjustment_multi_location(self): """ Simulate the following actions: - Generate those lines with scan: WH/stock product1 qty: 2 WH/stock product2 qty: 1 WH/stock/shelf1 product2 qty: 1 WH/stock/shelf2 product1 qty: 1 - Validate """ self.env.user.write({'group_ids': [Command.link(self.env.ref('stock.group_stock_multi_locations').id)]}) self.start_tour("/odoo/barcode", 'test_inventory_adjustment_multi_location', login='admin', timeout=180) inventory_moves = self.env['stock.move'].search([('product_id', 'in', [self.product1.id, self.product2.id]), ('is_inventory', '=', True)]) self.assertEqual(len(inventory_moves), 4) self.assertEqual(inventory_moves.mapped('state'), ['done', 'done', 'done', 'done']) inventory_move_in_WH_stock = inventory_moves.filtered(lambda l: l.location_dest_id == self.stock_location) self.assertEqual(set(inventory_move_in_WH_stock.mapped('product_id')), {self.product1, self.product2}) self.assertEqual(inventory_move_in_WH_stock.filtered(lambda l: l.product_id == self.product1).quantity, 2.0) self.assertEqual(inventory_move_in_WH_stock.filtered(lambda l: l.product_id == self.product2).quantity, 1.0) inventory_move_in_shelf1 = inventory_moves.filtered(lambda l: l.location_dest_id == self.shelf1) self.assertEqual(len(inventory_move_in_shelf1), 1) self.assertEqual(inventory_move_in_shelf1.product_id, self.product2) self.assertEqual(inventory_move_in_shelf1.quantity, 1.0) inventory_move_in_shelf2 = inventory_moves.filtered(lambda l: l.location_dest_id == self.shelf2) self.assertEqual(len(inventory_move_in_shelf2), 1) self.assertEqual(inventory_move_in_shelf2.product_id, self.product1) self.assertEqual(inventory_move_in_shelf2.quantity, 1.0) def test_inventory_adjustment_tracked_product(self): """ Simulate the following actions: - Generate those lines with scan: productlot1 with a lot named lot1 (qty 2) productserial1 with serial1 (qty 1) productserial1 with serial2 (qty 1) productserial1 with serial3 (qty 1) productserial1 without serial (qty 1) productlot1 with a lot named lot2 (qty 1) productlot1 with a lot named lot3 (qty 1) - Validate """ self.env.user.write({'group_ids': [Command.link(self.env.ref('stock.group_production_lot').id)]}) self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'True') self.start_tour("/odoo/barcode", 'test_inventory_adjustment_tracked_product', login='admin', timeout=180) inventory_moves = self.env['stock.move'].search([('product_id', 'in', [self.productlot1.id, self.productserial1.id]), ('is_inventory', '=', True)]) self.assertEqual(len(inventory_moves), 7) self.assertTrue(all(s == 'done' for s in inventory_moves.mapped('state'))) moves_with_lot = inventory_moves.filtered(lambda l: l.product_id == self.productlot1) moves_with_sn = inventory_moves.filtered(lambda l: l.product_id == self.productserial1) mls_with_lot = moves_with_lot.move_line_ids mls_with_sn = moves_with_sn.move_line_ids self.assertEqual(len(mls_with_lot), 3) self.assertEqual(len(mls_with_sn), 4) self.assertEqual(mls_with_lot.mapped('lot_id.name'), ['lot1', 'lot2', 'lot3']) self.assertEqual(mls_with_lot.filtered(lambda ml: ml.lot_id.name == 'lot1').qty_done, 3) self.assertEqual(mls_with_lot.filtered(lambda ml: ml.lot_id.name == 'lot2').qty_done, 1) self.assertEqual(mls_with_lot.filtered(lambda ml: ml.lot_id.name == 'lot3').qty_done, 1) self.assertEqual(set(mls_with_sn.mapped('lot_id.name')), {'serial1', 'serial2', 'serial3'}) self.assertFalse(moves_with_sn.lot_ids.company_id) def test_inventory_adjustment_tracked_product_multilocation(self): """ This test ensures two things: - When the user has to count the same lot from multiple locations, the right line will be incremented when they scan the lot's barcode, depending of the previous scanned location. - When scanning a tracked product, if this product alread has quants, it will retrieve and create a barcode line for each quant. """ self.env.user.write({ 'group_ids': [ Command.link(self.env.ref('stock.group_production_lot').id), Command.link(self.env.ref('stock.group_stock_multi_locations').id), Command.link(self.env.ref('stock.group_tracking_lot').id), ], }) self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'True') # Adds some quants for the product tracked by lots in two locations. lot_1 = self.env['stock.lot'].create({'product_id': self.productlot1.id, 'name': "lot1"}) self.env['stock.quant']._update_available_quantity(self.productlot1, self.shelf1, 3, lot_id=lot_1) self.env['stock.quant']._update_available_quantity(self.productlot1, self.shelf2, 5, lot_id=lot_1) # Marks them as to count by the user. quants = self.env['stock.quant'].search([('product_id', '=', self.productlot1.id)]) quants.user_id = self.env.user quants.inventory_quantity_set = True quants.inventory_date = fields.Date.today() quants[0].inventory_quantity = 3 quants[1].inventory_quantity = 0 # Adds some quants for the product tracked by serial numbers in two locations. for location, numbers in [(self.shelf1, [1, 2, 3]), (self.shelf3, [4, 5])]: for n in numbers: serial_number = self.env['stock.lot'].create({'product_id': self.productserial1.id, 'name': f'sn{n}'}) self.env['stock.quant']._update_available_quantity(self.productserial1, location, 1, lot_id=serial_number) # Opens the inventory adjustement and process it. self.start_tour("/odoo/barcode", "test_inventory_adjustment_tracked_product_multilocation", login="admin", timeout=180) def test_inventory_adjustment_tracked_product_permissive_quants(self): """Make an inventory adjustment for a product tracked by lot having quants without lot. The following actions are made in the barcode app: - Scan productlot1 - Scan lot1 twice - Set productlot1 quantity without lot to available - Validate """ self.env.user.write({'group_ids': [Command.link(self.env.ref('stock.group_production_lot').id)]}) self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'True') lot1 = self.env["stock.lot"].create({ 'name': 'lot1', 'product_id': self.productlot1.id, }) self.env['stock.quant']._update_available_quantity(self.productlot1, self.stock_location, 5) self.start_tour("/odoo/barcode", 'test_inventory_adjustment_tracked_product_permissive_quants', login='admin', timeout=180) inventory_moves = self.env['stock.move'].search([('product_id', '=', self.productlot1.id), ('is_inventory', '=', True)]) inventory_location = self.productlot1.property_stock_inventory self.assertEqual(len(inventory_moves), 2) self.assertEqual(inventory_moves.mapped('state'), ['done', 'done']) self.assertEqual(inventory_moves.product_id, self.productlot1) self.assertEqual(len(inventory_moves.move_line_ids), 2) self.assertRecordValues(inventory_moves.move_line_ids, [ {'quantity': 5, 'location_id': self.stock_location.id, 'location_dest_id': inventory_location.id, 'lot_id': False}, {'quantity': 2, 'location_id': inventory_location.id, 'location_dest_id': self.stock_location.id, 'lot_id': lot1.id}, ]) def test_inventory_create_quant(self): """ Creates a quant and checks it will not be deleted until the inventory was validated. """ Quant = self.env['stock.quant'] self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'True') self.start_tour("/odoo/barcode", 'test_inventory_create_quant', login='admin', timeout=180) Quant._unlink_zero_quants() product1_quant = Quant.search([('product_id', '=', self.product1.id)]) self.assertEqual(len(product1_quant), 1) self.assertEqual(product1_quant.inventory_quantity, 0.0) # Validates the inventory, then checks there is no 0 qty quant. product1_quant.action_validate() Quant.invalidate_model() # Updates the cache. Quant._unlink_zero_quants() product1_quant = Quant.search([('product_id', '=', self.product1.id)]) self.assertEqual(len(product1_quant), 0) def test_inventory_dialog_not_counted_serial_numbers(self): """ This test ensures when some SN were counted in a location, if there is uncounted SN in this location, a dialog asks the user if they want to count them as missing when the inventory adjustment is applied. """ self.env['ir.config_parameter'].set_param('stock_barcode.barcode_separator_regex', '[,;]') self.env.user.write({ 'group_ids': [ Command.link(self.env.ref('stock.group_production_lot').id), Command.link(self.env.ref('stock.group_stock_multi_locations').id), ], }) Quant = self.env['stock.quant'] # Creates some serial numbers and adds them in the stock. productserial2 = self.env['product.product'].create({ 'name': 'productserial2', 'is_storable': True, 'categ_id': self.env.ref('product.product_category_goods').id, 'barcode': 'productserial2', 'tracking': 'serial', }) serial1_sns = self.create_serial_numbers(self.productserial1, 1, 6) serial2_sns = self.create_serial_numbers(productserial2, 1, 3) # Adds 3 SNs in Shelf 1 and adds 6 SNs in Shelf 2. for sn in serial1_sns[:3]: Quant._update_available_quantity(sn.product_id, self.shelf1, 1, lot_id=sn) for sn in (serial1_sns[3:] | serial2_sns): Quant._update_available_quantity(sn.product_id, self.shelf2, 1, lot_id=sn) # Marks created quants as to count. quants = Quant.search([('product_id', 'in', [self.productserial1.id, productserial2.id])]) wizard_request_count = self.env['stock.request.count'].create({ 'user_id': self.env.user.id, 'quant_ids': quants.ids, 'show_expected_quantity': True, }) wizard_request_count.action_request_count() self.start_tour("/odoo/barcode", 'test_inventory_dialog_not_counted_serial_numbers', login='admin') self.assertRecordValues(quants, [ {'product_id': self.productserial1.id, 'lot_id': serial1_sns[0].id, 'quantity': 1, 'location_id': self.shelf1.id}, {'product_id': self.productserial1.id, 'lot_id': serial1_sns[1].id, 'quantity': 1, 'location_id': self.shelf1.id}, {'product_id': self.productserial1.id, 'lot_id': serial1_sns[2].id, 'quantity': 0, 'location_id': self.shelf1.id}, {'product_id': self.productserial1.id, 'lot_id': serial1_sns[3].id, 'quantity': 1, 'location_id': self.shelf2.id}, {'product_id': self.productserial1.id, 'lot_id': serial1_sns[4].id, 'quantity': 1, 'location_id': self.shelf2.id}, {'product_id': self.productserial1.id, 'lot_id': serial1_sns[5].id, 'quantity': 1, 'location_id': self.shelf2.id}, {'product_id': productserial2.id, 'lot_id': serial2_sns[0].id, 'quantity': 1, 'location_id': self.shelf2.id}, {'product_id': productserial2.id, 'lot_id': serial2_sns[1].id, 'quantity': 1, 'location_id': self.shelf2.id}, {'product_id': productserial2.id, 'lot_id': serial2_sns[2].id, 'quantity': 1, 'location_id': self.shelf2.id}, ]) def test_inventory_image_visible_for_quant(self): """ Ensure the product's image is visible in the Barcode quant form view.""" # Load an image and use it for a product. img_url = 'stock_barcode/static/img/barcode.png' img_content = base64.b64encode(file_open(img_url, "rb").read()) self.product1.image_1920 = img_content # Creates two quants to count. self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 4) self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 4) quants = self.env['stock.quant'].search([('product_id', 'in', [self.product1.id, self.product2.id])]) wizard_request_count = self.env['stock.request.count'].create({ 'user_id': self.env.user.id, 'quant_ids': quants.ids, 'show_expected_quantity': False, }) wizard_request_count.action_request_count() self.start_tour("/odoo/barcode/", 'test_inventory_image_visible_for_quant', login='admin', timeout=180) def test_inventory_nomenclature(self): """ Simulate scanning a product and its weight thanks to the barcode nomenclature """ self.env.company.nomenclature_id = self.env.ref('barcodes.default_barcode_nomenclature') product_weight = self.env['product.product'].create({ 'name': 'product_weight', 'is_storable': True, 'barcode': '2145631000000', }) self.start_tour("/odoo/barcode", 'test_inventory_nomenclature', login='admin', timeout=180) quantity = self.env['stock.move.line'].search([ ('product_id', '=', product_weight.id), ('state', '=', 'done'), ('location_id', '=', product_weight.property_stock_inventory.id), ]) self.assertEqual(quantity.qty_done, 12.35) def test_inventory_package(self): """ Simulate an adjustment where a package is scanned and edited """ self.env.user.write({'group_ids': [Command.link(self.env.ref('stock.group_tracking_lot').id)]}) parent_pack = self.env['stock.package'].create({ 'name': 'SUPERPACK', }) pack, pack2 = self.env['stock.package'].create([{ 'name': f'PACK00{i + 1}', 'parent_package_id': parent_pack.id, } for i in range(2) ]) self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 7, package_id=pack) self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 3, package_id=pack) self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 3, package_id=pack2) # Request count for these quants quants_to_count = self.env['stock.quant'].search([('package_id', 'in', pack.ids + pack2.ids)]) request_wizard = self.env['stock.request.count'].create({ 'quant_ids': quants_to_count.ids, 'show_expected_quantity': True, }) request_wizard.action_request_count() self.start_tour("/odoo/barcode", "test_inventory_package", login="admin", timeout=180) # Check the package is updated after adjustment self.assertRecordValues(quants_to_count, [ {'product_id': self.product1.id, 'quantity': 7.0, 'package_id': pack.id}, {'product_id': self.product2.id, 'quantity': 21.0, 'package_id': pack.id}, {'product_id': self.product1.id, 'quantity': 3.0, 'package_id': pack2.id}, ]) def test_inventory_packaging(self): """ Scans a product's packaging and ensures its quantity is correctly counted regardless if there is already products in stock or not. """ self.env.user.write({'group_ids': [Command.link(self.env.ref('uom.group_uom').id)]}) self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'True') pack_15 = self.env['uom.uom'].create({ 'name': 'Pack of 15', 'relative_factor': 15, 'relative_uom_id': self.env.ref('uom.product_uom_unit').id, }) self.product1.uom_ids = pack_15 self.env['product.uom'].create({ 'uom_id': pack_15.id, 'product_id': self.product1.id, 'barcode': 'pack007', }) self.start_tour("/odoo/barcode", 'test_inventory_packaging', login='admin', timeout=180) self.assertEqual(self.product1.qty_available, 15.0) def test_inventory_serial_product_packaging(self): """ This test ensures that correct packaging lines generated for serial product in inventory adjustments. """ self.env.user.write({ 'group_ids': [ Command.link(self.env.ref('stock.group_production_lot').id), Command.link(self.env.ref('uom.group_uom').id), ], }) pack_3 = self.env['uom.uom'].create({ 'name': 'Product Serial 1 Packaging', 'relative_factor': 3, 'relative_uom_id': self.env.ref('uom.product_uom_unit').id, }) self.productserial1.uom_ids = pack_3 self.env['product.uom'].create({ 'uom_id': pack_3.id, 'product_id': self.productserial1.id, 'barcode': 'PCK3', }) self.start_tour('/odoo/barcode', 'test_inventory_serial_product_packaging', login='admin', timeout=180) def test_inventory_packaging_button(self): """ Check that the product packaging button are correctly dipslayed on the digipad when creating an invetory adjustment. """ self.product1.name = "Lovely Product" self.product1.uom_ids = self.env['uom.uom'].create({ 'name': 'LP x15', 'relative_factor': 15, 'relative_uom_id': self.env.ref('uom.product_uom_unit').id, }) self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'True') self.start_tour("/odoo/barcode", "test_inventory_packaging_button", login="admin", timeout=180) quant = self.env['stock.quant'].search([("product_id", "=", self.product1.id)], limit=1) self.assertEqual(quant.inventory_quantity, 15.0) def test_inventory_packaging_location(self): """ Check that scanning a packaging on the barcode main screen shows the location of the product contained in that packaging. """ self.env.user.write({'group_ids': [Command.link(self.env.ref('uom.group_uom').id)]}) self.env['product.uom'].create({ 'product_id': self.product1.id, 'uom_id': self.env.ref('uom.product_uom_dozen').id, 'barcode': 'DOZENPACK001', }) self.env['stock.quant']._update_available_quantity(self.product1, self.shelf1, 40.0) self.env['stock.quant']._update_available_quantity(self.product1, self.shelf2, 80.0) self.start_tour("/odoo/barcode", "test_inventory_packaging_location", login="admin") # Relaunch the same tour with a mobile device config. self.browser_size = '375x667' self.touch_enabled = True self.start_tour("/odoo/barcode", "test_inventory_packaging_location", login="admin") def test_inventory_owner_scan_package(self): group_owner = self.env.ref('stock.group_tracking_owner') group_pack = self.env.ref('stock.group_tracking_lot') self.env.user.write({'group_ids': [(4, group_pack.id, 0)]}) self.env.user.write({'group_ids': [(4, group_owner.id, 0)]}) self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 7, package_id=self.package, owner_id=self.owner) self.start_tour("/odoo/barcode", 'test_inventory_owner_scan_package', login='admin', timeout=180) inventory_moves = self.env['stock.move'].search([('product_id', '=', self.product1.id), ('is_inventory', '=', True)]) self.assertEqual(len(inventory_moves), 1) self.assertEqual(inventory_moves.state, 'done') self.assertEqual(inventory_moves.move_line_ids.owner_id.id, self.owner.id) def test_inventory_setting_count_entire_locations_on(self): """ Check the scenario when the "Count Entire Locations" setting is enabled, considering both tracked and untracked products and the usage of multiple locations. """ self.env.user.write({'group_ids': [Command.link(self.env.ref('stock.group_stock_multi_locations').id)]}) self.env.user.write({'group_ids': [Command.link(self.env.ref('stock.group_production_lot').id)]}) Quant = self.env['stock.quant'] # Create lots and serial numbers. lots = self.env['stock.lot'].create([{ 'name': f'lot{i}', 'product_id': self.productlot1.id, } for i in range(1, 5)]) shelf1_serial_numbers = self.create_serial_numbers(self.productserial1, 1, 3) shelf2_serial_numbers = self.create_serial_numbers(self.productserial1, 4, 3) # Adds quantity in WH/Stock/Shelf1. Quant._update_available_quantity(self.product1, self.shelf1, 10) Quant._update_available_quantity(self.product2, self.shelf1, 20) Quant._update_available_quantity(self.productlot1, self.shelf1, 3, lot_id=lots[0]) Quant._update_available_quantity(self.productlot1, self.shelf1, 4, lot_id=lots[1]) for sn in shelf1_serial_numbers: Quant._update_available_quantity(self.productserial1, self.shelf1, 1, lot_id=sn) # Adds quantity in WH/Stock/Shelf2. Quant._update_available_quantity(self.product1, self.shelf2, 30) Quant._update_available_quantity(self.productlot1, self.shelf2, 5, lot_id=lots[2]) Quant._update_available_quantity(self.productlot1, self.shelf2, 2, lot_id=lots[3]) for sn in shelf2_serial_numbers: Quant._update_available_quantity(self.productserial1, self.shelf2, 1, lot_id=sn) # Mark quant for product1 in shelf 1 as to count. quants = Quant.search([('product_id', '=', self.product1.id), ('location_id', '=', self.shelf1.id)]) wizard_request_count = self.env['stock.request.count'].create({ 'user_id': self.env.user.id, 'quant_ids': quants.ids, 'show_expected_quantity': True, }) wizard_request_count.action_request_count() self.start_tour("/odoo/barcode", 'test_inventory_setting_count_entire_locations_on', login='admin', timeout=180) def test_inventory_count_on_lotless_tracked_quants(self): """ Check that lotless quants can be edited via scans in inventory counts. """ # Enables multilocations and tracking. self.env.user.write({ 'group_ids': [ Command.link(self.ref('stock.group_stock_multi_locations')), Command.link(self.ref('stock.group_production_lot')), ], }) self.env['stock.quant']._update_available_quantity(self.productlot1, self.shelf1, 2, lot_id=False) self.env['stock.quant']._update_available_quantity(self.productlot1, self.shelf2, 3, lot_id=False) self.env['stock.quant']._update_available_quantity(self.productserial1, self.shelf2, 1, lot_id=False) # Mark added quants as to count. quants = self.env['stock.quant'].search([('product_id', 'in', [self.productlot1.id, self.productserial1.id])]) wizard_request_count = self.env['stock.request.count'].create({ 'user_id': self.env.user.id, 'quant_ids': quants.ids, 'show_expected_quantity': True, }) wizard_request_count.action_request_count() self.start_tour("/odoo/barcode", 'test_inventory_count_on_lotless_tracked_quants', login='admin') lots = self.env['stock.lot'].search([('product_id', 'in', [self.productlot1.id, self.productserial1.id])]) quants = self.env['stock.quant'].search([ ('product_id', 'in', [self.productlot1.id, self.productserial1.id]), ('location_id', 'in', (self.shelf1.id, self.shelf2.id, self.warehouse.lot_stock_id.id)), ]) self.assertRecordValues(quants, [ {'product_id': self.productlot1.id, 'location_id': self.shelf1.id, 'lot_id': False, 'quantity': 2.0}, {'product_id': self.productlot1.id, 'location_id': self.shelf2.id, 'lot_id': False, 'quantity': 0.0}, {'product_id': self.productserial1.id, 'location_id': self.shelf2.id, 'lot_id': False, 'quantity': 0.0}, {'product_id': self.productlot1.id, 'location_id': self.shelf2.id, 'lot_id': lots[0].id, 'quantity': 2.0}, {'product_id': self.productserial1.id, 'location_id': self.shelf2.id, 'lot_id': lots[1].id, 'quantity': 1.0}, ]) def test_inventory_setting_count_entire_locations_off(self): """ Check the scenario when the "Count Entire Locations" setting is disabled, considering both tracked and untracked products and the usage of multiple locations. """ self.env.user.write({'group_ids': [Command.link(self.env.ref('stock.group_stock_multi_locations').id)]}) Quant = self.env['stock.quant'] # Adds quantity in WH/Stock/Shelf1. Quant._update_available_quantity(self.product1, self.shelf1, 10) Quant._update_available_quantity(self.product2, self.shelf1, 20) # Adds quantity in WH/Stock/Shelf2. Quant._update_available_quantity(self.product1, self.shelf2, 30) # Mark quant for product1 in shelf 1 as to count. quants = Quant.search([('product_id', '=', self.product1.id), ('location_id', '=', self.shelf1.id)]) wizard_request_count = self.env['stock.request.count'].create({ 'user_id': self.env.user.id, 'quant_ids': quants.ids, 'show_expected_quantity': True, }) wizard_request_count.action_request_count() self.start_tour('/odoo/barcode', 'test_inventory_setting_count_entire_locations_off', login='admin', timeout=180) def test_inventory_setting_show_quantity_to_count(self): """ Check the scenario when "Show Quantity to Count" setting is enabled or disabled, considering both tracked and untracked products. """ # Enables multilocations and tracking. self.env.user.write({ 'group_ids': [ Command.link(self.env.ref('stock.group_stock_multi_locations').id), Command.link(self.env.ref('stock.group_production_lot').id), Command.link(self.env.ref('uom.group_uom').id), ], }) Quant = self.env['stock.quant'] # Creates some lots and serial numbers. serial_numbers = self.create_serial_numbers(self.productserial1, 1, 3) lots = self.env['stock.lot'].create([{ 'name': lot_name, 'product_id': self.productlot1.id, } for lot_name in ['lot1', 'lot2']]) # Adds quantity in stock for the products. for sn in serial_numbers: Quant._update_available_quantity(self.productserial1, self.stock_location, 1, lot_id=sn) Quant._update_available_quantity(self.productlot1, self.stock_location, 3, lot_id=lots[0]) Quant._update_available_quantity(self.productlot1, self.stock_location, 4, lot_id=lots[1]) self.product1.uom_id = self.uom_dozen self.uom_dozen.action_archive() Quant._update_available_quantity(self.product1, self.stock_location, 5) # Mark added quants as to count. quants = Quant.search([('product_id', 'in', [self.product1.id, self.productlot1.id, self.productserial1.id])]) wizard_request_count = self.env['stock.request.count'].create({ 'user_id': self.env.user.id, 'quant_ids': quants.ids, 'show_expected_quantity': True, }) wizard_request_count.action_request_count() self.start_tour("/odoo/barcode", 'test_inventory_setting_show_quantity_to_count_on', login='admin', timeout=180) # Disable the "Show Quantity to Count" setting and launch second tour. self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'False') self.env.user.write({'group_ids': [Command.unlink(self.env.ref('uom.group_uom').id)]}) self.start_tour("/odoo/barcode", 'test_inventory_setting_show_quantity_to_count_off', login='admin', timeout=180) def test_inventory_using_buttons(self): """ Creates an inventory from scratch, then scans products and verifies the buttons behavior is right. """ self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'True') # Adds some quantities for product2. self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 10) self.start_tour("/odoo/barcode", 'test_inventory_using_buttons', login='admin', timeout=180) product1_quant = self.env['stock.quant'].search([ ('product_id', '=', self.product1.id), ('quantity', '>', 0) ]) self.assertEqual(len(product1_quant), 1) self.assertEqual(product1_quant.quantity, 1.0) self.assertEqual(product1_quant.location_id.id, self.stock_location.id) productlot1_quant = self.env['stock.quant'].search([ ('product_id', '=', self.productlot1.id), ('quantity', '>', 0) ]) self.assertEqual(len(product1_quant), 1) self.assertEqual(productlot1_quant.quantity, 1.0) self.assertEqual(productlot1_quant.lot_id.name, 'toto-42') self.assertEqual(productlot1_quant.location_id.id, self.stock_location.id) inventory_move_line = self.env['stock.move.line'].search([('product_id', '=', self.product1.id), ('is_inventory', '=', True)], limit=1) self.assertEqual(inventory_move_line.reference, 'Very important reason') def test_inventory_adjustment_with_no_internal_location_quant(self): self.env.user.write({'group_ids': [Command.link(self.env.ref('stock.group_stock_multi_locations').id)]}) # Simulate moving 1 unit of product1 from Customer Location to Inventory Adjustment Location self.env['stock.quant'].create({ 'product_id': self.product1.id, 'location_id': self.env.ref('stock.stock_location_customers').id, 'quantity': -1, }) # Create a quant in Inventory adjustment location self.env['stock.quant'].create({ 'product_id': self.product1.id, 'location_id': self.product1.property_stock_inventory.id, 'quantity': 1, }) self.start_tour("/odoo/barcode", 'test_inventory_adjustment_with_no_internal_location_quant', login='admin') inventory_moves = self.env['stock.move'].search([('product_id', '=', self.product1.id), ('is_inventory', '=', True)]) self.assertEqual(len(inventory_moves), 1) self.assertEqual(inventory_moves.quantity, 1.0) self.assertEqual(inventory_moves.state, 'done') self.assertEqual(inventory_moves.location_id.name, 'Inventory adjustment') self.assertEqual(inventory_moves.location_dest_id.name, 'Stock') quants = self.env['stock.quant'].search([('product_id', '=', self.product1.id), ('location_id.usage', '=', 'internal')]) self.assertEqual(quants.quantity, 1.0) self.assertEqual(quants.inventory_quantity, 0) self.assertEqual(quants.inventory_diff_quantity, 0) # === RFID TESTS ===# def test_rfid_inventory_scan_sgtin(self): """ Checks multiple products can be scanned at once for an Inventory Adjustment using RFID.""" self.env.user.write({'group_ids': [Command.link(self.env.ref('stock.group_production_lot').id)]}) self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'True') self.env.company.nomenclature_id = self.env.ref('barcodes_gs1_nomenclature.default_gs1_nomenclature') # Create a bunch of products with EAN13. product_common_vals = { 'is_storable': True, 'uom_id': self.env.ref('uom.product_uom_unit').id } products = self.env['product.product'].create([{ **product_common_vals, 'name': product_name, 'barcode': barcode, } for (product_name, barcode) in [ ('Lunch box', '2300001000015'), ('French wine', '2300001000084'), ('Japanese wine', '2300001000121'), ('Large plate', '5468123001239'), ('Inox knife', '5468123002465'), ]]) # Create a few products tracked by SN with EAN13. tracked_products = self.env['product.product'].create([{ **product_common_vals, 'name': product_name, 'tracking': 'serial', 'barcode': barcode, } for (product_name, barcode) in [ ('Handmade argyle plate', '8765432100002'), ('Chief suit', '8765432300044'), ]]) # Create serial numbers for those products. p1_serial_numbers = self.env['stock.lot'].create([{ 'name': str(i).rjust(5, '0'), 'product_id': tracked_products[0].id, } for i in range(128, 134)]) p2_serial_numbers = self.env['stock.lot'].create([{ 'name': str(i), 'product_id': tracked_products[1].id, } for i in [12, 10, 15, 16, 9]]) # Add some quantity in stock. self.env['stock.quant']._update_available_quantity(products[0], self.stock_location, 12) self.env['stock.quant']._update_available_quantity(products[3], self.stock_location, 35) self.env['stock.quant']._update_available_quantity(products[4], self.stock_location, 11) for serial_number in (p1_serial_numbers | p2_serial_numbers): self.env['stock.quant']._update_available_quantity( serial_number.product_id, self.stock_location, 1, lot_id=serial_number) # Mock the calls to the route to count the call amount. self1 = self get_specific_barcode_data_orig = StockBarcodeController.get_specific_barcode_data @http.route('/stock_barcode/get_specific_barcode_data', type='jsonrpc', auth='user') def mocked_data_batch_method(self, **kwargs): if self1.call_count == 0: # First call: product and serial numbers. self1.assertEqual(list(kwargs['barcodes_by_model'].keys()), ['product.product', 'product.uom', 'stock.lot']) elif self1.call_count == 1: # First call: serial numbers only (no new product's barcode scanned). self1.assertEqual(list(kwargs['barcodes_by_model'].keys()), ['stock.lot']) self1.call_count += 1 return get_specific_barcode_data_orig(self, **kwargs) with patch.object( StockBarcodeController, 'get_specific_barcode_data', mocked_data_batch_method ): self.start_tour('/odoo/barcode', 'test_rfid_inventory_scan_sgtin', login='admin', timeout=180) self.assertEqual(self.call_count, 2) def test_inventory_count_with_line_deletion(self): """ Check that the quant data of the barcode cache is not corrupted by line deletions. """ # Put 5 units of prodcut1 in stock and start an inventory count self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 5) self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'True') self.start_tour("/odoo/barcode", 'test_inventory_count_with_line_deletion', login='admin') # === GS1 TESTS ===# def test_gs1_inventory_gtin_8(self): """ Simulate scanning a product with his gs1 barcode """ self.env.company.nomenclature_id = self.env.ref('barcodes_gs1_nomenclature.default_gs1_nomenclature') product = self.env['product.product'].create({ 'name': 'PRO_GTIN_8', 'is_storable': True, 'barcode': '82655853', # GTIN-8 format 'uom_id': self.env.ref('uom.product_uom_unit').id }) self.start_tour("/odoo/barcode", 'test_gs1_inventory_gtin_8', login='admin', timeout=180) # Checks the inventory adjustment correclty created a move line. move_line = self.env['stock.move.line'].search([ ('product_id', '=', product.id), ('state', '=', 'done'), ('location_id', '=', product.property_stock_inventory.id), ]) self.assertEqual(move_line.qty_done, 78) def test_gs1_inventory_product_units(self): """ Scans a product with a GS1 barcode containing multiple quantities.""" self.env.company.nomenclature_id = self.env.ref('barcodes_gs1_nomenclature.default_gs1_nomenclature') product = self.env['product.product'].create({ 'name': 'PRO_GTIN_8', 'is_storable': True, 'barcode': '82655853', # GTIN-8 format 'uom_id': self.env.ref('uom.product_uom_unit').id }) self.start_tour("/odoo/barcode", 'test_gs1_inventory_product_units', login='admin', timeout=180) quantity = self.env['stock.move.line'].search([ ('product_id', '=', product.id), ('state', '=', 'done'), ('location_id', '=', product.property_stock_inventory.id), ]) self.assertEqual(quantity.qty_done, 102) def test_gs1_inventory_package(self): """ Scans existing packages and checks their products are correclty added to the inventory adjustment. Then scans some products, scans a new package and checks the package was created and correclty assigned to those products. """ self.env.company.nomenclature_id = self.env.ref('barcodes_gs1_nomenclature.default_gs1_nomenclature') self.env.user.write({ 'group_ids': [ Command.link(self.env.ref('stock.group_stock_multi_locations').id), Command.link(self.env.ref('stock.group_tracking_lot').id) ] }) self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'True') product = self.env['product.product'].create({ 'name': 'PRO_GTIN_8', 'is_storable': True, 'barcode': '82655853', # GTIN-8 format 'uom_id': self.env.ref('uom.product_uom_unit').id }) # Creates a first package in Section 1 and adds some products. pack_1 = self.env['stock.package'].create({'name': '987654123487568456'}) self.env['stock.quant']._update_available_quantity(self.product1, self.shelf1, 8, package_id=pack_1) # Creates a second package in Section 2 and adds some other products. pack_2 = self.env['stock.package'].create({'name': '487325612456785124'}) self.env['stock.quant']._update_available_quantity(self.product2, self.shelf2, 6, package_id=pack_2) self.start_tour("/odoo/barcode", 'test_gs1_inventory_package', login='admin', timeout=180) pack_3 = self.env['stock.package'].search([('name', '=', '122333444455555670')]) self.assertEqual(pack_3.location_id.id, self.shelf2.id) self.assertEqual(pack_3.quant_ids.product_id.ids, [product.id]) def test_gs1_inventory_lot_serial(self): """ Checks tracking numbers and quantites are correctly got from GS1 barcodes for tracked products. Also, this test is an opportunity to ensure custom GS1 separators are used clientside.""" self.env.user.write({'group_ids': [Command.link(self.env.ref('stock.group_production_lot').id)]}) self.env.company.nomenclature_id = self.env.ref('barcodes_gs1_nomenclature.default_gs1_nomenclature') self.env.company.nomenclature_id.gs1_separator_fnc1 = r'(Alt029|#|\x1D|~)' product_lot = self.env['product.product'].create({ 'name': 'PRO_GTIN_12_lot', 'is_storable': True, 'barcode': '111155555717', # GTIN-12 format 'uom_id': self.env.ref('uom.product_uom_unit').id, 'tracking': 'lot', }) product_serial = self.env['product.product'].create({ 'name': 'PRO_GTIN_14_serial', 'is_storable': True, 'barcode': '15222222222219', # GTIN-14 format 'uom_id': self.env.ref('uom.product_uom_unit').id, 'tracking': 'serial', }) self.start_tour("/odoo/barcode", 'test_gs1_inventory_lot_serial', login='admin', timeout=180) smls_lot = self.env['stock.move.line'].search([ ('product_id', '=', product_lot.id), ('state', '=', 'done'), ('location_id', '=', product_lot.property_stock_inventory.id), ]) self.assertEqual(len(smls_lot), 3) self.assertEqual(smls_lot[0].qty_done, 10) self.assertEqual(smls_lot[1].qty_done, 15) self.assertEqual(smls_lot[2].qty_done, 20) self.assertEqual( smls_lot.lot_id.mapped('name'), ['LOT-AAA', 'LOT-AAB', 'LOT-AAC'] ) smls_serial = self.env['stock.move.line'].search([ ('product_id', '=', product_serial.id), ('state', '=', 'done'), ('location_id', '=', product_serial.property_stock_inventory.id), ]) self.assertEqual(len(smls_serial), 5) self.assertEqual(smls_serial[0].qty_done, 1) self.assertEqual(smls_serial[1].qty_done, 1) self.assertEqual(smls_serial[2].qty_done, 1) self.assertEqual(smls_serial[3].qty_done, 20) self.assertEqual(smls_serial[4].qty_done, 1) self.assertEqual( smls_serial.lot_id.mapped('name'), ['Serial1', 'Serial2', 'Serial3', 'Serial4'] ) def test_scan_product_lot_with_package(self): """ Check that a lot can be scanned in the inventory Adjustments, when the package is set in the quant. """ # Enable the package option self.env['res.config.settings'].create({'group_stock_tracking_lot': True}).execute() product = self.env['product.product'].create({ 'name': 'Product', 'is_storable': True, 'tracking': 'lot', }) self.env["stock.quant"].create({ 'product_id': product.id, 'location_id': self.stock_location.id, 'quantity': 10, 'package_id': self.env['stock.package'].create({ 'name': 'Package-test', }).id, 'lot_id': self.env['stock.lot'].create({ 'name': 'Lot-test', 'product_id': product.id, }).id, }) self.assertEqual(product.qty_available, 10) self.start_tour("/odoo", 'stock_barcode_package_with_lot', login="admin") def test_inventory_count_lot_split_in_packages(self): grp_multi_loc = self.env.ref('stock.group_stock_multi_locations') grp_lot = self.env.ref('stock.group_production_lot') grp_pack = self.env.ref('stock.group_tracking_lot') self.env.user.write({'group_ids': [(4, grp_pack.id, 0), (4, grp_multi_loc.id, 0), (4, grp_lot.id, 0)]}) self.env['ir.config_parameter'].set_param('stock.show_expected_quantity_count', 'True') shelf1 = self.env['stock.location'].create({ 'name': 'Shelf 11', 'barcode': 'Shelf11', 'location_id': self.warehouse.lot_stock_id.id, }) product = self.env['product.product'].create({ 'name': 'Product', 'is_storable': True, 'tracking': 'lot', }) lot = self.env['stock.lot'].create({ 'name': 'Lot-test', 'product_id': product.id, }) pack1 = self.env['stock.package'].create({ 'name': 'Pack1', }) pack2 = self.env['stock.package'].create({ 'name': 'Pack2', }) self.env['stock.quant']._update_available_quantity(product, shelf1, 5, lot_id=lot, package_id=pack1) self.env['stock.quant']._update_available_quantity(product, shelf1, 5, lot_id=lot, package_id=pack2) action_id = self.env.ref('stock_barcode.stock_barcode_action_main_menu') url = "/web#action=" + str(action_id.id) self.start_tour(url, 'test_correct_inventory_with_packages', login="admin")