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