Files
odoo_source/addons/barcodes/models/barcode_nomenclature.py
T
Tiffany Chang (tic) 59e480439e [IMP] delivery, stock: print SSCC datamatrix for packages
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
2022-01-31 12:06:05 +00:00

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