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