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 Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
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', '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', '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)]}),
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,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(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
334 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
335 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
337 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
341 def has_stockable_product(self,cr, uid, ids, *args):
342 for order in self.browse(cr, uid, ids):
343 for order_line in order.order_line:
344 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
348 def action_cancel(self, cr, uid, ids, context={}):
350 purchase_order_line_obj = self.pool.get('purchase.order.line')
351 for purchase in self.browse(cr, uid, ids):
352 for pick in purchase.picking_ids:
353 if pick.state not in ('draft','cancel'):
354 raise osv.except_osv(
355 _('Could not cancel purchase order !'),
356 _('You must first cancel all packing attached to this purchase order.'))
357 for pick in purchase.picking_ids:
358 wf_service = netsvc.LocalService("workflow")
359 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
360 inv = purchase.invoice_id
361 if inv and inv.state not in ('cancel','draft'):
362 raise osv.except_osv(
363 _('Could not cancel this purchase order !'),
364 _('You must first cancel all invoices attached to this purchase order.'))
366 wf_service = netsvc.LocalService("workflow")
367 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
368 self.write(cr,uid,ids,{'state':'cancel'})
371 def action_picking_create(self,cr, uid, ids, *args):
373 for order in self.browse(cr, uid, ids):
374 loc_id = order.partner_id.property_stock_supplier.id
376 if order.invoice_method=='picking':
377 istate = '2binvoiced'
378 picking_id = self.pool.get('stock.picking').create(cr, uid, {
379 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
381 'address_id': order.dest_address_id.id or order.partner_address_id.id,
382 'invoice_state': istate,
383 'purchase_id': order.id,
385 for order_line in order.order_line:
386 if not order_line.product_id:
388 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
389 dest = order.location_id.id
390 self.pool.get('stock.move').create(cr, uid, {
391 'name': 'PO:'+order_line.name,
392 'product_id': order_line.product_id.id,
393 'product_qty': order_line.product_qty,
394 'product_uos_qty': order_line.product_qty,
395 'product_uom': order_line.product_uom.id,
396 'product_uos': order_line.product_uom.id,
397 'date_planned': order_line.date_planned,
398 'location_id': loc_id,
399 'location_dest_id': dest,
400 'picking_id': picking_id,
401 'move_dest_id': order_line.move_dest_id.id,
403 'purchase_line_id': order_line.id,
405 if order_line.move_dest_id:
406 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
407 wf_service = netsvc.LocalService("workflow")
408 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
410 def copy(self, cr, uid, id, default=None,context={}):
419 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
421 return super(purchase_order, self).copy(cr, uid, id, default, context)
425 class purchase_order_line(osv.osv):
426 def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
428 cur_obj=self.pool.get('res.currency')
429 for line in self.browse(cr, uid, ids):
430 cur = line.order_id.pricelist_id.currency_id
431 res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
435 'name': fields.char('Description', size=64, required=True),
436 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
437 'date_planned': fields.datetime('Scheduled date', required=True),
438 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
439 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
440 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
441 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
442 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
443 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
444 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
445 'notes': fields.text('Notes'),
446 'order_id': fields.many2one('purchase.order', 'Order Ref', select=True, required=True, ondelete='cascade'),
447 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
450 'product_qty': lambda *a: 1.0
452 _table = 'purchase_order_line'
453 _name = 'purchase.order.line'
454 _description = 'Purchase Order lines'
455 def copy_data(self, cr, uid, id, default=None,context={}):
458 default.update({'state':'draft', 'move_id':False})
459 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
461 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
462 partner_id, date_order=False, fiscal_position=False):
464 raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist in the purchase form !\nPlease set one before choosing a product.'))
466 raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
468 return {'value': {'price_unit': 0.0, 'name':'','notes':'', 'product_uom' : False}, 'domain':{'product_uom':[]}}
469 prod= self.pool.get('product.product').browse(cr, uid,product)
472 lang=self.pool.get('res.partner').read(cr, uid, partner_id)['lang']
473 context={'lang':lang}
474 context['partner_id'] = partner_id
476 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
477 prod_uom_po = prod.uom_po_id.id
481 date_order = time.strftime('%Y-%m-%d')
482 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
483 product, qty or 1.0, partner_id, {
490 for s in prod.seller_ids:
491 seller_delay = s.delay
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 = prod.partner_ref
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: