[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>
This commit is contained in:
Rémy Voet (ryv)
2021-02-26 08:21:08 +00:00
committed by svs-odoo
parent d1abf31fb4
commit a09e370ec5
5 changed files with 209 additions and 196 deletions
+115 -101
View File
@@ -1,163 +1,177 @@
import logging
import re
from odoo import models, fields
_logger = logging.getLogger(__name__)
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'),
('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',
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.")
# returns the checksum of the ean13, or -1 if the ean has not the correct length, ean must be a string
def ean_checksum(self, ean):
code = list(ean)
if len(code) != 13:
return -1
@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 = code[:-1] # Remove checksum
for i in range(len(code)):
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(code[i])
evensum += int(digit)
else:
oddsum += int(code[i])
total = oddsum * 3 + evensum
return int((10 - total % 10) % 10)
oddsum += int(digit)
total = evensum * 3 + oddsum
return (10 - total % 10) % 10
# returns the checksum of the ean8, or -1 if the ean has not the correct length, ean must be a string
def ean8_checksum(self,ean):
code = list(ean)
if len(code) != 8:
return -1
sum1 = int(ean[1]) + int(ean[3]) + int(ean[5])
sum2 = int(ean[0]) + int(ean[2]) + int(ean[4]) + int(ean[6])
total = sum1 + 3 * sum2
return int((10 - total % 10) % 10)
# returns true if the barcode is a valid EAN barcode
def check_ean(self, ean):
return re.match("^\d+$", ean) and self.ean_checksum(ean) == int(ean[-1])
# returns true if the barcode string is encoded with the provided encoding.
@api.model
def check_encoding(self, barcode, encoding):
if encoding == 'ean13':
return len(barcode) == 13 and re.match("^\d+$", barcode) and self.ean_checksum(barcode) == int(barcode[-1])
elif encoding == 'ean8':
return len(barcode) == 8 and re.match("^\d+$", barcode) and self.ean8_checksum(barcode) == int(barcode[-1])
elif encoding == 'upca':
return len(barcode) == 12 and re.match("^\d+$", barcode) and self.ean_checksum("0"+barcode) == int(barcode[-1])
elif encoding == 'any':
""" 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
else:
return False
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])
# Returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string.
@api.model
def sanitize_ean(self, ean):
ean = ean[0:13]
ean = ean + (13-len(ean))*'0'
return ean[0:12] + str(self.ean_checksum(ean))
""" Returns a valid zero padded EAN-13 from an EAN prefix.
# Returns a valid zero padded UPC-A from a UPC-A prefix. the UPC-A prefix must be a string.
: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):
return self.sanitize_ean('0'+upc)[1:]
""" Returns a valid zero padded UPC-A from a UPC-A prefix.
:type upc: str
"""
return self.sanitize_ean('0' + upc)[1:]
# Checks if barcode matches the pattern
# Additionaly retrieves the optional numerical content in barcode
# Returns 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
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,
'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
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
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
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['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
# Attempts to interpret an barcode (string encoding a barcode)
# It will return an object containing various information about the barcode.
# most importantly :
# - code : the barcode
# - type : the type of the barcode:
# - 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
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,
'encoding': '',
'type': 'error',
'code': barcode,
'base_code': barcode,
'value': 0,
}
rules = []
for rule in self.rule_ids:
rules.append({'type': rule.type, 'encoding': rule.encoding, 'sequence': rule.sequence, 'pattern': rule.pattern, 'alias': rule.alias})
for rule in rules:
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']:
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']):
if not self.check_encoding(barcode, rule.encoding):
continue
match = self.match_pattern(cur_barcode, rule['pattern'])
match = self.match_pattern(cur_barcode, rule.pattern)
if match['match']:
if rule['type'] == 'alias':
barcode = rule['alias']
if rule.type == 'alias':
barcode = rule.alias
parsed_result['code'] = barcode
else:
parsed_result['encoding'] = rule['encoding']
parsed_result['type'] = rule['type']
parsed_result['encoding'] = rule.encoding
parsed_result['type'] = rule.type
parsed_result['value'] = match['value']
parsed_result['code'] = cur_barcode
if rule['encoding'] == "ean13":
if rule.encoding == "ean13":
parsed_result['base_code'] = self.sanitize_ean(match['base_code'])
elif rule['encoding'] == "upca":
elif rule.encoding == "upca":
parsed_result['base_code'] = self.sanitize_upc(match['base_code'])
else:
parsed_result['base_code'] = match['base_code']
+14 -13
View File
@@ -7,30 +7,31 @@ from odoo.exceptions import ValidationError
class BarcodeRule(models.Model):
_name = 'barcode.rule'
_description = 'Barcode Rule'
_order = 'sequence asc'
_order = 'sequence asc, id'
name = fields.Char(string='Rule Name', size=32, required=True, help='An internal identification for this barcode nomenclature rule')
barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', string='Barcode Nomenclature')
sequence = fields.Integer(string='Sequence', help='Used to order rules such that rules with a smaller sequence match first')
encoding = fields.Selection([
('any', 'Any'),
('ean13', 'EAN-13'),
('ean8', 'EAN-8'),
('upca', 'UPC-A'),
], string='Encoding', required=True, default='any', help='This rule will apply only if the barcode is encoded with the specified encoding')
type = fields.Selection([
encoding = fields.Selection(
string='Encoding', required=True, default='any', selection=[
('any', 'Any'),
('ean13', 'EAN-13'),
('ean8', 'EAN-8'),
('upca', 'UPC-A'),
], help='This rule will apply only if the barcode is encoded with the specified encoding')
type = fields.Selection(
string='Type', required=True, selection=[
('alias', 'Alias'),
('product', 'Unit Product')
], string='Type', required=True, default='product')
('product', 'Unit Product'),
], default='product')
pattern = fields.Char(string='Barcode Pattern', size=32, help="The barcode matching pattern", required=True, default='.*')
alias = fields.Char(string='Alias', size=32, default='0', help='The matched pattern will alias to this barcode', required=True)
@api.constrains('pattern')
def _check_pattern(self):
for rule in self:
p = rule.pattern.replace("\\\\", "X").replace("\{", "X").replace("\}", "X")
findall = re.findall("[{]|[}]", p) # p does not contain escaped { or }
p = rule.pattern.replace('\\\\', 'X').replace('\\{', 'X').replace('\\}', 'X')
findall = re.findall("[{]|[}]", p) # p does not contain escaped { or }
if len(findall) == 2:
if not re.search("[{][N]*[D]*[}]", p):
raise ValidationError(_("There is a syntax error in the barcode pattern %(pattern)s: braces can only contain N's followed by D's.", pattern=rule.pattern))
+70 -69
View File
@@ -50,76 +50,77 @@ var BarcodeParser = Class.extend({
return this.loaded;
},
// returns the checksum of the ean13, or -1 if the ean has not the correct length, ean must be a string
ean_checksum: function(ean){
var code = ean.split('');
if(code.length !== 13){
return -1;
}
var oddsum = 0, evensum = 0, total = 0;
code = code.reverse().splice(1);
for(var i = 0; i < code.length; i++){
if(i % 2 === 0){
oddsum += Number(code[i]);
}else{
evensum += Number(code[i]);
/**
* This algorithm is identical for all fixed length numeric GS1 data structures.
*
* It is also valid for EAN-8, EAN-12 (UPC-A), EAN-13 check digit after sanitizing.
* https://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdf
*
* @param {String} numericBarcode Need to have a length of 18
* @returns {number} Check Digit
*/
get_barcode_check_digit(numericBarcode) {
let oddsum = 0, evensum = 0, total = 0;
// Reverses the barcode to be sure each digit will be in the right place
// regardless the barcode length.
const code = numericBarcode.split('').reverse();
// Removes the last barcode digit (should not be took in account for its own computing).
code.shift();
// 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 CHECK_DIGIT
for (let i = 0; i < code.length; i++) {
if (i % 2 === 0) {
evensum += parseInt(code[i]);
} else {
oddsum += parseInt(code[i]);
}
}
total = oddsum * 3 + evensum;
return Number((10 - total % 10) % 10);
total = evensum * 3 + oddsum;
return (10 - total % 10) % 10;
},
// returns the checksum of the ean8, or -1 if the ean has not the correct length, ean must be a string
ean8_checksum: function(ean){
var code = ean.split('');
if (code.length !== 8) {
return -1;
}
var sum1 = Number(code[1]) + Number(code[3]) + Number(code[5]);
var sum2 = Number(code[0]) + Number(code[2]) + Number(code[4]) + Number(code[6]);
var total = sum1 + 3 * sum2;
return Number((10 - total % 10) % 10);
},
// returns true if the ean is a valid EAN barcode number by checking the control digit.
// ean must be a string
check_ean: function(ean){
return /^\d+$/.test(ean) && this.ean_checksum(ean) === Number(ean[ean.length-1]);
},
// returns true if the barcode string is encoded with the provided encoding.
/**
* Checks if the barcode string is encoded with the provided encoding.
*
* @param {String} barcode
* @param {String} encoding could be 'any' (no encoding rules), 'ean8', 'upca' or 'ean13'
* @returns {boolean}
*/
check_encoding: function(barcode, encoding) {
var len = barcode.length;
var allnum = /^\d+$/.test(barcode);
var check = Number(barcode[len-1]);
if (encoding === 'ean13') {
return len === 13 && allnum && this.ean_checksum(barcode) === check;
} else if (encoding === 'ean8') {
return len === 8 && allnum && this.ean8_checksum(barcode) === check;
} else if (encoding === 'upca') {
return len === 12 && allnum && this.ean_checksum('0'+barcode) === check;
} else if (encoding === 'any') {
if (encoding === 'any') {
return true;
} else {
return false;
}
const barcodeSizes = {
ean8: 8,
ean13: 13,
upca: 12,
};
return barcode.length === barcodeSizes[encoding] && /^\d+$/.test(barcode) &&
this.get_barcode_check_digit(barcode) === parseInt(barcode[barcode.length - 1]);
},
// returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string.
/**
* Sanitizes a EAN-13 prefix by padding it with chars zero.
*
* @param {String} ean
* @returns {String}
*/
sanitize_ean: function(ean){
ean = ean.substr(0,13);
for(var n = 0, count = (13 - ean.length); n < count; n++){
ean = '0' + ean;
}
return ean.substr(0,12) + this.ean_checksum(ean);
ean = ean.substr(0, 13);
ean = "0".repeat(13 - ean.length) + ean;
return ean.substr(0, 12) + this.get_barcode_check_digit(ean);
},
// Returns a valid zero padded UPC-A from a UPC-A prefix. the UPC-A prefix must be a string.
/**
* Sanitizes a UPC-A prefix by padding it with chars zero.
*
* @param {String} upc
* @returns {String}
*/
sanitize_upc: function(upc) {
return this.sanitize_ean('0'+upc).substr(1,12);
return this.sanitize_ean(upc).substr(1, 12);
},
// Checks if barcode matches the pattern
@@ -149,33 +150,33 @@ var BarcodeParser = Class.extend({
if (whole_part === ''){
whole_part = '0';
}
match['value'] = parseInt(whole_part) + parseFloat(decimal_part);
match.value = parseInt(whole_part) + parseFloat(decimal_part);
// replace numerical content by 0's in barcode and pattern
match['base_code'] = barcode.substr(0,num_start);
var base_pattern = pattern.substr(0,num_start);
match.base_code = barcode.substr(0, num_start);
base_pattern = pattern.substr(0, num_start);
for(var i=0;i<(num_length-2);i++) {
match['base_code'] += "0";
match.base_code += "0";
base_pattern += "0";
}
match['base_code'] += barcode.substr(num_start+num_length-2,barcode.length-1);
base_pattern += pattern.substr(num_start+num_length,pattern.length-1);
match.base_code += barcode.substr(num_start + num_length - 2, barcode.length - 1);
base_pattern += pattern.substr(num_start + num_length, pattern.length - 1);
match['base_code'] = match['base_code']
match.base_code = match.base_code
.replace("\\\\", "\\")
.replace("\{", "{")
.replace("\}","}")
.replace("\.",".");
var base_code = match.base_code.split('')
var base_code = match.base_code.split('');
if (encoding === 'ean13') {
base_code[12] = '' + this.ean_checksum(match.base_code);
base_code[12] = '' + this.get_barcode_check_digit(match.base_code);
} else if (encoding === 'ean8') {
base_code[7] = '' + this.ean8_checksum(match.base_code);
base_code[7] = '' + this.get_barcode_check_digit(match.base_code);
} else if (encoding === 'upca') {
base_code[11] = '' + this.ean_checksum('0' + match.base_code);
base_code[11] = '' + this.get_barcode_check_digit(match.base_code);
}
match.base_code = base_code.join('')
match.base_code = base_code.join('');
}
if (base_pattern[0] !== '^') {
@@ -41,17 +41,17 @@ QUnit.module('Barcode Parser', {
await barcodeNomenclature.loaded;
let ean8 = "87111125";
assert.equal(barcodeNomenclature.ean8_checksum(ean8, 'ean8'), ean8.charAt(ean8.length - 1));
assert.equal(barcodeNomenclature.get_barcode_check_digit(ean8), ean8[ean8.length - 1]);
ean8 = "4725992";
assert.equal(barcodeNomenclature.ean8_checksum(ean8 + "0", 'ean8'), 8);
assert.equal(barcodeNomenclature.get_barcode_check_digit(ean8 + "0"), 8);
let ean13 = "1234567891231";
assert.equal(barcodeNomenclature.ean_checksum(ean13, 'ean13'), ean13.charAt(ean13.length - 1));
assert.equal(barcodeNomenclature.get_barcode_check_digit(ean13), ean13[ean13.length - 1]);
ean13 = "962434754318";
assert.equal(barcodeNomenclature.ean_checksum(ean13 + "0", 'ean13'), 4);
assert.equal(barcodeNomenclature.get_barcode_check_digit(ean13 + "0"), 4);
let utca = "692771981161";
assert.equal(barcodeNomenclature.ean_checksum("0" + utca, 'upca'), utca.charAt(utca.length - 1));
assert.equal(barcodeNomenclature.get_barcode_check_digit(utca), utca[utca.length - 1]);
utca = "71679131569";
assert.equal(barcodeNomenclature.ean_checksum("0" + utca + "0", 'upca'), 7);
assert.equal(barcodeNomenclature.get_barcode_check_digit(utca + "0"), 7);
});
});
});
@@ -12,15 +12,12 @@ class TestBarcodeNomenclature(common.TransactionCase):
'name': 'Barcode Nomenclature Test',
})
def test_ean8_checksum(self):
def test_barcode_check_digit(self):
barcode_nomenclature = self.env['barcode.nomenclature']
ean8 = "87111125"
checksum = barcode_nomenclature.ean8_checksum(ean8)
self.assertEqual(checksum, int(ean8[-1]))
checksum = barcode_nomenclature.ean8_checksum("8711112")
self.assertEqual(checksum, -1)
checksum = barcode_nomenclature.ean8_checksum("871111256")
self.assertEqual(checksum, -1)
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.