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