When a package name is a valid SSCC number, we prefer to auto-print the datamatrix to be used by shippers, which includes: - the SSCC number - the pack date (i.e. when it was put in pack) - the weight of the package (only when less or equal to 6 digits) Pack date field was added to accomodate this and defaults to when package is created (e.g. when buying packaged meat at the supermarket, it's assumed the pack date is when the package of meat was created). SSCC datamatrix will auto-print now in 3 package reports: - Package with contents - Package Barcode (PDF) - Package Barcode (ZPL) Note if ability to create datamatrix appears to not work (only in PDF versions) then standard 1D barcode will be used. Part of Task: 2494740 Related ENT PR: odoo/enterprise#23323 Upgrade PR: odoo/upgrade#3202 Part-of: odoo/odoo#82389
183 lines
8.0 KiB
Python
183 lines
8.0 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,
|
|
'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.
|
|
|
|
: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
|