[FIX] base: convert barcode type if incorrect encoding
To reproduce the issue:
(Need stock_barcode)
1. Create a product:
- Barcode: 1234567890123
2. Print the label
3. Try to scan the barcode and check the value read.
Error: The value is 1234567890128, the last digit is incorrect (8
instead of 3)
When printing a barcode, we use the library 'report-lab' to generate a
barcode image from a value and a barcode type. In case of EAN-13, if the
value contains a non-digit character, it will raise an error. We then
catch the error and retry to generate the barcode according to the
barcode type Code128:
https://github.com/odoo/odoo/blob/87698d90f02bfe93c6e643f4876a5ccd74788eff/odoo/addons/base/models/ir_actions_report.py#L569-L575
However, if the value contains only digits, the method will use the 12
first digits:
https://github.com/mattjmorrison/ReportLab/blob/dade0f303cb6fcdbe535c4cc92e6102c2417b699/src/reportlab/graphics/barcode/eanbc.py#L187-L188
and will then add the last one, the check digit, which is computed by
the library. This explains why, in the above use case, the barcode value
returned by the scanner is not the same than the expected one.
Note: Similar behavior with type EAN-8
OPW-2902150
closes odoo/odoo#97052
X-original-commit: a2c7470f8fd46b2520f7b8f0750c63216273ce96
Related: odoo/enterprise#30160
Signed-off-by: Steve Van Essche <svs@odoo.com>
Signed-off-by: Adrien Widart <awt@odoo.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
|
||||
from odoo import models, fields, api
|
||||
from odoo.tools import check_barcode_encoding, get_barcode_check_digit
|
||||
|
||||
|
||||
UPC_EAN_CONVERSIONS = [
|
||||
@@ -21,55 +22,6 @@ class BarcodeNomenclature(models.Model):
|
||||
UPC_EAN_CONVERSIONS, string='UPC/EAN Conversion', required=True, default='always',
|
||||
help="UPC Codes can be converted to EAN by prefixing them with a zero. This setting determines if a UPC/EAN barcode should be automatically converted in one way or another when trying to match a rule with the other encoding.")
|
||||
|
||||
@api.model
|
||||
def get_barcode_check_digit(self, numeric_barcode):
|
||||
""" Computes and returns the barcode check digit. The used algorithm
|
||||
follows the GTIN specifications and can be used by all compatible
|
||||
barcode nomenclature, like as EAN-8, EAN-12 (UPC-A) or EAN-13.
|
||||
|
||||
https://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdf
|
||||
https://www.gs1.org/services/how-calculate-check-digit-manually
|
||||
|
||||
:param numeric_barcode: the barcode to verify/recompute the check digit
|
||||
:type numeric_barcode: str
|
||||
:return: the number corresponding to the right check digit
|
||||
:rtype: int
|
||||
"""
|
||||
# Multiply value of each position by
|
||||
# N1 N2 N3 N4 N5 N6 N7 N8 N9 N10 N11 N12 N13 N14 N15 N16 N17 N18
|
||||
# x3 X1 x3 x1 x3 x1 x3 x1 x3 x1 x3 x1 x3 x1 x3 x1 x3 CHECKSUM
|
||||
oddsum = evensum = total = 0
|
||||
code = numeric_barcode[-2::-1] # Remove the check digit and reverse the barcode.
|
||||
# The CHECKSUM digit is removed because it will be recomputed and it must not interfer with
|
||||
# the computation. Also, the barcode is inverted, so the barcode length doesn't matter.
|
||||
# Otherwise, the digits' group (even or odd) could be different according to the barcode length.
|
||||
for i, digit in enumerate(code):
|
||||
if i % 2 == 0:
|
||||
evensum += int(digit)
|
||||
else:
|
||||
oddsum += int(digit)
|
||||
total = evensum * 3 + oddsum
|
||||
return (10 - total % 10) % 10
|
||||
|
||||
@api.model
|
||||
def check_encoding(self, barcode, encoding):
|
||||
""" Checks if the given barcode is correctly encoded.
|
||||
|
||||
:return: True if the barcode string is encoded with the provided encoding.
|
||||
:rtype: bool
|
||||
"""
|
||||
if encoding == "any":
|
||||
return True
|
||||
barcode_sizes = {
|
||||
'ean8': 8,
|
||||
'ean13': 13,
|
||||
'gtin14': 14,
|
||||
'upca': 12,
|
||||
'sscc': 18,
|
||||
}
|
||||
barcode_size = barcode_sizes[encoding]
|
||||
return len(barcode) == barcode_size and re.match(r"^\d+$", barcode) and self.get_barcode_check_digit(barcode) == int(barcode[-1])
|
||||
|
||||
@api.model
|
||||
def sanitize_ean(self, ean):
|
||||
""" Returns a valid zero padded EAN-13 from an EAN prefix.
|
||||
@@ -77,7 +29,7 @@ class BarcodeNomenclature(models.Model):
|
||||
:type ean: str
|
||||
"""
|
||||
ean = ean[0:13].zfill(13)
|
||||
return ean[0:-1] + str(self.get_barcode_check_digit(ean))
|
||||
return ean[0:-1] + str(get_barcode_check_digit(ean))
|
||||
|
||||
@api.model
|
||||
def sanitize_upc(self, upc):
|
||||
@@ -153,12 +105,12 @@ class BarcodeNomenclature(models.Model):
|
||||
|
||||
for rule in self.rule_ids:
|
||||
cur_barcode = barcode
|
||||
if rule.encoding == 'ean13' and self.check_encoding(barcode, 'upca') and self.upc_ean_conv in ['upc2ean', 'always']:
|
||||
if rule.encoding == 'ean13' and check_barcode_encoding(barcode, 'upca') and self.upc_ean_conv in ['upc2ean', 'always']:
|
||||
cur_barcode = '0' + cur_barcode
|
||||
elif rule.encoding == 'upca' and self.check_encoding(barcode, 'ean13') and barcode[0] == '0' and self.upc_ean_conv in ['ean2upc', 'always']:
|
||||
elif rule.encoding == 'upca' and check_barcode_encoding(barcode, 'ean13') and barcode[0] == '0' and self.upc_ean_conv in ['ean2upc', 'always']:
|
||||
cur_barcode = cur_barcode[1:]
|
||||
|
||||
if not self.check_encoding(barcode, rule.encoding):
|
||||
if not check_barcode_encoding(barcode, rule.encoding):
|
||||
continue
|
||||
|
||||
match = self.match_pattern(cur_barcode, rule.pattern)
|
||||
|
||||
@@ -12,13 +12,6 @@ class TestBarcodeNomenclature(common.TransactionCase):
|
||||
'name': 'Barcode Nomenclature Test',
|
||||
})
|
||||
|
||||
def test_barcode_check_digit(self):
|
||||
barcode_nomenclature = self.env['barcode.nomenclature']
|
||||
ean8 = "87111125"
|
||||
self.assertEqual(barcode_nomenclature.get_barcode_check_digit("0" * 10 + ean8), int(ean8[-1]))
|
||||
ean13 = "1234567891231"
|
||||
self.assertEqual(barcode_nomenclature.get_barcode_check_digit("0" * 5 + ean13), int(ean13[-1]))
|
||||
|
||||
def test_barcode_nomenclature_parse_barcode_ean8_01(self):
|
||||
""" Parses some barcodes with a simple EAN-8 barcode rule and checks the result.
|
||||
"""
|
||||
|
||||
@@ -4,6 +4,7 @@ import calendar
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import get_barcode_check_digit
|
||||
|
||||
FNC1_CHAR = '\x1D'
|
||||
|
||||
@@ -76,7 +77,7 @@ class BarcodeNomenclature(models.Model):
|
||||
rule.name))
|
||||
elif rule.gs1_content_type == 'identifier':
|
||||
# Check digit and remove it of the value
|
||||
if match.group(2)[-1] != str(self.get_barcode_check_digit("0" * (18 - len(match.group(2))) + match.group(2))):
|
||||
if match.group(2)[-1] != str(get_barcode_check_digit("0" * (18 - len(match.group(2))) + match.group(2))):
|
||||
return None
|
||||
result['value'] = match.group(2)
|
||||
elif rule.gs1_content_type == 'date':
|
||||
|
||||
@@ -9,7 +9,7 @@ from dateutil.relativedelta import relativedelta
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import float_is_zero
|
||||
from odoo.tools import float_is_zero, check_barcode_encoding
|
||||
from odoo.tools.float_utils import float_round
|
||||
from odoo.tools.mail import html2plaintext, is_html_empty
|
||||
|
||||
@@ -113,7 +113,7 @@ class Product(models.Model):
|
||||
self.valid_ean = False
|
||||
for product in self:
|
||||
if product.barcode:
|
||||
product.valid_ean = self.env['barcode.nomenclature'].check_encoding(product.barcode.rjust(14, '0'), 'gtin14')
|
||||
product.valid_ean = check_barcode_encoding(product.barcode.rjust(14, '0'), 'gtin14')
|
||||
|
||||
@api.depends('stock_move_ids.product_qty', 'stock_move_ids.state')
|
||||
@api.depends_context(
|
||||
|
||||
@@ -8,7 +8,7 @@ from psycopg2 import Error
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import groupby
|
||||
from odoo.tools import check_barcode_encoding, groupby
|
||||
from odoo.tools.float_utils import float_compare, float_is_zero
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -1101,7 +1101,7 @@ class QuantPackage(models.Model):
|
||||
self.valid_sscc = False
|
||||
for package in self:
|
||||
if package.name:
|
||||
package.valid_sscc = self.env['barcode.nomenclature'].check_encoding(package.name, 'sscc')
|
||||
package.valid_sscc = check_barcode_encoding(package.name, 'sscc')
|
||||
|
||||
def _search_owner(self, operator, value):
|
||||
if value:
|
||||
|
||||
@@ -6,7 +6,7 @@ from odoo import api, fields, models, tools, SUPERUSER_ID, _
|
||||
from odoo.exceptions import UserError, AccessError
|
||||
from odoo.tools.safe_eval import safe_eval, time
|
||||
from odoo.tools.misc import find_in_path
|
||||
from odoo.tools import config, is_html_empty, parse_version
|
||||
from odoo.tools import check_barcode_encoding, config, is_html_empty, parse_version
|
||||
from odoo.http import request
|
||||
from odoo.osv.expression import NEGATIVE_TERM_OPERATORS, FALSE_DOMAIN
|
||||
|
||||
@@ -550,6 +550,14 @@ class IrActionsReport(models.Model):
|
||||
if kwargs['quiet']:
|
||||
kwargs['barBorder'] = 0
|
||||
|
||||
if barcode_type in ('EAN8', 'EAN13') and not check_barcode_encoding(value, barcode_type):
|
||||
# If the barcode does not respect the encoding specifications, convert its type into Code128.
|
||||
# Otherwise, the report-lab method may return a barcode different from its value. For instance,
|
||||
# if the barcode type is EAN-8 and the value 11111111, the report-lab method will take the first
|
||||
# seven digits and will compute the check digit, which gives: 11111115 -> the barcode does not
|
||||
# match the expected value.
|
||||
barcode_type = 'Code128'
|
||||
|
||||
try:
|
||||
barcode = createBarcodeDrawing(barcode_type, value=value, format='png', **kwargs)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from . import common
|
||||
from . import test_acl
|
||||
from . import test_api
|
||||
from . import test_barcode
|
||||
from . import test_base
|
||||
from . import test_basecase
|
||||
from . import test_cache
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tools import check_barcode_encoding, get_barcode_check_digit
|
||||
|
||||
|
||||
class TestBarcode(TransactionCase):
|
||||
def test_barcode_check_digit(self):
|
||||
ean8 = "87111125"
|
||||
self.assertEqual(get_barcode_check_digit("0" * 10 + ean8), int(ean8[-1]))
|
||||
ean13 = "1234567891231"
|
||||
self.assertEqual(get_barcode_check_digit("0" * 5 + ean13), int(ean13[-1]))
|
||||
|
||||
def test_barcode_encoding(self):
|
||||
self.assertTrue(check_barcode_encoding('20220006', 'ean8'))
|
||||
self.assertTrue(check_barcode_encoding('93855341', 'ean8'))
|
||||
self.assertTrue(check_barcode_encoding('2022071416014', 'ean13'))
|
||||
self.assertTrue(check_barcode_encoding('9745213796142', 'ean13'))
|
||||
|
||||
self.assertFalse(check_barcode_encoding('2022a006', 'ean8'), 'should contains digits only')
|
||||
self.assertFalse(check_barcode_encoding('20220000', 'ean8'), 'incorrect check digit')
|
||||
self.assertFalse(check_barcode_encoding('93855341', 'ean13'), 'ean13 is a 13-digits barcode')
|
||||
self.assertFalse(check_barcode_encoding('9745213796142', 'ean8'), 'ean8 is a 8-digits barcode')
|
||||
self.assertFalse(check_barcode_encoding('9745213796148', 'ean13'), 'incorrect check digit')
|
||||
self.assertFalse(check_barcode_encoding('2022!71416014', 'ean13'), 'should contains digits only')
|
||||
self.assertFalse(check_barcode_encoding('0022071416014', 'ean13'), 'when starting with one zero, it indicates that a 12-digit UPC-A code follows')
|
||||
@@ -7,6 +7,7 @@ from . import cloc
|
||||
from . import pdf
|
||||
from . import pycompat
|
||||
from . import win32
|
||||
from .barcode import *
|
||||
from .config import config
|
||||
from .date_utils import *
|
||||
from .debugger import *
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def get_barcode_check_digit(numeric_barcode):
|
||||
""" Computes and returns the barcode check digit. The used algorithm
|
||||
follows the GTIN specifications and can be used by all compatible
|
||||
barcode nomenclature, like as EAN-8, EAN-12 (UPC-A) or EAN-13.
|
||||
https://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdf
|
||||
https://www.gs1.org/services/how-calculate-check-digit-manually
|
||||
:param numeric_barcode: the barcode to verify/recompute the check digit
|
||||
:type numeric_barcode: str
|
||||
:return: the number corresponding to the right check digit
|
||||
:rtype: int
|
||||
"""
|
||||
# Multiply value of each position by
|
||||
# N1 N2 N3 N4 N5 N6 N7 N8 N9 N10 N11 N12 N13 N14 N15 N16 N17 N18
|
||||
# x3 X1 x3 x1 x3 x1 x3 x1 x3 x1 x3 x1 x3 x1 x3 x1 x3 CHECKSUM
|
||||
oddsum = evensum = 0
|
||||
code = numeric_barcode[-2::-1] # Remove the check digit and reverse the barcode.
|
||||
# The CHECKSUM digit is removed because it will be recomputed and it must not interfer with
|
||||
# the computation. Also, the barcode is inverted, so the barcode length doesn't matter.
|
||||
# Otherwise, the digits' group (even or odd) could be different according to the barcode length.
|
||||
for i, digit in enumerate(code):
|
||||
if i % 2 == 0:
|
||||
evensum += int(digit)
|
||||
else:
|
||||
oddsum += int(digit)
|
||||
total = evensum * 3 + oddsum
|
||||
return (10 - total % 10) % 10
|
||||
|
||||
|
||||
def check_barcode_encoding(barcode, encoding):
|
||||
""" Checks if the given barcode is correctly encoded.
|
||||
:return: True if the barcode string is encoded with the provided encoding.
|
||||
:rtype: bool
|
||||
"""
|
||||
encoding = encoding.lower()
|
||||
if encoding == "any":
|
||||
return True
|
||||
barcode_sizes = {
|
||||
'ean8': 8,
|
||||
'ean13': 13,
|
||||
'gtin14': 14,
|
||||
'upca': 12,
|
||||
'sscc': 18,
|
||||
}
|
||||
barcode_size = barcode_sizes[encoding]
|
||||
return (encoding != 'ean13' or barcode[0] != '0') \
|
||||
and len(barcode) == barcode_size \
|
||||
and re.match(r"^\d+$", barcode) \
|
||||
and get_barcode_check_digit(barcode) == int(barcode[-1])
|
||||
Reference in New Issue
Block a user