1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29 ##############################################################################
31 from osv import fields
37 from mx import DateTime
39 from tools import config
40 from tools.translate import _
45 class purchase_order(osv.osv):
46 def _calc_amount(self, cr, uid, ids, prop, unknow_none, unknow_dict):
48 for order in self.browse(cr, uid, ids):
50 for oline in order.order_line:
51 res[order.id] += oline.price_unit * oline.product_qty
54 def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
56 cur_obj=self.pool.get('res.currency')
57 for purchase in self.browse(cr, uid, ids):
58 res[purchase.id] = 0.0
59 for line in purchase.order_line:
60 res[purchase.id] += line.price_subtotal
61 cur = purchase.pricelist_id.currency_id
62 res[purchase.id] = cur_obj.round(cr, uid, cur, res[purchase.id])
66 def _amount_tax(self, cr, uid, ids, field_name, arg, context):
68 cur_obj=self.pool.get('res.currency')
69 for order in self.browse(cr, uid, ids):
71 cur=order.pricelist_id.currency_id
72 for line in order.order_line:
73 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):
74 val+= cur_obj.round(cr, uid, cur, c['amount'])
75 res[order.id]=cur_obj.round(cr, uid, cur, val)
78 def _amount_total(self, cr, uid, ids, field_name, arg, context):
80 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context)
81 tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
82 cur_obj=self.pool.get('res.currency')
84 order=self.browse(cr, uid, [id])[0]
85 cur=order.pricelist_id.currency_id
86 res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
89 def _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context):
90 if not value: return False
91 if type(ids)!=type([]):
93 for po in self.browse(cr, uid, ids, context):
94 cr.execute("""update purchase_order_line set
98 (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
101 def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context):
103 purchase_obj=self.browse(cr, uid, ids, context=context)
104 for purchase in purchase_obj:
105 res[purchase.id] = False
106 if purchase.order_line:
107 min_date=purchase.order_line[0].date_planned
108 for line in purchase.order_line:
109 if line.date_planned < min_date:
110 min_date=line.date_planned
111 res[purchase.id]=min_date
114 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
116 for purchase in self.browse(cursor, user, ids, context=context):
118 if purchase.invoice_id and purchase.invoice_id.state not in ('draft','cancel'):
119 tot += purchase.invoice_id.amount_untaxed
120 if purchase.amount_untaxed:
121 res[purchase.id] = tot * 100.0 / purchase.amount_untaxed
123 res[purchase.id] = 0.0
126 def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
127 if not ids: return {}
132 p.purchase_id,sum(m.product_qty), m.state
136 stock_picking p on (p.id=m.picking_id)
138 p.purchase_id in ('''+','.join(map(str,ids))+''')
139 GROUP BY m.state, p.purchase_id''')
140 for oid,nbr,state in cr.fetchall():
144 res[oid][0] += nbr or 0.0
145 res[oid][1] += nbr or 0.0
147 res[oid][1] += nbr or 0.0
152 res[r] = 100.0 * res[r][0] / res[r][1]
156 'name': fields.char('Order Reference', size=64, required=True, select=True),
157 'origin': fields.char('Origin', size=64,
158 help="Reference of the document that generated this purchase order request."
160 'partner_ref': fields.char('Partner Ref.', size=64),
161 'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
162 'date_approve':fields.date('Date Approved', readonly=1),
163 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, change_default=True),
164 'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True, states={'posted':[('readonly',True)]}),
166 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', states={'posted':[('readonly',True)]},
167 help="Put an address if you want to deliver directly from the supplier to the customer." \
168 "In this case, it will remove the warehouse link and set the customer location."
170 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'posted':[('readonly',True)]}),
171 'location_id': fields.many2one('stock.location', 'Destination', required=True),
173 '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."),
175 '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),
176 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
177 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
178 'notes': fields.text('Notes'),
179 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
180 '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"),
181 'shipped':fields.boolean('Received', readonly=True, select=True),
182 'shipped_rate': fields.function(_shipped_rate, method=True, string='Received', type='float'),
183 'invoiced':fields.boolean('Invoiced & Paid', readonly=True, select=True),
184 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
185 'invoice_method': fields.selection([('manual','Manual'),('order','From Order'),('picking','From Picking')], 'Invoicing Control', required=True,
186 help="From Order: a draft invoice will be pre-generated based on the purchase order. The accountant " \
187 "will just have to validate this invoice for control.\n" \
188 "From Picking: a draft invoice will be pre-genearted based on validated receptions.\n" \
189 "Manual: no invoice will be pre-generated. The accountant will have to encode manually."
191 '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."),
192 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
193 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
194 'amount_total': fields.function(_amount_total, method=True, string='Total'),
197 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
198 'state': lambda *a: 'draft',
199 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
200 'shipped': lambda *a: 0,
201 'invoice_method': lambda *a: 'order',
202 'invoiced': lambda *a: 0,
203 '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'],
204 '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,
206 _name = "purchase.order"
207 _description = "Purchase order"
210 def unlink(self, cr, uid, ids):
211 purchase_orders = self.read(cr, uid, ids, ['state'])
213 for s in purchase_orders:
214 if s['state'] in ['draft','cancel']:
215 unlink_ids.append(s['id'])
217 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Purchase Order(s) which are in %s State!' % s['state']))
218 osv.osv.unlink(self, cr, uid, unlink_ids)
221 def button_dummy(self, cr, uid, ids, context={}):
224 def onchange_dest_address_id(self, cr, uid, ids, adr_id):
227 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
228 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
229 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
231 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
234 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
235 return {'value':{'location_id': res, 'dest_address_id': False}}
237 def onchange_partner_id(self, cr, uid, ids, part):
239 return {'value':{'partner_address_id': False}}
240 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
241 pricelist = self.pool.get('res.partner').property_get(cr, uid,
242 part,property_pref=['property_product_pricelist_purchase']).get('property_product_pricelist_purchase',False)
243 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist}}
245 def wkf_approve_order(self, cr, uid, ids):
246 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
249 def wkf_confirm_order(self, cr, uid, ids, context={}):
250 for po in self.browse(cr, uid, ids):
251 if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
252 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})
253 current_name = self.name_get(cr, uid, ids)[0][1]
255 self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
258 def wkf_warn_buyer(self, cr, uid, ids):
259 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
260 request = pooler.get_pool(cr.dbname).get('res.request')
261 for po in self.browse(cr, uid, ids):
263 for oline in po.order_line:
264 manager = oline.product_id.product_manager
265 if manager and not (manager.id in managers):
266 managers.append(manager.id)
267 for manager_id in managers:
268 request.create(cr, uid,
269 {'name' : "Purchase amount over the limit",
271 'act_to' : manager_id,
272 'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
273 'ref_partner_id': po.partner_id.id,
274 'ref_doc1': 'purchase.order,%d' % (po.id,),
276 def inv_line_create(self,a,ol):
280 'price_unit': ol.price_unit or 0.0,
281 'quantity': ol.product_qty,
282 'product_id': ol.product_id.id or False,
283 'uos_id': ol.product_uom.id or False,
284 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
285 'account_analytic_id': ol.account_analytic_id.id,
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 il.append(self.inv_line_create(a,ol))
303 # il.append((0, False, {
306 # 'price_unit': ol.price_unit or 0.0,
307 # 'quantity': ol.product_qty,
308 # 'product_id': ol.product_id.id or False,
309 # 'uos_id': ol.product_uom.id or False,
310 # 'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
311 # 'account_analytic_id': ol.account_analytic_id.id,
314 a = o.partner_id.property_account_payable.id
316 'name': o.partner_ref or o.name,
317 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
319 'type': 'in_invoice',
320 'partner_id': o.partner_id.id,
321 'currency_id': o.pricelist_id.currency_id.id,
322 'address_invoice_id': o.partner_address_id.id,
323 'address_contact_id': o.partner_address_id.id,
327 inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
328 self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
330 self.write(cr, uid, [o.id], {'invoice_id': inv_id})
334 def has_stockable_product(self,cr, uid, ids, *args):
335 for order in self.browse(cr, uid, ids):
336 for order_line in order.order_line:
337 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
341 def action_picking_create(self,cr, uid, ids, *args):
343 for order in self.browse(cr, uid, ids):
344 loc_id = order.partner_id.property_stock_supplier.id
346 if order.invoice_method=='picking':
347 istate = '2binvoiced'
348 picking_id = self.pool.get('stock.picking').create(cr, uid, {
349 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
351 'address_id': order.dest_address_id.id or order.partner_address_id.id,
352 'invoice_state': istate,
353 'purchase_id': order.id,
355 for order_line in order.order_line:
356 if not order_line.product_id:
358 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
359 dest = order.location_id.id
360 self.pool.get('stock.move').create(cr, uid, {
361 'name': 'PO:'+order_line.name,
362 'product_id': order_line.product_id.id,
363 'product_qty': order_line.product_qty,
364 'product_uos_qty': order_line.product_qty,
365 'product_uom': order_line.product_uom.id,
366 'product_uos': order_line.product_uom.id,
367 'date_planned': order_line.date_planned,
368 'location_id': loc_id,
369 'location_dest_id': dest,
370 'picking_id': picking_id,
371 'move_dest_id': order_line.move_dest_id.id,
373 'purchase_line_id': order_line.id,
375 if order_line.move_dest_id:
376 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
377 wf_service = netsvc.LocalService("workflow")
378 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
380 def copy(self, cr, uid, id, default=None,context={}):
389 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
391 return super(purchase_order, self).copy(cr, uid, id, default, context)
395 class purchase_order_line(osv.osv):
396 def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
398 cur_obj=self.pool.get('res.currency')
399 for line in self.browse(cr, uid, ids):
400 cur = line.order_id.pricelist_id.currency_id
401 res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
405 'name': fields.char('Description', size=64, required=True),
406 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
407 'date_planned': fields.datetime('Scheduled date', required=True),
408 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
409 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
410 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
411 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
412 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
413 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
414 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
415 'notes': fields.text('Notes'),
416 'order_id': fields.many2one('purchase.order', 'Order Ref', select=True, required=True, ondelete='cascade'),
417 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
420 'product_qty': lambda *a: 1.0
422 _table = 'purchase_order_line'
423 _name = 'purchase.order.line'
424 _description = 'Purchase Order lines'
425 def copy(self, cr, uid, id, default=None,context={}):
428 default.update({'state':'draft', 'move_id':False})
429 return super(purchase_order_line, self).copy(cr, uid, id, default, context)
431 def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
432 partner_id, date_order=False):
433 prod= self.pool.get('product.product').browse(cr, uid,product)
435 raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist in the purchase form !\nPlease set one before choosing a product.'))
437 return {'value': {'price_unit': 0.0, 'name':'','notes':'', 'product_uom' : False}, 'domain':{'product_uom':[]}}
440 lang=self.pool.get('res.partner').read(cr, uid, partner_id)['lang']
441 context={'lang':lang}
443 prod = self.pool.get('product.product').read(cr, uid, product, ['supplier_taxes_id','name','seller_delay','uom_po_id','description_purchase'],context=context)
444 prod_uom_po = prod['uom_po_id'][0]
448 date_order = time.strftime('%Y-%m-%d')
449 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
450 product, qty or 1.0, partner_id, {
454 dt = (DateTime.now() + DateTime.RelativeDateTime(days=prod['seller_delay'] or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
455 prod_name = self.pool.get('product.product').name_get(cr, uid, [product], context=context)[0][1]
457 res = {'value': {'price_unit': price, 'name':prod_name, 'taxes_id':prod['supplier_taxes_id'], 'date_planned': dt,'notes':prod['description_purchase'], 'product_uom': uom}}
461 taxes = self.pool.get('account.tax').browse(cr, uid,prod['supplier_taxes_id'])
464 taxep_id = self.pool.get('res.partner').property_get(cr, uid,partner_id,property_pref=['property_account_supplier_tax']).get('property_account_supplier_tax',False)
466 taxep=self.pool.get('account.tax').browse(cr, uid,taxep_id)
467 if not taxep or not taxep.id:
468 res['value']['taxes_id'] = [x.id for x in product.taxes_id]
472 if not t.tax_group==taxep.tax_group:
474 res['value']['taxes_id'] = res5
476 res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
477 res3 = self.pool.get('product.uom').read(cr, uid, [prod_uom_po], ['category_id'])
478 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
479 if res2[0]['category_id'] != res3[0]['category_id']:
480 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'))
482 res['domain'] = domain
485 def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
486 partner_id, date_order=False):
487 res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
488 partner_id, date_order=date_order)
489 if 'product_uom' in res['value']:
490 del res['value']['product_uom']
492 res['value']['price_unit'] = 0.0
494 purchase_order_line()
496 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: