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