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 _
28 # ------------------------------------------------------------------
30 # Produce, Buy or Find products and place a move
31 # then wizard for picking lists & move
34 class mrp_property_group(osv.osv):
36 Group of mrp properties.
38 _name = 'mrp.property.group'
39 _description = 'Property Group'
41 'name': fields.char('Property Group', size=64, required=True),
42 'description': fields.text('Description'),
46 class mrp_property(osv.osv):
50 _name = 'mrp.property'
51 _description = 'Property'
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'),
59 'composition': lambda *a: 'min',
63 class StockMove(osv.osv):
64 _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'
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."),
105 'note': fields.text('Note'),
106 'message': fields.char('Latest error', size=64, help="Exception occurred while computing procurement orders."),
107 'state': fields.selection([
109 ('confirmed','Confirmed'),
110 ('exception','Exception'),
111 ('running','Running'),
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),
124 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
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)
130 def unlink(self, cr, uid, ids, context=None):
131 procurements = self.read(cr, uid, ids, ['state'], context=context)
133 for s in procurements:
134 if s['state'] in ['draft','cancel']:
135 unlink_ids.append(s['id'])
137 raise osv.except_osv(_('Invalid action !'),
138 _('Cannot delete Procurement Order(s) which are in %s State!') % \
140 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
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.
148 w = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
150 'product_uom': w.uom_id.id,
151 'product_uos': w.uos_id and w.uos_id.id or w.uom_id.id
156 def check_product(self, cr, uid, ids):
157 """ Checks product type.
158 @return: True or False
160 return all(procurement.product_id.type in ('product', 'consu') for procurement in self.browse(cr, uid, ids))
162 def check_move_cancel(self, cr, uid, ids, context=None):
163 """ Checks if move is cancelled or not.
164 @return: True or False.
166 return all(procurement.move_id.state == 'cancel' for procurement in self.browse(cr, uid, ids, context=context))
168 def check_move_done(self, cr, uid, ids, context=None):
169 """ Checks if move is done or not.
170 @return: True or False.
174 return all(not procurement.move_id or procurement.move_id.state == 'done' for procurement in self.browse(cr, uid, ids, context=context))
176 # This method may be overrided by objects that override procurement.order
177 # for computing their own purpose
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.
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
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.
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
200 # Return the quantity of product shipped/produced/served, which may be
201 # different from the planned quantity
203 def quantity_get(self, cr, uid, id, context=None):
204 """ Finds quantity of product used in procurement.
205 @return: Quantity of product.
207 proc = self.browse(cr, uid, id, context=context)
208 result = self._quantity_compute_get(cr, uid, proc, context=context)
210 result = proc.product_qty
213 def uom_get(self, cr, uid, id, context=None):
214 """ Finds UoM of product used in procurement.
215 @return: UoM of product.
217 proc = self.browse(cr, uid, id, context=context)
218 result = self._uom_compute_get(cr, uid, proc, context=context)
220 result = proc.product_uom.id
223 def check_waiting(self, cr, uid, ids, context=None):
224 """ Checks state of move.
225 @return: True or False
227 for procurement in self.browse(cr, uid, ids, context=context):
228 if procurement.move_id and procurement.move_id.state == 'auto':
232 def check_produce_service(self, cr, uid, procurement, context=None):
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.
242 def check_make_to_stock(self, cr, uid, ids, context=None):
243 """ Checks product type.
244 @return: True or False
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)
251 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
254 def check_produce(self, cr, uid, ids, context=None):
255 """ Checks product type.
256 @return: True or Product Id.
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])
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:
269 if procurement.product_id.product_tmpl_id.type=='service':
270 res = res and self.check_produce_service(cr, uid, procurement, context)
272 res = res and self.check_produce_product(cr, uid, procurement, context)
277 def check_buy(self, cr, uid, ids):
278 """ Checks product type.
279 @return: True or Product Id.
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':
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))
290 partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
293 cr.execute('update procurement_order set message=%s where id=%s',
294 (_('No default supplier defined for this product'), procurement.id))
297 if user.company_id and user.company_id.partner_id:
298 if partner.id == user.company_id.partner_id.id:
301 address_id = partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']
303 cr.execute('update procurement_order set message=%s where id=%s',
304 (_('No address defined for the supplier'), procurement.id))
308 def test_cancel(self, cr, uid, ids):
309 """ Tests whether state of move is cancelled or not.
310 @return: True or False
312 for record in self.browse(cr, uid, ids):
313 if record.move_id and record.move_id.state == 'cancel':
317 def action_confirm(self, cr, uid, ids, context=None):
318 """ Confirms procurement and writes exception message if any.
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,
340 'company_id': procurement.company_id.id,
341 'auto_validate': True,
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': ''})
348 def action_move_assigned(self, cr, uid, ids, context=None):
349 """ Changes procurement state to Running and writes message.
352 self.write(cr, uid, ids, {'state': 'running',
353 'message': _('from stock: products assigned.')})
356 def _check_make_to_stock_service(self, cr, uid, procurement, context=None):
358 This method may be overrided by objects that override procurement.order
359 for computing their own purpose
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.
369 if procurement.move_id:
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.")
379 message = _("No minimum orderpoint rule defined.")
381 message = _("Not enough stock.")
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))
388 def action_produce_assign_service(self, cr, uid, ids, context=None):
389 """ Changes procurement state to Running.
392 for procurement in self.browse(cr, uid, ids, context=context):
393 self.write(cr, uid, [procurement.id], {'state': 'running'})
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
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
409 def action_cancel(self, cr, uid, ids):
410 """ Cancels procurement and writes move state to Assigned.
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)
421 if proc.move_id and proc.move_id.state == 'waiting':
422 todo.append(proc.move_id.id)
424 move_obj.action_cancel(cr, uid, todo2)
426 move_obj.write(cr, uid, todo, {'state': 'assigned'})
427 self.write(cr, uid, ids, {'state': 'cancel'})
428 wf_service = netsvc.LocalService("workflow")
430 wf_service.trg_trigger(uid, 'procurement.order', id, cr)
433 def action_check_finished(self, cr, uid, ids):
434 return self.check_move_done(cr, uid, ids)
436 def action_check(self, cr, uid, ids):
437 """ Checks procurement move state whether assigned or done.
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])
447 def action_ready(self, cr, uid, ids):
448 """ Changes procurement state to Ready.
451 res = self.write(cr, uid, ids, {'state': 'ready'})
454 def action_done(self, cr, uid, ids):
455 """ Changes procurement state to Done and writes Closed date.
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")
466 wf_service.trg_trigger(uid, 'procurement.order', id, cr)
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
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)
479 class StockPicking(osv.osv):
480 _inherit = 'stock.picking'
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)
495 class stock_warehouse_orderpoint(osv.osv):
497 Defines Minimum stock rules.
499 _name = "stock.warehouse.orderpoint"
500 _description = "Minimum Inventory Rule"
502 def _get_draft_procurements(self, cr, uid, ids, field_name, arg, context=None):
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
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"),
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)
542 ('qty_multiple_check', 'CHECK( qty_multiple > 0 )', 'Qty Multiple must be greater than zero.'),
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.
551 w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
552 v = {'location_id': w.lot_stock_id.id}
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.
562 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
563 v = {'product_uom': prod.uom_id.id}
567 def copy(self, cr, uid, id, default=None, context=None):
571 'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
573 return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context=context)
575 stock_warehouse_orderpoint()
576 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: