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