[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:
Adrien Widart
2022-08-11 12:51:05 +02:00
parent da5841ca3a
commit 320025ee63
10 changed files with 103 additions and 66 deletions
+5 -53
View File
@@ -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':
+2 -2
View File
@@ -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(
+2 -2
View File
@@ -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:
+9 -1
View File
@@ -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)
+1
View File
@@ -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
+27
View File
@@ -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')
+1
View File
@@ -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 *
+54
View File
@@ -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])