4f7a258bf9b3daca3062904a32c214ffde3f9ae3
[odoo/odoo.git] / openerp / addons / base / ir / ir_sequence.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-TODAY OpenERP S.A. <http://www.openerp.com>
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 time
24
25 import openerp
26
27 _logger = logging.getLogger('ir_sequence')
28
29 class ir_sequence_type(openerp.osv.osv.osv):
30     _name = 'ir.sequence.type'
31     _order = 'name'
32     _columns = {
33         'name': openerp.osv.fields.char('Name', size=64, required=True),
34         'code': openerp.osv.fields.char('Code', size=32, required=True),
35     }
36
37     _sql_constraints = [
38         ('code_unique', 'unique(code)', '`code` must be unique.'),
39     ]
40
41 def _code_get(self, cr, uid, context={}):
42     cr.execute('select code, name from ir_sequence_type')
43     return cr.fetchall()
44
45 class ir_sequence(openerp.osv.osv.osv):
46     """ Sequence model.
47
48     The sequence model allows to define and use so-called sequence objects.
49     Such objects are used to generate unique identifiers in a transaction-safe
50     way.
51
52     """
53     _name = 'ir.sequence'
54     _order = 'name'
55     _columns = {
56         'name': openerp.osv.fields.char('Name', size=64, required=True),
57         'code': openerp.osv.fields.selection(_code_get, 'Code', size=64, required=True),
58         'implementation': openerp.osv.fields.selection( # TODO update the view
59             [('standard', 'Standard'), ('no_gap', 'No gap')],
60             'Implementation', required=True,
61             help="Two sequence object implementations are offered: Standard "
62             "and 'No gap'. The later is slower than the former but forbids any"
63             " gap in the sequence (while they are possible in the former)."),
64         'active': openerp.osv.fields.boolean('Active'),
65         'prefix': openerp.osv.fields.char('Prefix', size=64, help="Prefix value of the record for the sequence"),
66         'suffix': openerp.osv.fields.char('Suffix', size=64, help="Suffix value of the record for the sequence"),
67         'number_next': openerp.osv.fields.integer('Next Number', required=True, help="Next number of this sequence"),
68         'number_increment': openerp.osv.fields.integer('Increment Number', required=True, help="The next number of the sequence will be incremented by this number"),
69         'padding' : openerp.osv.fields.integer('Number Padding', required=True, help="OpenERP will automatically adds some '0' on the left of the 'Next Number' to get the required padding size."),
70         'company_id': openerp.osv.fields.many2one('res.company', 'Company'),
71     }
72     _defaults = {
73         'implementation': 'standard',
74         'active': True,
75         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'ir.sequence', context=c),
76         'number_increment': 1,
77         'number_next': 1,
78         'padding' : 0,
79     }
80
81     def init(self, cr):
82         return # Don't do the following index yet.
83         # CONSTRAINT/UNIQUE INDEX on (code, company_id) 
84         # /!\ The unique constraint 'unique_name_company_id' is not sufficient, because SQL92
85         # only support field names in constraint definitions, and we need a function here:
86         # we need to special-case company_id to treat all NULL company_id as equal, otherwise
87         # we would allow duplicate (code, NULL) ir_sequences.
88         cr.execute("""
89             SELECT indexname FROM pg_indexes WHERE indexname =
90             'ir_sequence_unique_code_company_id_idx'""")
91         if not cr.fetchone():
92             cr.execute("""
93                 CREATE UNIQUE INDEX ir_sequence_unique_code_company_id_idx
94                 ON ir_sequence (code, (COALESCE(company_id,-1)))""")
95
96     def _create_sequence(self, cr, id, number_increment, number_next):
97         """ Create a PostreSQL sequence.
98
99         There is no access rights check.
100         """
101         assert isinstance(id, (int, long))
102         sql = "CREATE SEQUENCE ir_sequence_%03d INCREMENT BY %%s START WITH %%s" % id
103         cr.execute(sql, (number_increment, number_next))
104
105     def _drop_sequence(self, cr, ids):
106         """ Drop the PostreSQL sequence if it exists.
107
108         There is no access rights check.
109         """
110
111         ids = ids if isinstance(ids, (list, tuple)) else [ids]
112         assert all(isinstance(i, (int, long)) for i in ids), \
113             "Only ids in (int, long) allowed."
114         names = ','.join('ir_sequence_%03d' % i for i in ids)
115
116         # RESTRICT is the default; it prevents dropping the sequence if an
117         # object depends on it.
118         cr.execute("DROP SEQUENCE IF EXISTS %s RESTRICT " % names)
119
120     def _alter_sequence(self, cr, id, number_increment, number_next):
121         """ Alter a PostreSQL sequence.
122
123         There is no access rights check.
124         """
125         assert isinstance(id, (int, long))
126         cr.execute("""
127             ALTER SEQUENCE ir_sequence_%03d INCREMENT BY %%s RESTART WITH %%s
128             """ % id, (number_increment, number_next))
129
130     def create(self, cr, uid, values, context=None):
131         """ Create a sequence, in implementation == standard a fast gaps-allowed PostgreSQL sequence is used.
132         """
133         values = self._add_missing_default_values(cr, uid, values, context)
134         values['id'] = super(ir_sequence, self).create(cr, uid, values, context)
135         if values['implementation'] == 'standard':
136             f = self._create_sequence(cr, values['id'], values['number_increment'], values['number_next'])
137         return values['id']
138
139     def unlink(self, cr, uid, ids, context=None):
140         super(ir_sequence, self).unlink(cr, uid, ids, context)
141         self._drop_sequence(cr, ids)
142         return True
143
144     def write(self, cr, uid, ids, values, context=None):
145         if not isinstance(ids, (list, tuple)):
146             ids = [ids]
147         new_implementation = values.get('implementation')
148         rows = self.read(cr, uid, ids, ['implementation', 'number_increment', 'number_next'], context)
149         super(ir_sequence, self).write(cr, uid, ids, values, context)
150
151         for row in rows:
152             # 4 cases: we test the previous impl. against the new one.
153             if row['implementation'] == 'standard':
154                 i = values.get('number_increment', row['number_increment'])
155                 n = values.get('number_next', row['number_next'])
156                 if new_implementation in ('standard', None):
157                     self._alter_sequence(cr, row['id'], i, n)
158                 else:
159                     self._drop_sequence(cr, row['id'])
160             else:
161                 if new_implementation in ('no_gap', None):
162                     pass
163                 else:
164                     self._create_sequence(cr, row['id'], i, n)
165
166         return True
167
168     def _interpolate(self, s, d):
169         if s:
170             return s % d
171         return  ''
172
173     def _interpolation_dict(self):
174         t = time.localtime() # Actually, the server is always in UTC.
175         return {
176             'year': time.strftime('%Y', t),
177             'month': time.strftime('%m', t),
178             'day': time.strftime('%d', t),
179             'y': time.strftime('%y', t),
180             'doy': time.strftime('%j', t),
181             'woy': time.strftime('%W', t),
182             'weekday': time.strftime('%w', t),
183             'h24': time.strftime('%H', t),
184             'h12': time.strftime('%I', t),
185             'min': time.strftime('%M', t),
186             'sec': time.strftime('%S', t),
187         }
188
189     def _next(self, cr, uid, seq_ids, context=None):
190         if not seq_ids:
191             return False
192         seq = self.read(cr, uid, seq_ids[:1], ['implementation','number_next','prefix','suffix','padding'])[0]
193         if seq['implementation'] == 'standard':
194             cr.execute("SELECT nextval('ir_sequence_%03d')" % seq['id'])
195             seq['number_next'] = cr.fetchone()
196         else:
197             cr.execute("SELECT number_next FROM ir_sequence WHERE id=%s FOR UPDATE NOWAIT", (seq['id'],))
198             cr.execute("UPDATE ir_sequence SET number_next=number_next+number_increment WHERE id=%s ", (seq['id'],))
199         d = self._interpolation_dict()
200         interpolated_prefix = self._interpolate(seq['prefix'], d)
201         interpolated_suffix = self._interpolate(seq['suffix'], d)
202         return interpolated_prefix + '%%0%sd' % seq['padding'] % seq['number_next'] + interpolated_suffix
203
204     def next_by_id(self, cr, uid, sequence_id, context=None):
205         """ Draw an interpolated string using the specified sequence."""
206         self.check_read(cr, uid)
207         company_ids = self.pool.get('res.company').search(cr, uid, [], context=context) + [False]
208         ids = self.search(cr, uid, ['&',('id','=', sequence_id),('company_id','in',company_ids)])
209         return self._next(cr, uid, ids, context)
210
211     def next_by_code(self, cr, uid, sequence_code, context=None):
212         """ Draw an interpolated string using the specified sequence."""
213         self.check_read(cr, uid)
214         company_ids = self.pool.get('res.company').search(cr, uid, [], context=context) + [False]
215         ids = self.search(cr, uid, ['&',('code','=', sequence_code),('company_id','in',company_ids)])
216         return self._next(cr, uid, ids, context)
217
218     def get_id(self, cr, uid, sequence_code_or_id, code_or_id='id', context=None):
219         """ Draw an interpolated string using the specified sequence.
220
221         The sequence to use is specified by the ``sequence_code_or_id``
222         argument, which can be a code or an id (as controlled by the
223         ``code_or_id`` argument. This method is deprecated.
224         """
225         _logger.warning("ir_sequence.get() and ir_sequence.get_id() are deprecated. "
226             "Please use ir_sequence.next_by_code() or ir_sequence.next_by_id().")
227         if code_or_id == 'id':
228             return self.next_by_id(cr, uid, sequence_code_or_id, context)
229         else:
230             return self.next_by_code(cr, uid, sequence_code_or_id, context)
231
232     def get(self, cr, uid, code, context=None):
233         """ Draw an interpolated string using the specified sequence.
234
235         The sequence to use is specified by its code. This method is
236         deprecated.
237         """
238         return self.get_id(cr, uid, code, 'code', context)
239
240
241 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: