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=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': 'draft',
123         'priority': '1',
124         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
125         'close_move': 0,
126         'procure_method': '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'], context=context)
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=None):
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=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=None):
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, context=context))
167
168     def check_move_done(self, cr, uid, ids, context=None):
169         """ Checks if move is done or not.
170         @return: True or False.
171         """
172         if not context:
173             context = {}
174         return all(not procurement.move_id or procurement.move_id.state == 'done' for procurement in self.browse(cr, uid, ids, context=context))
175     #
176     # This method may be overrided by objects that override procurement.order
177     # for computing their own purpose
178     #
179     def _quantity_compute_get(self, cr, uid, proc, context=None):
180         """ Finds sold quantity of product.
181         @param proc: Current procurement.
182         @return: Quantity or False.
183         """
184         if proc.product_id.type == 'product' and proc.move_id:
185             if proc.move_id.product_uos:
186                 return proc.move_id.product_uos_qty
187         return False
188
189     def _uom_compute_get(self, cr, uid, proc, context=None):
190         """ Finds UoS if product is Stockable Product.
191         @param proc: Current procurement.
192         @return: UoS or False.
193         """
194         if proc.product_id.type == 'product' and proc.move_id:
195             if proc.move_id.product_uos:
196                 return proc.move_id.product_uos.id
197         return False
198
199     #
200     # Return the quantity of product shipped/produced/served, which may be
201     # different from the planned quantity
202     #
203     def quantity_get(self, cr, uid, id, context=None):
204         """ Finds quantity of product used in procurement.
205         @return: Quantity of product.
206         """
207         proc = self.browse(cr, uid, id, context=context)
208         result = self._quantity_compute_get(cr, uid, proc, context=context)
209         if not result:
210             result = proc.product_qty
211         return result
212
213     def uom_get(self, cr, uid, id, context=None):
214         """ Finds UoM of product used in procurement.
215         @return: UoM of product.
216         """
217         proc = self.browse(cr, uid, id, context=context)
218         result = self._uom_compute_get(cr, uid, proc, context=context)
219         if not result:
220             result = proc.product_uom.id
221         return result
222
223     def check_waiting(self, cr, uid, ids, context=None):
224         """ Checks state of move.
225         @return: True or False
226         """
227         for procurement in self.browse(cr, uid, ids, context=context):
228             if procurement.move_id and procurement.move_id.state == 'auto':
229                 return True
230         return False
231
232     def check_produce_service(self, cr, uid, procurement, context=None):
233         return False
234
235     def check_produce_product(self, cr, uid, procurement, context=None):
236         """ Finds BoM of a product if not found writes exception message.
237         @param procurement: Current procurement.
238         @return: True or False.
239         """
240         return True
241
242     def check_make_to_stock(self, cr, uid, ids, context=None):
243         """ Checks product type.
244         @return: True or False
245         """
246         ok = True
247         for procurement in self.browse(cr, uid, ids, context=context):
248             if procurement.product_id.type == 'service':
249                 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
250             else:
251                 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
252         return ok
253
254     def check_produce(self, cr, uid, ids, context=None):
255         """ Checks product type.
256         @return: True or Product Id.
257         """
258         res = True
259         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
260         for procurement in self.browse(cr, uid, ids, context=context):
261             if procurement.product_id.product_tmpl_id.supply_method <> 'produce':
262                 partner_list = sorted([(partner_id.sequence, partner_id) for partner_id in  procurement.product_id.seller_ids if partner_id])
263                 if partner_list:
264                     partner = partner_list and partner_list[0] and partner_list[0][1] and partner_list[0][1].name or False
265                     if user.company_id and user.company_id.partner_id:
266                         if partner.id == user.company_id.partner_id.id:
267                             return True
268                 return False
269             if procurement.product_id.product_tmpl_id.type=='service':
270                 res = res and self.check_produce_service(cr, uid, procurement, context)
271             else:
272                 res = res and self.check_produce_product(cr, uid, procurement, context)
273             if not res:
274                 return False
275         return res
276
277     def check_buy(self, cr, uid, ids):
278         """ Checks product type.
279         @return: True or Product Id.
280         """
281         user = self.pool.get('res.users').browse(cr, uid, uid)
282         partner_obj = self.pool.get('res.partner')
283         for procurement in self.browse(cr, uid, ids):
284             if procurement.product_id.product_tmpl_id.supply_method <> 'buy':
285                 return False
286             if not procurement.product_id.seller_ids:
287                 cr.execute('update procurement_order set message=%s where id=%s',
288                         (_('No supplier defined for this product !'), procurement.id))
289                 return False
290             partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
291
292             if not partner:
293                 cr.execute('update procurement_order set message=%s where id=%s',
294                            (_('No default supplier defined for this product'), procurement.id))
295                 return False
296
297             if user.company_id and user.company_id.partner_id:
298                 if partner.id == user.company_id.partner_id.id:
299                     return False
300
301             address_id = partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']
302             if not address_id:
303                 cr.execute('update procurement_order set message=%s where id=%s',
304                         (_('No address defined for the supplier'), procurement.id))
305                 return False
306         return True
307
308     def test_cancel(self, cr, uid, ids):
309         """ Tests whether state of move is cancelled or not.
310         @return: True or False
311         """
312         for record in self.browse(cr, uid, ids):
313             if record.move_id and record.move_id.state == 'cancel':
314                 return True
315         return False
316
317     def action_confirm(self, cr, uid, ids, context=None):
318         """ Confirms procurement and writes exception message if any.
319         @return: True
320         """
321         move_obj = self.pool.get('stock.move')
322         for procurement in self.browse(cr, uid, ids, context=context):
323             if procurement.product_qty <= 0.00:
324                 raise osv.except_osv(_('Data Insufficient !'),
325                     _('Please check the Quantity in Procurement Order(s), it should not be less than 1!'))
326             if procurement.product_id.type in ('product', 'consu'):
327                 if not procurement.move_id:
328                     source = procurement.location_id.id
329                     if procurement.procure_method == 'make_to_order':
330                         source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
331                     id = move_obj.create(cr, uid, {
332                         'name': procurement.name,
333                         'location_id': source,
334                         'location_dest_id': procurement.location_id.id,
335                         'product_id': procurement.product_id.id,
336                         'product_qty': procurement.product_qty,
337                         'product_uom': procurement.product_uom.id,
338                         'date_expected': procurement.date_planned,
339                         'state': 'draft',
340                         'company_id': procurement.company_id.id,
341                         'auto_validate': True,
342                     })
343                     move_obj.action_confirm(cr, uid, [id], context=context)
344                     self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move': 1})
345         self.write(cr, uid, ids, {'state': 'confirmed', 'message': ''})
346         return True
347
348     def action_move_assigned(self, cr, uid, ids, context=None):
349         """ Changes procurement state to Running and writes message.
350         @return: True
351         """
352         self.write(cr, uid, ids, {'state': 'running',
353                 'message': _('from stock: products assigned.')})
354         return True
355
356     def _check_make_to_stock_service(self, cr, uid, procurement, context=None):
357         """
358            This method may be overrided by objects that override procurement.order
359            for computing their own purpose
360         @return: True"""
361         return True
362
363     def _check_make_to_stock_product(self, cr, uid, procurement, context=None):
364         """ Checks procurement move state.
365         @param procurement: Current procurement.
366         @return: True or move id.
367         """
368         ok = True
369         if procurement.move_id:
370             message = False
371             id = procurement.move_id.id
372             if not (procurement.move_id.state in ('done','assigned','cancel')):
373                 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
374                 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%s', (procurement.product_id.id,))
375                 res = cr.fetchone()[0]
376                 if not res and not ok:
377                      message = _("Not enough stock and no minimum orderpoint rule defined.")
378                 elif not res:
379                     message = _("No minimum orderpoint rule defined.")
380                 elif not ok:
381                     message = _("Not enough stock.")
382
383                 if message:
384                     self.log(cr, uid, procurement.id, _("Procurement '%s' is in exception: ") % (procurement.name) + message)
385                     cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
386         return ok
387
388     def action_produce_assign_service(self, cr, uid, ids, context=None):
389         """ Changes procurement state to Running.
390         @return: True
391         """
392         for procurement in self.browse(cr, uid, ids, context=context):
393             self.write(cr, uid, [procurement.id], {'state': 'running'})
394         return True
395
396     def action_produce_assign_product(self, cr, uid, ids, context=None):
397         """ This is action which call from workflow to assign production order to procurements
398         @return: True
399         """
400         return 0
401
402
403     def action_po_assign(self, cr, uid, ids, context=None):
404         """ This is action which call from workflow to assign purchase order to procurements
405         @return: True
406         """
407         return 0
408
409     def action_cancel(self, cr, uid, ids):
410         """ Cancels procurement and writes move state to Assigned.
411         @return: True
412         """
413         todo = []
414         todo2 = []
415         move_obj = self.pool.get('stock.move')
416         for proc in self.browse(cr, uid, ids):
417             if proc.close_move and proc.move_id:
418                 if proc.move_id.state not in ('done', 'cancel'):
419                     todo2.append(proc.move_id.id)
420             else:
421                 if proc.move_id and proc.move_id.state == 'waiting':
422                     todo.append(proc.move_id.id)
423         if len(todo2):
424             move_obj.action_cancel(cr, uid, todo2)
425         if len(todo):
426             move_obj.write(cr, uid, todo, {'state': 'assigned'})
427         self.write(cr, uid, ids, {'state': 'cancel'})
428         wf_service = netsvc.LocalService("workflow")
429         for id in ids:
430             wf_service.trg_trigger(uid, 'procurement.order', id, cr)
431         return True
432
433     def action_check_finished(self, cr, uid, ids):
434         return self.check_move_done(cr, uid, ids)
435
436     def action_check(self, cr, uid, ids):
437         """ Checks procurement move state whether assigned or done.
438         @return: True
439         """
440         ok = False
441         for procurement in self.browse(cr, uid, ids):
442             if procurement.move_id and procurement.move_id.state == 'assigned' or procurement.move_id.state == 'done':
443                 self.action_done(cr, uid, [procurement.id])
444                 ok = True
445         return ok
446
447     def action_ready(self, cr, uid, ids):
448         """ Changes procurement state to Ready.
449         @return: True
450         """
451         res = self.write(cr, uid, ids, {'state': 'ready'})
452         return res
453
454     def action_done(self, cr, uid, ids):
455         """ Changes procurement state to Done and writes Closed date.
456         @return: True
457         """
458         move_obj = self.pool.get('stock.move')
459         for procurement in self.browse(cr, uid, ids):
460             if procurement.move_id:
461                 if procurement.close_move and (procurement.move_id.state <> 'done'):
462                     move_obj.action_done(cr, uid, [procurement.move_id.id])
463         res = self.write(cr, uid, ids, {'state': 'done', 'date_close': time.strftime('%Y-%m-%d')})
464         wf_service = netsvc.LocalService("workflow")
465         for id in ids:
466             wf_service.trg_trigger(uid, 'procurement.order', id, cr)
467         return res
468
469     def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
470         ''' Runs through scheduler.
471         @param use_new_cursor: False or the dbname
472         '''
473         self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
474         self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
475                 use_new_cursor=use_new_cursor, context=context)
476
477 procurement_order()
478
479 class StockPicking(osv.osv):
480     _inherit = 'stock.picking'
481
482     def test_finished(self, cursor, user, ids):
483         wf_service = netsvc.LocalService("workflow")
484         res = super(StockPicking, self).test_finished(cursor, user, ids)
485         for picking in self.browse(cursor, user, ids):
486             for move in picking.move_lines:
487                 if move.state == 'done' and move.procurements:
488                     for procurement in move.procurements:
489                         wf_service.trg_validate(user, 'procurement.order',
490                                 procurement.id, 'button_check', cursor)
491         return res
492
493 StockPicking()
494
495 class stock_warehouse_orderpoint(osv.osv):
496     """
497     Defines Minimum stock rules.
498     """
499     _name = "stock.warehouse.orderpoint"
500     _description = "Minimum Inventory Rule"
501
502     def _get_draft_procurements(self, cr, uid, ids, field_name, arg, context=None):
503         if context is None:
504             context = {}
505         result = {}
506         procurement_obj = self.pool.get('procurement.order')
507         for orderpoint in self.browse(cr, uid, ids, context=context):
508             procurement_ids = procurement_obj.search(cr, uid , [('state', '=', 'draft'), ('product_id', '=', orderpoint.product_id.id), ('location_id', '=', orderpoint.location_id.id)])
509             result[orderpoint.id] = procurement_ids
510         return result
511
512     _columns = {
513         'name': fields.char('Name', size=32, required=True),
514         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the orderpoint without removing it."),
515         'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
516         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
517         'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
518         'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type','=','product')]),
519         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
520         'product_min_qty': fields.float('Min Quantity', required=True,
521             help="When the virtual stock goes belong the Min Quantity, OpenERP generates "\
522             "a procurement to bring the virtual stock to the Max Quantity."),
523         'product_max_qty': fields.float('Max Quantity', required=True,
524             help="When the virtual stock goes belong the Max Quantity, OpenERP generates "\
525             "a procurement to bring the virtual stock to the Max Quantity."),
526         'qty_multiple': fields.integer('Qty Multiple', required=True,
527             help="The procurement quantity will by rounded up to this multiple."),
528         'procurement_id': fields.many2one('procurement.order', 'Latest procurement', ondelete="set null"),
529         'company_id': fields.many2one('res.company','Company',required=True),
530         'procurement_draft_ids': fields.function(_get_draft_procurements, method=True, type='many2many', relation="procurement.order", \
531                                 string="Related Procurement Orders",help="Draft procurement of the product and location of that orderpoint"),
532     }
533     _defaults = {
534         'active': lambda *a: 1,
535         'logic': lambda *a: 'max',
536         'qty_multiple': lambda *a: 1,
537         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'stock.orderpoint') or '',
538         'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
539         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=c)
540     }
541     _sql_constraints = [
542         ('qty_multiple_check', 'CHECK( qty_multiple > 0 )', 'Qty Multiple must be greater than zero.'),
543     ]
544
545     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
546         """ Finds location id for changed warehouse.
547         @param warehouse_id: Changed id of warehouse.
548         @return: Dictionary of values.
549         """
550         if warehouse_id:
551             w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
552             v = {'location_id': w.lot_stock_id.id}
553             return {'value': v}
554         return {}
555
556     def onchange_product_id(self, cr, uid, ids, product_id, context=None):
557         """ Finds UoM for changed product.
558         @param product_id: Changed id of product.
559         @return: Dictionary of values.
560         """
561         if product_id:
562             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
563             v = {'product_uom': prod.uom_id.id}
564             return {'value': v}
565         return {}
566     
567     def copy(self, cr, uid, id, default=None, context=None):
568         if not default:
569             default = {}
570         default.update({
571             'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
572         })
573         return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context=context)
574     
575 stock_warehouse_orderpoint()
576 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: