[MERGE] merge with parent branch
[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 product_template(osv.osv):
36     _inherit="product.template"
37     _columns = {
38         'type': fields.selection([('product','Stockable Product'),('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Will change the way procurements are processed. Consumable are product where you don't manage stock."),
39         'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procurement Method', required=True, help="'Make to Stock': When needed, take from the stock or wait until re-supplying. 'Make to Order': When needed, purchase or produce for the procurement request."),
40         'supply_method': fields.selection([('produce','Manufacture'),('buy','Buy')], 'Supply Method', required=True, help="Produce will generate production order or tasks, according to the product type. Buy will trigger purchase orders when requested."),
41     }
42     _defaults = {
43         'procure_method': lambda *a: 'make_to_stock',
44         'supply_method': lambda *a: 'buy',
45     }
46 product_template()
47
48 class mrp_property_group(osv.osv):
49     """
50     Group of mrp properties.
51     """
52     _name = 'mrp.property.group'
53     _description = 'Property Group'
54     _columns = {
55         'name': fields.char('Property Group', size=64, required=True),
56         'description': fields.text('Description'),
57     }
58 mrp_property_group()
59
60 class mrp_property(osv.osv):
61     """
62     Properties of mrp.
63     """
64     _name = 'mrp.property'
65     _description = 'Property'
66     _columns = {
67         'name': fields.char('Name', size=64, required=True),
68         'composition': fields.selection([('min','min'),('max','max'),('plus','plus')], 'Properties composition', required=True, help="Not used in computations, for information purpose only."),
69         'group_id': fields.many2one('mrp.property.group', 'Property Group', required=True),
70         'description': fields.text('Description'),
71     }
72     _defaults = {
73         'composition': lambda *a: 'min',
74     }
75 mrp_property()
76
77 class StockMove(osv.osv):
78     _inherit = 'stock.move'
79     _columns= {
80         'procurements': fields.one2many('procurement.order', 'move_id', 'Procurements'),
81     }
82
83     def copy(self, cr, uid, id, default=None, context=None):
84         default = default or {}
85         default['procurements'] = []
86         return super(StockMove, self).copy(cr, uid, id, default, context=context)
87
88 StockMove()
89
90 class procurement_order(osv.osv):
91     """
92     Procurement Orders
93     """
94     _name = "procurement.order"
95     _description = "Procurement"
96     _order = 'priority desc,date_planned'
97     _inherit = ['mail.thread']
98     _log_create = False
99     _columns = {
100         'name': fields.char('Reason', size=64, required=True, help='Procurement name.'),
101         'origin': fields.char('Source Document', size=64,
102             help="Reference of the document that created this Procurement.\n"
103             "This is automatically completed by OpenERP."),
104         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True, select=True),
105         'date_planned': fields.datetime('Scheduled date', required=True, select=True),
106         'date_close': fields.datetime('Date Closed'),
107         'product_id': fields.many2one('product.product', 'Product', required=True, states={'draft':[('readonly',False)]}, readonly=True),
108         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, states={'draft':[('readonly',False)]}, readonly=True),
109         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, states={'draft':[('readonly',False)]}, readonly=True),
110         'product_uos_qty': fields.float('UoS Quantity', states={'draft':[('readonly',False)]}, readonly=True),
111         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
112         'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
113         'close_move': fields.boolean('Close Move at end'),
114         'location_id': fields.many2one('stock.location', 'Location', required=True, states={'draft':[('readonly',False)]}, readonly=True),
115         'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procurement Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]},
116             readonly=True, required=True, help="If you encode manually a Procurement, you probably want to use" \
117             " a make to order method."),
118         'note': fields.text('Note'),
119         'message': fields.char('Latest error', size=124, help="Exception occurred while computing procurement orders."),
120         'state': fields.selection([
121             ('draft','Draft'),
122             ('cancel','Cancelled'),
123             ('confirmed','Confirmed'),
124             ('exception','Exception'),
125             ('running','Running'),
126             ('ready','Ready'),
127             ('done','Done'),
128             ('waiting','Waiting')], 'Status', required=True,
129             help='When a procurement is created the state is set to \'Draft\'.\n If the procurement is confirmed, the state is set to \'Confirmed\'.\
130             \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.'),
131         'note': fields.text('Note'),
132         'company_id': fields.many2one('res.company','Company',required=True),
133     }
134     _defaults = {
135         'state': 'draft',
136         'priority': '1',
137         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
138         'close_move': 0,
139         'procure_method': 'make_to_order',
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.') % \
152                         s['state'])
153         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
154
155     def onchange_product_id(self, cr, uid, ids, product_id, context=None):
156         """ Finds UoM and UoS of changed product.
157         @param product_id: Changed id of product.
158         @return: Dictionary of values.
159         """
160         if product_id:
161             w = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
162             v = {
163                 'product_uom': w.uom_id.id,
164                 'product_uos': w.uos_id and w.uos_id.id or w.uom_id.id
165             }
166             return {'value': v}
167         return {}
168
169     def check_product(self, cr, uid, ids, context=None):
170         """ Checks product type.
171         @return: True or False
172         """
173         return all(proc.product_id.type in ('product', 'consu') for proc in self.browse(cr, uid, ids, context=context))
174
175     def check_move_cancel(self, cr, uid, ids, context=None):
176         """ Checks if move is cancelled or not.
177         @return: True or False.
178         """
179         return all(procurement.move_id.state == 'cancel' for procurement in self.browse(cr, uid, ids, context=context))
180
181     #This Function is create to avoid  a server side Error Like 'ERROR:tests.mrp:name 'check_move' is not defined'
182     def check_move(self, cr, uid, ids, context=None):
183         pass
184
185     def check_move_done(self, cr, uid, ids, context=None):
186         """ Checks if move is done or not.
187         @return: True or False.
188         """
189         return all(proc.product_id.type == 'service' or (proc.move_id and proc.move_id.state == 'done') \
190                     for proc in self.browse(cr, uid, ids, context=context))
191
192     #
193     # This method may be overrided by objects that override procurement.order
194     # for computing their own purpose
195     #
196     def _quantity_compute_get(self, cr, uid, proc, context=None):
197         """ Finds sold quantity of product.
198         @param proc: Current procurement.
199         @return: Quantity or False.
200         """
201         if proc.product_id.type == 'product' and proc.move_id:
202             if proc.move_id.product_uos:
203                 return proc.move_id.product_uos_qty
204         return False
205
206     def _uom_compute_get(self, cr, uid, proc, context=None):
207         """ Finds UoS if product is Stockable Product.
208         @param proc: Current procurement.
209         @return: UoS or False.
210         """
211         if proc.product_id.type == 'product' and proc.move_id:
212             if proc.move_id.product_uos:
213                 return proc.move_id.product_uos.id
214         return False
215
216     #
217     # Return the quantity of product shipped/produced/served, which may be
218     # different from the planned quantity
219     #
220     def quantity_get(self, cr, uid, id, context=None):
221         """ Finds quantity of product used in procurement.
222         @return: Quantity of product.
223         """
224         proc = self.browse(cr, uid, id, context=context)
225         result = self._quantity_compute_get(cr, uid, proc, context=context)
226         if not result:
227             result = proc.product_qty
228         return result
229
230     def uom_get(self, cr, uid, id, context=None):
231         """ Finds UoM of product used in procurement.
232         @return: UoM of product.
233         """
234         proc = self.browse(cr, uid, id, context=context)
235         result = self._uom_compute_get(cr, uid, proc, context=context)
236         if not result:
237             result = proc.product_uom.id
238         return result
239
240     def check_waiting(self, cr, uid, ids, context=None):
241         """ Checks state of move.
242         @return: True or False
243         """
244         for procurement in self.browse(cr, uid, ids, context=context):
245             if procurement.move_id and procurement.move_id.state == 'auto':
246                 return True
247         return False
248
249     def check_produce_service(self, cr, uid, procurement, context=None):
250         return False
251
252     def check_produce_product(self, cr, uid, procurement, context=None):
253         """ Finds BoM of a product if not found writes exception message.
254         @param procurement: Current procurement.
255         @return: True or False.
256         """
257         return True
258
259     def check_make_to_stock(self, cr, uid, ids, context=None):
260         """ Checks product type.
261         @return: True or False
262         """
263         ok = True
264         for procurement in self.browse(cr, uid, ids, context=context):
265             if procurement.product_id.type == 'service':
266                 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
267             else:
268                 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
269         return ok
270
271     def check_produce(self, cr, uid, ids, context=None):
272         """ Checks product type.
273         @return: True or False
274         """
275         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
276         for procurement in self.browse(cr, uid, ids, context=context):
277             product = procurement.product_id
278             #TOFIX: if product type is 'service' but supply_method is 'buy'.
279             if product.supply_method <> 'produce':
280                 supplier = product.seller_id
281                 if supplier and user.company_id and user.company_id.partner_id:
282                     if supplier.id == user.company_id.partner_id.id:
283                         continue
284                 return False
285             if product.type=='service':
286                 res = self.check_produce_service(cr, uid, procurement, context)
287             else:
288                 res = self.check_produce_product(cr, uid, procurement, context)
289             if not res:
290                 return False
291         return True
292
293     def check_buy(self, cr, uid, ids):
294         """ Checks product type.
295         @return: True or Product Id.
296         """
297         user = self.pool.get('res.users').browse(cr, uid, uid)
298         partner_obj = self.pool.get('res.partner')
299         for procurement in self.browse(cr, uid, ids):
300             if procurement.product_id.product_tmpl_id.supply_method <> 'buy':
301                 return False
302             if not procurement.product_id.seller_ids:
303                 message = _('No supplier defined for this product !')
304                 self.message_post(cr, uid, [procurement.id], body=message)
305                 cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
306                 return False
307             partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
308
309             if not partner:
310                 message = _('No default supplier defined for this product')
311                 self.message_post(cr, uid, [procurement.id], body=message)
312                 cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
313                 return False
314             if user.company_id and user.company_id.partner_id:
315                 if partner.id == user.company_id.partner_id.id:
316                     return False
317
318             address_id = partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']
319             if not address_id:
320                 message = _('No address defined for the supplier')
321                 self.message_post(cr, uid, [procurement.id], body=message)
322                 cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
323                 return False
324         return True
325
326     def test_cancel(self, cr, uid, ids):
327         """ Tests whether state of move is cancelled or not.
328         @return: True or False
329         """
330         for record in self.browse(cr, uid, ids):
331             if record.move_id and record.move_id.state == 'cancel':
332                 return True
333         return False
334
335     #Initialize get_phantom_bom_id method as it is raising an error from yml of mrp_jit
336     #when one install first mrp and after that, mrp_jit. get_phantom_bom_id defined in mrp module
337     #which is not dependent for mrp_jit.
338     def get_phantom_bom_id(self, cr, uid, ids, context=None):
339         return False
340
341     def action_confirm(self, cr, uid, ids, context=None):
342         """ Confirms procurement and writes exception message if any.
343         @return: True
344         """
345         move_obj = self.pool.get('stock.move')
346         for procurement in self.browse(cr, uid, ids, context=context):
347             if procurement.product_qty <= 0.00:
348                 raise osv.except_osv(_('Data Insufficient !'),
349                     _('Please check the quantity in procurement order(s) for the product "%s", it should not be 0 or less!' % procurement.product_id.name))
350             if procurement.product_id.type in ('product', 'consu'):
351                 if not procurement.move_id:
352                     source = procurement.location_id.id
353                     if procurement.procure_method == 'make_to_order':
354                         source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
355                     id = move_obj.create(cr, uid, {
356                         'name': procurement.name,
357                         'location_id': source,
358                         'location_dest_id': procurement.location_id.id,
359                         'product_id': procurement.product_id.id,
360                         'product_qty': procurement.product_qty,
361                         'product_uom': procurement.product_uom.id,
362                         'date_expected': procurement.date_planned,
363                         'state': 'draft',
364                         'company_id': procurement.company_id.id,
365                         'auto_validate': True,
366                     })
367                     move_obj.action_confirm(cr, uid, [id], context=context)
368                     self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move': 1})
369         self.write(cr, uid, ids, {'state': 'confirmed', 'message': ''})
370         self.confirm_send_note(cr, uid, ids, context)
371         return True
372
373     def action_move_assigned(self, cr, uid, ids, context=None):
374         """ Changes procurement state to Running and writes message.
375         @return: True
376         """
377         message = _('From stock: products assigned.')
378         self.write(cr, uid, ids, {'state': 'running',
379                 'message': message}, context=context)
380         self.message_post(cr, uid, ids, body=message, context=context)
381         self.running_send_note(cr, uid, ids, context=context)
382         return True
383
384     def _check_make_to_stock_service(self, cr, uid, procurement, context=None):
385         """
386            This method may be overrided by objects that override procurement.order
387            for computing their own purpose
388         @return: True"""
389         return True
390
391     def _check_make_to_stock_product(self, cr, uid, procurement, context=None):
392         """ Checks procurement move state.
393         @param procurement: Current procurement.
394         @return: True or move id.
395         """
396         ok = True
397         if procurement.move_id:
398             message = False
399             id = procurement.move_id.id
400             if not (procurement.move_id.state in ('done','assigned','cancel')):
401                 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
402                 order_point_id = self.pool.get('stock.warehouse.orderpoint').search(cr, uid, [('product_id', '=', procurement.product_id.id)], context=context)
403                 if not order_point_id and not ok:
404                     message = _("Not enough stock and no minimum orderpoint rule defined.")
405                 elif not order_point_id:
406                     message = _("No minimum orderpoint rule defined.")
407                 elif not ok:
408                     message = _("Not enough stock.")
409
410                 if message:
411                     message = _("Procurement '%s' is in exception: ") % (procurement.name) + message
412                     cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
413                     self.message_post(cr, uid, [procurement.id], body=message, context=context)
414         return ok
415
416     def action_produce_assign_service(self, cr, uid, ids, context=None):
417         """ Changes procurement state to Running.
418         @return: True
419         """
420         for procurement in self.browse(cr, uid, ids, context=context):
421             self.write(cr, uid, [procurement.id], {'state': 'running'})
422         self.running_send_note(cr, uid, ids, context=None)
423         return True
424
425     def action_produce_assign_product(self, cr, uid, ids, context=None):
426         """ This is action which call from workflow to assign production order to procurements
427         @return: True
428         """
429         return 0
430
431
432     def action_po_assign(self, cr, uid, ids, context=None):
433         """ This is action which call from workflow to assign purchase order to procurements
434         @return: True
435         """
436         return 0
437
438     # XXX action_cancel() should accept a context argument
439     def action_cancel(self, cr, uid, ids):
440         """Cancel Procurements and either cancel or assign the related Stock Moves, depending on the procurement configuration.
441         
442         @return: True
443         """
444         to_assign = []
445         to_cancel = []
446         move_obj = self.pool.get('stock.move')
447         for proc in self.browse(cr, uid, ids):
448             if proc.close_move and proc.move_id:
449                 if proc.move_id.state not in ('done', 'cancel'):
450                     to_cancel.append(proc.move_id.id)
451             else:
452                 if proc.move_id and proc.move_id.state == 'waiting':
453                     to_assign.append(proc.move_id.id)
454         if len(to_cancel):
455             move_obj.action_cancel(cr, uid, to_cancel)
456         if len(to_assign):
457             move_obj.write(cr, uid, to_assign, {'state': 'assigned'})
458         self.write(cr, uid, ids, {'state': 'cancel'})
459         self.cancel_send_note(cr, uid, ids, context=None)
460         wf_service = netsvc.LocalService("workflow")
461         for id in ids:
462             wf_service.trg_trigger(uid, 'procurement.order', id, cr)
463         return True
464
465     def action_check_finished(self, cr, uid, ids):
466         return self.check_move_done(cr, uid, ids)
467
468     def action_check(self, cr, uid, ids):
469         """ Checks procurement move state whether assigned or done.
470         @return: True
471         """
472         ok = False
473         for procurement in self.browse(cr, uid, ids):
474             if procurement.move_id and procurement.move_id.state == 'assigned' or procurement.move_id.state == 'done':
475                 self.action_done(cr, uid, [procurement.id])
476                 ok = True
477         return ok
478
479     def action_ready(self, cr, uid, ids):
480         """ Changes procurement state to Ready.
481         @return: True
482         """
483         res = self.write(cr, uid, ids, {'state': 'ready'})
484         self.ready_send_note(cr, uid, ids, context=None)
485         return res
486
487     def action_done(self, cr, uid, ids):
488         """ Changes procurement state to Done and writes Closed date.
489         @return: True
490         """
491         move_obj = self.pool.get('stock.move')
492         for procurement in self.browse(cr, uid, ids):
493             if procurement.move_id:
494                 if procurement.close_move and (procurement.move_id.state <> 'done'):
495                     move_obj.action_done(cr, uid, [procurement.move_id.id])
496         res = self.write(cr, uid, ids, {'state': 'done', 'date_close': time.strftime('%Y-%m-%d')})
497         self.done_send_note(cr, uid, ids, context=None)
498         wf_service = netsvc.LocalService("workflow")
499         for id in ids:
500             wf_service.trg_trigger(uid, 'procurement.order', id, cr)
501         return res
502
503     # ----------------------------------------
504     # OpenChatter methods and notifications
505     # ----------------------------------------
506
507     def create(self, cr, uid, vals, context=None):
508         obj_id = super(procurement_order, self).create(cr, uid, vals, context)
509         self.create_send_note(cr, uid, [obj_id], context=context)
510         return obj_id
511
512     def create_send_note(self, cr, uid, ids, context=None):
513         self.message_post(cr, uid, ids, body=_("Procurement has been <b>created</b>."), context=context)
514
515     def confirm_send_note(self, cr, uid, ids, context=None):
516         self.message_post(cr, uid, ids, body=_("Procurement has been <b>confirmed</b>."), context=context)
517
518     def running_send_note(self, cr, uid, ids, context=None):
519         self.message_post(cr, uid, ids, body=_("Procurement has been set to <b>running</b>."), context=context)
520
521     def ready_send_note(self, cr, uid, ids, context=None):
522         self.message_post(cr, uid, ids, body=_("Procurement has been set to <b>ready</b>."), context=context)
523
524     def cancel_send_note(self, cr, uid, ids, context=None):
525         self.message_post(cr, uid, ids, body=_("Procurement has been <b>cancelled</b>."), context=context)
526
527     def done_send_note(self, cr, uid, ids, context=None):
528         self.message_post(cr, uid, ids, body=_("Procurement has been <b>done</b>."), context=context)
529
530 procurement_order()
531
532 class StockPicking(osv.osv):
533     _inherit = 'stock.picking'
534
535     def test_finished(self, cursor, user, ids):
536         wf_service = netsvc.LocalService("workflow")
537         res = super(StockPicking, self).test_finished(cursor, user, ids)
538         for picking in self.browse(cursor, user, ids):
539             for move in picking.move_lines:
540                 if move.state == 'done' and move.procurements:
541                     for procurement in move.procurements:
542                         wf_service.trg_validate(user, 'procurement.order',
543                             procurement.id, 'button_check', cursor)
544         return res
545
546 StockPicking()
547
548 class stock_warehouse_orderpoint(osv.osv):
549     """
550     Defines Minimum stock rules.
551     """
552     _name = "stock.warehouse.orderpoint"
553     _description = "Minimum Inventory Rule"
554
555     def _get_draft_procurements(self, cr, uid, ids, field_name, arg, context=None):
556         if context is None:
557             context = {}
558         result = {}
559         procurement_obj = self.pool.get('procurement.order')
560         for orderpoint in self.browse(cr, uid, ids, context=context):
561             procurement_ids = procurement_obj.search(cr, uid , [('state', '=', 'draft'), ('product_id', '=', orderpoint.product_id.id), ('location_id', '=', orderpoint.location_id.id)])
562             result[orderpoint.id] = procurement_ids
563         return result
564
565     def _check_product_uom(self, cr, uid, ids, context=None):
566         '''
567         Check if the UoM has the same category as the product standard UoM
568         '''
569         if not context:
570             context = {}
571             
572         for rule in self.browse(cr, uid, ids, context=context):
573             if rule.product_id.uom_id.category_id.id != rule.product_uom.category_id.id:
574                 return False
575             
576         return True
577
578     _columns = {
579         'name': fields.char('Name', size=32, required=True),
580         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the orderpoint without removing it."),
581         'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
582         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
583         'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
584         'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type','!=','service')]),
585         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
586         'product_min_qty': fields.float('Minimum Quantity', required=True,
587             help="When the virtual stock goes below the Min Quantity specified for this field, OpenERP generates "\
588             "a procurement to bring the virtual stock to the Max Quantity."),
589         'product_max_qty': fields.float('Maximum Quantity', required=True,
590             help="When the virtual stock goes below the Min Quantity, OpenERP generates "\
591             "a procurement to bring the virtual stock to the Quantity specified as Max Quantity."),
592         'qty_multiple': fields.integer('Qty Multiple', required=True,
593             help="The procurement quantity will be rounded up to this multiple."),
594         'procurement_id': fields.many2one('procurement.order', 'Latest procurement', ondelete="set null"),
595         'company_id': fields.many2one('res.company','Company',required=True),
596         'procurement_draft_ids': fields.function(_get_draft_procurements, type='many2many', relation="procurement.order", \
597                                 string="Related Procurement Orders",help="Draft procurement of the product and location of that orderpoint"),
598     }
599     _defaults = {
600         'active': lambda *a: 1,
601         'logic': lambda *a: 'max',
602         'qty_multiple': lambda *a: 1,
603         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'stock.orderpoint') or '',
604         'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
605         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=c)
606     }
607     _sql_constraints = [
608         ('qty_multiple_check', 'CHECK( qty_multiple > 0 )', 'Qty Multiple must be greater than zero.'),
609     ]
610     _constraints = [
611         (_check_product_uom, 'You have to select a product unit of measure in the same category than the default unit of measure of the product', ['product_id', 'product_uom']),
612     ]
613
614     def default_get(self, cr, uid, fields, context=None):
615         res = super(stock_warehouse_orderpoint, self).default_get(cr, uid, fields, context)
616         # default 'warehouse_id' and 'location_id'
617         if 'warehouse_id' not in res:
618             warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0', context)
619             res['warehouse_id'] = warehouse.id
620         if 'location_id' not in res:
621             warehouse = self.pool.get('stock.warehouse').browse(cr, uid, res['warehouse_id'], context)
622             res['location_id'] = warehouse.lot_stock_id.id
623         return res
624
625     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
626         """ Finds location id for changed warehouse.
627         @param warehouse_id: Changed id of warehouse.
628         @return: Dictionary of values.
629         """
630         if warehouse_id:
631             w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
632             v = {'location_id': w.lot_stock_id.id}
633             return {'value': v}
634         return {}
635
636     def onchange_product_id(self, cr, uid, ids, product_id, context=None):
637         """ Finds UoM for changed product.
638         @param product_id: Changed id of product.
639         @return: Dictionary of values.
640         """
641         if product_id:
642             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
643             d = {'product_uom': [('category_id', '=', prod.uom_id.category_id.id)]}
644             v = {'product_uom': prod.uom_id.id}
645             return {'value': v, 'domain': d}
646         return {'domain': {'product_uom': []}}
647
648     def copy(self, cr, uid, id, default=None, context=None):
649         if not default:
650             default = {}
651         default.update({
652             'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
653         })
654         return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context=context)
655
656 stock_warehouse_orderpoint()
657
658 class product_product(osv.osv):
659     _inherit="product.product"
660     _columns = {
661         'orderpoint_ids': fields.one2many('stock.warehouse.orderpoint', 'product_id', 'Minimum Stock Rules'),
662     }
663
664
665 product_product()
666 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: