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