1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
23 from osv import fields
29 from mx import DateTime
31 from tools import config
32 from tools.translate import _
37 class purchase_order(osv.osv):
38 def _calc_amount(self, cr, uid, ids, prop, unknow_none, unknow_dict):
40 for order in self.browse(cr, uid, ids):
42 for oline in order.order_line:
43 res[order.id] += oline.price_unit * oline.product_qty
46 def _amount_all(self, cr, uid, ids, field_name, arg, context):
48 cur_obj=self.pool.get('res.currency')
49 for order in self.browse(cr, uid, ids):
51 'amount_untaxed': 0.0,
56 cur=order.pricelist_id.currency_id
57 for line in order.order_line:
58 for c in self.pool.get('account.tax').compute(cr, uid, line.taxes_id, line.price_unit, line.product_qty, order.partner_address_id.id, line.product_id, order.partner_id):
60 val1 += line.price_subtotal
61 res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
62 res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
63 res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
66 def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context):
67 if not value: return False
68 if type(ids)!=type([]):
70 for po in self.browse(cr, uid, ids, context):
71 cr.execute("""update purchase_order_line set
75 (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
78 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context):
80 purchase_obj=self.browse(cr, uid, ids, context=context)
81 for purchase in purchase_obj:
82 res[purchase.id] = False
83 if purchase.order_line:
84 min_date=purchase.order_line[0].date_planned
85 for line in purchase.order_line:
86 if line.date_planned < min_date:
87 min_date=line.date_planned
88 res[purchase.id]=min_date
91 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
93 for purchase in self.browse(cursor, user, ids, context=context):
95 if purchase.invoice_id and purchase.invoice_id.state not in ('draft','cancel'):
96 tot += purchase.invoice_id.amount_untaxed
97 if purchase.amount_untaxed:
98 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
100 res[purchase.id] = 0.0
103 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
104 if not ids: return {}
109 p.purchase_id,sum(m.product_qty), m.state
113 stock_picking p on (p.id=m.picking_id)
115 p.purchase_id in ('''+','.join(map(str,ids))+''')
116 GROUP BY m.state, p.purchase_id''')
117 for oid,nbr,state in cr.fetchall():
121 res[oid][0] += nbr or 0.0
122 res[oid][1] += nbr or 0.0
124 res[oid][1] += nbr or 0.0
129 res[r] = 100.0 * res[r][0] / res[r][1]
132 def _get_order(self, cr, uid, ids, context={}):
134 for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
135 result[line.order_id.id] = True
138 def _invoiced(self, cursor, user, ids, name, arg, context=None):
140 for purchase in self.browse(cursor, user, ids, context=context):
141 if purchase.invoice_id.reconciled:
142 res[purchase.id] = purchase.invoice_id.reconciled
144 res[purchase.id] = False
148 'name': fields.char('Order Reference', size=64, required=True, select=True),
149 'origin': fields.char('Origin', size=64,
150 help="Reference of the document that generated this purchase order request."
152 'partner_ref': fields.char('Partner Ref.', size=64),
153 'date_order':fields.date('Date', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, help="Date on which this document has been created."),
154 'date_approve':fields.date('Date Approved', readonly=1),
155 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, change_default=True),
156 'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True, states={'posted':[('readonly',True)]}),
158 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', states={'posted':[('readonly',True)]},
159 help="Put an address if you want to deliver directly from the supplier to the customer." \
160 "In this case, it will remove the warehouse link and set the customer location."
162 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'posted':[('readonly',True)]}),
163 'location_id': fields.many2one('stock.location', 'Destination', required=True),
165 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, help="The pricelist sets the currency used for this purchase order. It also computes the supplier price for the selected products/quantities."),
167 'state': fields.selection([('draft', 'Request for Quotation'), ('wait', 'Waiting'), ('confirmed', 'Confirmed'), ('approved', 'Approved'),('except_picking', 'Shipping Exception'), ('except_invoice', 'Invoice Exception'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Order Status', readonly=True, help="The state of the purchase order or the quotation request. A quotation is a purchase order in a 'Draft' state. Then the order has to be confirmed by the user, the state switch to 'Confirmed'. Then the supplier must confirm the order to change the state to 'Approved'. When the purchase order is paid and received, the state becomes 'Done'. If a cancel action occurs in the invoice or in the reception of goods, the state becomes in exception.", select=True),
168 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'approved':[('readonly',True)],'done':[('readonly',True)]}),
169 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
170 'notes': fields.text('Notes'),
171 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
172 'picking_ids': fields.one2many('stock.picking', 'purchase_id', 'Picking List', readonly=True, help="This is the list of picking list that have been generated for this purchase"),
173 'shipped':fields.boolean('Received', readonly=True, select=True),
174 'shipped_rate': fields.function(_shipped_rate, method=True, string='Received', type='float'),
175 'invoiced': fields.function(_invoiced, method=True, string='Invoiced & Paid', type='boolean'),
176 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
177 'invoice_method': fields.selection([('manual','Manual'),('order','From Order'),('picking','From Picking')], 'Invoicing Control', required=True,
178 help="From Order: a draft invoice will be pre-generated based on the purchase order. The accountant " \
179 "will just have to validate this invoice for control.\n" \
180 "From Picking: a draft invoice will be pre-genearted based on validated receptions.\n" \
181 "Manual: no invoice will be pre-generated. The accountant will have to encode manually."
183 'minimum_planned_date':fields.function(_minimum_planned_date, fnct_inv=_set_minimum_planned_date, method=True,store=True, string='Planned Date', type='datetime', help="This is computed as the minimum scheduled date of all purchase order lines' products."),
184 'amount_untaxed': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Untaxed Amount',
186 'purchase.order.line': (_get_order, None, 10),
188 'amount_tax': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Taxes',
190 'purchase.order.line': (_get_order, None, 10),
192 'amount_total': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Total',
194 'purchase.order.line': (_get_order, None, 10),
196 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position')
199 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
200 'state': lambda *a: 'draft',
201 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
202 'shipped': lambda *a: 0,
203 'invoice_method': lambda *a: 'order',
204 'invoiced': lambda *a: 0,
205 'partner_address_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['default'])['default'],
206 'pricelist_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').browse(cr, uid, context['partner_id']).property_product_pricelist_purchase.id,
208 _name = "purchase.order"
209 _description = "Purchase order"
212 def unlink(self, cr, uid, ids, context=None):
213 purchase_orders = self.read(cr, uid, ids, ['state'])
215 for s in purchase_orders:
216 if s['state'] in ['draft','cancel']:
217 unlink_ids.append(s['id'])
219 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Purchase Order(s) which are in %s State!' % s['state']))
221 # TODO: temporary fix in 5.0, to remove in 5.2 when subflows support
222 # automatically sending subflow.delete upon deletion
223 wf_service = netsvc.LocalService("workflow")
224 for id in unlink_ids:
225 wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
227 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
229 def button_dummy(self, cr, uid, ids, context={}):
232 def onchange_dest_address_id(self, cr, uid, ids, adr_id):
235 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
236 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
237 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
239 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
242 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
243 return {'value':{'location_id': res, 'dest_address_id': False}}
245 def onchange_partner_id(self, cr, uid, ids, part):
247 return {'value':{'partner_address_id': False, 'fiscal_position': False}}
248 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
249 part = self.pool.get('res.partner').browse(cr, uid, part)
250 pricelist = part.property_product_pricelist_purchase.id
251 fiscal_position = part.property_account_position and part.property_account_position.id or False
252 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist, 'fiscal_position': fiscal_position}}
254 def wkf_approve_order(self, cr, uid, ids, context={}):
255 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
258 def wkf_confirm_order(self, cr, uid, ids, context={}):
259 for po in self.browse(cr, uid, ids):
260 if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
261 self.pool.get('res.partner.event').create(cr, uid, {'name':'Purchase Order: '+po.name, 'partner_id':po.partner_id.id, 'date':time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id':uid, 'partner_type':'retailer', 'probability': 1.0, 'planned_cost':po.amount_untaxed})
262 current_name = self.name_get(cr, uid, ids)[0][1]
264 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
267 def wkf_warn_buyer(self, cr, uid, ids):
268 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
269 request = pooler.get_pool(cr.dbname).get('res.request')
270 for po in self.browse(cr, uid, ids):
272 for oline in po.order_line:
273 manager = oline.product_id.product_manager
274 if manager and not (manager.id in managers):
275 managers.append(manager.id)
276 for manager_id in managers:
277 request.create(cr, uid,
278 {'name' : "Purchase amount over the limit",
280 'act_to' : manager_id,
281 'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
282 'ref_partner_id': po.partner_id.id,
283 'ref_doc1': 'purchase.order,%d' % (po.id,),
285 def inv_line_create(self, cr, uid, a, ol):
289 'price_unit': ol.price_unit or 0.0,
290 'quantity': ol.product_qty,
291 'product_id': ol.product_id.id or False,
292 'uos_id': ol.product_uom.id or False,
293 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
294 'account_analytic_id': ol.account_analytic_id.id,
297 def action_cancel_draft(self, cr, uid, ids, *args):
300 self.write(cr, uid, ids, {'state':'draft','shipped':0})
301 wf_service = netsvc.LocalService("workflow")
303 # Deleting the existing instance of workflow for PO
304 wf_service.trg_delete(uid, 'purchase.order', p_id, cr)
305 wf_service.trg_create(uid, 'purchase.order', p_id, cr)
308 def action_invoice_create(self, cr, uid, ids, *args):
310 journal_obj = self.pool.get('account.journal')
311 for o in self.browse(cr, uid, ids):
313 for ol in o.order_line:
316 a = ol.product_id.product_tmpl_id.property_account_expense.id
318 a = ol.product_id.categ_id.property_account_expense_categ.id
320 raise osv.except_osv(_('Error !'), _('There is no expense account defined for this product: "%s" (id:%d)') % (ol.product_id.name, ol.product_id.id,))
322 a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
323 fpos = o.fiscal_position or False
324 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
325 il.append(self.inv_line_create(cr, uid, a, ol))
327 a = o.partner_id.property_account_payable.id
328 journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase')], limit=1)
330 'name': o.partner_ref or o.name,
331 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
333 'type': 'in_invoice',
334 'partner_id': o.partner_id.id,
335 'currency_id': o.pricelist_id.currency_id.id,
336 'address_invoice_id': o.partner_address_id.id,
337 'address_contact_id': o.partner_address_id.id,
338 'journal_id': len(journal_ids) and journal_ids[0] or False,
341 'fiscal_position': o.partner_id.property_account_position.id,
342 'payment_term':o.partner_id.property_payment_term and o.partner_id.property_payment_term.id or False,
344 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
345 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
347 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
351 def has_stockable_product(self,cr, uid, ids, *args):
352 for order in self.browse(cr, uid, ids):
353 for order_line in order.order_line:
354 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
358 def action_cancel(self, cr, uid, ids, context={}):
360 purchase_order_line_obj = self.pool.get('purchase.order.line')
361 for purchase in self.browse(cr, uid, ids):
362 for pick in purchase.picking_ids:
363 if pick.state not in ('draft','cancel'):
364 raise osv.except_osv(
365 _('Could not cancel purchase order !'),
366 _('You must first cancel all packing attached to this purchase order.'))
367 for pick in purchase.picking_ids:
368 wf_service = netsvc.LocalService("workflow")
369 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
370 inv = purchase.invoice_id
371 if inv and inv.state not in ('cancel','draft'):
372 raise osv.except_osv(
373 _('Could not cancel this purchase order !'),
374 _('You must first cancel all invoices attached to this purchase order.'))
376 wf_service = netsvc.LocalService("workflow")
377 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
378 self.write(cr,uid,ids,{'state':'cancel'})
381 def action_picking_create(self,cr, uid, ids, *args):
383 for order in self.browse(cr, uid, ids):
384 loc_id = order.partner_id.property_stock_supplier.id
386 if order.invoice_method=='picking':
387 istate = '2binvoiced'
388 picking_id = self.pool.get('stock.picking').create(cr, uid, {
389 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
391 'address_id': order.dest_address_id.id or order.partner_address_id.id,
392 'invoice_state': istate,
393 'purchase_id': order.id,
396 for order_line in order.order_line:
397 if not order_line.product_id:
399 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
400 dest = order.location_id.id
401 move = self.pool.get('stock.move').create(cr, uid, {
402 'name': 'PO:'+order_line.name,
403 'product_id': order_line.product_id.id,
404 'product_qty': order_line.product_qty,
405 'product_uos_qty': order_line.product_qty,
406 'product_uom': order_line.product_uom.id,
407 'product_uos': order_line.product_uom.id,
408 'date_planned': order_line.date_planned,
409 'location_id': loc_id,
410 'location_dest_id': dest,
411 'picking_id': picking_id,
412 'move_dest_id': order_line.move_dest_id.id,
414 'purchase_line_id': order_line.id,
416 if order_line.move_dest_id:
417 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
418 todo_moves.append(move)
419 self.pool.get('stock.move').action_confirm(cr, uid, todo_moves)
420 self.pool.get('stock.move').force_assign(cr, uid, todo_moves)
421 wf_service = netsvc.LocalService("workflow")
422 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
424 def copy(self, cr, uid, id, default=None,context={}):
433 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
435 return super(purchase_order, self).copy(cr, uid, id, default, context)
439 class purchase_order_line(osv.osv):
440 def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
442 cur_obj=self.pool.get('res.currency')
443 for line in self.browse(cr, uid, ids):
444 cur = line.order_id.pricelist_id.currency_id
445 res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
449 'name': fields.char('Description', size=256, required=True),
450 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
451 'date_planned': fields.datetime('Scheduled date', required=True),
452 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
453 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
454 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
455 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
456 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
457 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
458 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits=(16, int(config['price_accuracy']))),
459 'notes': fields.text('Notes'),
460 'order_id': fields.many2one('purchase.order', 'Order Ref', select=True, required=True, ondelete='cascade'),
461 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
464 'product_qty': lambda *a: 1.0
466 _table = 'purchase_order_line'
467 _name = 'purchase.order.line'
468 _description = 'Purchase Order lines'
469 def copy_data(self, cr, uid, id, default=None,context={}):
472 default.update({'state':'draft', 'move_ids':[]})
473 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
475 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
476 partner_id, date_order=False, fiscal_position=False):
478 raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist in the purchase form !\nPlease set one before choosing a product.'))
480 raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
482 return {'value': {'price_unit': 0.0, 'name':'','notes':'', 'product_uom' : False}, 'domain':{'product_uom':[]}}
483 prod= self.pool.get('product.product').browse(cr, uid,product)
486 lang=self.pool.get('res.partner').read(cr, uid, partner_id)['lang']
487 context={'lang':lang}
488 context['partner_id'] = partner_id
490 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
491 prod_uom_po = prod.uom_po_id.id
495 date_order = time.strftime('%Y-%m-%d')
499 for s in prod.seller_ids:
500 if s.name.id == partner_id:
501 seller_delay = s.delay
502 temp_qty = s.qty # supplier _qty assigned to temp
503 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
506 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
507 product, qty or 1.0, partner_id, {
511 dt = (DateTime.now() + DateTime.RelativeDateTime(days=int(seller_delay) or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
512 prod_name = prod.partner_ref
515 res = {'value': {'price_unit': price, 'name':prod_name, 'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
516 'date_planned': dt,'notes':prod.description_purchase,
521 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
522 taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
523 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
524 res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
526 res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
527 res3 = prod.uom_id.category_id.id
528 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
529 if res2[0]['category_id'][0] != res3:
530 raise osv.except_osv(_('Wrong Product UOM !'), _('You have to select a product UOM in the same category than the purchase UOM of the product'))
532 res['domain'] = domain
535 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
536 partner_id, date_order=False):
537 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
538 partner_id, date_order=date_order)
539 if 'product_uom' in res['value']:
540 del res['value']['product_uom']
542 res['value']['price_unit'] = 0.0
544 purchase_order_line()
546 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: