543d364a218536ef81cf7acc0d8a55f5fdb9fdc7
[odoo/odoo.git] / addons / barcodes / barcodes.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import logging
23 import re
24
25 import openerp
26 from openerp import tools, models, fields, api
27 from openerp.osv import fields, osv
28 from openerp.tools.translate import _
29 from openerp.exceptions import ValidationError
30
31 _logger = logging.getLogger(__name__)
32
33 class barcode_nomenclature(osv.osv):
34     _name = 'barcode.nomenclature'
35     _columns = {
36         'name': fields.char('Nomenclature Name', size=32, required=True, help='An internal identification of the barcode nomenclature'),
37         'rule_ids': fields.one2many('barcode.rule','barcode_nomenclature_id','Rules', help='The list of barcode rules'),
38         'strict_ean': fields.boolean('Use strict EAN13', 
39             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.')
40     }
41
42     _defaults = {
43         'strict_ean': False,
44     }
45
46     def use_strict_ean(self):
47         return self.strict_ean
48
49     # returns the checksum of the ean13, or -1 if the ean has not the correct length, ean must be a string
50     def ean_checksum(self, ean):
51         code = list(ean)
52         if len(code) != 13:
53             return -1
54
55         oddsum = evensum = total = 0
56         code = code[:-1] # Remove checksum
57         for i in range(len(code)):
58             if i % 2 == 0:
59                 evensum += int(code[i])
60             else:
61                 oddsum += int(code[i])
62         total = oddsum * 3 + evensum
63         return int((10 - total % 10) % 10)
64
65     # returns true if the barcode is a valid EAN barcode
66     def check_ean(self, ean):
67        return re.match("^\d+$", ean) and self.ean_checksum(ean) == int(ean[len(ean)-1])
68         
69     # Returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string.
70     def sanitize_ean(self, ean):
71         ean = ean[0:13]
72         ean = ean + (13-len(ean))*'0'
73         return ean[0:12] + str(self.ean_checksum(ean))
74
75     # Attempts to interpret an barcode (string encoding a barcode)
76     # It will return an object containing various information about the barcode.
77     # most importantly : 
78     #  - code    : the barcode
79     #  - type   : the type of the barcode: 
80     #  - value  : if the id encodes a numerical value, it will be put there
81     #  - base_code : the barcode code with all the encoding parts set to zero; the one put on
82     #                the product in the backend
83     def parse_barcode(self, barcode):
84         parsed_result = {
85             'encoding': '', 
86             'type': 'error', 
87             'code': barcode, 
88             'base_code': barcode, 
89             'value': 0}
90
91         # Checks if barcode matches the pattern
92         # Additionnaly retrieves the optional numerical content in barcode
93         # Returns an object containing:
94         # - value: the numerical value encoded in the barcode (0 if no value encoded)
95         # - base_code: the barcode in which numerical content is replaced by 0's
96         # - match: boolean
97         def match_pattern(barcode, pattern):
98             match = {
99                 "value": 0,
100                 "base_code": barcode,
101                 "match": False,
102             }
103
104             barcode = barcode.replace("\\", "\\\\").replace("{", '\{').replace("}", "\}").replace(".", "\.")
105             numerical_content = re.search("[{][N]*[D]*[}]", pattern) # look for numerical content in pattern
106
107             if numerical_content: # the pattern encodes a numerical content
108                 num_start = numerical_content.start() # start index of numerical content
109                 num_end = numerical_content.end() # end index of numerical content
110                 value_string = barcode[num_start:num_end-2] # numerical content in barcode
111
112                 whole_part_match = re.search("[{][N]*[D}]", numerical_content.group()) # looks for whole part of numerical content
113                 decimal_part_match = re.search("[{N][D]*[}]", numerical_content.group()) # looks for decimal part
114                 whole_part = value_string[:whole_part_match.end()-2] # retrieve whole part of numerical content in barcode
115                 decimal_part = "0." + value_string[decimal_part_match.start():decimal_part_match.end()-1] # retrieve decimal part
116                 if whole_part == '':
117                     whole_part = '0'
118                 match['value'] = int(whole_part) + float(decimal_part)
119
120                 match['base_code'] = barcode[:num_start] + (num_end-num_start-2)*"0" + barcode[num_end-2:] # replace numerical content by 0's in barcode
121                 match['base_code'] = match['base_code'].replace("\\\\", "\\").replace("\{", "{").replace("\}","}").replace("\.",".")
122                 pattern = pattern[:num_start] + (num_end-num_start-2)*"0" + pattern[num_end:] # replace numerical content by 0's in pattern to match
123
124             match['match'] = re.match(pattern, match['base_code'][:len(pattern)])
125
126             return match
127
128
129         rules = []
130         for rule in self.rule_ids:
131             rules.append({'type': rule.type, 'encoding': rule.encoding, 'sequence': rule.sequence, 'pattern': rule.pattern, 'alias': rule.alias})
132
133         # If the nomenclature does not use strict EAN, prepend the barcode with a 0 if it seems
134         # that it has been striped by the barcode scanner, when trying to match an EAN13 rule
135         prepend_zero = False
136         if not self.strict_ean and len(barcode) == 12 and self.check_ean("0"+barcode):
137             prepend_zero = True
138
139         for rule in rules:
140             cur_barcode = barcode
141             if prepend_zero and rule['encoding'] == "ean13":
142                 cur_barcode = '0'+cur_barcode
143
144             match = match_pattern(cur_barcode, rule['pattern'])
145             if match['match']:
146                 if rule['type'] == 'alias':
147                     barcode = rule['alias']
148                     parsed_result['code'] = barcode
149                 else:
150                     parsed_result['encoding'] = rule['encoding']
151                     parsed_result['type'] = rule['type']
152                     parsed_result['value'] = match['value']
153                     parsed_result['code'] = cur_barcode
154                     if rule['encoding'] == "ean13":
155                         parsed_result['base_code'] = self.sanitize_ean(match['base_code'])
156                     else:
157                         parsed_result['base_code'] = match['base_code']
158                     return parsed_result
159
160         return parsed_result
161
162 class barcode_rule(models.Model):
163     _name = 'barcode.rule'
164     _order = 'sequence asc'
165
166     @api.model
167     def _get_type_selection(self):
168         return [('alias','Alias'),('product','Unit Product')]
169         
170     _columns = {
171         'name':     fields.char('Rule Name', size=32, required=True, help='An internal identification for this barcode nomenclature rule'),
172         'barcode_nomenclature_id':     fields.many2one('barcode.nomenclature','Barcode Nomenclature'),
173         'sequence': fields.integer('Sequence', help='Used to order rules such that rules with a smaller sequence match first'),
174         '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'),
175         'type':     fields.selection('_get_type_selection','Type', required=True),
176         'pattern':  fields.char('Barcode Pattern', size=32, help="The barcode matching pattern"),
177         'alias':    fields.char('Alias',size=32,help='The matched pattern will alias to this barcode',required=True),      
178     }
179
180     _defaults = {
181         'type': 'product',
182         'pattern': '*',
183         'encoding': 'any',
184         'alias': "0",
185     }
186
187     @api.one
188     @api.constrains('pattern')
189     def _check_pattern(self):
190         p = self.pattern.replace("\\\\", "X").replace("\{", "X").replace("\}", "X")
191         findall = re.findall("[{]|[}]", p) # p does not contain escaped { or }
192         if len(findall) == 2: 
193             if not re.search("[{][N]*[D]*[}]", p):
194                 raise ValidationError(_("There is a syntax error in the barcode pattern ") + self.pattern + _(": braces can only contain N's followed by D's."))
195             elif re.search("[{][}]", p):
196                 raise ValidationError(_("There is a syntax error in the barcode pattern ") + self.pattern + _(": empty braces."))
197         elif len(findall) != 0:
198             raise ValidationError(_("There is a syntax error in the barcode pattern ") + self.pattern + _(": a rule can only contain one pair of braces."))