12d40a39be12a207858a2c4e0ee491e6603ca94a
[odoo/odoo.git] / addons / procurement / procurement.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 time
23 from psycopg2 import OperationalError
24
25 from openerp import SUPERUSER_ID
26 from openerp.osv import fields, osv
27 import openerp.addons.decimal_precision as dp
28 from openerp.tools.translate import _
29 import openerp
30
31 PROCUREMENT_PRIORITIES = [('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')]
32
33 class procurement_group(osv.osv):
34     '''
35     The procurement group class is used to group products together
36     when computing procurements. (tasks, physical products, ...)
37
38     The goal is that when you have one sale order of several products
39     and the products are pulled from the same or several location(s), to keep
40     having the moves grouped into pickings that represent the sale order.
41
42     Used in: sales order (to group delivery order lines like the so), pull/push
43     rules (to pack like the delivery order), on orderpoints (e.g. for wave picking
44     all the similar products together).
45
46     Grouping is made only if the source and the destination is the same.
47     Suppose you have 4 lines on a picking from Output where 2 lines will need
48     to come from Input (crossdock) and 2 lines coming from Stock -> Output As
49     the four procurement orders will have the same group ids from the SO, the
50     move from input will have a stock.picking with 2 grouped lines and the move
51     from stock will have 2 grouped lines also.
52
53     The name is usually the name of the original document (sale order) or a
54     sequence computed if created manually.
55     '''
56     _name = 'procurement.group'
57     _description = 'Procurement Requisition'
58     _order = "id desc"
59     _columns = {
60         'name': fields.char('Reference', required=True),
61         'move_type': fields.selection([
62             ('direct', 'Partial'), ('one', 'All at once')],
63             'Delivery Method', required=True),
64         'procurement_ids': fields.one2many('procurement.order', 'group_id', 'Procurements'),
65     }
66     _defaults = {
67         'name': lambda self, cr, uid, c: self.pool.get('ir.sequence').get(cr, uid, 'procurement.group') or '',
68         'move_type': lambda self, cr, uid, c: 'direct'
69     }
70
71 class procurement_rule(osv.osv):
72     '''
73     A rule describe what a procurement should do; produce, buy, move, ...
74     '''
75     _name = 'procurement.rule'
76     _description = "Procurement Rule"
77     _order = "name"
78
79     def _get_action(self, cr, uid, context=None):
80         return []
81
82     _columns = {
83         'name': fields.char('Name', required=True,
84             help="This field will fill the packing origin and the name of its moves"),
85         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the rule without removing it."),
86         'group_propagation_option': fields.selection([('none', 'Leave Empty'), ('propagate', 'Propagate'), ('fixed', 'Fixed')], string="Propagation of Procurement Group"),
87         'group_id': fields.many2one('procurement.group', 'Fixed Procurement Group'),
88         'action': fields.selection(selection=lambda s, cr, uid, context=None: s._get_action(cr, uid, context=context),
89             string='Action', required=True),
90         'sequence': fields.integer('Sequence'),
91         'company_id': fields.many2one('res.company', 'Company'),
92     }
93
94     _defaults = {
95         'group_propagation_option': 'propagate',
96         'sequence': 20,
97         'active': True,
98     }
99
100
101 class procurement_order(osv.osv):
102     """
103     Procurement Orders
104     """
105     _name = "procurement.order"
106     _description = "Procurement"
107     _order = 'priority desc, date_planned, id asc'
108     _inherit = ['mail.thread']
109     _log_create = False
110     _columns = {
111         'name': fields.text('Description', required=True),
112
113         'origin': fields.char('Source Document',
114             help="Reference of the document that created this Procurement.\n"
115             "This is automatically completed by Odoo."),
116         'company_id': fields.many2one('res.company', 'Company', required=True),
117
118         # These two fields are used for shceduling
119         'priority': fields.selection(PROCUREMENT_PRIORITIES, 'Priority', required=True, select=True, track_visibility='onchange'),
120         'date_planned': fields.datetime('Scheduled Date', required=True, select=True, track_visibility='onchange'),
121
122         'group_id': fields.many2one('procurement.group', 'Procurement Group'),
123         'rule_id': fields.many2one('procurement.rule', 'Rule', track_visibility='onchange', help="Chosen rule for the procurement resolution. Usually chosen by the system but can be manually set by the procurement manager to force an unusual behavior."),
124
125         'product_id': fields.many2one('product.product', 'Product', required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
126         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
127         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, states={'confirmed': [('readonly', False)]}, readonly=True),
128
129         'product_uos_qty': fields.float('UoS Quantity', states={'confirmed': [('readonly', False)]}, readonly=True),
130         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'confirmed': [('readonly', False)]}, readonly=True),
131
132         'state': fields.selection([
133             ('cancel', 'Cancelled'),
134             ('confirmed', 'Confirmed'),
135             ('exception', 'Exception'),
136             ('running', 'Running'),
137             ('done', 'Done')
138         ], 'Status', required=True, track_visibility='onchange', copy=False),
139     }
140
141     _defaults = {
142         'state': 'confirmed',
143         'priority': '1',
144         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
145         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c)
146     }
147
148     def unlink(self, cr, uid, ids, context=None):
149         procurements = self.read(cr, uid, ids, ['state'], context=context)
150         unlink_ids = []
151         for s in procurements:
152             if s['state'] == 'cancel':
153                 unlink_ids.append(s['id'])
154             else:
155                 raise osv.except_osv(_('Invalid Action!'),
156                         _('Cannot delete Procurement Order(s) which are in %s state.') % s['state'])
157         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
158
159     def do_view_procurements(self, cr, uid, ids, context=None):
160         '''
161         This function returns an action that display existing procurement orders
162         of same procurement group of given ids.
163         '''
164         act_obj = self.pool.get('ir.actions.act_window')
165         action_id = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'procurement.do_view_procurements', raise_if_not_found=True)
166         result = act_obj.read(cr, uid, [action_id], context=context)[0]
167         group_ids = set([proc.group_id.id for proc in self.browse(cr, uid, ids, context=context) if proc.group_id])
168         result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]"
169         return result
170
171     def onchange_product_id(self, cr, uid, ids, product_id, context=None):
172         """ Finds UoM and UoS of changed product.
173         @param product_id: Changed id of product.
174         @return: Dictionary of values.
175         """
176         if product_id:
177             w = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
178             v = {
179                 'product_uom': w.uom_id.id,
180                 'product_uos': w.uos_id and w.uos_id.id or w.uom_id.id
181             }
182             return {'value': v}
183         return {}
184
185     def get_cancel_ids(self, cr, uid, ids, context=None):
186         return [proc.id for proc in self.browse(cr, uid, ids, context=context) if proc.state != 'done']
187
188     def cancel(self, cr, uid, ids, context=None):
189         #cancel only the procurements that aren't done already
190         to_cancel_ids = self.get_cancel_ids(cr, uid, ids, context=context)
191         if to_cancel_ids:
192             return self.write(cr, uid, to_cancel_ids, {'state': 'cancel'}, context=context)
193
194     def reset_to_confirmed(self, cr, uid, ids, context=None):
195         return self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
196
197     def run(self, cr, uid, ids, autocommit=False, context=None):
198         for procurement_id in ids:
199             #we intentionnaly do the browse under the for loop to avoid caching all ids which would be resource greedy
200             #and useless as we'll make a refresh later that will invalidate all the cache (and thus the next iteration
201             #will fetch all the ids again) 
202             procurement = self.browse(cr, uid, procurement_id, context=context)
203             if procurement.state not in ("running", "done"):
204                 try:
205                     if self._assign(cr, uid, procurement, context=context):
206                         procurement.refresh()
207                         res = self._run(cr, uid, procurement, context=context or {})
208                         if res:
209                             self.write(cr, uid, [procurement.id], {'state': 'running'}, context=context)
210                         else:
211                             self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
212                     else:
213                         self.message_post(cr, uid, [procurement.id], body=_('No rule matching this procurement'), context=context)
214                         self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
215                     if autocommit:
216                         cr.commit()
217                 except OperationalError:
218                     if autocommit:
219                         cr.rollback()
220                         continue
221                     else:
222                         raise
223         return True
224
225     def check(self, cr, uid, ids, autocommit=False, context=None):
226         done_ids = []
227         for procurement in self.browse(cr, uid, ids, context=context):
228             try:
229                 result = self._check(cr, uid, procurement, context=context)
230                 if result:
231                     done_ids.append(procurement.id)
232                 if autocommit:
233                     cr.commit()
234             except OperationalError:
235                 if autocommit:
236                     cr.rollback()
237                     continue
238                 else:
239                     raise
240         if done_ids:
241             self.write(cr, uid, done_ids, {'state': 'done'}, context=context)
242         return done_ids
243
244     #
245     # Method to overwrite in different procurement modules
246     #
247     def _find_suitable_rule(self, cr, uid, procurement, context=None):
248         '''This method returns a procurement.rule that depicts what to do with the given procurement
249         in order to complete its needs. It returns False if no suiting rule is found.
250             :param procurement: browse record
251             :rtype: int or False
252         '''
253         return False
254
255     def _assign(self, cr, uid, procurement, context=None):
256         '''This method check what to do with the given procurement in order to complete its needs.
257         It returns False if no solution is found, otherwise it stores the matching rule (if any) and
258         returns True.
259             :param procurement: browse record
260             :rtype: boolean
261         '''
262         #if the procurement already has a rule assigned, we keep it (it has a higher priority as it may have been chosen manually)
263         if procurement.rule_id:
264             return True
265         elif procurement.product_id.type != 'service':
266             rule_id = self._find_suitable_rule(cr, uid, procurement, context=context)
267             if rule_id:
268                 self.write(cr, uid, [procurement.id], {'rule_id': rule_id}, context=context)
269                 return True
270         return False
271
272     def _run(self, cr, uid, procurement, context=None):
273         '''This method implements the resolution of the given procurement
274             :param procurement: browse record
275             :returns: True if the resolution of the procurement was a success, False otherwise to set it in exception
276         '''
277         return True
278
279     def _check(self, cr, uid, procurement, context=None):
280         '''Returns True if the given procurement is fulfilled, False otherwise
281             :param procurement: browse record
282             :rtype: boolean
283         '''
284         return False
285
286     #
287     # Scheduler
288     #
289     def run_scheduler(self, cr, uid, use_new_cursor=False, company_id = False, context=None):
290         '''
291         Call the scheduler to check the procurement order. This is intented to be done for all existing companies at
292         the same time, so we're running all the methods as SUPERUSER to avoid intercompany and access rights issues.
293
294         @param self: The object pointer
295         @param cr: The current row, from the database cursor,
296         @param uid: The current user ID for security checks
297         @param ids: List of selected IDs
298         @param use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement.
299             This is appropriate for batch jobs only.
300         @param context: A standard dictionary for contextual values
301         @return:  Dictionary of values
302         '''
303         if context is None:
304             context = {}
305         try:
306             if use_new_cursor:
307                 cr = openerp.registry(cr.dbname).cursor()
308
309             # Run confirmed procurements
310             dom = [('state', '=', 'confirmed')]
311             if company_id:
312                 dom += [('company_id', '=', company_id)]
313             prev_ids = []
314             while True:
315                 ids = self.search(cr, SUPERUSER_ID, dom, context=context)
316                 if not ids or prev_ids == ids:
317                     break
318                 else:
319                     prev_ids = ids
320                 self.run(cr, SUPERUSER_ID, ids, autocommit=use_new_cursor, context=context)
321                 if use_new_cursor:
322                     cr.commit()
323
324             # Check if running procurements are done
325             offset = 0
326             dom = [('state', '=', 'running')]
327             if company_id:
328                 dom += [('company_id', '=', company_id)]
329             prev_ids = []
330             while True:
331                 ids = self.search(cr, SUPERUSER_ID, dom, offset=offset, context=context)
332                 if not ids or prev_ids == ids:
333                     break
334                 else:
335                     prev_ids = ids
336                 self.check(cr, SUPERUSER_ID, ids, autocommit=use_new_cursor, context=context)
337                 if use_new_cursor:
338                     cr.commit()
339
340         finally:
341             if use_new_cursor:
342                 try:
343                     cr.close()
344                 except Exception:
345                     pass
346
347         return {}
348 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: