[MERGE] Forward-port saas-4 up to 5ceded9
[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 from openerp.osv import osv
27 from openerp.tools.translate import _
28
29 _logger = logging.getLogger(__name__)
30
31 class ir_sequence_type(openerp.osv.osv.osv):
32     _name = 'ir.sequence.type'
33     _order = 'name'
34     _columns = {
35         'name': openerp.osv.fields.char('Name', size=64, required=True),
36         'code': openerp.osv.fields.char('Code', size=32, required=True),
37     }
38
39     _sql_constraints = [
40         ('code_unique', 'unique(code)', '`code` must be unique.'),
41     ]
42
43 def _code_get(self, cr, uid, context=None):
44     cr.execute('select code, name from ir_sequence_type')
45     return cr.fetchall()
46
47 class ir_sequence(openerp.osv.osv.osv):
48     """ Sequence model.
49
50     The sequence model allows to define and use so-called sequence objects.
51     Such objects are used to generate unique identifiers in a transaction-safe
52     way.
53
54     """
55     _name = 'ir.sequence'
56     _order = 'name'
57     
58     def _get_number_next_actual(self, cr, user, ids, field_name, arg, context=None):
59         '''Return number from ir_sequence row when no_gap implementation,
60         and number from postgres sequence when standard implementation.'''
61         res = dict.fromkeys(ids)
62         for element in self.browse(cr, user, ids, context=context):
63             if  element.implementation != 'standard':
64                 res[element.id] = element.number_next
65             else:
66                 # get number from postgres sequence. Cannot use
67                 # currval, because that might give an error when
68                 # not having used nextval before.
69                 statement = (
70                     "SELECT last_value, increment_by, is_called"
71                     " FROM ir_sequence_%03d"
72                     % element.id)
73                 cr.execute(statement)
74                 (last_value, increment_by, is_called) = cr.fetchone()
75                 if is_called:
76                     res[element.id] = last_value + increment_by
77                 else:
78                     res[element.id] = last_value
79         return res
80
81     def _set_number_next_actual(self, cr, uid, id, name, value, args=None, context=None):
82         return self.write(cr, uid, id, {'number_next': value or 0}, context=context)
83
84
85     _columns = {
86         'name': openerp.osv.fields.char('Name', size=64, required=True),
87         'code': openerp.osv.fields.selection(_code_get, 'Sequence Type', size=64),
88         'implementation': openerp.osv.fields.selection( # TODO update the view
89             [('standard', 'Standard'), ('no_gap', 'No gap')],
90             'Implementation', required=True,
91             help="Two sequence object implementations are offered: Standard "
92             "and 'No gap'. The later is slower than the former but forbids any"
93             " gap in the sequence (while they are possible in the former)."),
94         'active': openerp.osv.fields.boolean('Active'),
95         'prefix': openerp.osv.fields.char('Prefix', size=64, help="Prefix value of the record for the sequence"),
96         'suffix': openerp.osv.fields.char('Suffix', size=64, help="Suffix value of the record for the sequence"),
97         'number_next': openerp.osv.fields.integer('Next Number', required=True, help="Next number of this sequence"),
98         'number_next_actual': openerp.osv.fields.function(_get_number_next_actual, fnct_inv=_set_number_next_actual, type='integer', required=True, string='Next Number', help='Next number that will be used. This number can be incremented frequently so the displayed value might already be obsolete'),
99         'number_increment': openerp.osv.fields.integer('Increment Number', required=True, help="The next number of the sequence will be incremented by this number"),
100         '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."),
101         'company_id': openerp.osv.fields.many2one('res.company', 'Company'),
102     }
103     _defaults = {
104         'implementation': 'standard',
105         'active': True,
106         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'ir.sequence', context=c),
107         'number_increment': 1,
108         'number_next': 1,
109         'number_next_actual': 1,
110         'padding' : 0,
111     }
112
113     def init(self, cr):
114         return # Don't do the following index yet.
115         # CONSTRAINT/UNIQUE INDEX on (code, company_id) 
116         # /!\ The unique constraint 'unique_name_company_id' is not sufficient, because SQL92
117         # only support field names in constraint definitions, and we need a function here:
118         # we need to special-case company_id to treat all NULL company_id as equal, otherwise
119         # we would allow duplicate (code, NULL) ir_sequences.
120         cr.execute("""
121             SELECT indexname FROM pg_indexes WHERE indexname =
122             'ir_sequence_unique_code_company_id_idx'""")
123         if not cr.fetchone():
124             cr.execute("""
125                 CREATE UNIQUE INDEX ir_sequence_unique_code_company_id_idx
126                 ON ir_sequence (code, (COALESCE(company_id,-1)))""")
127
128     def _create_sequence(self, cr, id, number_increment, number_next):
129         """ Create a PostreSQL sequence.
130
131         There is no access rights check.
132         """
133         if number_increment == 0:
134              raise osv.except_osv(_('Warning!'),_("Increment number must not be zero."))
135         assert isinstance(id, (int, long))
136         sql = "CREATE SEQUENCE ir_sequence_%03d INCREMENT BY %%s START WITH %%s" % id
137         cr.execute(sql, (number_increment, number_next))
138
139     def _drop_sequence(self, cr, ids):
140         """ Drop the PostreSQL sequence if it exists.
141
142         There is no access rights check.
143         """
144
145         ids = ids if isinstance(ids, (list, tuple)) else [ids]
146         assert all(isinstance(i, (int, long)) for i in ids), \
147             "Only ids in (int, long) allowed."
148         names = ','.join('ir_sequence_%03d' % i for i in ids)
149
150         # RESTRICT is the default; it prevents dropping the sequence if an
151         # object depends on it.
152         cr.execute("DROP SEQUENCE IF EXISTS %s RESTRICT " % names)
153
154     def _alter_sequence(self, cr, id, number_increment, number_next=None):
155         """ Alter a PostreSQL sequence.
156
157         There is no access rights check.
158         """
159         if number_increment == 0:
160              raise osv.except_osv(_('Warning!'),_("Increment number must not be zero."))
161         assert isinstance(id, (int, long))
162         seq_name = 'ir_sequence_%03d' % (id,)
163         cr.execute("SELECT relname FROM pg_class WHERE relkind = %s AND relname=%s", ('S', seq_name))
164         if not cr.fetchone():
165             # sequence is not created yet, we're inside create() so ignore it, will be set later
166             return
167         statement = "ALTER SEQUENCE %s INCREMENT BY %d" % (seq_name, number_increment)
168         if number_next is not None:
169             statement += " RESTART WITH %d" % (number_next, )
170         cr.execute(statement)
171
172     def create(self, cr, uid, values, context=None):
173         """ Create a sequence, in implementation == standard a fast gaps-allowed PostgreSQL sequence is used.
174         """
175         values = self._add_missing_default_values(cr, uid, values, context)
176         values['id'] = super(ir_sequence, self).create(cr, uid, values, context)
177         if values['implementation'] == 'standard':
178             self._create_sequence(cr, values['id'], values['number_increment'], values['number_next'])
179         return values['id']
180
181     def unlink(self, cr, uid, ids, context=None):
182         super(ir_sequence, self).unlink(cr, uid, ids, context)
183         self._drop_sequence(cr, ids)
184         return True
185
186     def write(self, cr, uid, ids, values, context=None):
187         if not isinstance(ids, (list, tuple)):
188             ids = [ids]
189         new_implementation = values.get('implementation')
190         rows = self.read(cr, uid, ids, ['implementation', 'number_increment', 'number_next'], context)
191         super(ir_sequence, self).write(cr, uid, ids, values, context)
192
193         for row in rows:
194             # 4 cases: we test the previous impl. against the new one.
195             i = values.get('number_increment', row['number_increment'])
196             n = values.get('number_next', row['number_next'])
197             if row['implementation'] == 'standard':
198                 if new_implementation in ('standard', None):
199                     # Implementation has NOT changed.
200                     # Only change sequence if really requested.
201                     if row['number_next'] != n:
202                         self._alter_sequence(cr, row['id'], i, n)
203                     else:
204                         # Just in case only increment changed
205                         self._alter_sequence(cr, row['id'], i)
206                 else:
207                     self._drop_sequence(cr, row['id'])
208             else:
209                 if new_implementation in ('no_gap', None):
210                     pass
211                 else:
212                     self._create_sequence(cr, row['id'], i, n)
213
214         return True
215
216     def _interpolate(self, s, d):
217         if s:
218             return s % d
219         return  ''
220
221     def _interpolation_dict(self):
222         t = time.localtime() # Actually, the server is always in UTC.
223         return {
224             'year': time.strftime('%Y', t),
225             'month': time.strftime('%m', t),
226             'day': time.strftime('%d', t),
227             'y': time.strftime('%y', t),
228             'doy': time.strftime('%j', t),
229             'woy': time.strftime('%W', t),
230             'weekday': time.strftime('%w', t),
231             'h24': time.strftime('%H', t),
232             'h12': time.strftime('%I', t),
233             'min': time.strftime('%M', t),
234             'sec': time.strftime('%S', t),
235         }
236
237     def _next(self, cr, uid, seq_ids, context=None):
238         if not seq_ids:
239             return False
240         if context is None:
241             context = {}
242         force_company = context.get('force_company')
243         if not force_company:
244             force_company = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
245         sequences = self.read(cr, uid, seq_ids, ['name','company_id','implementation','number_next','prefix','suffix','padding'])
246         preferred_sequences = [s for s in sequences if s['company_id'] and s['company_id'][0] == force_company ]
247         seq = preferred_sequences[0] if preferred_sequences else sequences[0]
248         if seq['implementation'] == 'standard':
249             cr.execute("SELECT nextval('ir_sequence_%03d')" % seq['id'])
250             seq['number_next'] = cr.fetchone()
251         else:
252             cr.execute("SELECT number_next FROM ir_sequence WHERE id=%s FOR UPDATE NOWAIT", (seq['id'],))
253             cr.execute("UPDATE ir_sequence SET number_next=number_next+number_increment WHERE id=%s ", (seq['id'],))
254         d = self._interpolation_dict()
255         try:
256             interpolated_prefix = self._interpolate(seq['prefix'], d)
257             interpolated_suffix = self._interpolate(seq['suffix'], d)
258         except ValueError:
259             raise osv.except_osv(_('Warning'), _('Invalid prefix or suffix for sequence \'%s\'') % (seq.get('name')))
260         return interpolated_prefix + '%%0%sd' % seq['padding'] % seq['number_next'] + interpolated_suffix
261
262     def next_by_id(self, cr, uid, sequence_id, context=None):
263         """ Draw an interpolated string using the specified sequence."""
264         self.check_access_rights(cr, uid, 'read')
265         company_ids = self.pool.get('res.company').search(cr, uid, [], context=context) + [False]
266         ids = self.search(cr, uid, ['&',('id','=', sequence_id),('company_id','in',company_ids)])
267         return self._next(cr, uid, ids, context)
268
269     def next_by_code(self, cr, uid, sequence_code, context=None):
270         """ Draw an interpolated string using a sequence with the requested code.
271             If several sequences with the correct code are available to the user
272             (multi-company cases), the one from the user's current company will
273             be used.
274
275             :param dict context: context dictionary may contain a
276                 ``force_company`` key with the ID of the company to
277                 use instead of the user's current company for the
278                 sequence selection. A matching sequence for that
279                 specific company will get higher priority. 
280         """
281         self.check_access_rights(cr, uid, 'read')
282         company_ids = self.pool.get('res.company').search(cr, uid, [], context=context) + [False]
283         ids = self.search(cr, uid, ['&', ('code', '=', sequence_code), ('company_id', 'in', company_ids)])
284         return self._next(cr, uid, ids, context)
285
286     def get_id(self, cr, uid, sequence_code_or_id, code_or_id='id', context=None):
287         """ Draw an interpolated string using the specified sequence.
288
289         The sequence to use is specified by the ``sequence_code_or_id``
290         argument, which can be a code or an id (as controlled by the
291         ``code_or_id`` argument. This method is deprecated.
292         """
293         # TODO: bump up to warning after 6.1 release
294         _logger.debug("ir_sequence.get() and ir_sequence.get_id() are deprecated. "
295             "Please use ir_sequence.next_by_code() or ir_sequence.next_by_id().")
296         if code_or_id == 'id':
297             return self.next_by_id(cr, uid, sequence_code_or_id, context)
298         else:
299             return self.next_by_code(cr, uid, sequence_code_or_id, context)
300
301     def get(self, cr, uid, code, context=None):
302         """ Draw an interpolated string using the specified sequence.
303
304         The sequence to use is specified by its code. This method is
305         deprecated.
306         """
307         return self.get_id(cr, uid, code, 'code', context)
308
309
310 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: