You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

225 lines
10 KiB

  1. import logging
  2. import re
  3. import openerp
  4. from openerp import tools, models, fields, api
  5. from openerp.osv import fields, osv
  6. from openerp.tools.translate import _
  7. from openerp.exceptions import ValidationError
  8. _logger = logging.getLogger(__name__)
  9. UPC_EAN_CONVERSIONS = [
  10. ('none','Never'),
  11. ('ean2upc','EAN-13 to UPC-A'),
  12. ('upc2ean','UPC-A to EAN-13'),
  13. ('always','Always'),
  14. ]
  15. class barcode_nomenclature(osv.osv):
  16. _name = 'barcode.nomenclature'
  17. _columns = {
  18. 'name': fields.char('Nomenclature Name', size=32, required=True, help='An internal identification of the barcode nomenclature'),
  19. 'rule_ids': fields.one2many('barcode.rule','barcode_nomenclature_id','Rules', help='The list of barcode rules'),
  20. 'upc_ean_conv': fields.selection(UPC_EAN_CONVERSIONS, 'UPC/EAN Conversion', required=True,
  21. 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.'),
  22. }
  23. _defaults = {
  24. 'upc_ean_conv': 'always',
  25. }
  26. # returns the checksum of the ean13, or -1 if the ean has not the correct length, ean must be a string
  27. def ean_checksum(self, ean):
  28. code = list(ean)
  29. if len(code) != 13:
  30. return -1
  31. oddsum = evensum = total = 0
  32. code = code[:-1] # Remove checksum
  33. for i in range(len(code)):
  34. if i % 2 == 0:
  35. evensum += int(code[i])
  36. else:
  37. oddsum += int(code[i])
  38. total = oddsum * 3 + evensum
  39. return int((10 - total % 10) % 10)
  40. # returns the checksum of the ean8, or -1 if the ean has not the correct length, ean must be a string
  41. def ean8_checksum(self,ean):
  42. code = list(ean)
  43. if len(code) != 8:
  44. return -1
  45. sum1 = ean[1] + ean[3] + ean[5]
  46. sum2 = ean[0] + ean[2] + ean[4] + ean[6]
  47. total = sum1 + 3 * sum2
  48. return int((10 - total % 10) % 10)
  49. # returns true if the barcode is a valid EAN barcode
  50. def check_ean(self, ean):
  51. return re.match("^\d+$", ean) and self.ean_checksum(ean) == int(ean[-1])
  52. # returns true if the barcode string is encoded with the provided encoding.
  53. def check_encoding(self, barcode, encoding):
  54. if encoding == 'ean13':
  55. return len(barcode) == 13 and re.match("^\d+$", barcode) and self.ean_checksum(barcode) == int(barcode[-1])
  56. elif encoding == 'ean8':
  57. return len(barcode) == 8 and re.match("^\d+$", barcode) and self.ean8_checksum(barcode) == int(barcode[-1])
  58. elif encoding == 'upca':
  59. return len(barcode) == 12 and re.match("^\d+$", barcode) and self.ean_checksum("0"+barcode) == int(barcode[-1])
  60. elif encoding == 'any':
  61. return True
  62. else:
  63. return False
  64. # Returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string.
  65. def sanitize_ean(self, ean):
  66. ean = ean[0:13]
  67. ean = ean + (13-len(ean))*'0'
  68. return ean[0:12] + str(self.ean_checksum(ean))
  69. # Returns a valid zero padded UPC-A from a UPC-A prefix. the UPC-A prefix must be a string.
  70. def sanitize_upc(self, upc):
  71. return self.sanitize_ean('0'+upc)[1:]
  72. # Checks if barcode matches the pattern
  73. # Additionnaly retrieves the optional numerical content in barcode
  74. # Returns an object containing:
  75. # - value: the numerical value encoded in the barcode (0 if no value encoded)
  76. # - base_code: the barcode in which numerical content is replaced by 0's
  77. # - match: boolean
  78. def match_pattern(self, barcode, pattern):
  79. match = {
  80. "value": 0,
  81. "base_code": barcode,
  82. "match": False,
  83. }
  84. barcode = barcode.replace("\\", "\\\\").replace("{", '\{').replace("}", "\}").replace(".", "\.")
  85. numerical_content = re.search("[{][N]*[D]*[}]", pattern) # look for numerical content in pattern
  86. if numerical_content: # the pattern encodes a numerical content
  87. num_start = numerical_content.start() # start index of numerical content
  88. num_end = numerical_content.end() # end index of numerical content
  89. value_string = barcode[num_start:num_end-2] # numerical content in barcode
  90. whole_part_match = re.search("[{][N]*[D}]", numerical_content.group()) # looks for whole part of numerical content
  91. decimal_part_match = re.search("[{N][D]*[}]", numerical_content.group()) # looks for decimal part
  92. whole_part = value_string[:whole_part_match.end()-2] # retrieve whole part of numerical content in barcode
  93. decimal_part = "0." + value_string[decimal_part_match.start():decimal_part_match.end()-1] # retrieve decimal part
  94. if whole_part == '':
  95. whole_part = '0'
  96. match['value'] = int(whole_part) + float(decimal_part)
  97. match['base_code'] = barcode[:num_start] + (num_end-num_start-2)*"0" + barcode[num_end-2:] # replace numerical content by 0's in barcode
  98. match['base_code'] = match['base_code'].replace("\\\\", "\\").replace("\{", "{").replace("\}","}").replace("\.",".")
  99. pattern = pattern[:num_start] + (num_end-num_start-2)*"0" + pattern[num_end:] # replace numerical content by 0's in pattern to match
  100. match['match'] = re.match(pattern, match['base_code'][:len(pattern)])
  101. return match
  102. # Attempts to interpret an barcode (string encoding a barcode)
  103. # It will return an object containing various information about the barcode.
  104. # most importantly :
  105. # - code : the barcode
  106. # - type : the type of the barcode:
  107. # - value : if the id encodes a numerical value, it will be put there
  108. # - base_code : the barcode code with all the encoding parts set to zero; the one put on
  109. # the product in the backend
  110. def parse_barcode(self, barcode):
  111. parsed_result = {
  112. 'encoding': '',
  113. 'type': 'error',
  114. 'code': barcode,
  115. 'base_code': barcode,
  116. 'value': 0,
  117. }
  118. rules = []
  119. for rule in self.rule_ids:
  120. rules.append({'type': rule.type, 'encoding': rule.encoding, 'sequence': rule.sequence, 'pattern': rule.pattern, 'alias': rule.alias})
  121. for rule in rules:
  122. cur_barcode = barcode
  123. if rule['encoding'] == 'ean13' and self.check_encoding(barcode,'upca') and self.upc_ean_conv in ['upc2ean','always']:
  124. cur_barcode = '0'+cur_barcode
  125. elif rule['encoding'] == 'upca' and self.check_encoding(barcode,'ean13') and barcode[0] == '0' and self.upc_ean_conv in ['ean2upc','always']:
  126. cur_barcode = cur_barcode[1:]
  127. if not self.check_encoding(barcode,rule['encoding']):
  128. continue
  129. match = self.match_pattern(cur_barcode, rule['pattern'])
  130. if match['match']:
  131. if rule['type'] == 'alias':
  132. barcode = rule['alias']
  133. parsed_result['code'] = barcode
  134. else:
  135. parsed_result['encoding'] = rule['encoding']
  136. parsed_result['type'] = rule['type']
  137. parsed_result['value'] = match['value']
  138. parsed_result['code'] = cur_barcode
  139. if rule['encoding'] == "ean13":
  140. parsed_result['base_code'] = self.sanitize_ean(match['base_code'])
  141. elif rule['encoding'] == "upca":
  142. parsed_result['base_code'] = self.sanitize_upc(match['base_code'])
  143. else:
  144. parsed_result['base_code'] = match['base_code']
  145. return parsed_result
  146. return parsed_result
  147. class barcode_rule(models.Model):
  148. _name = 'barcode.rule'
  149. _order = 'sequence asc'
  150. @api.model
  151. def _encoding_selection_list(self):
  152. return [
  153. ('any', _('Any')),
  154. ('ean13', 'EAN-13'),
  155. ('ean8', 'EAN-8'),
  156. ('upca', 'UPC-A'),
  157. ]
  158. @api.model
  159. def _get_type_selection(self):
  160. return [('alias', _('Alias')), ('product', _('Unit Product'))]
  161. _columns = {
  162. 'name': fields.char('Rule Name', size=32, required=True, help='An internal identification for this barcode nomenclature rule'),
  163. 'barcode_nomenclature_id': fields.many2one('barcode.nomenclature','Barcode Nomenclature'),
  164. 'sequence': fields.integer('Sequence', help='Used to order rules such that rules with a smaller sequence match first'),
  165. 'encoding': fields.selection('_encoding_selection_list','Encoding',required=True,help='This rule will apply only if the barcode is encoded with the specified encoding'),
  166. 'type': fields.selection('_get_type_selection','Type', required=True),
  167. 'pattern': fields.char('Barcode Pattern', size=32, help="The barcode matching pattern", required=True),
  168. 'alias': fields.char('Alias',size=32,help='The matched pattern will alias to this barcode', required=True),
  169. }
  170. _defaults = {
  171. 'type': 'product',
  172. 'pattern': '.*',
  173. 'encoding': 'any',
  174. 'alias': "0",
  175. }
  176. @api.one
  177. @api.constrains('pattern')
  178. def _check_pattern(self):
  179. p = self.pattern.replace("\\\\", "X").replace("\{", "X").replace("\}", "X")
  180. findall = re.findall("[{]|[}]", p) # p does not contain escaped { or }
  181. if len(findall) == 2:
  182. if not re.search("[{][N]*[D]*[}]", p):
  183. raise ValidationError(_("There is a syntax error in the barcode pattern ") + self.pattern + _(": braces can only contain N's followed by D's."))
  184. elif re.search("[{][}]", p):
  185. raise ValidationError(_("There is a syntax error in the barcode pattern ") + self.pattern + _(": empty braces."))
  186. elif len(findall) != 0:
  187. raise ValidationError(_("There is a syntax error in the barcode pattern ") + self.pattern + _(": a rule can only contain one pair of braces."))
  188. elif p == '*':
  189. raise ValidationError(_(" '*' is not a valid Regex Barcode Pattern. Did you mean '.*' ?"))