Files
odoo_source/addons/barcodes/models/barcode_nomenclature.py
T
Rémy Voet (ryv) a09e370ec5 [REF] barcodes: refactor check digit computation
To prepare gs1-128 feature, refactors the check digit computation.
Also, does some cleanings:
  - Removes unusused check_ean method;
  - Converts comments into docstrings;
  - Makes needed changes to avoid linter errors (PEP 8).

task-1968113

Co-authored-by: ryv-odoo<ryv@odoo.com>
2021-02-26 08:21:08 +00:00

181 lines
7.9 KiB
Python

import re
from odoo import models, fields, api
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 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,
'upca': 12,
}
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.
:type ean: str
"""
ean = ean[0:13].zfill(13)
return ean[0:-1] + str(self.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 self.check_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']:
cur_barcode = cur_barcode[1:]
if not self.check_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