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)
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),
122 'state': lambda *a: 'draft',
123 'priority': lambda *a: '1',
124 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
125 'close_move': lambda *a: 0,
126 'procure_method': lambda *a: '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'])
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={}):
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)
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={}):
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))
168 def check_move_done(self, cr, uid, ids, context={}):
169 """ Checks if move is done or not.
170 @return: True or False.
172 return all(procurement.move_id.state == 'done' for procurement in self.browse(cr, uid, ids))
175 # This method may be overrided by objects that override procurement.order
176 # for computing their own purpose
178 def _quantity_compute_get(self, cr, uid, proc, context={}):
179 """ Finds sold quantity of product.
180 @param proc: Current procurement.
181 @return: Quantity or False.
183 if proc.product_id.type == 'product' and proc.move_id:
184 if proc.move_id.product_uos:
185 return proc.move_id.product_uos_qty
188 def _uom_compute_get(self, cr, uid, proc, context={}):
189 """ Finds UoS if product is Stockable Product.
190 @param proc: Current procurement.
191 @return: UoS or False.
193 if proc.product_id.type == 'product' and proc.move_id:
194 if proc.move_id.product_uos:
195 return proc.move_id.product_uos.id
199 # Return the quantity of product shipped/produced/served, which may be
200 # different from the planned quantity
202 def quantity_get(self, cr, uid, id, context={}):
203 """ Finds quantity of product used in procurement.
204 @return: Quantity of product.
206 proc = self.browse(cr, uid, id, context)
207 result = self._quantity_compute_get(cr, uid, proc, context)
209 result = proc.product_qty
212 def uom_get(self, cr, uid, id, context=None):
213 """ Finds UoM of product used in procurement.
214 @return: UoM of product.
216 proc = self.browse(cr, uid, id, context)
217 result = self._uom_compute_get(cr, uid, proc, context)
219 result = proc.product_uom.id
222 def check_waiting(self, cr, uid, ids, context=[]):
223 """ Checks state of move.
224 @return: True or False
226 for procurement in self.browse(cr, uid, ids, context=context):
227 if procurement.move_id and procurement.move_id.state == 'auto':
231 def check_produce_service(self, cr, uid, procurement, context=[]):
234 def check_produce_product(self, cr, uid, procurement, context=[]):
235 """ Finds BoM of a product if not found writes exception message.
236 @param procurement: Current procurement.
237 @return: True or False.
241 def check_make_to_stock(self, cr, uid, ids, context={}):
242 """ Checks product type.
243 @return: True or False
246 for procurement in self.browse(cr, uid, ids, context=context):
247 if procurement.product_id.type == 'service':
248 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
250 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
253 def check_produce(self, cr, uid, ids, context={}):
254 """ Checks product type.
255 @return: True or Product Id.
258 user = self.pool.get('res.users').browse(cr, uid, uid)
259 for procurement in self.browse(cr, uid, ids):
260 if procurement.product_id.product_tmpl_id.supply_method <> 'produce':
261 partner_list = sorted([(partner_id.sequence, partner_id) for partner_id in procurement.product_id.seller_ids if partner_id])
263 partner = partner_list and partner_list[0] and partner_list[0][1] and partner_list[0][1].name or False
264 if user.company_id and user.company_id.partner_id:
265 if partner.id == user.company_id.partner_id.id:
268 if procurement.product_id.product_tmpl_id.type=='service':
269 res = res and self.check_produce_service(cr, uid, procurement, context)
271 res = res and self.check_produce_product(cr, uid, procurement, context)
276 def check_buy(self, cr, uid, ids):
277 """ Checks product type.
278 @return: True or Product Id.
280 user = self.pool.get('res.users').browse(cr, uid, uid)
281 partner_obj = self.pool.get('res.partner')
282 for procurement in self.browse(cr, uid, ids):
283 if procurement.product_id.product_tmpl_id.supply_method <> 'buy':
285 if not procurement.product_id.seller_ids:
286 cr.execute('update procurement_order set message=%s where id=%s',
287 (_('No supplier defined for this product !'), procurement.id))
289 partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
291 if user.company_id and user.company_id.partner_id:
292 if partner.id == user.company_id.partner_id.id:
294 address_id = partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']
296 cr.execute('update procurement_order set message=%s where id=%s',
297 (_('No address defined for the supplier'), procurement.id))
301 def test_cancel(self, cr, uid, ids):
302 """ Tests whether state of move is cancelled or not.
303 @return: True or False
305 for record in self.browse(cr, uid, ids):
306 if record.move_id and record.move_id.state == 'cancel':
310 def action_confirm(self, cr, uid, ids, context={}):
311 """ Confirms procurement and writes exception message if any.
314 move_obj = self.pool.get('stock.move')
315 for procurement in self.browse(cr, uid, ids):
316 if procurement.product_qty <= 0.00:
317 raise osv.except_osv(_('Data Insufficient !'),
318 _('Please check the Quantity in Procurement Order(s), it should not be less than 1!'))
319 if procurement.product_id.type in ('product', 'consu'):
320 if not procurement.move_id:
321 source = procurement.location_id.id
322 if procurement.procure_method == 'make_to_order':
323 source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
324 id = move_obj.create(cr, uid, {
325 'name': procurement.name,
326 'location_id': source,
327 'location_dest_id': procurement.location_id.id,
328 'product_id': procurement.product_id.id,
329 'product_qty': procurement.product_qty,
330 'product_uom': procurement.product_uom.id,
331 'date_expected': procurement.date_planned,
333 'company_id': procurement.company_id.id,
334 'auto_validate': True,
336 move_obj.action_confirm(cr, uid, [id], context=context)
337 self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move': 1})
338 self.write(cr, uid, ids, {'state': 'confirmed', 'message': ''})
341 def action_move_assigned(self, cr, uid, ids, context={}):
342 """ Changes procurement state to Running and writes message.
345 self.write(cr, uid, ids, {'state': 'running',
346 'message': _('from stock: products assigned.')})
349 def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
351 This method may be overrided by objects that override procurement.order
352 for computing their own purpose
356 def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
357 """ Checks procurement move state.
358 @param procurement: Current procurement.
359 @return: True or move id.
362 if procurement.move_id:
363 id = procurement.move_id.id
364 if not (procurement.move_id.state in ('done','assigned','cancel')):
365 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
366 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%s', (procurement.product_id.id,))
367 if not cr.fetchone()[0]:
368 cr.execute('update procurement_order set message=%s where id=%s',
369 (_('Not enough stock and no minimum orderpoint rule defined.'),
371 message = _("Procurement '%s' is in exception: not enough stock.") % \
373 self.log(cr, uid, procurement.id, message)
376 def action_produce_assign_service(self, cr, uid, ids, context={}):
377 """ Changes procurement state to Running.
380 for procurement in self.browse(cr, uid, ids):
381 self.write(cr, uid, [procurement.id], {'state': 'running'})
384 def action_produce_assign_product(self, cr, uid, ids, context={}):
385 """ This is action which call from workflow to assign production order to procurements
391 def action_po_assign(self, cr, uid, ids, context={}):
392 """ This is action which call from workflow to assign purchase order to procurements
397 def action_cancel(self, cr, uid, ids):
398 """ Cancels procurement and writes move state to Assigned.
403 move_obj = self.pool.get('stock.move')
404 for proc in self.browse(cr, uid, ids):
405 if proc.close_move and proc.move_id:
406 if proc.move_id.state not in ('done', 'cancel'):
407 todo2.append(proc.move_id.id)
409 if proc.move_id and proc.move_id.state == 'waiting':
410 todo.append(proc.move_id.id)
412 move_obj.action_cancel(cr, uid, todo2)
414 move_obj.write(cr, uid, todo, {'state': 'assigned'})
415 self.write(cr, uid, ids, {'state': 'cancel'})
416 wf_service = netsvc.LocalService("workflow")
418 wf_service.trg_trigger(uid, 'procurement.order', id, cr)
421 def action_check_finished(self, cr, uid, ids):
422 return self.check_move_done(cr, uid, ids)
424 def action_check(self, cr, uid, ids):
425 """ Checks procurement move state whether assigned or done.
429 for procurement in self.browse(cr, uid, ids):
430 if procurement.move_id and procurement.move_id.state == 'assigned' or procurement.move_id.state == 'done':
431 self.action_done(cr, uid, [procurement.id])
435 def action_ready(self, cr, uid, ids):
436 """ Changes procurement state to Ready.
439 res = self.write(cr, uid, ids, {'state': 'ready'})
442 def action_done(self, cr, uid, ids):
443 """ Changes procurement state to Done and writes Closed date.
446 move_obj = self.pool.get('stock.move')
447 for procurement in self.browse(cr, uid, ids):
448 if procurement.move_id:
449 if procurement.close_move and (procurement.move_id.state <> 'done'):
450 move_obj.action_done(cr, uid, [procurement.move_id.id])
451 res = self.write(cr, uid, ids, {'state': 'done', 'date_close': time.strftime('%Y-%m-%d')})
452 wf_service = netsvc.LocalService("workflow")
454 wf_service.trg_trigger(uid, 'procurement.order', id, cr)
457 def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
458 ''' Runs through scheduler.
459 @param use_new_cursor: False or the dbname
463 self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
464 self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
465 use_new_cursor=use_new_cursor, context=context)
469 class StockPicking(osv.osv):
470 _inherit = 'stock.picking'
472 def test_finished(self, cursor, user, ids):
473 wf_service = netsvc.LocalService("workflow")
474 res = super(StockPicking, self).test_finished(cursor, user, ids)
475 for picking in self.browse(cursor, user, ids):
476 for move in picking.move_lines:
477 if move.state == 'done' and move.procurements:
478 for procurement in move.procurements:
479 wf_service.trg_validate(user, 'procurement.order',
480 procurement.id, 'button_check', cursor)
485 class stock_warehouse_orderpoint(osv.osv):
487 Defines Minimum stock rules.
489 _name = "stock.warehouse.orderpoint"
490 _description = "Minimum Inventory Rule"
493 'name': fields.char('Name', size=32, required=True),
494 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the orderpoint without removing it."),
495 'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
496 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
497 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
498 'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type','=','product')]),
499 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
500 'product_min_qty': fields.float('Min Quantity', required=True,
501 help="When the virtual stock goes belong the Min Quantity, OpenERP generates "\
502 "a procurement to bring the virtual stock to the Max Quantity."),
503 'product_max_qty': fields.float('Max Quantity', required=True,
504 help="When the virtual stock goes belong the Max Quantity, OpenERP generates "\
505 "a procurement to bring the virtual stock to the Max Quantity."),
506 'qty_multiple': fields.integer('Qty Multiple', required=True,
507 help="The procurement quantity will by rounded up to this multiple."),
508 'procurement_id': fields.many2one('procurement.order', 'Latest procurement', ondelete="set null"),
509 'company_id': fields.many2one('res.company','Company',required=True),
512 'active': lambda *a: 1,
513 'logic': lambda *a: 'max',
514 'qty_multiple': lambda *a: 1,
515 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'stock.orderpoint') or '',
516 'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
517 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=c)
520 ('qty_multiple_check', 'CHECK( qty_multiple > 0 )',
521 _('Qty Multiple must be greater than zero.')),
524 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
525 """ Finds location id for changed warehouse.
526 @param warehouse_id: Changed id of warehouse.
527 @return: Dictionary of values.
530 w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context)
531 v = {'location_id': w.lot_stock_id.id}
535 def onchange_product_id(self, cr, uid, ids, product_id, context={}):
536 """ Finds UoM for changed product.
537 @param product_id: Changed id of product.
538 @return: Dictionary of values.
541 prod = self.pool.get('product.product').browse(cr,uid,product_id)
542 v = {'product_uom': prod.uom_id.id}
546 def copy(self, cr, uid, id, default=None,context={}):
550 'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
552 return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context)
554 stock_warehouse_orderpoint()
555 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: