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>
135 lines
5.9 KiB
Python
135 lines
5.9 KiB
Python
import re
|
|
|
|
from odoo import models, fields, api
|
|
from odoo.tools import check_barcode_encoding, get_barcode_check_digit
|
|
|
|
|
|
UPC_EAN_CONVERSIONS = [
|
|
('none', 'Never'),
|
|
('ean2upc', 'EAN-13 to UPC-A'),
|
|
('upc2ean', 'UPC-A to EAN-13'),
|
|
('always', 'Always'),
|
|
]
|
|
|
|
|
|
class BarcodeNomenclature(models.Model):
|
|
_name = 'barcode.nomenclature'
|
|
_description = 'Barcode Nomenclature'
|
|
|
|
name = fields.Char(string='Barcode Nomenclature', size=32, required=True, help='An internal identification of the barcode nomenclature')
|
|
rule_ids = fields.One2many('barcode.rule', 'barcode_nomenclature_id', string='Rules', help='The list of barcode rules')
|
|
upc_ean_conv = fields.Selection(
|
|
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 sanitize_ean(self, ean):
|
|
""" Returns a valid zero padded EAN-13 from an EAN prefix.
|
|
|
|
:type ean: str
|
|
"""
|
|
ean = ean[0:13].zfill(13)
|
|
return ean[0:-1] + str(get_barcode_check_digit(ean))
|
|
|
|
@api.model
|
|
def sanitize_upc(self, upc):
|
|
""" Returns a valid zero padded UPC-A from a UPC-A prefix.
|
|
|
|
:type upc: str
|
|
"""
|
|
return self.sanitize_ean('0' + upc)[1:]
|
|
|
|
def match_pattern(self, barcode, pattern):
|
|
"""Checks barcode matches the pattern and retrieves the optional numeric value in barcode.
|
|
|
|
:param barcode:
|
|
:type barcode: str
|
|
:param pattern:
|
|
:type pattern: str
|
|
:return: an object containing:
|
|
- value: the numerical value encoded in the barcode (0 if no value encoded)
|
|
- base_code: the barcode in which numerical content is replaced by 0's
|
|
- match: boolean
|
|
:rtype: dict
|
|
"""
|
|
match = {
|
|
'value': 0,
|
|
'base_code': barcode,
|
|
'match': False,
|
|
}
|
|
|
|
barcode = barcode.replace('\\', '\\\\').replace('{', '\\{').replace('}', '\\}').replace('.', '\\.')
|
|
numerical_content = re.search("[{][N]*[D]*[}]", pattern) # look for numerical content in pattern
|
|
|
|
if numerical_content: # the pattern encodes a numerical content
|
|
num_start = numerical_content.start() # start index of numerical content
|
|
num_end = numerical_content.end() # end index of numerical content
|
|
value_string = barcode[num_start:num_end - 2] # numerical content in barcode
|
|
|
|
whole_part_match = re.search("[{][N]*[D}]", numerical_content.group()) # looks for whole part of numerical content
|
|
decimal_part_match = re.search("[{N][D]*[}]", numerical_content.group()) # looks for decimal part
|
|
whole_part = value_string[:whole_part_match.end() - 2] # retrieve whole part of numerical content in barcode
|
|
decimal_part = "0." + value_string[decimal_part_match.start():decimal_part_match.end() - 1] # retrieve decimal part
|
|
if whole_part == '':
|
|
whole_part = '0'
|
|
match['value'] = int(whole_part) + float(decimal_part)
|
|
|
|
match['base_code'] = barcode[:num_start] + (num_end - num_start - 2) * "0" + barcode[num_end - 2:] # replace numerical content by 0's in barcode
|
|
match['base_code'] = match['base_code'].replace("\\\\", "\\").replace("\\{", "{").replace("\\}", "}").replace("\\.", ".")
|
|
pattern = pattern[:num_start] + (num_end - num_start - 2) * "0" + pattern[num_end:] # replace numerical content by 0's in pattern to match
|
|
|
|
match['match'] = re.match(pattern, match['base_code'][:len(pattern)])
|
|
|
|
return match
|
|
|
|
def parse_barcode(self, barcode):
|
|
""" Attempts to interpret and parse a barcode.
|
|
|
|
:param barcode:
|
|
:type barcode: str
|
|
:return: A object containing various information about the barcode, like as:
|
|
- code: the barcode
|
|
- type: the barcode's type
|
|
- value: if the id encodes a numerical value, it will be put there
|
|
- base_code: the barcode code with all the encoding parts set to
|
|
zero; the one put on the product in the backend
|
|
:rtype: dict
|
|
"""
|
|
parsed_result = {
|
|
'encoding': '',
|
|
'type': 'error',
|
|
'code': barcode,
|
|
'base_code': barcode,
|
|
'value': 0,
|
|
}
|
|
|
|
for rule in self.rule_ids:
|
|
cur_barcode = barcode
|
|
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 check_barcode_encoding(barcode, 'ean13') and barcode[0] == '0' and self.upc_ean_conv in ['ean2upc', 'always']:
|
|
cur_barcode = cur_barcode[1:]
|
|
|
|
if not check_barcode_encoding(barcode, rule.encoding):
|
|
continue
|
|
|
|
match = self.match_pattern(cur_barcode, rule.pattern)
|
|
if match['match']:
|
|
if rule.type == 'alias':
|
|
barcode = rule.alias
|
|
parsed_result['code'] = barcode
|
|
else:
|
|
parsed_result['encoding'] = rule.encoding
|
|
parsed_result['type'] = rule.type
|
|
parsed_result['value'] = match['value']
|
|
parsed_result['code'] = cur_barcode
|
|
if rule.encoding == "ean13":
|
|
parsed_result['base_code'] = self.sanitize_ean(match['base_code'])
|
|
elif rule.encoding == "upca":
|
|
parsed_result['base_code'] = self.sanitize_upc(match['base_code'])
|
|
else:
|
|
parsed_result['base_code'] = match['base_code']
|
|
return parsed_result
|
|
|
|
return parsed_result
|