1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2008 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_untaxed(self, cr, uid, ids, field_name, arg, context):
48 cur_obj=self.pool.get('res.currency')
49 for purchase in self.browse(cr, uid, ids):
50 res[purchase.id] = 0.0
51 for line in purchase.order_line:
52 res[purchase.id] += line.price_subtotal
53 cur = purchase.pricelist_id.currency_id
54 res[purchase.id] = cur_obj.round(cr, uid, cur, res[purchase.id])
58 def _amount_tax(self, cr, uid, ids, field_name, arg, context):
60 cur_obj=self.pool.get('res.currency')
61 for order in self.browse(cr, uid, ids):
63 cur=order.pricelist_id.currency_id
64 for line in order.order_line:
65 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):
67 res[order.id]=cur_obj.round(cr, uid, cur, val)
70 def _amount_total(self, cr, uid, ids, field_name, arg, context):
72 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context)
73 tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
74 cur_obj=self.pool.get('res.currency')
76 order=self.browse(cr, uid, [id])[0]
77 cur=order.pricelist_id.currency_id
78 res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
81 def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context):
82 if not value: return False
83 if type(ids)!=type([]):
85 for po in self.browse(cr, uid, ids, context):
86 cr.execute("""update purchase_order_line set
90 (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
93 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context):
95 purchase_obj=self.browse(cr, uid, ids, context=context)
96 for purchase in purchase_obj:
97 res[purchase.id] = False
98 if purchase.order_line:
99 min_date=purchase.order_line[0].date_planned
100 for line in purchase.order_line:
101 if line.date_planned < min_date:
102 min_date=line.date_planned
103 res[purchase.id]=min_date
106 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
108 for purchase in self.browse(cursor, user, ids, context=context):
110 if purchase.invoice_id and purchase.invoice_id.state not in ('draft','cancel'):
111 tot += purchase.invoice_id.amount_untaxed
112 if purchase.amount_untaxed:
113 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
115 res[purchase.id] = 0.0
118 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
119 if not ids: return {}
124 p.purchase_id,sum(m.product_qty), m.state
128 stock_picking p on (p.id=m.picking_id)
130 p.purchase_id in ('''+','.join(map(str,ids))+''')
131 GROUP BY m.state, p.purchase_id''')
132 for oid,nbr,state in cr.fetchall():
136 res[oid][0] += nbr or 0.0
137 res[oid][1] += nbr or 0.0
139 res[oid][1] += nbr or 0.0
144 res[r] = 100.0 * res[r][0] / res[r][1]
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.boolean('Invoiced & Paid', readonly=True, select=True),
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_untaxed, method=True, string='Untaxed Amount'),
185 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
186 'amount_total': fields.function(_amount_total, method=True, string='Total'),
189 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
190 'state': lambda *a: 'draft',
191 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
192 'shipped': lambda *a: 0,
193 'invoice_method': lambda *a: 'order',
194 'invoiced': lambda *a: 0,
195 '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'],
196 '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,
198 _name = "purchase.order"
199 _description = "Purchase order"
202 def unlink(self, cr, uid, ids):
203 purchase_orders = self.read(cr, uid, ids, ['state'])
205 for s in purchase_orders:
206 if s['state'] in ['draft','cancel']:
207 unlink_ids.append(s['id'])
209 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Purchase Order(s) which are in %s State!' % s['state']))
210 return osv.osv.unlink(self, cr, uid, unlink_ids)
212 def button_dummy(self, cr, uid, ids, context={}):
215 def onchange_dest_address_id(self, cr, uid, ids, adr_id):
218 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
219 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
220 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
222 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
225 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
226 return {'value':{'location_id': res, 'dest_address_id': False}}
228 def onchange_partner_id(self, cr, uid, ids, part):
230 return {'value':{'partner_address_id': False}}
231 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
232 part = self.pool.get('res.partner').browse(cr, uid, part)
233 pricelist = part.property_product_pricelist_purchase.id
234 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist}}
236 def wkf_approve_order(self, cr, uid, ids, context={}):
237 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
240 def wkf_confirm_order(self, cr, uid, ids, context={}):
241 for po in self.browse(cr, uid, ids):
242 if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
243 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})
244 current_name = self.name_get(cr, uid, ids)[0][1]
246 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
249 def wkf_warn_buyer(self, cr, uid, ids):
250 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
251 request = pooler.get_pool(cr.dbname).get('res.request')
252 for po in self.browse(cr, uid, ids):
254 for oline in po.order_line:
255 manager = oline.product_id.product_manager
256 if manager and not (manager.id in managers):
257 managers.append(manager.id)
258 for manager_id in managers:
259 request.create(cr, uid,
260 {'name' : "Purchase amount over the limit",
262 'act_to' : manager_id,
263 'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
264 'ref_partner_id': po.partner_id.id,
265 'ref_doc1': 'purchase.order,%d' % (po.id,),
267 def inv_line_create(self,a,ol):
271 'price_unit': ol.price_unit or 0.0,
272 'quantity': ol.product_qty,
273 'product_id': ol.product_id.id or False,
274 'uos_id': ol.product_uom.id or False,
275 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
276 'account_analytic_id': ol.account_analytic_id.id,
279 def action_cancel_draft(self, cr, uid, ids, *args):
282 self.write(cr, uid, ids, {'state':'draft','shipped':0})
283 wf_service = netsvc.LocalService("workflow")
285 wf_service.trg_create(uid, 'purchase.order', p_id, cr)
288 def action_invoice_create(self, cr, uid, ids, *args):
290 for o in self.browse(cr, uid, ids):
292 for ol in o.order_line:
295 a = ol.product_id.product_tmpl_id.property_account_expense.id
297 a = ol.product_id.categ_id.property_account_expense_categ.id
299 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,))
301 a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
302 a = self.pool.get('account.fiscal.position').map_account(cr, uid, o.partner_id, a)
303 il.append(self.inv_line_create(a,ol))
305 a = o.partner_id.property_account_payable.id
307 'name': o.partner_ref or o.name,
308 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
310 'type': 'in_invoice',
311 'partner_id': o.partner_id.id,
312 'currency_id': o.pricelist_id.currency_id.id,
313 'address_invoice_id': o.partner_address_id.id,
314 'address_contact_id': o.partner_address_id.id,
318 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
319 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
321 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
325 def has_stockable_product(self,cr, uid, ids, *args):
326 for order in self.browse(cr, uid, ids):
327 for order_line in order.order_line:
328 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
332 def action_cancel(self, cr, uid, ids, context={}):
334 purchase_order_line_obj = self.pool.get('purchase.order.line')
335 for purchase in self.browse(cr, uid, ids):
336 for pick in purchase.picking_ids:
337 if pick.state not in ('draft','cancel'):
338 raise osv.except_osv(
339 _('Could not cancel purchase order !'),
340 _('You must first cancel all packings attached to this purchase order.'))
341 for pick in purchase.picking_ids:
342 wf_service = netsvc.LocalService("workflow")
343 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_cancel', cr)
344 inv = purchase.invoice_id
345 if inv and inv.state not in ('cancel','draft'):
346 raise osv.except_osv(
347 _('Could not cancel this purchase order !'),
348 _('You must first cancel all invoices attached to this purchase order.'))
350 wf_service = netsvc.LocalService("workflow")
351 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'invoice_cancel', cr)
352 self.write(cr,uid,ids,{'state':'cancel'})
355 def action_picking_create(self,cr, uid, ids, *args):
357 for order in self.browse(cr, uid, ids):
358 loc_id = order.partner_id.property_stock_supplier.id
360 if order.invoice_method=='picking':
361 istate = '2binvoiced'
362 picking_id = self.pool.get('stock.picking').create(cr, uid, {
363 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
365 'address_id': order.dest_address_id.id or order.partner_address_id.id,
366 'invoice_state': istate,
367 'purchase_id': order.id,
369 for order_line in order.order_line:
370 if not order_line.product_id:
372 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
373 dest = order.location_id.id
374 self.pool.get('stock.move').create(cr, uid, {
375 'name': 'PO:'+order_line.name,
376 'product_id': order_line.product_id.id,
377 'product_qty': order_line.product_qty,
378 'product_uos_qty': order_line.product_qty,
379 'product_uom': order_line.product_uom.id,
380 'product_uos': order_line.product_uom.id,
381 'date_planned': order_line.date_planned,
382 'location_id': loc_id,
383 'location_dest_id': dest,
384 'picking_id': picking_id,
385 'move_dest_id': order_line.move_dest_id.id,
387 'purchase_line_id': order_line.id,
389 if order_line.move_dest_id:
390 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
391 wf_service = netsvc.LocalService("workflow")
392 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
394 def copy(self, cr, uid, id, default=None,context={}):
403 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
405 return super(purchase_order, self).copy(cr, uid, id, default, context)
409 class purchase_order_line(osv.osv):
410 def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
412 cur_obj=self.pool.get('res.currency')
413 for line in self.browse(cr, uid, ids):
414 cur = line.order_id.pricelist_id.currency_id
415 res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
419 'name': fields.char('Description', size=64, required=True),
420 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
421 'date_planned': fields.datetime('Scheduled date', required=True),
422 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
423 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
424 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
425 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
426 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
427 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
428 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
429 'notes': fields.text('Notes'),
430 'order_id': fields.many2one('purchase.order', 'Order Ref', select=True, required=True, ondelete='cascade'),
431 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
434 'product_qty': lambda *a: 1.0
436 _table = 'purchase_order_line'
437 _name = 'purchase.order.line'
438 _description = 'Purchase Order lines'
439 def copy(self, cr, uid, id, default=None,context={}):
442 default.update({'state':'draft', 'move_id':False})
443 return super(purchase_order_line, self).copy(cr, uid, id, default, context)
445 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
446 partner_id, date_order=False):
448 raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist in the purchase form !\nPlease set one before choosing a product.'))
450 return {'value': {'price_unit': 0.0, 'name':'','notes':'', 'product_uom' : False}, 'domain':{'product_uom':[]}}
451 prod= self.pool.get('product.product').browse(cr, uid,product)
454 lang=self.pool.get('res.partner').read(cr, uid, partner_id)['lang']
455 context={'lang':lang}
456 context['partner_id'] = partner_id
458 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
459 prod_uom_po = prod.uom_po_id.id
463 date_order = time.strftime('%Y-%m-%d')
464 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
465 product, qty or 1.0, partner_id, {
472 for s in prod.seller_ids:
473 seller_delay = s.delay
474 if s.name.id == partner_id:
475 seller_delay = s.delay
477 dt = (DateTime.now() + DateTime.RelativeDateTime(days=seller_delay or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
478 prod_name = prod.partner_ref
481 res = {'value': {'price_unit': price, 'name':prod_name, 'taxes_id':map(lambda x: x.id, prod.supplier_taxes_id),
482 'date_planned': dt,'notes':prod.description_purchase,
487 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
488 taxes = self.pool.get('account.tax').browse(cr, uid,map(lambda x: x.id, prod.supplier_taxes_id))
489 res['value']['taxes_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner, taxes)
491 res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
492 res3 = prod.uom_id.category_id.id
493 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
494 if res2[0]['category_id'][0] != res3:
495 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'))
497 res['domain'] = domain
500 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
501 partner_id, date_order=False):
502 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
503 partner_id, date_order=date_order)
504 if 'product_uom' in res['value']:
505 del res['value']['product_uom']
507 res['value']['price_unit'] = 0.0
509 purchase_order_line()
511 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: