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.

240 lines
11 KiB

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