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('Supplier Ref.', size=64),
153 'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('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)]}, change_default=True),
156 'partner_address_id':fields.many2one('res.partner.address', 'Supplier 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)]}, 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', 'Waiting Supplier Ack'), ('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)]}),
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, string='Untaxed Amount',
186 'purchase.order.line': (_get_order, None, 10),
188 'amount_tax': fields.function(_amount_all, method=True, string='Taxes',
190 'purchase.order.line': (_get_order, None, 10),
192 'amount_total': fields.function(_amount_all, method=True, 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']))
220 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
222 def button_dummy(self, cr, uid, ids, context={}):
225 def onchange_dest_address_id(self, cr, uid, ids, adr_id):
228 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
229 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
230 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
232 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
235 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
236 return {'value':{'location_id': res, 'dest_address_id': False}}
238 def onchange_partner_id(self, cr, uid, ids, part):
240 return {'value':{'partner_address_id': False, 'fiscal_position': False}}
241 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
242 part = self.pool.get('res.partner').browse(cr, uid, part)
243 pricelist = part.property_product_pricelist_purchase.id
244 fiscal_position = part.property_account_position and part.property_account_position.id or False
245 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist, 'fiscal_position': fiscal_position}}
247 def wkf_approve_order(self, cr, uid, ids, context={}):
248 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
251 def wkf_confirm_order(self, cr, uid, ids, context={}):
252 for po in self.browse(cr, uid, ids):
253 if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
254 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})
255 current_name = self.name_get(cr, uid, ids)[0][1]
257 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
260 def wkf_warn_buyer(self, cr, uid, ids):
261 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
262 request = pooler.get_pool(cr.dbname).get('res.request')
263 for po in self.browse(cr, uid, ids):
265 for oline in po.order_line:
266 manager = oline.product_id.product_manager
267 if manager and not (manager.id in managers):
268 managers.append(manager.id)
269 for manager_id in managers:
270 request.create(cr, uid,
271 {'name' : "Purchase amount over the limit",
273 'act_to' : manager_id,
274 'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
275 'ref_partner_id': po.partner_id.id,
276 'ref_doc1': 'purchase.order,%d' % (po.id,),
278 def inv_line_create(self, cr, uid, a, ol):
282 'price_unit': ol.price_unit or 0.0,
283 'quantity': ol.product_qty,
284 'product_id': ol.product_id.id or False,
285 'uos_id': ol.product_uom.id or False,
286 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
287 'account_analytic_id': ol.account_analytic_id.id,
290 def action_cancel_draft(self, cr, uid, ids, *args):
293 self.write(cr, uid, ids, {'state':'draft','shipped':0})
294 wf_service = netsvc.LocalService("workflow")
296 wf_service.trg_create(uid, 'purchase.order', p_id, cr)
299 def action_invoice_create(self, cr, uid, ids, *args):
301 journal_obj = self.pool.get('account.journal')
302 for o in self.browse(cr, uid, ids):
304 for ol in o.order_line:
307 a = ol.product_id.product_tmpl_id.property_account_expense.id
309 a = ol.product_id.categ_id.property_account_expense_categ.id
311 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,))
313 a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
314 fpos = o.fiscal_position or False
315 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
316 il.append(self.inv_line_create(cr, uid, a, ol))
318 a = o.partner_id.property_account_payable.id
319 journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase')], limit=1)
321 'name': o.partner_ref or o.name,
322 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
324 'type': 'in_invoice',
325 'partner_id': o.partner_id.id,
326 'currency_id': o.pricelist_id.currency_id.id,
327 'address_invoice_id': o.partner_address_id.id,
328 'address_contact_id': o.partner_address_id.id,
329 'journal_id': len(journal_ids) and journal_ids[0] or False,
332 'fiscal_position': o.partner_id.property_account_position.id,
333 'payment_term':o.partner_id.property_payment_term and o.partner_id.property_payment_term.id or False,
335 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
336 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
338 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
342 def has_stockable_product(self,cr, uid, ids, *args):
343 for order in self.browse(cr, uid, ids):
344 for order_line in order.order_line:
345 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
349 def action_cancel(self, cr, uid, ids, context={}):
351 purchase_order_line_obj = self.pool.get('purchase.order.line')
352 for purchase in self.browse(cr, uid, ids):
353 for pick in purchase.picking_ids:
354 if pick.state not in ('draft','cancel'):
355 raise osv.except_osv(
356 _('Could not cancel purchase order !'),
357 _('You must first cancel all packing attached to this purchase order.'))
358 for pick in purchase.picking_ids:
359 wf_service = netsvc.LocalService("workflow")
360 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
361 inv = purchase.invoice_id
362 if inv and inv.state not in ('cancel','draft'):
363 raise osv.except_osv(
364 _('Could not cancel this purchase order !'),
365 _('You must first cancel all invoices attached to this purchase order.'))
367 wf_service = netsvc.LocalService("workflow")
368 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
369 self.write(cr,uid,ids,{'state':'cancel'})
372 def action_picking_create(self,cr, uid, ids, *args):
374 for order in self.browse(cr, uid, ids):
375 loc_id = order.partner_id.property_stock_supplier.id
377 if order.invoice_method=='picking':
378 istate = '2binvoiced'
379 picking_id = self.pool.get('stock.picking').create(cr, uid, {
380 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
382 'address_id': order.dest_address_id.id or order.partner_address_id.id,
383 'invoice_state': istate,
384 'purchase_id': order.id,
386 for order_line in order.order_line:
387 if not order_line.product_id:
389 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
390 dest = order.location_id.id
391 self.pool.get('stock.move').create(cr, uid, {
392 'name': 'PO:'+order_line.name,
393 'product_id': order_line.product_id.id,
394 'product_qty': order_line.product_qty,
395 'product_uos_qty': order_line.product_qty,
396 'product_uom': order_line.product_uom.id,
397 'product_uos': order_line.product_uom.id,
398 'date_planned': order_line.date_planned,
399 'location_id': loc_id,
400 'location_dest_id': dest,
401 'picking_id': picking_id,
402 'move_dest_id': order_line.move_dest_id.id,
404 'purchase_line_id': order_line.id,
406 if order_line.move_dest_id:
407 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
408 wf_service = netsvc.LocalService("workflow")
409 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
411 def copy(self, cr, uid, id, default=None,context={}):
420 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
422 return super(purchase_order, self).copy(cr, uid, id, default, context)
426 class purchase_order_line(osv.osv):
427 def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
429 cur_obj=self.pool.get('res.currency')
430 for line in self.browse(cr, uid, ids):
431 cur = line.order_id.pricelist_id.currency_id
432 res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
436 'name': fields.char('Description', size=256, required=True),
437 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
438 'date_planned': fields.datetime('Scheduled date', required=True),
439 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
440 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
441 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
442 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
443 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
444 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
445 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
446 'notes': fields.text('Notes'),
447 'order_id': fields.many2one('purchase.order', 'Order Ref', select=True, required=True, ondelete='cascade'),
448 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
451 'product_qty': lambda *a: 1.0
453 _table = 'purchase_order_line'
454 _name = 'purchase.order.line'
455 _description = 'Purchase Order lines'
456 def copy_data(self, cr, uid, id, default=None,context={}):
459 default.update({'state':'draft', 'move_ids':[]})
460 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
462 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
463 partner_id, date_order=False, fiscal_position=False):
465 raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist in the purchase form !\nPlease set one before choosing a product.'))
467 raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
469 return {'value': {'price_unit': 0.0, 'name':'','notes':'', 'product_uom' : False}, 'domain':{'product_uom':[]}}
470 prod= self.pool.get('product.product').browse(cr, uid,product)
473 lang=self.pool.get('res.partner').read(cr, uid, partner_id)['lang']
474 context={'lang':lang}
475 context['partner_id'] = partner_id
477 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
478 prod_uom_po = prod.uom_po_id.id
482 date_order = time.strftime('%Y-%m-%d')
483 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
484 product, qty or 1.0, partner_id, {
491 for s in prod.seller_ids:
492 if s.name.id == partner_id:
493 seller_delay = s.delay
494 temp_qty = s.qty # supplier _qty assigned to temp
495 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
498 dt = (DateTime.now() + DateTime.RelativeDateTime(days=seller_delay or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
499 prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id])[0][1]
502 res = {'value': {'price_unit': price, 'name':prod_name, 'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
503 'date_planned': dt,'notes':prod.description_purchase,
508 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
509 taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
510 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
511 res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
513 res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
514 res3 = prod.uom_id.category_id.id
515 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
516 if res2[0]['category_id'][0] != res3:
517 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'))
519 res['domain'] = domain
522 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
523 partner_id, date_order=False):
524 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
525 partner_id, date_order=date_order)
526 if 'product_uom' in res['value']:
527 del res['value']['product_uom']
529 res['value']['price_unit'] = 0.0
531 purchase_order_line()
533 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: