1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 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 fields
28 from mx import DateTime
30 from tools import config
31 from tools.translate import _
36 class purchase_order(osv.osv):
37 def _calc_amount(self, cr, uid, ids, prop, unknow_none, unknow_dict):
39 for order in self.browse(cr, uid, ids):
41 for oline in order.order_line:
42 res[order.id] += oline.price_unit * oline.product_qty
45 def _amount_all(self, cr, uid, ids, field_name, arg, context):
47 cur_obj=self.pool.get('res.currency')
48 for order in self.browse(cr, uid, ids):
50 'amount_untaxed': 0.0,
55 cur=order.pricelist_id.currency_id
56 for line in order.order_line:
57 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):
59 val1 += line.price_subtotal
60 res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
61 res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
62 res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
65 def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context):
66 if not value: return False
67 if type(ids)!=type([]):
69 for po in self.browse(cr, uid, ids, context):
70 cr.execute("""update purchase_order_line set
74 (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
77 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context):
79 purchase_obj=self.browse(cr, uid, ids, context=context)
80 for purchase in purchase_obj:
81 res[purchase.id] = False
82 if purchase.order_line:
83 min_date=purchase.order_line[0].date_planned
84 for line in purchase.order_line:
85 if line.date_planned < min_date:
86 min_date=line.date_planned
87 res[purchase.id]=min_date
90 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
92 for purchase in self.browse(cursor, user, ids, context=context):
94 if purchase.invoice_id and purchase.invoice_id.state not in ('draft','cancel'):
95 tot += purchase.invoice_id.amount_untaxed
96 if purchase.amount_untaxed:
97 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
99 res[purchase.id] = 0.0
102 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
103 if not ids: return {}
108 p.purchase_id,sum(m.product_qty), m.state
112 stock_picking p on (p.id=m.picking_id)
114 p.purchase_id in ('''+','.join(map(str, ids))+''')
115 GROUP BY m.state, p.purchase_id''')
116 for oid,nbr,state in cr.fetchall():
120 res[oid][0] += nbr or 0.0
121 res[oid][1] += nbr or 0.0
123 res[oid][1] += nbr or 0.0
128 res[r] = 100.0 * res[r][0] / res[r][1]
131 def _get_order(self, cr, uid, ids, context={}):
133 for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context):
134 result[line.order_id.id] = True
137 def _invoiced(self, cursor, user, ids, name, arg, context=None):
139 for purchase in self.browse(cursor, user, ids, context=context):
140 if purchase.invoice_id.reconciled:
141 res[purchase.id] = purchase.invoice_id.reconciled
143 res[purchase.id] = False
147 'name': fields.char('Order Reference', size=64, required=True, select=True),
148 'origin': fields.char('Origin', size=64,
149 help="Reference of the document that generated this purchase order request."
151 'partner_ref': fields.char('Supplier Reference', size=64),
152 'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, help="Date on which this document has been created."),
153 'date_approve':fields.date('Date Approved', readonly=1),
154 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, change_default=True),
155 'partner_address_id':fields.many2one('res.partner.address', 'Supplier Address', required=True, states={'posted':[('readonly',True)]}),
157 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', states={'posted':[('readonly',True)]},
158 help="Put an address if you want to deliver directly from the supplier to the customer." \
159 "In this case, it will remove the warehouse link and set the customer location."
161 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'posted':[('readonly',True)]}),
162 'location_id': fields.many2one('stock.location', 'Destination', required=True),
164 '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."),
166 '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')], 'State', 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),
167 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'approved':[('readonly',True)]}),
168 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
169 'notes': fields.text('Notes', translate=True),
170 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
171 '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"),
172 'shipped':fields.boolean('Received', readonly=True, select=True),
173 'shipped_rate': fields.function(_shipped_rate, method=True, string='Received', type='float'),
174 'invoiced': fields.function(_invoiced, method=True, string='Invoiced & Paid', type='boolean'),
175 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
176 'invoice_method': fields.selection([('manual','Manual'),('order','From Order'),('picking','From Picking')], 'Invoicing Control', required=True,
177 help="From Order: a draft invoice will be pre-generated based on the purchase order. The accountant " \
178 "will just have to validate this invoice for control.\n" \
179 "From Picking: a draft invoice will be pre-generated based on validated receptions.\n" \
180 "Manual: no invoice will be pre-generated. The accountant will have to encode manually."
182 '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."),
183 'amount_untaxed': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Untaxed Amount',
185 'purchase.order.line': (_get_order, None, 10),
187 'amount_tax': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Taxes',
189 'purchase.order.line': (_get_order, None, 10),
191 'amount_total': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Total',
193 'purchase.order.line': (_get_order, None, 10),
195 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
196 'product_id': fields.related('order_line','product_id', type='many2one', relation='product.product', string='Product'),
197 'create_uid': fields.many2one('res.users', 'Responsible'),
198 'company_id': fields.many2one('res.company','Company',required=True,select=1),
201 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
202 'state': lambda *a: 'draft',
203 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
204 'shipped': lambda *a: 0,
205 'invoice_method': lambda *a: 'order',
206 'invoiced': lambda *a: 0,
207 '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'],
208 '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,
209 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c),
211 _name = "purchase.order"
212 _description = "Purchase order"
215 def unlink(self, cr, uid, ids, context=None):
216 purchase_orders = self.read(cr, uid, ids, ['state'])
218 for s in purchase_orders:
219 if s['state'] in ['draft','cancel']:
220 unlink_ids.append(s['id'])
222 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Purchase Order(s) which are in %s State!' % s['state']))
223 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
225 def button_dummy(self, cr, uid, ids, context={}):
228 def onchange_dest_address_id(self, cr, uid, ids, adr_id):
231 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
232 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
233 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
235 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
238 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
239 return {'value':{'location_id': res, 'dest_address_id': False}}
241 def onchange_partner_id(self, cr, uid, ids, part):
243 return {'value':{'partner_address_id': False, 'fiscal_position': False}}
244 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
245 part = self.pool.get('res.partner').browse(cr, uid, part)
246 pricelist = part.property_product_pricelist_purchase.id
247 fiscal_position = part.property_account_position and part.property_account_position.id or False
248 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist, 'fiscal_position': fiscal_position}}
250 def wkf_approve_order(self, cr, uid, ids, context={}):
251 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
254 def wkf_confirm_order(self, cr, uid, ids, context={}):
255 for po in self.browse(cr, uid, ids):
256 if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
257 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})
258 current_name = self.name_get(cr, uid, ids)[0][1]
260 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
263 def wkf_warn_buyer(self, cr, uid, ids):
264 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
265 request = pooler.get_pool(cr.dbname).get('res.request')
266 for po in self.browse(cr, uid, ids):
268 for oline in po.order_line:
269 manager = oline.product_id.product_manager
270 if manager and not (manager.id in managers):
271 managers.append(manager.id)
272 for manager_id in managers:
273 request.create(cr, uid,
274 {'name' : "Purchase amount over the limit",
276 'act_to' : manager_id,
277 'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
278 'ref_partner_id': po.partner_id.id,
279 'ref_doc1': 'purchase.order,%d' % (po.id,),
281 def inv_line_create(self, cr, uid, a, ol):
285 'price_unit': ol.price_unit or 0.0,
286 'quantity': ol.product_qty,
287 'product_id': ol.product_id.id or False,
288 'uos_id': ol.product_uom.id or False,
289 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
290 'account_analytic_id': ol.account_analytic_id.id,
293 def action_cancel_draft(self, cr, uid, ids, *args):
296 self.write(cr, uid, ids, {'state':'draft','shipped':0})
297 wf_service = netsvc.LocalService("workflow")
299 wf_service.trg_create(uid, 'purchase.order', p_id, cr)
302 def action_invoice_create(self, cr, uid, ids, *args):
304 journal_obj = self.pool.get('account.journal')
305 for o in self.browse(cr, uid, ids):
307 for ol in o.order_line:
310 a = ol.product_id.product_tmpl_id.property_account_expense.id
312 a = ol.product_id.categ_id.property_account_expense_categ.id
314 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,))
316 a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
317 fpos = o.fiscal_position or False
318 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
319 il.append(self.inv_line_create(cr, uid, a, ol))
321 a = o.partner_id.property_account_payable.id
322 journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', o.company_id.id)], limit=1)
324 raise osv.except_osv(_('Error !'),
325 _('There is no purchase journal defined for this company: "%s" (id:%d)') % (o.company_id.name, o.company_id.id))
327 'name': o.partner_ref or o.name,
328 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
330 'type': 'in_invoice',
331 'partner_id': o.partner_id.id,
332 'currency_id': o.pricelist_id.currency_id.id,
333 'address_invoice_id': o.partner_address_id.id,
334 'address_contact_id': o.partner_address_id.id,
335 'journal_id': len(journal_ids) and journal_ids[0] or False,
338 'fiscal_position': o.partner_id.property_account_position.id,
339 'payment_term': o.partner_id.property_payment_term and o.partner_id.property_payment_term.id or False,
340 'company_id': o.company_id.id,
342 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
343 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
345 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
349 def has_stockable_product(self,cr, uid, ids, *args):
350 for order in self.browse(cr, uid, ids):
351 for order_line in order.order_line:
352 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
356 def action_cancel(self, cr, uid, ids, context={}):
358 purchase_order_line_obj = self.pool.get('purchase.order.line')
359 for purchase in self.browse(cr, uid, ids):
360 for pick in purchase.picking_ids:
361 if pick.state not in ('draft','cancel'):
362 raise osv.except_osv(
363 _('Could not cancel purchase order !'),
364 _('You must first cancel all picking attached to this purchase order.'))
365 for pick in purchase.picking_ids:
366 wf_service = netsvc.LocalService("workflow")
367 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
368 inv = purchase.invoice_id
369 if inv and inv.state not in ('cancel','draft'):
370 raise osv.except_osv(
371 _('Could not cancel this purchase order !'),
372 _('You must first cancel all invoices attached to this purchase order.'))
374 wf_service = netsvc.LocalService("workflow")
375 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
376 self.write(cr,uid,ids,{'state':'cancel'})
379 def action_picking_create(self,cr, uid, ids, *args):
381 for order in self.browse(cr, uid, ids):
382 loc_id = order.partner_id.property_stock_supplier.id
384 if order.invoice_method=='picking':
385 istate = '2binvoiced'
386 picking_id = self.pool.get('stock.picking').create(cr, uid, {
387 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
389 'address_id': order.dest_address_id.id or order.partner_address_id.id,
390 'invoice_state': istate,
391 'purchase_id': order.id,
392 'company_id': order.company_id.id,
394 for order_line in order.order_line:
395 if not order_line.product_id:
397 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
398 dest = order.location_id.id
399 self.pool.get('stock.move').create(cr, uid, {
400 'name': 'PO:'+order_line.name,
401 'product_id': order_line.product_id.id,
402 'product_qty': order_line.product_qty,
403 'product_uos_qty': order_line.product_qty,
404 'product_uom': order_line.product_uom.id,
405 'product_uos': order_line.product_uom.id,
406 'date_planned': order_line.date_planned,
407 'location_id': loc_id,
408 'location_dest_id': dest,
409 'picking_id': picking_id,
410 'move_dest_id': order_line.move_dest_id.id,
412 'purchase_line_id': order_line.id,
413 'company_id': order.company_id.id,
415 if order_line.move_dest_id:
416 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
417 wf_service = netsvc.LocalService("workflow")
418 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
421 def copy(self, cr, uid, id, default=None,context={}):
430 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
432 return super(purchase_order, self).copy(cr, uid, id, default, context)
436 class purchase_order_line(osv.osv):
437 def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
439 cur_obj=self.pool.get('res.currency')
440 for line in self.browse(cr, uid, ids):
441 cur = line.order_id.pricelist_id.currency_id
442 res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
446 'name': fields.char('Description', size=256, required=True),
447 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
448 'date_planned': fields.datetime('Scheduled date', required=True),
449 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
450 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
451 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
452 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'),
453 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
454 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
455 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits=(16, int(config['price_accuracy']))),
456 'notes': fields.text('Notes', translate=True),
457 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'),
458 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
459 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company')
462 'product_qty': lambda *a: 1.0
464 _table = 'purchase_order_line'
465 _name = 'purchase.order.line'
466 _description = 'Purchase Order lines'
467 def copy_data(self, cr, uid, id, default=None,context={}):
470 default.update({'state':'draft', 'move_ids':[]})
471 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
473 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
474 partner_id, date_order=False, fiscal_position=False, date_planned=False,
475 name=False, price_unit=False, notes=False):
477 raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist in the purchase form !\nPlease set one before choosing a product.'))
479 raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
481 return {'value': {'price_unit': price_unit or 0.0, 'name': name or '',
482 'notes': notes or'', 'product_uom' : uom or 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 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
500 product, qty or 1.0, partner_id, {
507 for s in prod.seller_ids:
508 if s.name.id == partner_id:
509 seller_delay = s.delay
510 temp_qty = s.qty # supplier _qty assigned to temp
511 if qty < temp_qty: # If the supplier quantity is greater than entered from user, set minimal.
514 dt = (DateTime.now() + DateTime.RelativeDateTime(days=seller_delay or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
515 prod_name = self.pool.get('product.product').name_get(cr, uid, [prod.id])[0][1]
518 res = {'value': {'price_unit': price, 'name': name or prod_name,
519 'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
520 'date_planned': date_planned or dt,'notes': notes or prod.description_purchase,
525 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
526 taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
527 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
528 res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
530 res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
531 res3 = prod.uom_id.category_id.id
532 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
533 if res2[0]['category_id'][0] != res3:
534 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'))
536 res['domain'] = domain
539 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
540 partner_id, date_order=False):
541 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
542 partner_id, date_order=date_order)
543 if 'product_uom' in res['value']:
544 del res['value']['product_uom']
546 res['value']['price_unit'] = 0.0
548 purchase_order_line()
550 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: