Launchpad automatic translations update.
[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 from osv import osv, fields
23 from tools.translate import _
24 import netsvc
25 import time
26
27 # Procurement
28 # ------------------------------------------------------------------
29 #
30 # Produce, Buy or Find products and place a move
31 #     then wizard for picking lists & move
32 #
33
34 class mrp_property_group(osv.osv):
35     """
36     Group of mrp properties.
37     """
38     _name = 'mrp.property.group'
39     _description = 'Property Group'
40     _columns = {
41         'name': fields.char('Property Group', size=64, required=True),
42         'description': fields.text('Description'),
43     }
44 mrp_property_group()
45
46 class mrp_property(osv.osv):
47     """
48     Properties of mrp.
49     """
50     _name = 'mrp.property'
51     _description = 'Property'
52     _columns = {
53         'name': fields.char('Name', size=64, required=True),
54         'composition': fields.selection([('min','min'),('max','max'),('plus','plus')], 'Properties composition', required=True, help="Not used in computations, for information purpose only."),
55         'group_id': fields.many2one('mrp.property.group', 'Property Group', required=True),
56         'description': fields.text('Description'),
57     }
58     _defaults = {
59         'composition': lambda *a: 'min',
60     }
61 mrp_property()
62
63 class StockMove(osv.osv):
64     _inherit = 'stock.move'
65     
66     _columns= {
67         'procurements': fields.one2many('procurement.order', 'move_id', 'Procurements'),
68     }
69     
70     def copy(self, cr, uid, id, default=None, context=None):
71         default = default or {}
72         default['procurements'] = []
73         return super(StockMove, self).copy(cr, uid, id, default, context)
74
75 StockMove()
76
77 class procurement_order(osv.osv):
78     """
79     Procurement Orders
80     """
81     _name = "procurement.order"
82     _description = "Procurement"
83     _order = 'priority,date_planned desc'
84     _log_create = False
85     _columns = {
86         'name': fields.char('Reason', size=64, required=True, help='Procurement name.'),
87         'origin': fields.char('Source Document', size=64,
88             help="Reference of the document that created this Procurement.\n"
89             "This is automatically completed by OpenERP."),
90         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True),
91         'date_planned': fields.datetime('Scheduled date', required=True),
92         'date_close': fields.datetime('Date Closed'),
93         'product_id': fields.many2one('product.product', 'Product', required=True, states={'draft':[('readonly',False)]}, readonly=True),
94         'product_qty': fields.float('Quantity', required=True, states={'draft':[('readonly',False)]}, readonly=True),
95         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
96         'product_uos_qty': fields.float('UoS Quantity', states={'draft':[('readonly',False)]}, readonly=True),
97         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
98         'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
99         'close_move': fields.boolean('Close Move at end', required=True),
100         'location_id': fields.many2one('stock.location', 'Location', required=True, states={'draft':[('readonly',False)]}, readonly=True),
101         'procure_method': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')], 'Procurement Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]},
102             readonly=True, required=True, help="If you encode manually a Procurement, you probably want to use" \
103             " a make to order method."),
104
105         'note': fields.text('Note'),
106         'message': fields.char('Latest error', size=64, help="Exception occurred while computing procurement orders."),
107         'state': fields.selection([
108             ('draft','Draft'),
109             ('confirmed','Confirmed'),
110             ('exception','Exception'),
111             ('running','Running'),
112             ('cancel','Cancel'),
113             ('ready','Ready'),
114             ('done','Done'),
115             ('waiting','Waiting')], 'State', required=True,
116             help='When a procurement is created the state is set to \'Draft\'.\n If the procurement is confirmed, the state is set to \'Confirmed\'.\
117             \nAfter confirming the state is set to \'Running\'.\n If any exception arises in the order then the state is set to \'Exception\'.\n Once the exception is removed the state becomes \'Ready\'.\n It is in \'Waiting\'. state when the procurement is waiting for another one to finish.'),
118         'note': fields.text('Note'),
119         'company_id': fields.many2one('res.company','Company',required=True),
120     }
121     _defaults = {
122         'state': lambda *a: 'draft',
123         'priority': lambda *a: '1',
124         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
125         'close_move': lambda *a: 0,
126         'procure_method': lambda *a: 'make_to_order',
127         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c)
128     }
129
130     def unlink(self, cr, uid, ids, context=None):
131         procurements = self.read(cr, uid, ids, ['state'])
132         unlink_ids = []
133         for s in procurements:
134             if s['state'] in ['draft','cancel']:
135                 unlink_ids.append(s['id'])
136             else:
137                 raise osv.except_osv(_('Invalid action !'), 
138                         _('Cannot delete Procurement Order(s) which are in %s State!') % \
139                         s['state'])
140         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
141
142     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
143         """ Finds UoM and UoS of changed product.
144         @param product_id: Changed id of product.
145         @return: Dictionary of values.
146         """
147         if product_id:
148             w = self.pool.get('product.product').browse(cr, uid, product_id, context)
149             v = {
150                 'product_uom': w.uom_id.id,
151                 'product_uos': w.uos_id and w.uos_id.id or w.uom_id.id
152             }
153             return {'value': v}
154         return {}
155
156     def check_product(self, cr, uid, ids):
157         """ Checks product type.
158         @return: True or False
159         """
160         return all(procurement.product_id.type in ('product', 'consu') for procurement in self.browse(cr, uid, ids))
161
162     def check_move_cancel(self, cr, uid, ids, context={}):
163         """ Checks if move is cancelled or not.
164         @return: True or False.
165         """
166         return all(procurement.move_id.state == 'cancel' for procurement in self.browse(cr, uid, ids))
167
168     def check_move_done(self, cr, uid, ids, context={}):
169         """ Checks if move is done or not.
170         @return: True or False.
171         """
172         return all(procurement.move_id.state == 'done' for procurement in self.browse(cr, uid, ids))
173
174     #
175     # This method may be overrided by objects that override procurement.order
176     # for computing their own purpose
177     #
178     def _quantity_compute_get(self, cr, uid, proc, context={}):
179         """ Finds sold quantity of product.
180         @param proc: Current procurement.
181         @return: Quantity or False.
182         """
183         if proc.product_id.type == 'product' and proc.move_id:
184             if proc.move_id.product_uos:
185                 return proc.move_id.product_uos_qty
186         return False
187
188     def _uom_compute_get(self, cr, uid, proc, context={}):
189         """ Finds UoS if product is Stockable Product.
190         @param proc: Current procurement.
191         @return: UoS or False.
192         """
193         if proc.product_id.type == 'product' and proc.move_id:
194             if proc.move_id.product_uos:
195                 return proc.move_id.product_uos.id
196         return False
197
198     #
199     # Return the quantity of product shipped/produced/served, which may be
200     # different from the planned quantity
201     #
202     def quantity_get(self, cr, uid, id, context={}):
203         """ Finds quantity of product used in procurement.
204         @return: Quantity of product.
205         """
206         proc = self.browse(cr, uid, id, context)
207         result = self._quantity_compute_get(cr, uid, proc, context)
208         if not result:
209             result = proc.product_qty
210         return result
211
212     def uom_get(self, cr, uid, id, context=None):
213         """ Finds UoM of product used in procurement.
214         @return: UoM of product.
215         """
216         proc = self.browse(cr, uid, id, context)
217         result = self._uom_compute_get(cr, uid, proc, context)
218         if not result:
219             result = proc.product_uom.id
220         return result
221
222     def check_waiting(self, cr, uid, ids, context=[]):
223         """ Checks state of move.
224         @return: True or False
225         """
226         for procurement in self.browse(cr, uid, ids, context=context):
227             if procurement.move_id and procurement.move_id.state == 'auto':
228                 return True
229         return False
230
231     def check_produce_service(self, cr, uid, procurement, context=[]):
232         return False
233
234     def check_produce_product(self, cr, uid, procurement, context=[]):
235         """ Finds BoM of a product if not found writes exception message.
236         @param procurement: Current procurement.
237         @return: True or False.
238         """
239         return True
240
241     def check_make_to_stock(self, cr, uid, ids, context={}):
242         """ Checks product type.
243         @return: True or False
244         """
245         ok = True
246         for procurement in self.browse(cr, uid, ids, context=context):
247             if procurement.product_id.type == 'service':
248                 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
249             else:
250                 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
251         return ok
252
253     def check_produce(self, cr, uid, ids, context={}):
254         """ Checks product type.
255         @return: True or Product Id.
256         """
257         res = True
258         user = self.pool.get('res.users').browse(cr, uid, uid)
259         for procurement in self.browse(cr, uid, ids):
260             if procurement.product_id.product_tmpl_id.supply_method <> 'produce':
261                 partner_list = sorted([(partner_id.sequence, partner_id) for partner_id in  procurement.product_id.seller_ids if partner_id])
262                 if partner_list:
263                     partner = partner_list and partner_list[0] and partner_list[0][1] and partner_list[0][1].name or False
264                     if user.company_id and user.company_id.partner_id:
265                         if partner.id == user.company_id.partner_id.id:
266                             return True
267                 return False
268             if procurement.product_id.product_tmpl_id.type=='service':
269                 res = res and self.check_produce_service(cr, uid, procurement, context)
270             else:
271                 res = res and self.check_produce_product(cr, uid, procurement, context)
272             if not res:
273                 return False
274         return res
275
276     def check_buy(self, cr, uid, ids):
277         """ Checks product type.
278         @return: True or Product Id.
279         """
280         user = self.pool.get('res.users').browse(cr, uid, uid)
281         partner_obj = self.pool.get('res.partner')
282         for procurement in self.browse(cr, uid, ids):
283             if procurement.product_id.product_tmpl_id.supply_method <> 'buy':
284                 return False
285             if not procurement.product_id.seller_ids:
286                 cr.execute('update procurement_order set message=%s where id=%s', 
287                         (_('No supplier defined for this product !'), procurement.id))
288                 return False
289             partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
290
291             if user.company_id and user.company_id.partner_id:
292                 if partner.id == user.company_id.partner_id.id:
293                     return False
294             address_id = partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']
295             if not address_id:
296                 cr.execute('update procurement_order set message=%s where id=%s', 
297                         (_('No address defined for the supplier'), procurement.id))
298                 return False
299         return True
300
301     def test_cancel(self, cr, uid, ids):
302         """ Tests whether state of move is cancelled or not.
303         @return: True or False
304         """
305         for record in self.browse(cr, uid, ids):
306             if record.move_id and record.move_id.state == 'cancel':
307                 return True
308         return False
309
310     def action_confirm(self, cr, uid, ids, context={}):
311         """ Confirms procurement and writes exception message if any.
312         @return: True
313         """
314         move_obj = self.pool.get('stock.move')
315         for procurement in self.browse(cr, uid, ids):
316             if procurement.product_qty <= 0.00:
317                 raise osv.except_osv(_('Data Insufficient !'), 
318                     _('Please check the Quantity in Procurement Order(s), it should not be less than 1!'))
319             if procurement.product_id.type in ('product', 'consu'):
320                 if not procurement.move_id:
321                     source = procurement.location_id.id
322                     if procurement.procure_method == 'make_to_order':
323                         source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
324                     id = move_obj.create(cr, uid, {
325                         'name': procurement.name,
326                         'location_id': source,
327                         'location_dest_id': procurement.location_id.id,
328                         'product_id': procurement.product_id.id,
329                         'product_qty': procurement.product_qty,
330                         'product_uom': procurement.product_uom.id,
331                         'date_expected': procurement.date_planned,
332                         'state': 'draft',
333                         'company_id': procurement.company_id.id,
334                         'auto_validate': True,
335                     })
336                     move_obj.action_confirm(cr, uid, [id], context=context)
337                     self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move': 1})
338         self.write(cr, uid, ids, {'state': 'confirmed', 'message': ''})
339         return True
340
341     def action_move_assigned(self, cr, uid, ids, context={}):
342         """ Changes procurement state to Running and writes message.
343         @return: True
344         """
345         self.write(cr, uid, ids, {'state': 'running', 
346                 'message': _('from stock: products assigned.')})
347         return True
348
349     def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
350         """
351            This method may be overrided by objects that override procurement.order
352            for computing their own purpose
353         @return: True"""
354         return True
355
356     def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
357         """ Checks procurement move state.
358         @param procurement: Current procurement.
359         @return: True or move id.
360         """
361         ok = True
362         if procurement.move_id:
363             id = procurement.move_id.id
364             if not (procurement.move_id.state in ('done','assigned','cancel')):
365                 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
366                 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%s', (procurement.product_id.id,))
367                 if not cr.fetchone()[0]:
368                     cr.execute('update procurement_order set message=%s where id=%s', 
369                             (_('Not enough stock and no minimum orderpoint rule defined.'), 
370                             procurement.id))
371                     message = _("Procurement '%s' is in exception: not enough stock.") % \
372                             (procurement.name,)
373                     self.log(cr, uid, procurement.id, message)
374         return ok
375
376     def action_produce_assign_service(self, cr, uid, ids, context={}):
377         """ Changes procurement state to Running.
378         @return: True
379         """
380         for procurement in self.browse(cr, uid, ids):
381             self.write(cr, uid, [procurement.id], {'state': 'running'})
382         return True
383
384     def action_produce_assign_product(self, cr, uid, ids, context={}):
385         """ This is action which call from workflow to assign production order to procurements
386         @return: True
387         """
388         return 0
389
390
391     def action_po_assign(self, cr, uid, ids, context={}):
392         """ This is action which call from workflow to assign purchase order to procurements
393         @return: True
394         """
395         return 0
396
397     def action_cancel(self, cr, uid, ids):
398         """ Cancels procurement and writes move state to Assigned.
399         @return: True
400         """
401         todo = []
402         todo2 = []
403         move_obj = self.pool.get('stock.move')
404         for proc in self.browse(cr, uid, ids):
405             if proc.close_move and proc.move_id:
406                 if proc.move_id.state not in ('done', 'cancel'):
407                     todo2.append(proc.move_id.id)
408             else:
409                 if proc.move_id and proc.move_id.state == 'waiting':
410                     todo.append(proc.move_id.id)
411         if len(todo2):
412             move_obj.action_cancel(cr, uid, todo2)
413         if len(todo):
414             move_obj.write(cr, uid, todo, {'state': 'assigned'})
415         self.write(cr, uid, ids, {'state': 'cancel'})
416         wf_service = netsvc.LocalService("workflow")
417         for id in ids:
418             wf_service.trg_trigger(uid, 'procurement.order', id, cr)
419         return True
420
421     def action_check_finished(self, cr, uid, ids):
422         return self.check_move_done(cr, uid, ids)
423
424     def action_check(self, cr, uid, ids):
425         """ Checks procurement move state whether assigned or done.
426         @return: True
427         """
428         ok = False
429         for procurement in self.browse(cr, uid, ids):
430             if procurement.move_id and procurement.move_id.state == 'assigned' or procurement.move_id.state == 'done':
431                 self.action_done(cr, uid, [procurement.id])
432                 ok = True
433         return ok
434
435     def action_ready(self, cr, uid, ids):
436         """ Changes procurement state to Ready.
437         @return: True
438         """
439         res = self.write(cr, uid, ids, {'state': 'ready'})
440         return res
441
442     def action_done(self, cr, uid, ids):
443         """ Changes procurement state to Done and writes Closed date.
444         @return: True
445         """
446         move_obj = self.pool.get('stock.move')
447         for procurement in self.browse(cr, uid, ids):
448             if procurement.move_id:
449                 if procurement.close_move and (procurement.move_id.state <> 'done'):
450                     move_obj.action_done(cr, uid, [procurement.move_id.id])
451         res = self.write(cr, uid, ids, {'state': 'done', 'date_close': time.strftime('%Y-%m-%d')})
452         wf_service = netsvc.LocalService("workflow")
453         for id in ids:
454             wf_service.trg_trigger(uid, 'procurement.order', id, cr)
455         return res
456
457     def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
458         ''' Runs through scheduler.
459         @param use_new_cursor: False or the dbname
460         '''
461         if not context:
462             context={}
463         self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
464         self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
465                 use_new_cursor=use_new_cursor, context=context)
466
467 procurement_order()
468
469 class StockPicking(osv.osv):
470     _inherit = 'stock.picking'
471
472     def test_finished(self, cursor, user, ids):
473         wf_service = netsvc.LocalService("workflow")
474         res = super(StockPicking, self).test_finished(cursor, user, ids)
475         for picking in self.browse(cursor, user, ids):
476             for move in picking.move_lines:
477                 if move.state == 'done' and move.procurements:
478                     for procurement in move.procurements:
479                         wf_service.trg_validate(user, 'procurement.order',
480                                 procurement.id, 'button_check', cursor)
481         return res
482     
483 StockPicking()
484
485 class stock_warehouse_orderpoint(osv.osv):
486     """
487     Defines Minimum stock rules.
488     """
489     _name = "stock.warehouse.orderpoint"
490     _description = "Minimum Inventory Rule"
491
492     _columns = {
493         'name': fields.char('Name', size=32, required=True),
494         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the orderpoint without removing it."),
495         'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
496         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
497         'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
498         'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type','=','product')]),
499         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
500         'product_min_qty': fields.float('Min Quantity', required=True,
501             help="When the virtual stock goes belong the Min Quantity, OpenERP generates "\
502             "a procurement to bring the virtual stock to the Max Quantity."),
503         'product_max_qty': fields.float('Max Quantity', required=True,
504             help="When the virtual stock goes belong the Max Quantity, OpenERP generates "\
505             "a procurement to bring the virtual stock to the Max Quantity."),
506         'qty_multiple': fields.integer('Qty Multiple', required=True,
507             help="The procurement quantity will by rounded up to this multiple."),
508         'procurement_id': fields.many2one('procurement.order', 'Latest procurement', ondelete="set null"),
509         'company_id': fields.many2one('res.company','Company',required=True),
510     }
511     _defaults = {
512         'active': lambda *a: 1,
513         'logic': lambda *a: 'max',
514         'qty_multiple': lambda *a: 1,
515         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'stock.orderpoint') or '',
516         'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
517         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=c)
518     }
519     _sql_constraints = [
520         ('qty_multiple_check', 'CHECK( qty_multiple > 0 )', 
521                 _('Qty Multiple must be greater than zero.')),
522     ]
523
524     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
525         """ Finds location id for changed warehouse.
526         @param warehouse_id: Changed id of warehouse.
527         @return: Dictionary of values.
528         """
529         if warehouse_id:
530             w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context)
531             v = {'location_id': w.lot_stock_id.id}
532             return {'value': v}
533         return {}
534     
535     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
536         """ Finds UoM for changed product.
537         @param product_id: Changed id of product.
538         @return: Dictionary of values.
539         """
540         if product_id:
541             prod = self.pool.get('product.product').browse(cr,uid,product_id)
542             v = {'product_uom': prod.uom_id.id}
543             return {'value': v}
544         return {}
545     
546     def copy(self, cr, uid, id, default=None,context={}):
547         if not default:
548             default = {}
549         default.update({
550             'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
551         })
552         return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context)
553     
554 stock_warehouse_orderpoint()
555 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: