Merge with openobject-addons
[odoo/odoo.git] / addons / purchase / purchase.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
5 #
6 # $Id$
7 #
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
13 # Service Company
14 #
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.
19 #
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.
24 #
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.
28 #
29 ##############################################################################
30
31 from osv import fields
32 from osv import osv
33 import time
34 import netsvc
35
36 import ir
37 from mx import DateTime
38 import pooler
39 from tools import config
40 from tools.translate import _
41
42 #
43 # Model definition
44 #
45 class purchase_order(osv.osv):
46     def _calc_amount(self, cr, uid, ids, prop, unknow_none, unknow_dict):
47         res = {}
48         for order in self.browse(cr, uid, ids):
49             res[order.id] = 0
50             for oline in order.order_line:
51                 res[order.id] += oline.price_unit * oline.product_qty
52         return res
53
54     def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
55         res = {}
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])
63
64         return res
65
66     def _amount_tax(self, cr, uid, ids, field_name, arg, context):
67         res = {}
68         cur_obj=self.pool.get('res.currency')
69         for order in self.browse(cr, uid, ids):
70             val = 0.0
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)
76         return res
77
78     def _amount_total(self, cr, uid, ids, field_name, arg, context):
79         res = {}
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')
83         for id in ids:
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))
87         return res
88
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([]):
92             ids=[ids]
93         for po in self.browse(cr, uid, ids, context):
94             cr.execute("""update purchase_order_line set
95                     date_planned=%s
96                 where
97                     order_id=%d and
98                     (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value))
99         return True
100
101     def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context):
102         res={}
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
112         return res
113
114     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
115         res = {}
116         for purchase in self.browse(cursor, user, ids, context=context):
117             tot = 0.0
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
122             else:
123                 res[purchase.id] = 0.0
124         return res
125
126     def _shipped_rate(self, cr, uid, ids, name, arg, context=None):
127         if not ids: return {}
128         res = {}
129         for id in ids:
130             res[id] = [0.0,0.0]
131         cr.execute('''SELECT
132                 p.purchase_id,sum(m.product_qty), m.state
133             FROM
134                 stock_move m
135             LEFT JOIN
136                 stock_picking p on (p.id=m.picking_id)
137             WHERE
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():
141             if state=='cancel':
142                 continue
143             if state=='done':
144                 res[oid][0] += nbr or 0.0
145                 res[oid][1] += nbr or 0.0
146             else:
147                 res[oid][1] += nbr or 0.0
148         for r in res:
149             if not res[r][1]:
150                 res[r] = 0.0
151             else:
152                 res[r] = 100.0 * res[r][0] / res[r][1]
153         return res
154
155     _columns = {
156         'name': fields.char('Order Reference', size=64, required=True, select=True),
157         'origin': fields.char('Origin', size=64),
158         'partner_ref': fields.char('Partner Ref.', size=64),
159         'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
160         'date_approve':fields.date('Date Approved'),
161         'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, change_default=True),
162         'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True, states={'posted':[('readonly',True)]}),
163
164         'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', states={'posted':[('readonly',True)]}),
165         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'posted':[('readonly',True)]}),
166         'location_id': fields.many2one('stock.location', 'Destination', required=True),
167
168         '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."),
169
170         '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),
171         'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
172         'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
173         'notes': fields.text('Notes'),
174         'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
175         '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"),
176         'shipped':fields.boolean('Received', readonly=True, select=True),
177         'shipped_rate': fields.function(_shipped_rate, method=True, string='Received', type='float'),
178         'invoiced':fields.boolean('Invoiced & Paid', readonly=True, select=True),
179         'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
180         'invoice_method': fields.selection([('manual','Manual'),('order','From order'),('picking','From picking')], 'Invoicing Control', required=True),
181         '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."),
182         'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
183         'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
184         'amount_total': fields.function(_amount_total, method=True, string='Total'),
185     }
186     _defaults = {
187         'date_order': lambda *a: time.strftime('%Y-%m-%d'),
188         'state': lambda *a: 'draft',
189         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
190         'shipped': lambda *a: 0,
191         'invoice_method': lambda *a: 'order',
192         'invoiced': lambda *a: 0,
193         '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'],
194         '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,
195     }
196     _name = "purchase.order"
197     _description = "Purchase order"
198     _order = "name desc"
199
200     def button_dummy(self, cr, uid, ids, context={}):
201         return True
202
203     def onchange_dest_address_id(self, cr, uid, ids, adr_id):
204         if not adr_id:
205             return {}
206         part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
207         loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer.id
208         return {'value':{'location_id': loc_id, 'warehouse_id': False}}
209
210     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
211         if not warehouse_id:
212             return {}
213         res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
214         return {'value':{'location_id': res, 'dest_address_id': False}}
215
216     def onchange_partner_id(self, cr, uid, ids, part):
217         if not part:
218             return {'value':{'partner_address_id': False}}
219         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
220         pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist_purchase.id
221         return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist}}
222
223     def wkf_approve_order(self, cr, uid, ids):
224         self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
225         return True
226
227     def wkf_confirm_order(self, cr, uid, ids, context={}):
228         for po in self.browse(cr, uid, ids):
229             if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
230                 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})
231         current_name = self.name_get(cr, uid, ids)[0][1]
232         for id in ids:
233             self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
234         return True
235
236     def wkf_warn_buyer(self, cr, uid, ids):
237         self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
238         request = pooler.get_pool(cr.dbname).get('res.request')
239         for po in self.browse(cr, uid, ids):
240             managers = []
241             for oline in po.order_line:
242                 manager = oline.product_id.product_manager
243                 if manager and not (manager.id in managers):
244                     managers.append(manager.id)
245             for manager_id in managers:
246                 request.create(cr, uid,
247                       {'name' : "Purchase amount over the limit",
248                        'act_from' : uid,
249                        'act_to' : manager_id,
250                        'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
251                        'ref_partner_id': po.partner_id.id,
252                        'ref_doc1': 'purchase.order,%d' % (po.id,),
253                        })
254     def inv_line_create(self,a,ol):
255         return (0, False, {
256                     'name': ol.name,
257                     'account_id': a,
258                     'price_unit': ol.price_unit or 0.0,
259                     'quantity': ol.product_qty,
260                     'product_id': ol.product_id.id or False,
261                     'uos_id': ol.product_uom.id or False,
262                     'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
263                     'account_analytic_id': ol.account_analytic_id.id,
264                 })
265
266     def action_invoice_create(self, cr, uid, ids, *args):
267         res = False
268         for o in self.browse(cr, uid, ids):
269             il = []
270             for ol in o.order_line:
271
272                 if ol.product_id:
273                     a = ol.product_id.product_tmpl_id.property_account_expense.id
274                     if not a:
275                         a = ol.product_id.categ_id.property_account_expense_categ.id
276                     if not a:
277                         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,))
278                 else:
279                     a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
280                 il.append(self.inv_line_create(a,ol))
281 #               il.append((0, False, {
282 #                   'name': ol.name,
283 #                   'account_id': a,
284 #                   'price_unit': ol.price_unit or 0.0,
285 #                   'quantity': ol.product_qty,
286 #                   'product_id': ol.product_id.id or False,
287 #                   'uos_id': ol.product_uom.id or False,
288 #                   'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])],
289 #                   'account_analytic_id': ol.account_analytic_id.id,
290 #               }))
291
292             a = o.partner_id.property_account_payable.id
293             inv = {
294                 'name': o.partner_ref or o.name,
295                 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
296                 'account_id': a,
297                 'type': 'in_invoice',
298                 'partner_id': o.partner_id.id,
299                 'currency_id': o.pricelist_id.currency_id.id,
300                 'address_invoice_id': o.partner_address_id.id,
301                 'address_contact_id': o.partner_address_id.id,
302                 'origin': o.name,
303                 'invoice_line': il,
304             }
305             inv_id = self.pool.get('account.invoice').create(cr, uid, inv, {'type':'in_invoice'})
306             self.pool.get('account.invoice').button_compute(cr, uid, [inv_id], {'type':'in_invoice'}, set_total=True)
307
308             self.write(cr, uid, [o.id], {'invoice_id': inv_id})
309             res = inv_id
310         return res
311
312     def has_stockable_product(self,cr, uid, ids, *args):
313         for order in self.browse(cr, uid, ids):
314             for order_line in order.order_line:
315                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
316                     return True
317         return False
318
319     def action_picking_create(self,cr, uid, ids, *args):
320         picking_id = False
321         for order in self.browse(cr, uid, ids):
322             loc_id = order.partner_id.property_stock_supplier.id
323             istate = 'none'
324             if order.invoice_method=='picking':
325                 istate = '2binvoiced'
326             picking_id = self.pool.get('stock.picking').create(cr, uid, {
327                 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
328                 'type': 'in',
329                 'address_id': order.dest_address_id.id or order.partner_address_id.id,
330                 'invoice_state': istate,
331                 'purchase_id': order.id,
332             })
333             for order_line in order.order_line:
334                 if not order_line.product_id:
335                     continue
336                 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
337                     dest = order.location_id.id
338                     self.pool.get('stock.move').create(cr, uid, {
339                         'name': 'PO:'+order_line.name,
340                         'product_id': order_line.product_id.id,
341                         'product_qty': order_line.product_qty,
342                         'product_uos_qty': order_line.product_qty,
343                         'product_uom': order_line.product_uom.id,
344                         'product_uos': order_line.product_uom.id,
345                         'date_planned': order_line.date_planned,
346                         'location_id': loc_id,
347                         'location_dest_id': dest,
348                         'picking_id': picking_id,
349                         'move_dest_id': order_line.move_dest_id.id,
350                         'state': 'assigned',
351                         'purchase_line_id': order_line.id,
352                     })
353                     if order_line.move_dest_id:
354                         self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
355             wf_service = netsvc.LocalService("workflow")
356             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
357         return picking_id
358     def copy(self, cr, uid, id, default=None,context={}):
359         if not default:
360             default = {}
361         default.update({
362             'state':'draft',
363             'shipped':False,
364             'invoiced':False,
365             'invoice_id':False,
366             'picking_ids':[],
367             'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
368         })
369         return super(purchase_order, self).copy(cr, uid, id, default, context)
370
371 purchase_order()
372
373 class purchase_order_line(osv.osv):
374     def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
375         res = {}
376         cur_obj=self.pool.get('res.currency')
377         for line in self.browse(cr, uid, ids):
378             cur = line.order_id.pricelist_id.currency_id
379             res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
380         return res
381
382     _columns = {
383         'name': fields.char('Description', size=64, required=True),
384         'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
385         'date_planned': fields.datetime('Scheduled date', required=True),
386         'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
387         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
388         'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True),
389         'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
390         'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
391         'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
392         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
393         'notes': fields.text('Notes'),
394         'order_id': fields.many2one('purchase.order', 'Order Ref', select=True, required=True, ondelete='cascade'),
395         'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',),
396     }
397     _defaults = {
398         'product_qty': lambda *a: 1.0
399     }
400     _table = 'purchase_order_line'
401     _name = 'purchase.order.line'
402     _description = 'Purchase Order lines'
403     def copy(self, cr, uid, id, default=None,context={}):
404         if not default:
405             default = {}
406         default.update({'state':'draft', 'move_id':False})
407         return super(purchase_order_line, self).copy(cr, uid, id, default, context)
408
409     def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
410             partner_id, date_order=False):
411         if not pricelist:
412             raise osv.except_osv(_('No Pricelist !'), _('You have to select a pricelist in the purchase form !\nPlease set one before choosing a product.'))
413         if not product:
414             return {'value': {'price_unit': 0.0, 'name':'','notes':'', 'product_uom' : False}, 'domain':{'product_uom':[]}}
415         lang=False
416         if partner_id:
417             lang=self.pool.get('res.partner').read(cr, uid, [partner_id])[0]['lang']
418         context={'lang':lang}
419
420         prod = self.pool.get('product.product').read(cr, uid, [product], ['supplier_taxes_id','name','seller_delay','uom_po_id','description_purchase'])[0]
421         prod_uom_po = prod['uom_po_id'][0]
422         if not uom:
423             uom = prod_uom_po
424         if not date_order:
425             date_order = time.strftime('%Y-%m-%d')
426         price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist],
427                 product, qty or 1.0, partner_id, {
428                     'uom': uom,
429                     'date': date_order,
430                     })[pricelist]
431         dt = (DateTime.now() + DateTime.RelativeDateTime(days=prod['seller_delay'] or 0.0)).strftime('%Y-%m-%d %H:%M:%S')
432         prod_name = self.pool.get('product.product').name_get(cr, uid, [product], context=context)[0][1]
433
434         res = {'value': {'price_unit': price, 'name':prod_name, 'taxes_id':prod['supplier_taxes_id'], 'date_planned': dt,'notes':prod['description_purchase'], 'product_uom': uom}}
435         domain = {}
436
437         if res['value']['taxes_id']:
438             taxes = self.pool.get('account.tax').browse(cr, uid,
439                     [x.id for x in product.supplier_taxes_id])
440             taxep = None
441             if partner_id:
442                 taxep = self.pool.get('res.partner').browse(cr, uid,
443                         partner_id).property_account_supplier_tax
444             if not taxep or not taxep.id:
445                 res['value']['taxes_id'] = [x.id for x in product.taxes_id]
446             else:
447                 res5 = [taxep.id]
448                 for t in taxes:
449                     if not t.tax_group==taxep.tax_group:
450                         res5.append(t.id)
451                 res['value']['taxes_id'] = res5
452
453         res2 = self.pool.get('product.uom').read(cr, uid, [uom], ['category_id'])
454         res3 = self.pool.get('product.uom').read(cr, uid, [prod_uom_po], ['category_id'])
455         domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
456         if res2[0]['category_id'] != res3[0]['category_id']:
457             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'))
458
459         res['domain'] = domain
460         return res
461
462     def product_uom_change(self, cr, uid, ids, pricelist, product, qty, uom,
463             partner_id, date_order=False):
464         res = self.product_id_change(cr, uid, ids, pricelist, product, qty, uom,
465                 partner_id, date_order=date_order)
466         if 'product_uom' in res['value']:
467             del res['value']['product_uom']
468         if not uom:
469             res['value']['price_unit'] = 0.0
470         return res
471 purchase_order_line()
472
473 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
474