summaryrefslogtreecommitdiff
path: root/addons/barcodes/barcodes.py
diff options
context:
space:
mode:
authorFrédéric van der Essen <fvdessen@gmail.com>2015-01-13 16:01:12 +0100
committerFrédéric van der Essen <fvdessen@gmail.com>2015-01-13 16:19:39 +0100
commit59b0907d2e9c46b9b71ae0532bc0e73ef046a3e4 (patch)
tree69eed8c68bf12e7b927f1853ea69077d17a300ea /addons/barcodes/barcodes.py
parent8b442057b81f7a0513bc5f7dba3b82b595ae788d (diff)
[IMP] barcodes, point_of_sale : add support for UPC-A barcodes
- Added support for UPC-A Barcodes - Add a way to automatically convert UPC-A to and from EAN13. This is usefull if you wish to standardise your product barcode encoding in one standard but have a mix of the two in your merchandise. - Various bugfixes.
Diffstat (limited to 'addons/barcodes/barcodes.py')
-rw-r--r--addons/barcodes/barcodes.py162
1 files changed, 104 insertions, 58 deletions
diff --git a/addons/barcodes/barcodes.py b/addons/barcodes/barcodes.py
index ad72084bc03..c9b9118728a 100644
--- a/addons/barcodes/barcodes.py
+++ b/addons/barcodes/barcodes.py
@@ -9,22 +9,27 @@ from openerp.exceptions import ValidationError
_logger = logging.getLogger(__name__)
+
+UPC_EAN_CONVERSIONS = [
+ ('none','Never'),
+ ('ean2upc','EAN-13 to UPC-A'),
+ ('upc2ean','UPC-A to EAN-13'),
+ ('always','Always'),
+]
+
class barcode_nomenclature(osv.osv):
_name = 'barcode.nomenclature'
_columns = {
'name': fields.char('Nomenclature Name', size=32, required=True, help='An internal identification of the barcode nomenclature'),
'rule_ids': fields.one2many('barcode.rule','barcode_nomenclature_id','Rules', help='The list of barcode rules'),
- 'strict_ean': fields.boolean('Use strict EAN13',
- help='Many barcode scanners strip the leading zero of EAN13 barcodes. By using strict EAN13, we consider the scanned barcode directly. Otherwise, we prepend scanned barcodes of length 12 by a zero before looking for the associated item.')
+ 'upc_ean_conv': fields.selection(UPC_EAN_CONVERSIONS, 'UPC/EAN Conversion', required=True,
+ 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.'),
}
_defaults = {
- 'strict_ean': False,
+ 'upc_ean_conv': 'always',
}
- def use_strict_ean(self):
- return self.strict_ean
-
# 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)
@@ -41,9 +46,34 @@ class barcode_nomenclature(osv.osv):
total = oddsum * 3 + evensum
return int((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 = ean[1] + ean[3] + ean[5]
+ sum2 = ean[0] + ean[2] + ean[4] + 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[len(ean)-1])
+ 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.
+ 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':
+ return True
+ else:
+ return False
+
# Returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string.
def sanitize_ean(self, ean):
@@ -51,6 +81,47 @@ class barcode_nomenclature(osv.osv):
ean = ean + (13-len(ean))*'0'
return ean[0:12] + str(self.ean_checksum(ean))
+ # Returns a valid zero padded UPC-A from a UPC-A prefix. the UPC-A prefix must be a string.
+ def sanitize_upc(self, upc):
+ return self.sanitize_ean('0'+upc)[1:]
+
+ # Checks if barcode matches the pattern
+ # Additionnaly 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):
+ 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
+
# Attempts to interpret an barcode (string encoding a barcode)
# It will return an object containing various information about the barcode.
# most importantly :
@@ -65,62 +136,24 @@ class barcode_nomenclature(osv.osv):
'type': 'error',
'code': barcode,
'base_code': barcode,
- 'value': 0}
-
- # Checks if barcode matches the pattern
- # Additionnaly 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(barcode, pattern):
- 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
-
+ '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})
- # If the nomenclature does not use strict EAN, prepend the barcode with a 0 if it seems
- # that it has been striped by the barcode scanner, when trying to match an EAN13 rule
- prepend_zero = False
- if not self.strict_ean and len(barcode) == 12 and self.check_ean("0"+barcode):
- prepend_zero = True
-
for rule in rules:
cur_barcode = barcode
- if prepend_zero and rule['encoding'] == "ean13":
+ 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:]
- match = match_pattern(cur_barcode, rule['pattern'])
+ 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']
@@ -132,12 +165,21 @@ class barcode_nomenclature(osv.osv):
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
+ENCODINGS = [
+ ('any','Any'),
+ ('ean13','EAN-13'),
+ ('ean8','EAN-8'),
+ ('upca','UPC-A'),
+]
+
class barcode_rule(models.Model):
_name = 'barcode.rule'
_order = 'sequence asc'
@@ -150,15 +192,15 @@ class barcode_rule(models.Model):
'name': fields.char('Rule Name', size=32, required=True, help='An internal identification for this barcode nomenclature rule'),
'barcode_nomenclature_id': fields.many2one('barcode.nomenclature','Barcode Nomenclature'),
'sequence': fields.integer('Sequence', help='Used to order rules such that rules with a smaller sequence match first'),
- 'encoding': fields.selection([('any','Any'),('ean13','EAN-13')],'Encoding',required=True,help='This rule will apply only if the barcode is encoded with the specified encoding'),
+ 'encoding': fields.selection(ENCODINGS,'Encoding',required=True,help='This rule will apply only if the barcode is encoded with the specified encoding'),
'type': fields.selection('_get_type_selection','Type', required=True),
- 'pattern': fields.char('Barcode Pattern', size=32, help="The barcode matching pattern"),
- 'alias': fields.char('Alias',size=32,help='The matched pattern will alias to this barcode',required=True),
+ 'pattern': fields.char('Barcode Pattern', size=32, help="The barcode matching pattern", required=True),
+ 'alias': fields.char('Alias',size=32,help='The matched pattern will alias to this barcode', required=True),
}
_defaults = {
'type': 'product',
- 'pattern': '*',
+ 'pattern': '.*',
'encoding': 'any',
'alias': "0",
}
@@ -175,3 +217,7 @@ class barcode_rule(models.Model):
raise ValidationError(_("There is a syntax error in the barcode pattern ") + self.pattern + _(": empty braces."))
elif len(findall) != 0:
raise ValidationError(_("There is a syntax error in the barcode pattern ") + self.pattern + _(": a rule can only contain one pair of braces."))
+ elif p == '*':
+ raise ValidationError(_(" '*' is not a valid Regex Barcode Pattern. Did you mean '.*' ?"))
+
+