1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from osv import osv, fields
23 from tools.translate import _
26 import decimal_precision as dp
29 # ------------------------------------------------------------------
31 # Produce, Buy or Find products and place a move
32 # then wizard for picking lists & move
35 class mrp_property_group(osv.osv):
37 Group of mrp properties.
39 _name = 'mrp.property.group'
40 _description = 'Property Group'
42 'name': fields.char('Property Group', size=64, required=True),
43 'description': fields.text('Description'),
47 class mrp_property(osv.osv):
51 _name = 'mrp.property'
52 _description = 'Property'
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'),
60 'composition': lambda *a: 'min',
64 class StockMove(osv.osv):
65 _inherit = 'stock.move'
67 'procurements': fields.one2many('procurement.order', 'move_id', 'Procurements'),
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)
77 class procurement_order(osv.osv):
81 _name = "procurement.order"
82 _description = "Procurement"
83 _order = 'priority,date_planned desc'
84 _inherit = ['ir.needaction_mixin', 'mail.thread']
87 'name': fields.char('Reason', size=64, required=True, help='Procurement name.'),
88 'origin': fields.char('Source Document', size=64,
89 help="Reference of the document that created this Procurement.\n"
90 "This is automatically completed by OpenERP."),
91 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True, select=True),
92 'date_planned': fields.datetime('Scheduled date', required=True, select=True),
93 'date_close': fields.datetime('Date Closed'),
94 'product_id': fields.many2one('product.product', 'Product', required=True, states={'draft':[('readonly',False)]}, readonly=True),
95 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product UoM'), required=True, states={'draft':[('readonly',False)]}, readonly=True),
96 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
97 'product_uos_qty': fields.float('UoS Quantity', states={'draft':[('readonly',False)]}, readonly=True),
98 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
99 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
100 'close_move': fields.boolean('Close Move at end', required=True),
101 'location_id': fields.many2one('stock.location', 'Location', required=True, states={'draft':[('readonly',False)]}, readonly=True),
102 'procure_method': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')], 'Procurement Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]},
103 readonly=True, required=True, help="If you encode manually a Procurement, you probably want to use" \
104 " a make to order method."),
106 'note': fields.text('Note'),
107 'message': fields.char('Latest error', size=64, help="Exception occurred while computing procurement orders."),
108 'state': fields.selection([
110 ('confirmed','Confirmed'),
111 ('exception','Exception'),
112 ('running','Running'),
116 ('waiting','Waiting')], 'State', required=True,
117 help='When a procurement is created the state is set to \'Draft\'.\n If the procurement is confirmed, the state is set to \'Confirmed\'.\
118 \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.'),
119 'note': fields.text('Note'),
120 'company_id': fields.many2one('res.company','Company',required=True),
121 'user_id': fields.many2one('res.users', 'Salesman'),
126 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
128 'procure_method': 'make_to_order',
129 'user_id': lambda obj, cr, uid, context: uid,
130 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c)
133 def unlink(self, cr, uid, ids, context=None):
134 procurements = self.read(cr, uid, ids, ['state'], context=context)
136 for s in procurements:
137 if s['state'] in ['draft','cancel']:
138 unlink_ids.append(s['id'])
140 raise osv.except_osv(_('Invalid action !'),
141 _('Cannot delete Procurement Order(s) which are in %s state!') % \
143 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
145 def onchange_product_id(self, cr, uid, ids, product_id, context=None):
146 """ Finds UoM and UoS of changed product.
147 @param product_id: Changed id of product.
148 @return: Dictionary of values.
151 w = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
153 'product_uom': w.uom_id.id,
154 'product_uos': w.uos_id and w.uos_id.id or w.uom_id.id
159 def check_product(self, cr, uid, ids, context=None):
160 """ Checks product type.
161 @return: True or False
163 return all(proc.product_id.type in ('product', 'consu') for proc in self.browse(cr, uid, ids, context=context))
165 def check_move_cancel(self, cr, uid, ids, context=None):
166 """ Checks if move is cancelled or not.
167 @return: True or False.
169 return all(procurement.move_id.state == 'cancel' for procurement in self.browse(cr, uid, ids, context=context))
171 #This Function is create to avoid a server side Error Like 'ERROR:tests.mrp:name 'check_move' is not defined'
172 def check_move(self, cr, uid, ids, context=None):
175 def check_move_done(self, cr, uid, ids, context=None):
176 """ Checks if move is done or not.
177 @return: True or False.
179 return all(proc.product_id.type == 'service' or (proc.move_id and proc.move_id.state == 'done') \
180 for proc in self.browse(cr, uid, ids, context=context))
183 # This method may be overrided by objects that override procurement.order
184 # for computing their own purpose
186 def _quantity_compute_get(self, cr, uid, proc, context=None):
187 """ Finds sold quantity of product.
188 @param proc: Current procurement.
189 @return: Quantity or False.
191 if proc.product_id.type == 'product' and proc.move_id:
192 if proc.move_id.product_uos:
193 return proc.move_id.product_uos_qty
196 def _uom_compute_get(self, cr, uid, proc, context=None):
197 """ Finds UoS if product is Stockable Product.
198 @param proc: Current procurement.
199 @return: UoS or False.
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.id
207 # Return the quantity of product shipped/produced/served, which may be
208 # different from the planned quantity
210 def quantity_get(self, cr, uid, id, context=None):
211 """ Finds quantity of product used in procurement.
212 @return: Quantity of product.
214 proc = self.browse(cr, uid, id, context=context)
215 result = self._quantity_compute_get(cr, uid, proc, context=context)
217 result = proc.product_qty
220 def uom_get(self, cr, uid, id, context=None):
221 """ Finds UoM of product used in procurement.
222 @return: UoM of product.
224 proc = self.browse(cr, uid, id, context=context)
225 result = self._uom_compute_get(cr, uid, proc, context=context)
227 result = proc.product_uom.id
230 def check_waiting(self, cr, uid, ids, context=None):
231 """ Checks state of move.
232 @return: True or False
234 for procurement in self.browse(cr, uid, ids, context=context):
235 if procurement.move_id and procurement.move_id.state == 'auto':
239 def check_produce_service(self, cr, uid, procurement, context=None):
242 def check_produce_product(self, cr, uid, procurement, context=None):
243 """ Finds BoM of a product if not found writes exception message.
244 @param procurement: Current procurement.
245 @return: True or False.
249 def check_make_to_stock(self, cr, uid, ids, context=None):
250 """ Checks product type.
251 @return: True or False
254 for procurement in self.browse(cr, uid, ids, context=context):
255 if procurement.product_id.type == 'service':
256 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
258 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
261 def check_produce(self, cr, uid, ids, context=None):
262 """ Checks product type.
263 @return: True or False
265 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
266 for procurement in self.browse(cr, uid, ids, context=context):
267 product = procurement.product_id
268 #TOFIX: if product type is 'service' but supply_method is 'buy'.
269 if product.supply_method <> 'produce':
270 supplier = product.seller_id
271 if supplier and user.company_id and user.company_id.partner_id:
272 if supplier.id == user.company_id.partner_id.id:
275 if product.type=='service':
276 res = self.check_produce_service(cr, uid, procurement, context)
278 res = self.check_produce_product(cr, uid, procurement, context)
283 def check_buy(self, cr, uid, ids):
284 """ Checks product type.
285 @return: True or Product Id.
287 user = self.pool.get('res.users').browse(cr, uid, uid)
288 partner_obj = self.pool.get('res.partner')
289 for procurement in self.browse(cr, uid, ids):
290 if procurement.product_id.product_tmpl_id.supply_method <> 'buy':
292 if not procurement.product_id.seller_ids:
293 cr.execute('update procurement_order set message=%s where id=%s',
294 (_('No supplier defined for this product !'), procurement.id))
296 partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
299 cr.execute('update procurement_order set message=%s where id=%s',
300 (_('No default supplier defined for this product'), procurement.id))
303 if user.company_id and user.company_id.partner_id:
304 if partner.id == user.company_id.partner_id.id:
307 address_id = partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']
309 cr.execute('update procurement_order set message=%s where id=%s',
310 (_('No address defined for the supplier'), procurement.id))
314 def test_cancel(self, cr, uid, ids):
315 """ Tests whether state of move is cancelled or not.
316 @return: True or False
318 for record in self.browse(cr, uid, ids):
319 if record.move_id and record.move_id.state == 'cancel':
323 def action_confirm(self, cr, uid, ids, context=None):
324 """ Confirms procurement and writes exception message if any.
327 move_obj = self.pool.get('stock.move')
328 for procurement in self.browse(cr, uid, ids, context=context):
329 if procurement.product_qty <= 0.00:
330 raise osv.except_osv(_('Data Insufficient !'),
331 _('Please check the quantity in procurement order(s), it should not be 0 or less!'))
332 if procurement.product_id.type in ('product', 'consu'):
333 if not procurement.move_id:
334 source = procurement.location_id.id
335 if procurement.procure_method == 'make_to_order':
336 source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
337 id = move_obj.create(cr, uid, {
338 'name': procurement.name,
339 'location_id': source,
340 'location_dest_id': procurement.location_id.id,
341 'product_id': procurement.product_id.id,
342 'product_qty': procurement.product_qty,
343 'product_uom': procurement.product_uom.id,
344 'date_expected': procurement.date_planned,
346 'company_id': procurement.company_id.id,
347 'auto_validate': True,
349 move_obj.action_confirm(cr, uid, [id], context=context)
350 self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move': 1})
351 self.write(cr, uid, ids, {'state': 'confirmed', 'message': ''})
352 self.state_change_send_note(cr, uid, ids ,'confirmed', context)
355 def action_move_assigned(self, cr, uid, ids, context=None):
356 """ Changes procurement state to Running and writes message.
359 self.write(cr, uid, ids, {'state': 'running',
360 'message': _('from stock: products assigned.')})
361 self.change_state_send_note(cr, uid, ids, 'running', context)
364 def _check_make_to_stock_service(self, cr, uid, procurement, context=None):
366 This method may be overrided by objects that override procurement.order
367 for computing their own purpose
371 def _check_make_to_stock_product(self, cr, uid, procurement, context=None):
372 """ Checks procurement move state.
373 @param procurement: Current procurement.
374 @return: True or move id.
377 if procurement.move_id:
379 id = procurement.move_id.id
380 if not (procurement.move_id.state in ('done','assigned','cancel')):
381 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
382 order_point_id = self.pool.get('stock.warehouse.orderpoint').search(cr, uid, [('product_id', '=', procurement.product_id.id)], context=context)
383 if not order_point_id and not ok:
384 message = _("Not enough stock and no minimum orderpoint rule defined.")
385 elif not order_point_id:
386 message = _("No minimum orderpoint rule defined.")
388 message = _("Not enough stock.")
391 self.log(cr, uid, procurement.id, _("Procurement '%s' is in exception: ") % (procurement.name) + message)
392 cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
395 def action_produce_assign_service(self, cr, uid, ids, context=None):
396 """ Changes procurement state to Running.
399 for procurement in self.browse(cr, uid, ids, context=context):
400 self.write(cr, uid, [procurement.id], {'state': 'running'})
401 self.change_state_send_note(cr, uid, ids, context=None)
404 def action_produce_assign_product(self, cr, uid, ids, context=None):
405 """ This is action which call from workflow to assign production order to procurements
411 def action_po_assign(self, cr, uid, ids, context=None):
412 """ This is action which call from workflow to assign purchase order to procurements
417 def action_cancel(self, cr, uid, ids):
418 """ Cancels procurement and writes move state to Assigned.
423 move_obj = self.pool.get('stock.move')
424 for proc in self.browse(cr, uid, ids):
425 if proc.close_move and proc.move_id:
426 if proc.move_id.state not in ('done', 'cancel'):
427 todo2.append(proc.move_id.id)
429 if proc.move_id and proc.move_id.state == 'waiting':
430 todo.append(proc.move_id.id)
432 move_obj.action_cancel(cr, uid, todo2)
434 move_obj.write(cr, uid, todo, {'state': 'assigned'})
435 self.write(cr, uid, ids, {'state': 'cancel'})
436 self.state_change_send_note(cr, uid, ids, 'cancelled', context=None)
437 wf_service = netsvc.LocalService("workflow")
439 wf_service.trg_trigger(uid, 'procurement.order', id, cr)
442 def action_check_finished(self, cr, uid, ids):
443 return self.check_move_done(cr, uid, ids)
445 def action_check(self, cr, uid, ids):
446 """ Checks procurement move state whether assigned or done.
450 for procurement in self.browse(cr, uid, ids):
451 if procurement.move_id and procurement.move_id.state == 'assigned' or procurement.move_id.state == 'done':
452 self.action_done(cr, uid, [procurement.id])
456 def action_ready(self, cr, uid, ids):
457 """ Changes procurement state to Ready.
460 res = self.write(cr, uid, ids, {'state': 'ready'})
461 self.change_state_send_note(cr, uid, ids, 'ready', context=None)
464 def action_done(self, cr, uid, ids):
465 """ Changes procurement state to Done and writes Closed date.
468 move_obj = self.pool.get('stock.move')
469 for procurement in self.browse(cr, uid, ids):
470 if procurement.move_id:
471 if procurement.close_move and (procurement.move_id.state <> 'done'):
472 move_obj.action_done(cr, uid, [procurement.move_id.id])
473 res = self.write(cr, uid, ids, {'state': 'done', 'date_close': time.strftime('%Y-%m-%d')})
474 self.state_change_send_note(cr, uid, ids, 'done', context=None)
475 wf_service = netsvc.LocalService("workflow")
477 wf_service.trg_trigger(uid, 'procurement.order', id, cr)
480 # ----------------------------------------
481 # OpenChatter methods and notifications
482 # ----------------------------------------
484 def get_needaction_user_ids(self, cr, uid, ids, context=None):
485 result = dict.fromkeys(ids, [])
486 for obj in self.browse(cr, uid, ids, context=context):
487 if (obj.state == 'draft' or obj.state == 'waiting'):
488 result[obj.id] = [obj.user_id.id]
491 def create(self, cr, uid, vals, context=None):
492 obj_id = super(procurement_order, self).create(cr, uid, vals, context)
493 self.state_change_send_note(cr, uid, [obj_id], 'created', context=context)
496 def state_change_send_note(self, cr, uid, ids, state, context=None):
497 for obj in self.browse(cr, uid, ids, context=context):
498 self.message_subscribe(cr, uid, [obj.id], [obj.user_id.id], context=context)
499 self.message_append_note(cr, uid, [obj.id], body=_("Procurement has been <b>%s</b>.") % (state), context=context)
501 def change_state_send_note(self, cr, uid, ids, state, context=None):
502 for obj in self.browse(cr, uid, ids, context=context):
503 self.message_subscribe(cr, uid, [obj.id], [obj.user_id.id], context=context)
504 self.message_append_note(cr, uid, [obj.id], body=_("Procurement has been set to <b>%s</b> state.") % (state), context=context)
508 class StockPicking(osv.osv):
509 _inherit = 'stock.picking'
511 def test_finished(self, cursor, user, ids):
512 wf_service = netsvc.LocalService("workflow")
513 res = super(StockPicking, self).test_finished(cursor, user, ids)
514 for picking in self.browse(cursor, user, ids):
515 for move in picking.move_lines:
516 if move.state == 'done' and move.procurements:
517 for procurement in move.procurements:
518 wf_service.trg_validate(user, 'procurement.order',
519 procurement.id, 'button_check', cursor)
524 class stock_warehouse_orderpoint(osv.osv):
526 Defines Minimum stock rules.
528 _name = "stock.warehouse.orderpoint"
529 _description = "Minimum Inventory Rule"
531 def _get_draft_procurements(self, cr, uid, ids, field_name, arg, context=None):
535 procurement_obj = self.pool.get('procurement.order')
536 for orderpoint in self.browse(cr, uid, ids, context=context):
537 procurement_ids = procurement_obj.search(cr, uid , [('state', '=', 'draft'), ('product_id', '=', orderpoint.product_id.id), ('location_id', '=', orderpoint.location_id.id)])
538 result[orderpoint.id] = procurement_ids
542 'name': fields.char('Name', size=32, required=True),
543 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the orderpoint without removing it."),
544 'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
545 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
546 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
547 'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type','=','product')]),
548 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
549 'product_min_qty': fields.float('Min Quantity', required=True,
550 help="When the virtual stock goes below the Min Quantity specified for this field, OpenERP generates "\
551 "a procurement to bring the virtual stock to the Max Quantity."),
552 'product_max_qty': fields.float('Max Quantity', required=True,
553 help="When the virtual stock goes below the Min Quantity, OpenERP generates "\
554 "a procurement to bring the virtual stock to the Quantity specified as Max Quantity."),
555 'qty_multiple': fields.integer('Qty Multiple', required=True,
556 help="The procurement quantity will be rounded up to this multiple."),
557 'procurement_id': fields.many2one('procurement.order', 'Latest procurement', ondelete="set null"),
558 'company_id': fields.many2one('res.company','Company',required=True),
559 'procurement_draft_ids': fields.function(_get_draft_procurements, type='many2many', relation="procurement.order", \
560 string="Related Procurement Orders",help="Draft procurement of the product and location of that orderpoint"),
563 'active': lambda *a: 1,
564 'logic': lambda *a: 'max',
565 'qty_multiple': lambda *a: 1,
566 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'stock.orderpoint') or '',
567 'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
568 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=c)
571 ('qty_multiple_check', 'CHECK( qty_multiple > 0 )', 'Qty Multiple must be greater than zero.'),
574 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
575 """ Finds location id for changed warehouse.
576 @param warehouse_id: Changed id of warehouse.
577 @return: Dictionary of values.
580 w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
581 v = {'location_id': w.lot_stock_id.id}
585 def onchange_product_id(self, cr, uid, ids, product_id, context=None):
586 """ Finds UoM for changed product.
587 @param product_id: Changed id of product.
588 @return: Dictionary of values.
591 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
592 v = {'product_uom': prod.uom_id.id}
596 def copy(self, cr, uid, id, default=None, context=None):
600 'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
602 return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context=context)
604 stock_warehouse_orderpoint()
606 class product_product(osv.osv):
607 _inherit="product.product"
609 'orderpoint_ids': fields.one2many('stock.warehouse.orderpoint', 'product_id', 'Minimum Stock Rule')
613 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: