Improve tax:
[odoo/odoo.git] / addons / purchase / purchase.py
1 ##############################################################################
2 #
3 # Copyright (c) 2004-2006 TINY SPRL. (http://tiny.be) All Rights Reserved.
4 #
5 # WARNING: This program as such is intended to be used by professional
6 # programmers who take the whole responsability of assessing all potential
7 # consequences resulting from its eventual inadequacies and bugs
8 # End users who are looking for a ready-to-use solution with commercial
9 # garantees and support are strongly adviced to contract a Free Software
10 # Service Company
11 #
12 # This program is Free Software; you can redistribute it and/or
13 # modify it under the terms of the GNU General Public License
14 # as published by the Free Software Foundation; either version 2
15 # of the License, or (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
25 #
26 ##############################################################################
27
28 from osv import fields
29 from osv import osv
30 import time
31 import netsvc
32
33 import ir
34 from mx import DateTime
35 import pooler
36 from tools import config
37
38 #
39 # Model definition
40 #
41 class purchase_order(osv.osv):
42         def _calc_amount(self, cr, uid, ids, prop, unknow_none, unknow_dict):
43                 res = {}
44                 for order in self.browse(cr, uid, ids):
45                         res[order.id] = 0
46                         for oline in order.order_line:
47                                 res[order.id] += oline.price_unit * oline.product_qty
48                 return res
49
50         def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
51                 id_set = ",".join(map(str, ids))
52                 cr.execute("SELECT s.id,COALESCE(SUM(l.price_unit*l.product_qty),0) AS amount FROM purchase_order s LEFT OUTER JOIN purchase_order_line l ON (s.id=l.order_id) WHERE s.id IN ("+id_set+") GROUP BY s.id ")
53                 res = dict(cr.fetchall())
54                 cur_obj=self.pool.get('res.currency')
55                 for id in res.keys():
56                         order=self.browse(cr, uid, [id])[0]
57                         cur=order.pricelist_id.currency_id
58                         res[id]=cur_obj.round(cr, uid, cur, res[id])
59                 return res
60
61         def _amount_tax(self, cr, uid, ids, field_name, arg, context):
62                 res = {}
63                 cur_obj=self.pool.get('res.currency')
64                 for order in self.browse(cr, uid, ids):
65                         val = 0.0
66                         cur=order.pricelist_id.currency_id
67                         for line in order.order_line:
68                                 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):
69                                         val+= cur_obj.round(cr, uid, cur, c['amount'])
70                         res[order.id]=cur_obj.round(cr, uid, cur, val)
71                 return res
72
73         def _amount_total(self, cr, uid, ids, field_name, arg, context):
74                 res = {}
75                 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context) 
76                 tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
77                 cur_obj=self.pool.get('res.currency')
78                 for id in ids:
79                         order=self.browse(cr, uid, [id])[0]
80                         cur=order.pricelist_id.currency_id
81                         res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
82                 return res
83
84         _columns = {
85                 'name': fields.char('Order Description', size=64, required=True, select=True),
86                 'origin': fields.char('Origin', size=64),
87                 'ref': fields.char('Order Reference', size=64),
88                 'partner_ref': fields.char('Partner Reference', size=64),
89                 'date_order':fields.date('Date Ordered', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
90                 'date_approve':fields.date('Date Approved'),
91                 'partner_id':fields.many2one('res.partner', 'Partner', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, change_default=True, relate=True),
92                 'partner_address_id':fields.many2one('res.partner.address', 'Address', required=True, states={'posted':[('readonly',True)]}),
93
94                 'dest_address_id':fields.many2one('res.partner.address', 'Destination Address', states={'posted':[('readonly',True)]}),
95                 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', states={'posted':[('readonly',True)]}, relate=True),
96                 'location_id': fields.many2one('stock.location', 'Delivery destination', required=True),
97                 'project_id':fields.many2one('account.analytic.account', 'Analytic Account', states={'posted':[('readonly',True)]}),
98
99                 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
100
101                 'state': fields.selection([('draft', 'Request for Quotation'), ('wait', 'Waiting'), ('confirmed', 'Confirmed'), ('approved', 'Approved'),('except_ship', '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),
102                 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order State', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
103                 'validator' : fields.many2one('res.users', 'Validated by', readonly=True),
104                 'notes': fields.text('Notes'),
105                 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
106                 '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"),
107                 'shipped':fields.boolean('Received', readonly=True, select=True),
108                 'invoiced':fields.boolean('Invoiced & Paid', readonly=True, select=True),
109                 'invoice_method': fields.selection([('manual','Manual'),('order','From order'),('picking','From picking')], 'Invoicing method', required=True),
110
111                 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
112                 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
113                 'amount_total': fields.function(_amount_total, method=True, string='Total'),
114         }
115         _defaults = {
116                 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
117                 'state': lambda *a: 'draft',
118                 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
119                 'shipped': lambda *a: 0,
120                 'invoice_method': lambda *a: 'order',
121                 'invoiced': lambda *a: 0
122         }
123         _name = "purchase.order"
124         _description = "Purchase order"
125
126         def button_dummy(self, cr, uid, ids, context={}):
127                 return True
128
129         def onchange_dest_address_id(self, cr, uid, ids, adr_id):
130                 if not adr_id:
131                         return {}
132                 part_id = self.pool.get('res.partner.address').read(cr, uid, [adr_id], ['partner_id'])[0]['partner_id'][0]
133                 loc_id = self.pool.get('res.partner').browse(cr, uid, part_id).property_stock_customer[0]
134                 return {'value':{'location_id': loc_id, 'warehouse_id': False}}
135
136         def onchange_warehouse_id(self, cr, uid, ids, warehouse_id):
137                 if not warehouse_id:
138                         return {}
139                 res = self.pool.get('stock.warehouse').read(cr, uid, [warehouse_id], ['lot_input_id'])[0]['lot_input_id'][0]
140                 return {'value':{'location_id': res, 'dest_address_id': False}}
141
142         def onchange_partner_id(self, cr, uid, ids, part):
143                 if not part:
144                         return {'value':{'partner_address_id': False}}
145                 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['default'])
146                 pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist_purchase[0]
147                 return {'value':{'partner_address_id': addr['default'], 'pricelist_id': pricelist}}
148
149         def wkf_approve_order(self, cr, uid, ids):
150                 self.write(cr, uid, ids, {'state': 'approved', 'date_approve': time.strftime('%Y-%m-%d')})
151                 return True
152
153         def wkf_confirm_order(self, cr, uid, ids, context={}):
154                 for po in self.browse(cr, uid, ids):
155                         if self.pool.get('res.partner.event.type').check(cr, uid, 'purchase_open'):
156                                 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})
157                 current_name = self.name_get(cr, uid, ids)[0][1]
158                 for id in ids:
159                         self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
160                 return True
161         
162         def wkf_warn_buyer(self, cr, uid, ids):
163                 self.write(cr, uid, ids, {'state' : 'wait', 'validator' : uid})
164                 request = pooler.get_pool(cr.dbname).get('res.request')
165                 for po in self.browse(cr, uid, ids):
166                         managers = []
167                         for oline in po.order_line:
168                                 manager = oline.product_id.product_manager
169                                 if manager and not (manager.id in managers):
170                                         managers.append(manager.id)
171                         for manager_id in managers:
172                                 request.create(cr, uid, 
173                                           {'name' : "Purchase amount over the limit",
174                                            'act_from' : uid,
175                                            'act_to' : manager_id,
176                                            'body': 'Somebody has just confirmed a purchase with an amount over the defined limit',
177                                            'ref_partner_id': po.partner_id.id,
178                                            'ref_doc1': 'purchase.order,%d' % (po.id,),
179                                            })
180
181         def action_invoice_create(self, cr, uid, ids, *args):
182                 res = False
183                 for o in self.browse(cr, uid, ids):
184                         il = []
185                         for ol in o.order_line:
186
187                                 if ol.product_id:
188                                         a = ol.product_id.product_tmpl_id.property_account_expense
189                                         if not a:
190                                                 a = ol.product_id.categ_id.property_account_expense_categ[0]
191                                         else:
192                                                 a = a[0]
193                                 else:
194                                         a = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category')
195                                 il.append((0, False, {
196                                         'name': ol.name,
197                                         'account_id': a,
198                                         'price_unit': ol.price_unit or 0.0,
199                                         'quantity': ol.product_qty,
200                                         'product_id': ol.product_id.id or False,
201                                         'uos_id': ol.product_uom.id or False,
202                                         'invoice_line_tax_id': [(6, 0, [x.id for x in ol.taxes_id])]
203                                 }))
204
205                         a = o.partner_id.property_account_payable[0]
206                         inv = {
207                                 'name': o.name,
208                                 'reference': "P%dPO%d" % (o.partner_id.id, o.id),
209                                 'account_id': a,
210                                 'type': 'in_invoice',
211                                 'partner_id': o.partner_id.id,
212                                 'currency_id': o.pricelist_id.currency_id.id,
213                                 'project_id': o.project_id.id,
214                                 'address_invoice_id': o.partner_address_id.id,
215                                 'address_contact_id': o.partner_address_id.id,
216                                 'origin': o.name,
217                                 'invoice_line': il,
218                         }
219                         inv_id = self.pool.get('account.invoice').create(cr, uid, inv)
220
221                         self.write(cr, uid, [o.id], {'invoice_id': inv_id})
222                         res = inv_id
223                 return res
224
225         def has_stockable_product(self,cr, uid, ids, *args):
226                 for order in self.browse(cr, uid, ids):
227                         for order_line in order.order_line:
228                                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
229                                         return True
230                 return False
231
232         def action_picking_create(self,cr, uid, ids, *args):
233                 picking_id = False
234                 for order in self.browse(cr, uid, ids):
235                         loc_id = order.partner_id.property_stock_supplier[0]
236                         istate = 'none'
237                         if order.invoice_method=='picking':
238                                 istate = '2binvoiced'
239                         picking_id = self.pool.get('stock.picking').create(cr, uid, {
240                                 'origin': order.name+((order.origin and (':'+order.origin)) or ''),
241                                 'type': 'in',
242                                 'address_id': order.dest_address_id.id or order.partner_address_id.id,
243                                 'invoice_state': istate,
244                                 'purchase_id': order.id,
245                         })
246                         for order_line in order.order_line:
247                                 if not order_line.product_id:
248                                         continue
249                                 if order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
250                                         dest = order.location_id.id
251                                         self.pool.get('stock.move').create(cr, uid, {
252                                                 'name': 'PO:'+order_line.name,
253                                                 'product_id': order_line.product_id.id,
254                                                 'product_qty': order_line.product_qty,
255                                                 'product_uos_qty': order_line.product_qty,
256                                                 'product_uom': order_line.product_uom.id,
257                                                 'product_uos': order_line.product_uom.id,
258                                                 'date_planned': order_line.date_planned,
259                                                 'location_id': loc_id,
260                                                 'location_dest_id': dest,
261                                                 'picking_id': picking_id,
262                                                 'move_dest_id': order_line.move_dest_id.id,
263                                                 'state': 'assigned',
264                                                 'purchase_line_id': order_line.id,
265                                         })
266                                         if order_line.move_dest_id:
267                                                 self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id})
268                         wf_service = netsvc.LocalService("workflow")
269                         wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
270                 return picking_id
271         def copy(self, cr, uid, id, default=None,context={}):
272                 if not default:
273                         default = {}
274                 default.update({
275                         'state':'draft',
276                         'shipped':False,
277                         'invoiced':False,
278                         'invoice_id':False,
279                         'picking_ids':[],
280                         'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
281                 })
282                 return super(purchase_order, self).copy(cr, uid, id, default, context)
283
284 purchase_order()
285
286 class purchase_order_line(osv.osv):
287         def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
288                 res = {}
289                 cur_obj=self.pool.get('res.currency')
290                 for line in self.browse(cr, uid, ids):
291                         cur = line.order_id.pricelist_id.currency_id
292                         res[line.id] = cur_obj.round(cr, uid, cur, line.price_unit * line.product_qty)
293                 return res
294         
295         _columns = {
296                 'name': fields.char('Description', size=64, required=True),
297                 'product_qty': fields.float('Quantity', required=True, digits=(16,2)),
298                 'date_planned': fields.date('Date Promised', required=True),
299                 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'),
300                 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
301                 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True, relate=True),
302                 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
303                 'move_dest_id': fields.many2one('stock.move', 'Reservation Destination', ondelete='set null'),
304                 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
305                 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
306                 'notes': fields.text('Notes'),
307                 'order_id': fields.many2one('purchase.order', 'Order Ref', select=True, required=True, ondelete='cascade')
308         }
309         _defaults = {
310                 'product_qty': lambda *a: 1.0
311         }
312         _table = 'purchase_order_line'
313         _name = 'purchase.order.line'
314         _description = 'Purchase Order line'
315         def copy(self, cr, uid, id, default=None,context={}):
316                 if not default:
317                         default = {}
318                 default.update({'state':'draft', 'move_id':False})
319                 return super(purchase_order_line, self).copy(cr, uid, id, default, context)
320
321         def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom, partner_id):
322                 if not pricelist:
323                         raise osv.except_osv('No Pricelist !', 'You have to select a pricelist in the sale form !\n Please set one before choosing a product.')
324                 if not product:
325                         return {'value': {'price_unit': 0.0, 'name':'','notes':''}, 'domain':{'product_uom':[]}}
326                 lang=False
327                 if partner_id:
328                         lang=self.pool.get('res.partner').read(cr, uid, [partner_id])[0]['lang']
329                 context={'lang':lang}
330                 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], product, qty or 1.0, partner_id, {'uom': uom})[pricelist]
331                 prod = self.pool.get('product.product').read(cr, uid, [product], ['supplier_taxes_id','name','seller_delay','uom_po_id','description_purchase'])[0]
332                 dt = (DateTime.now() + DateTime.RelativeDateTime(days=prod['seller_delay'] or 0.0)).strftime('%Y-%m-%d')
333                 prod_name = self.pool.get('product.product').name_get(cr, uid, [product], context=context)[0][1]
334                 res = {'value': {'price_unit': price, 'name':prod_name, 'taxes_id':prod['supplier_taxes_id'], 'date_planned': dt,'notes':prod['description_purchase']}}
335                 domain = {}
336                 if not uom:
337                         res['value']['product_uom'] = prod['uom_po_id'][0]
338                         if res['value']['product_uom']:
339                                 res2 = self.pool.get('product.uom').read(cr, uid, [res['value']['product_uom']], ['category_id'])
340                                 if res2 and res2[0]['category_id']:
341                                         domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
342                 res['domain'] = domain
343                 return res
344 purchase_order_line()