[IMP] stock: show related products on duplicated barcode
Previously, product barcodes uniqueness was enforced by a SQL constraint, resulting in a generic error message. The SQL constraint has been replaced by a Python check, which allows the listing of conflicting barcodes in the error message, including those resulting from batch edits. Task: 2654703-7 Part-of: odoo/odoo#84056
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
from operator import itemgetter
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import ValidationError
|
||||
@@ -168,9 +169,35 @@ class ProductProduct(models.Model):
|
||||
self.env.cr.execute("CREATE UNIQUE INDEX IF NOT EXISTS product_product_combination_unique ON %s (product_tmpl_id, combination_indices) WHERE active is true"
|
||||
% self._table)
|
||||
|
||||
_sql_constraints = [
|
||||
('barcode_uniq', 'unique(barcode)', "A barcode can only be assigned to one product !"),
|
||||
]
|
||||
@api.constrains("barcode")
|
||||
def _check_barcode_unique(self):
|
||||
|
||||
# Collect `product.product.id` of objects that share a barcode with any of those in `self`,
|
||||
# and group them by barcode.
|
||||
candidate_duplicates = self.env['product.product'].sudo().read_group(
|
||||
domain=[("barcode", "in", list(set(product.barcode for product in self if product.barcode)))],
|
||||
fields=['ids:array_agg(id)', 'barcode'],
|
||||
groupby=['barcode']
|
||||
)
|
||||
|
||||
# Keep only barcodes that match more than one `product.product` object.
|
||||
duplicates = [candidate for candidate in candidate_duplicates if candidate['barcode_count'] > 1]
|
||||
if duplicates:
|
||||
# Prepare a mapping of "{ `product.barcode`: [`product.display_name`, ... ] }" to show the user.
|
||||
duplicates_joined = {dup['barcode']: ', '.join(
|
||||
sorted([product.display_name for product in
|
||||
self.env['product.product'].browse(dup['ids'])])
|
||||
) for dup in sorted(duplicates, key=itemgetter('barcode'))}
|
||||
|
||||
# Transform the mapping into a prettified string and raise.
|
||||
duplicates_as_str = "\n".join(
|
||||
f"Barcode \"{barcode}\" already assigned to product(s): {products}"
|
||||
for barcode, products in duplicates_joined.items()
|
||||
)
|
||||
|
||||
raise ValidationError(
|
||||
"Barcode(s) already assigned:\n\n{duplicates}".format(duplicates=duplicates_as_str)
|
||||
)
|
||||
|
||||
@api.constrains('barcode')
|
||||
def _check_barcode_uniqueness(self):
|
||||
|
||||
@@ -7,3 +7,4 @@ from . import test_pricelist
|
||||
from . import test_product_pricelist
|
||||
from . import test_seller
|
||||
from . import test_product_attribute_value_config
|
||||
from . import test_barcode
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import exceptions
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestProductBarcode(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.env['product.product'].create({'name': 'BC1', 'barcode': '1'})
|
||||
self.env['product.product'].create({'name': 'BC2', 'barcode': '2'})
|
||||
|
||||
def test_blank_barcodes_allowed(self):
|
||||
"""Makes sure duplicated blank barcodes are allowed."""
|
||||
for i in range(2):
|
||||
self.env['product.product'].create({'name': f'BC_{i}'})
|
||||
|
||||
def test_false_barcodes_allowed(self):
|
||||
"""Makes sure duplicated False barcodes are allowed."""
|
||||
for i in range(2):
|
||||
self.env['product.product'].create({'name': f'BC_{i}', 'barcode': False})
|
||||
|
||||
def test_duplicated_barcode(self):
|
||||
"""Tests for simple duplication."""
|
||||
with self.assertRaises(exceptions.ValidationError):
|
||||
self.env['product.product'].create({'name': 'BC3', 'barcode': '1'})
|
||||
|
||||
def test_duplicated_barcode_in_batch_edit(self):
|
||||
"""Tests for duplication in batch edits."""
|
||||
batch = [
|
||||
{'name': 'BC3', 'barcode': '3'},
|
||||
{'name': 'BC4', 'barcode': '4'},
|
||||
]
|
||||
self.env['product.product'].create(batch)
|
||||
batch.append({'name': 'BC5', 'barcode': '1'})
|
||||
with self.assertRaises(exceptions.ValidationError):
|
||||
self.env['product.product'].create(batch)
|
||||
|
||||
def test_test_duplicated_barcode_error_msg_content(self):
|
||||
"""Validates the error message shown when duplicated barcodes are found."""
|
||||
batch = [
|
||||
{'name': 'BC3', 'barcode': '3'},
|
||||
{'name': 'BC4', 'barcode': '3'},
|
||||
{'name': 'BC5', 'barcode': '4'},
|
||||
{'name': 'BC6', 'barcode': '4'},
|
||||
{'name': 'BC7', 'barcode': '1'},
|
||||
]
|
||||
try:
|
||||
self.env['product.product'].create(batch)
|
||||
except exceptions.ValidationError as exc:
|
||||
assert 'Barcode "3" already assigned to product(s): BC3, BC4' in exc.args[0]
|
||||
assert 'Barcode "4" already assigned to product(s): BC5, BC6' in exc.args[0]
|
||||
assert 'Barcode "1" already assigned to product(s): BC1' in exc.args[0]
|
||||
Reference in New Issue
Block a user