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