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
139 'name': fields.char('Order Reference', size=64, required=True, select=True),
140 'origin': fields.char('Origin', size=64,
141 help="Reference of the document that generated this purchase order request."
143 'partner_ref': fields.char('Partner Ref.', size=64),
144 'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
145 'date_approve':fields.date('Date Approved', readonly=1),
146 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, change_default=True),
147 'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True, states={'posted':[('readonly',True)]}),
149 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', states={'posted':[('readonly',True)]},
150 help="Put an address if you want to deliver directly from the supplier to the customer." \
151 "In this case, it will remove the warehouse link and set the customer location."
153 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'posted':[('readonly',True)]}),
154 'location_id': fields.many2one('stock.location', 'Destination', required=True),
156 '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."),
158 '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),
159 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'approved':[('readonly',True)]}),
160 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
161 'notes': fields.text('Notes'),
162 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
163 '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"),
164 'shipped':fields.boolean('Received', readonly=True, select=True),
165 'shipped_rate': fields.function(_shipped_rate, method=True, string='Received', type='float'),
166 'invoiced':fields.boolean('Invoiced & Paid', readonly=True, select=True),
167 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
168 'invoice_method': fields.selection([('manual','Manual'),('order','From Order'),('picking','From Picking')], 'Invoicing Control', required=True,
169 help="From Order: a draft invoice will be pre-generated based on the purchase order. The accountant " \
170 "will just have to validate this invoice for control.\n" \
171 "From Picking: a draft invoice will be pre-genearted based on validated receptions.\n" \
172 "Manual: no invoice will be pre-generated. The accountant will have to encode manually."
174 '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."),
175 'amount_untaxed': fields.function(_amount_all, method=True, string='Untaxed Amount',
177 'purchase.order.line': (_get_order, None, 10),
179 'amount_tax': fields.function(_amount_all, method=True, string='Taxes',
181 'purchase.order.line': (_get_order, None, 10),
183 'amount_total': fields.function(_amount_all, method=True, string='Total',
185 'purchase.order.line': (_get_order, None, 10),
187 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position')
190 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
191 'state': lambda *a: 'draft',
192 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
193 'shipped': lambda *a: 0,
194 'invoice_method': lambda *a: 'order',
195 'invoiced': lambda *a: 0,
196 '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'],
197 '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,
199 _name = "purchase.order"
200 _description = "Purchase order"
203 def unlink(self, cr, uid, ids, context=None):
204 purchase_orders = self.read(cr, uid, ids, ['state'])
206 for s in purchase_orders:
207 if s['state'] in ['draft','cancel']:
208 unlink_ids.append(s['id'])
210 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Purchase Order(s) which are in %s State!' % s['state']))
211 return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context)
213 def button_dummy(self, cr, uid, ids, context={}):
216 def onchange_dest_address_id(self, cr, uid, ids, adr_id):
219 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
220 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
221 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
223 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
226 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
227 return {'value':{'location_id': res, 'dest_address_id': False}}
229 def onchange_partner_id(self, cr, uid, ids, part):
231 return {'value':{'partner_address_id': False, 'fiscal_position': False}}
232 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
233 part = self.pool.get('res.partner').browse(cr, uid, part)
234 pricelist = part.property_product_pricelist_purchase.id
235 fiscal_position = part.property_account_position and part.property_account_position.id or False
236 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist, 'fiscal_position': fiscal_position}}
238 def wkf_approve_order(self, cr, uid, ids, context={}):
239 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
242 def wkf_confirm_order(self, cr, uid, ids, context={}):
243 for po in self.browse(cr, uid, ids):
244 if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
245 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})
246 current_name = self.name_get(cr, uid, ids)[0][1]
248 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
251 def wkf_warn_buyer(self, cr, uid, ids):
252 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
253 request = pooler.get_pool(cr.dbname).get('res.request')
254 for po in self.browse(cr, uid, ids):
256 for oline in po.order_line:
257 manager = oline.product_id.product_manager
258 if manager and not (manager.id in managers):
259 managers.append(manager.id)
260 for manager_id in managers:
261 request.create(cr, uid,
262 {'name' : "Purchase amount over the limit",
264 'act_to' : manager_id,
265 'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
266 'ref_partner_id': po.partner_id.id,
267 'ref_doc1': 'purchase.order,%d' % (po.id,),
269 def inv_line_create(self,a,ol):
273 'price_unit': ol.price_unit or 0.0,
274 'quantity': ol.product_qty,
275 'product_id': ol.product_id.id or False,
276 'uos_id': ol.product_uom.id or False,
277 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
278 'account_analytic_id': ol.account_analytic_id.id,
281 def action_cancel_draft(self, cr, uid, ids, *args):
284 self.write(cr, uid, ids, {'state':'draft','shipped':0})
285 wf_service = netsvc.LocalService("workflow")
287 wf_service.trg_create(uid, 'purchase.order', p_id, cr)
290 def action_invoice_create(self, cr, uid, ids, *args):
292 journal_obj = self.pool.get('account.journal')
293 for o in self.browse(cr, uid, ids):
295 for ol in o.order_line:
298 a = ol.product_id.product_tmpl_id.property_account_expense.id
300 a = ol.product_id.categ_id.property_account_expense_categ.id
302 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,))
304 a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
305 fpos = o.fiscal_position or False
306 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
307 il.append(self.inv_line_create(a,ol))
309 a = o.partner_id.property_account_payable.id
310 journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase')], limit=1)
312 'name': o.partner_ref or o.name,
313 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
315 'type': 'in_invoice',
316 'partner_id': o.partner_id.id,
317 'currency_id': o.pricelist_id.currency_id.id,
318 'address_invoice_id': o.partner_address_id.id,
319 'address_contact_id': o.partner_address_id.id,
320 'journal_id': len(journal_ids) and journal_ids[0] or False,
323 'fiscal_position': o.partner_id.property_account_position.id
325 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
326 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
328 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
332 def has_stockable_product(self,cr, uid, ids, *args):
333 for order in self.browse(cr, uid, ids):
334 for order_line in order.order_line:
335 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
339 def action_cancel(self, cr, uid, ids, context={}):
341 purchase_order_line_obj = self.pool.get('purchase.order.line')
342 for purchase in self.browse(cr, uid, ids):
343 for pick in purchase.picking_ids:
344 if pick.state not in ('draft','cancel'):
345 raise osv.except_osv(
346 _('Could not cancel purchase order !'),
347 _('You must first cancel all packing attached to this purchase order.'))
348 for pick in purchase.picking_ids:
349 wf_service = netsvc.LocalService("workflow")
350 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
351 inv = purchase.invoice_id
352 if inv and inv.state not in ('cancel','draft'):
353 raise osv.except_osv(
354 _('Could not cancel this purchase order !'),
355 _('You must first cancel all invoices attached to this purchase order.'))
357 wf_service = netsvc.LocalService("workflow")
358 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
359 self.write(cr,uid,ids,{'state':'cancel'})
362 def action_picking_create(self,cr, uid, ids, *args):
364 for order in self.browse(cr, uid, ids):
365 loc_id = order.partner_id.property_stock_supplier.id
367 if order.invoice_method=='picking':
368 istate = '2binvoiced'
369 picking_id = self.pool.get('stock.picking').create(cr, uid, {
370 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
372 'address_id': order.dest_address_id.id or order.partner_address_id.id,
373 'invoice_state': istate,
374 'purchase_id': order.id,
376 for order_line in order.order_line:
377 if not order_line.product_id:
379 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
380 dest = order.location_id.id
381 self.pool.get('stock.move').create(cr, uid, {
382 'name': 'PO:'+order_line.name,
383 'product_id': order_line.product_id.id,
384 'product_qty': order_line.product_qty,
385 'product_uos_qty': order_line.product_qty,
386 'product_uom': order_line.product_uom.id,
387 'product_uos': order_line.product_uom.id,
388 'date_planned': order_line.date_planned,
389 'location_id': loc_id,
390 'location_dest_id': dest,
391 'picking_id': picking_id,
392 'move_dest_id': order_line.move_dest_id.id,
394 'purchase_line_id': order_line.id,
396 if order_line.move_dest_id:
397 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
398 wf_service = netsvc.LocalService("workflow")
399 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
401 def copy(self, cr, uid, id, default=None,context={}):
410 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
412 return super(purchase_order, self).copy(cr, uid, id, default, context)
416 class purchase_order_line(osv.osv):
417 def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
419 cur_obj=self.pool.get('res.currency')
420 for line in self.browse(cr, uid, ids):
421 cur = line.order_id.pricelist_id.currency_id
422 res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
426 'name': fields.char('Description', size=64, required=True),
427 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
428 'date_planned': fields.datetime('Scheduled date', required=True),
429 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
430 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
431 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
432 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
433 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
434 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
435 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
436 'notes': fields.text('Notes'),
437 'order_id': fields.many2one('purchase.order', 'Order Ref', select=True, required=True, ondelete='cascade'),
438 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
441 'product_qty': lambda *a: 1.0
443 _table = 'purchase_order_line'
444 _name = 'purchase.order.line'
445 _description = 'Purchase Order lines'
446 def copy_data(self, cr, uid, id, default=None,context={}):
449 default.update({'state':'draft', 'move_id':False})
450 return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
452 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
453 partner_id, date_order=False, fiscal_position=False):
455 raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist in the purchase form !\nPlease set one before choosing a product.'))
457 raise osv.except_osv(_('No Partner!'), _('You have to select a partner in the purchase form !\nPlease set one partner before choosing a product.'))
459 return {'value': {'price_unit': 0.0, 'name':'','notes':'', 'product_uom' : False}, 'domain':{'product_uom':[]}}
460 prod= self.pool.get('product.product').browse(cr, uid,product)
463 lang=self.pool.get('res.partner').read(cr, uid, partner_id)['lang']
464 context={'lang':lang}
465 context['partner_id'] = partner_id
467 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
468 prod_uom_po = prod.uom_po_id.id
472 date_order = time.strftime('%Y-%m-%d')
473 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
474 product, qty or 1.0, partner_id, {
481 for s in prod.seller_ids:
482 seller_delay = s.delay
483 if s.name.id == partner_id:
484 seller_delay = s.delay
486 dt = (DateTime.now() + DateTime.RelativeDateTime(days=seller_delay or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
487 prod_name = prod.partner_ref
490 res = {'value': {'price_unit': price, 'name':prod_name, 'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
491 'date_planned': dt,'notes':prod.description_purchase,
496 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
497 taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
498 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
499 res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
501 res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
502 res3 = prod.uom_id.category_id.id
503 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
504 if res2[0]['category_id'][0] != res3:
505 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'))
507 res['domain'] = domain
510 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
511 partner_id, date_order=False):
512 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
513 partner_id, date_order=date_order)
514 if 'product_uom' in res['value']:
515 del res['value']['product_uom']
517 res['value']['price_unit'] = 0.0
519 purchase_order_line()
521 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: