Files
enterprise-addons/stock_barcode/tests/test_barcode_client_action_inventory.py
T

971 lines
50 KiB
Python

# 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")