[MERGE] forward port of branch 8.0 up to 2e092ac
[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').next_by_code(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                         res = self._run(cr, uid, procurement, context=context or {})
207                         if res:
208                             self.write(cr, uid, [procurement.id], {'state': 'running'}, context=context)
209                         else:
210                             self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
211                     else:
212                         self.message_post(cr, uid, [procurement.id], body=_('No rule matching this procurement'), context=context)
213                         self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
214                     if autocommit:
215                         cr.commit()
216                 except OperationalError:
217                     if autocommit:
218                         cr.rollback()
219                         continue
220                     else:
221                         raise
222         return True
223
224     def check(self, cr, uid, ids, autocommit=False, context=None):
225         done_ids = []
226         for procurement in self.browse(cr, uid, ids, context=context):
227             try:
228                 result = self._check(cr, uid, procurement, context=context)
229                 if result:
230                     done_ids.append(procurement.id)
231                 if autocommit:
232                     cr.commit()
233             except OperationalError:
234                 if autocommit:
235                     cr.rollback()
236                     continue
237                 else:
238                     raise
239         if done_ids:
240             self.write(cr, uid, done_ids, {'state': 'done'}, context=context)
241         return done_ids
242
243     #
244     # Method to overwrite in different procurement modules
245     #
246     def _find_suitable_rule(self, cr, uid, procurement, context=None):
247         '''This method returns a procurement.rule that depicts what to do with the given procurement
248         in order to complete its needs. It returns False if no suiting rule is found.
249             :param procurement: browse record
250             :rtype: int or False
251         '''
252         return False
253
254     def _assign(self, cr, uid, procurement, context=None):
255         '''This method check what to do with the given procurement in order to complete its needs.
256         It returns False if no solution is found, otherwise it stores the matching rule (if any) and
257         returns True.
258             :param procurement: browse record
259             :rtype: boolean
260         '''
261         #if the procurement already has a rule assigned, we keep it (it has a higher priority as it may have been chosen manually)
262         if procurement.rule_id:
263             return True
264         elif procurement.product_id.type != 'service':
265             rule_id = self._find_suitable_rule(cr, uid, procurement, context=context)
266             if rule_id:
267                 self.write(cr, uid, [procurement.id], {'rule_id': rule_id}, context=context)
268                 return True
269         return False
270
271     def _run(self, cr, uid, procurement, context=None):
272         '''This method implements the resolution of the given procurement
273             :param procurement: browse record
274             :returns: True if the resolution of the procurement was a success, False otherwise to set it in exception
275         '''
276         return True
277
278     def _check(self, cr, uid, procurement, context=None):
279         '''Returns True if the given procurement is fulfilled, False otherwise
280             :param procurement: browse record
281             :rtype: boolean
282         '''
283         return False
284
285     #
286     # Scheduler
287     #
288     def run_scheduler(self, cr, uid, use_new_cursor=False, company_id = False, context=None):
289         '''
290         Call the scheduler to check the procurement order. This is intented to be done for all existing companies at
291         the same time, so we're running all the methods as SUPERUSER to avoid intercompany and access rights issues.
292
293         @param self: The object pointer
294         @param cr: The current row, from the database cursor,
295         @param uid: The current user ID for security checks
296         @param ids: List of selected IDs
297         @param use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement.
298             This is appropriate for batch jobs only.
299         @param context: A standard dictionary for contextual values
300         @return:  Dictionary of values
301         '''
302         if context is None:
303             context = {}
304         try:
305             if use_new_cursor:
306                 cr = openerp.registry(cr.dbname).cursor()
307
308             # Run confirmed procurements
309             dom = [('state', '=', 'confirmed')]
310             if company_id:
311                 dom += [('company_id', '=', company_id)]
312             prev_ids = []
313             while True:
314                 ids = self.search(cr, SUPERUSER_ID, dom, context=context)
315                 if not ids or prev_ids == ids:
316                     break
317                 else:
318                     prev_ids = ids
319                 self.run(cr, SUPERUSER_ID, ids, autocommit=use_new_cursor, context=context)
320                 if use_new_cursor:
321                     cr.commit()
322
323             # Check if running procurements are done
324             offset = 0
325             dom = [('state', '=', 'running')]
326             if company_id:
327                 dom += [('company_id', '=', company_id)]
328             prev_ids = []
329             while True:
330                 ids = self.search(cr, SUPERUSER_ID, dom, offset=offset, context=context)
331                 if not ids or prev_ids == ids:
332                     break
333                 else:
334                     prev_ids = ids
335                 self.check(cr, SUPERUSER_ID, ids, autocommit=use_new_cursor, context=context)
336                 if use_new_cursor:
337                     cr.commit()
338
339         finally:
340             if use_new_cursor:
341                 try:
342                     cr.close()
343                 except Exception:
344                     pass
345
346         return {}
347 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: