*bugfixed: the customer tax and supplier tax were inversed in the product_id_change...
[odoo/odoo.git] / addons / account / invoice.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 import time
32 import netsvc
33 from osv import fields, osv
34 import ir
35 import pooler
36 import mx.DateTime
37 from mx.DateTime import RelativeDateTime
38 from tools import config
39 from tools.translate import _
40
41 class account_invoice(osv.osv):
42     def _amount_untaxed(self, cr, uid, ids, name, args, context={}):
43         id_set=",".join(map(str,ids))
44         cr.execute("SELECT s.id,COALESCE(SUM(l.price_subtotal),0)::decimal(16,2) AS amount FROM account_invoice s LEFT OUTER JOIN account_invoice_line l ON (s.id=l.invoice_id) WHERE s.id IN ("+id_set+") GROUP BY s.id ")
45         res=dict(cr.fetchall())
46         return res
47
48     def _amount_tax(self, cr, uid, ids, name, args, context={}):
49         id_set=",".join(map(str,ids))
50         cr.execute("SELECT s.id,COALESCE(SUM(l.amount),0)::decimal(16,2) AS amount FROM account_invoice s LEFT OUTER JOIN account_invoice_tax l ON (s.id=l.invoice_id) WHERE s.id IN ("+id_set+") GROUP BY s.id ")
51         res=dict(cr.fetchall())
52         return res
53
54     def _amount_total(self, cr, uid, ids, name, args, context={}):
55         untax = self._amount_untaxed(cr, uid, ids, name, args, context)
56         tax = self._amount_tax(cr, uid, ids, name, args, context)
57         res = {}
58         for id in ids:
59             res[id] = untax.get(id,0.0) + tax.get(id,0.0)
60         return res
61
62     def _get_journal(self, cr, uid, context):
63         type_inv = context.get('type', 'out_invoice')
64         type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale', 'in_refund': 'purchase'}
65         journal_obj = self.pool.get('account.journal')
66         res = journal_obj.search(cr, uid, [('type', '=', type2journal.get(type_inv, 'sale'))], limit=1)
67         if res:
68             return res[0]
69         else:
70             return False
71
72     def _get_currency(self, cr, uid, context):
73         user = pooler.get_pool(cr.dbname).get('res.users').browse(cr, uid, [uid])[0]
74         if user.company_id:
75             return user.company_id.currency_id.id
76         else:
77             return pooler.get_pool(cr.dbname).get('res.currency').search(cr, uid, [('rate','=',1.0)])[0]
78
79     def _get_journal_analytic(self, cr, uid, type_inv, context={}):
80         type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale', 'in_refund': 'purchase'}
81         tt = type2journal.get(type_inv, 'sale')
82         cr.execute("select id from account_analytic_journal where type=%s limit 1", (tt,))
83         result = cr.fetchone()
84         if not result:
85             raise osv.except_osv(_('No Analytic Journal !'),("You have to define an analytic journal of type '%s' !") % (tt,))
86         return result[0]
87
88     def _get_type(self, cr, uid, context={}):
89         type = context.get('type', 'out_invoice')
90         return type
91
92     def _reconciled(self, cr, uid, ids, name, args, context):
93         res = {}
94         for id in ids:
95             res[id] = self.test_paid(cr, uid, [id])
96         return res
97
98     def _get_reference_type(self, cursor, user, context=None):
99         return [('none', 'Free Reference')]
100
101     _name = "account.invoice"
102     _description = 'Invoice'
103     _order = "number"
104     _columns = {
105         'name': fields.char('Description', size=64, select=True,readonly=True, states={'draft':[('readonly',False)]}),
106         'origin': fields.char('Origin', size=64),
107         'type': fields.selection([
108             ('out_invoice','Customer Invoice'),
109             ('in_invoice','Supplier Invoice'),
110             ('out_refund','Customer Refund'),
111             ('in_refund','Supplier Refund'),
112             ],'Type', readonly=True, select=True),
113
114         'number': fields.char('Invoice Number', size=32, readonly=True),
115         'reference': fields.char('Invoice Reference', size=64),
116         'reference_type': fields.selection(_get_reference_type, 'Reference Type',
117             required=True),
118         'comment': fields.text('Additionnal Information'),
119
120         'state': fields.selection([
121             ('draft','Draft'),
122             ('proforma','Pro-forma'),
123             ('open','Open'),
124             ('paid','Done'),
125             ('cancel','Canceled')
126         ],'State', select=True, readonly=True),
127
128         'date_invoice': fields.date('Date Invoiced', required=True, states={'open':[('readonly',True)],'close':[('readonly',True)]}),
129         'date_due': fields.date('Due Date', states={'open':[('readonly',True)],'close':[('readonly',True)]}),
130
131         'partner_id': fields.many2one('res.partner', 'Partner', change_default=True, readonly=True, required=True, states={'draft':[('readonly',False)]}),
132         'address_contact_id': fields.many2one('res.partner.address', 'Contact Address', readonly=True, states={'draft':[('readonly',False)]}),
133         'address_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft':[('readonly',False)]}),
134
135         'payment_term': fields.many2one('account.payment.term', 'Payment Term',readonly=True, states={'draft':[('readonly',False)]} ),
136
137         'period_id': fields.many2one('account.period', 'Force Period', help="Keep empty to use the period of the validation date."),
138
139         'account_id': fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}),
140         'invoice_line': fields.one2many('account.invoice.line', 'invoice_id', 'Invoice Lines', readonly=True, states={'draft':[('readonly',False)]}),
141         'tax_line': fields.one2many('account.invoice.tax', 'invoice_id', 'Tax Lines', readonly=True, states={'draft':[('readonly',False)]}),
142
143         'move_id': fields.many2one('account.move', 'Invoice Movement', readonly=True),
144         'amount_untaxed': fields.function(_amount_untaxed, method=True, digits=(16,2),string='Untaxed', store=True),
145         'amount_tax': fields.function(_amount_tax, method=True, digits=(16,2), string='Tax', store=True),
146         'amount_total': fields.function(_amount_total, method=True, digits=(16,2), string='Total', store=True),
147         'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}),
148         'journal_id': fields.many2one('account.journal', 'Journal', required=True,readonly=True, states={'draft':[('readonly',False)]}),
149         'company_id': fields.many2one('res.company', 'Company', required=True),
150         'check_total': fields.float('Total', digits=(16,2), states={'open':[('readonly',True)],'close':[('readonly',True)]}),
151         'reconciled': fields.function(_reconciled, method=True, string='Paid/Reconciled', type='boolean'),
152         'partner_bank': fields.many2one('res.partner.bank', 'Bank Account',
153             help='The bank account to pay to or to be paid from'),
154     }
155     _defaults = {
156         'type': _get_type,
157         'date_invoice': lambda *a: time.strftime('%Y-%m-%d'),
158         'state': lambda *a: 'draft',
159         'journal_id': _get_journal,
160         'currency_id': _get_currency,
161         'company_id': lambda self, cr, uid, context: \
162                 self.pool.get('res.users').browse(cr, uid, uid,
163                     context=context).company_id.id,
164         'reference_type': lambda *a: 'none',
165     }
166
167     def unlink(self, cr, uid, ids):
168         invoices = self.read(cr, uid, ids, ['state'])
169         unlink_ids = []
170         for t in invoices:
171             if t['state'] in ('draft', 'cancel'):
172                 unlink_ids.append(t['id'])
173             else:
174                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete invoice(s) which are already opened or paid !'))
175         osv.osv.unlink(self, cr, uid, unlink_ids)
176         return True
177
178 #   def get_invoice_address(self, cr, uid, ids):
179 #       res = self.pool.get('res.partner').address_get(cr, uid, [part], ['invoice'])
180 #       return [{}]
181
182     def onchange_partner_id(self, cr, uid, ids, type, partner_id,
183             date_invoice=False, payment_term=False, partner_bank_id=False):
184         invoice_addr_id = False
185         contact_addr_id = False
186         partner_payment_term = False
187         acc_id = False
188         bank_id = False
189
190         opt = [('uid', str(uid))]
191         if partner_id:
192
193             opt.insert(0, ('id', partner_id))
194             res = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['contact', 'invoice'])
195             contact_addr_id = res['contact']
196             invoice_addr_id = res['invoice']
197             p = self.pool.get('res.partner').browse(cr, uid, partner_id)
198             if type in ('out_invoice', 'out_refund'):
199                 acc_id = p.property_account_receivable.id
200             else:
201                 acc_id = p.property_account_payable.id
202
203             partner_payment_term = p.property_payment_term and p.property_payment_term.id or False
204             if p.bank_ids:
205                 bank_id = p.bank_ids[0].id
206
207         result = {'value': {
208             'address_contact_id': contact_addr_id,
209             'address_invoice_id': invoice_addr_id,
210             'account_id': acc_id,
211             'payment_term': partner_payment_term,
212             }
213         }
214
215         if type in ('in_invoice', 'in_refund'):
216             result['value']['partner_bank'] = bank_id
217
218         if payment_term != partner_payment_term:
219             if partner_payment_term:
220                 to_update = self.onchange_payment_term_date_invoice(
221                     cr,uid,ids,partner_payment_term,date_invoice)
222                 result['value'].update(to_update['value'])
223             else:
224                 result['value']['date_due'] = False
225
226         if partner_bank_id != bank_id:
227             to_update = self.onchange_partner_bank(cr, uid, ids, bank_id)
228             result['value'].update(to_update['value'])
229         return result
230
231     def onchange_currency_id(self, cr, uid, ids, curr_id):
232         return {}
233
234     def onchange_payment_term_date_invoice(self, cr, uid, ids, payment_term_id, date_invoice):
235         if not payment_term_id:
236             return {}
237         res={}
238         pt_obj= self.pool.get('account.payment.term')
239
240         if not date_invoice :
241             date_invoice = self._defaults["date_invoice"](cr,uid,{})
242
243         pterm_list= pt_obj.compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice)
244
245         if pterm_list:
246             pterm_list = [line[0] for line in pterm_list]
247             pterm_list.sort()
248             res= {'value':{'date_due': pterm_list[-1]}}
249
250         return res
251
252     def onchange_invoice_line(self, cr, uid, ids, lines):
253         return {}
254
255     def onchange_partner_bank(self, cursor, user, ids, partner_bank_id):
256         return {'value': {}}
257
258     # go from canceled state to draft state
259     def action_cancel_draft(self, cr, uid, ids, *args):
260         self.write(cr, uid, ids, {'state':'draft'})
261         wf_service = netsvc.LocalService("workflow")
262         for inv_id in ids:
263             wf_service.trg_create(uid, 'account.invoice', inv_id, cr)
264         return True
265
266     # Workflow stuff
267     #################
268
269     # return the ids of the move lines which has the same account than the invoice
270     # whose id is in ids
271     def move_line_id_payment_get(self, cr, uid, ids, *args):
272         ml = self.pool.get('account.move.line')
273         res = []
274         for inv in self.read(cr, uid, ids, ['move_id','account_id']):
275             if inv['move_id']:
276                 move_line_ids = ml.search(cr, uid, [('move_id', '=', inv['move_id'][0])])
277                 for line in ml.read(cr, uid, move_line_ids, ['account_id']):
278                     if line['account_id']==inv['account_id']:
279                         res.append(line['id'])
280         return res
281
282     def copy(self, cr, uid, id, default=None, context=None):
283         if default is None:
284             default = {}
285         default = default.copy()
286         default.update({'state':'draft', 'number':False, 'move_id':False,})
287         if 'date_invoice' not in default:
288             default['date_invoice'] = time.strftime('%Y-%m-%d')
289         if 'date_due' not in default:
290             default['date_due'] = False
291         return super(account_invoice, self).copy(cr, uid, id, default, context)
292
293     def test_paid(self, cr, uid, ids, *args):
294         res = self.move_line_id_payment_get(cr, uid, ids)
295         if not res:
296             return False
297         ok = True
298         for id in res:
299             cr.execute('select reconcile_id from account_move_line where id=%d', (id,))
300             ok = ok and  bool(cr.fetchone()[0])
301         return ok
302
303     def button_reset_taxes(self, cr, uid, ids, context={}):
304         ait_obj = self.pool.get('account.invoice.tax')
305         for id in ids:
306             cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%d", (id,))
307             for taxe in ait_obj.compute(cr, uid, id).values():
308                 ait_obj.create(cr, uid, taxe)
309         return True
310
311     def button_compute(self, cr, uid, ids, context={}, set_total=False):
312         ait_obj = self.pool.get('account.invoice.tax')
313         cur_obj = self.pool.get('res.currency')
314         for inv in self.browse(cr, uid, ids):
315             company_currency = inv.company_id.currency_id.id
316             compute_taxes = ait_obj.compute(cr, uid, inv.id)
317             if not inv.tax_line:
318                 for tax in compute_taxes.values():
319                     ait_obj.create(cr, uid, tax)
320             else:
321                 tax_key = []
322                 for tax in inv.tax_line:
323                     if tax.manual:
324                         continue
325                     key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
326                     tax_key.append(key)
327                     if not key in compute_taxes:
328                         ait_obj.unlink(cr, uid, [tax.id])
329                         continue
330                     compute_taxes[key]['base'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, compute_taxes[key]['base'], context={'date': inv.date_invoice})
331                     if abs(compute_taxes[key]['base'] - tax.base) > inv.company_id.currency_id.rounding:
332                         ait_obj.write(cr, uid, [tax.id], compute_taxes[key])
333                 for key in compute_taxes:
334                     if not key in tax_key:
335                         ait_obj.create(cr, uid, compute_taxes[key])
336             if set_total:
337                 self.pool.get('account.invoice').write(cr, uid, [inv.id], {'check_total': inv.amount_total})
338         return True
339
340     def _convert_ref(self, cr, uid, ref):
341         return (ref or '').replace('/','')
342
343     def _get_analityc_lines(self, cr, uid, id):
344         inv = self.browse(cr, uid, [id])[0]
345         cur_obj = self.pool.get('res.currency')
346
347         company_currency = inv.company_id.currency_id.id
348         if inv.type in ('out_invoice', 'in_refund'):
349             sign = 1
350         else:
351             sign = -1
352
353         iml = self.pool.get('account.invoice.line').move_line_get(cr, uid, inv.id)
354         for il in iml:
355             if il['account_analytic_id']:
356                 if inv.type in ('in_invoice', 'in_refund'):
357                     ref = inv.reference
358                 else:
359                     ref = self._convert_ref(cr, uid, inv.number)
360                 il['analytic_lines'] = [(0,0, {
361                     'name': il['name'],
362                     'date': inv['date_invoice'],
363                     'account_id': il['account_analytic_id'],
364                     'unit_amount': il['quantity'],
365                     'amount': cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, il['price'], context={'date': inv.date_invoice}) * sign,
366                     'product_id': il['product_id'],
367                     'product_uom_id': il['uos_id'],
368                     'general_account_id': il['account_id'],
369                     'journal_id': self._get_journal_analytic(cr, uid, inv.type),
370                     'ref': ref,
371                 })]
372         return iml
373
374     def action_move_create(self, cr, uid, ids, *args):
375         ait_obj = self.pool.get('account.invoice.tax')
376         cur_obj = self.pool.get('res.currency')
377         for inv in self.browse(cr, uid, ids):
378             if inv.move_id:
379                 continue
380             if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0):
381                 raise osv.except_osv(_('Bad total !'), _('Please verify the price of the invoice !\nThe real total does not match the computed total.'))
382             company_currency = inv.company_id.currency_id.id
383             # create the analytical lines
384             line_ids = self.read(cr, uid, [inv.id], ['invoice_line'])[0]['invoice_line']
385             ils = self.pool.get('account.invoice.line').read(cr, uid, line_ids)
386             # one move line per invoice line
387             iml = self._get_analityc_lines(cr, uid, inv.id)
388             # check if taxes are all computed
389             compute_taxes = ait_obj.compute(cr, uid, inv.id)
390             if not inv.tax_line:
391                 for tax in compute_taxes.values():
392                     ait_obj.create(cr, uid, tax)
393             else:
394                 tax_key = []
395                 for tax in inv.tax_line:
396                     if tax.manual:
397                         continue
398                     key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
399                     tax_key.append(key)
400                     if not key in compute_taxes:
401                         raise osv.except_osv(_('Warning !'), _('Global taxes defined, but not in invoice lines !'))
402                     base = compute_taxes[key]['base']
403                     if abs(base - tax.base) > inv.company_id.currency_id.rounding:
404                         raise osv.except_osv(_('Warning !'), _('Tax base different !\nClick on compute to update tax base'))
405                 for key in compute_taxes:
406                     if not key in tax_key:
407                         raise osv.except_osv(_('Warning !'), _('Taxes missing !'))
408
409             # one move line per tax line
410             iml += ait_obj.move_line_get(cr, uid, inv.id)
411
412             if inv.type in ('in_invoice', 'in_refund'):
413                 ref = inv.reference
414             else:
415                 ref = self._convert_ref(cr, uid, inv.number)
416
417             diff_currency_p = inv.currency_id.id <> company_currency
418             # create one move line for the total and possibly adjust the other lines amount
419             total = 0
420             total_currency = 0
421             for i in iml:
422                 if inv.currency_id.id != company_currency:
423                     i['currency_id'] = inv.currency_id.id
424                     i['amount_currency'] = i['price']
425                     i['price'] = cur_obj.compute(cr, uid, inv.currency_id.id,
426                             company_currency, i['price'],
427                             context={'date': inv.date_invoice})
428                 else:
429                     i['amount_currency'] = False
430                     i['currency_id'] = False
431                 i['ref'] = ref
432                 if inv.type in ('out_invoice','in_refund'):
433                     total += i['price']
434                     total_currency += i['amount_currency'] or i['price']
435                     i['price'] = - i['price']
436                 else:
437                     total -= i['price']
438                     total_currency -= i['amount_currency'] or i['price']
439             acc_id = inv.account_id.id
440
441             name = inv['name'] or '/'
442             totlines = False
443             if inv.payment_term:
444                 totlines = self.pool.get('account.payment.term').compute(cr,
445                         uid, inv.payment_term.id, total, inv.date_invoice or False)
446             if totlines:
447                 res_amount_currency = total_currency
448                 i = 0
449                 for t in totlines:
450                     if inv.currency_id.id != company_currency:
451                         amount_currency = cur_obj.compute(cr, uid,
452                                 company_currency, inv.currency_id.id, t[1])
453                     else:
454                         amount_currency = False
455
456                     # last line add the diff
457                     res_amount_currency -= amount_currency or 0
458                     i += 1
459                     if i == len(totlines):
460                         amount_currency += res_amount_currency
461
462                     iml.append({
463                         'type': 'dest',
464                         'name': name,
465                         'price': t[1],
466                         'account_id': acc_id,
467                         'date_maturity': t[0],
468                         'amount_currency': diff_currency_p \
469                                 and  amount_currency or False,
470                         'currency_id': diff_currency_p \
471                                 and inv.currency_id.id or False,
472                         'ref': ref,
473                     })
474             else:
475                 iml.append({
476                     'type': 'dest',
477                     'name': name,
478                     'price': total,
479                     'account_id': acc_id,
480                     'date_maturity' : inv.date_due or False,
481                     'amount_currency': diff_currency_p \
482                             and total_currency or False,
483                     'currency_id': diff_currency_p \
484                             and inv.currency_id.id or False,
485                     'ref': ref
486             })
487
488             date = inv.date_invoice
489             part = inv.partner_id.id
490             line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part, date, context={})) ,iml)
491
492             journal_id = inv.journal_id.id #self._get_journal(cr, uid, {'type': inv['type']})
493             journal = self.pool.get('account.journal').browse(cr, uid, journal_id)
494             if journal.sequence_id:
495                 name = self.pool.get('ir.sequence').get_id(cr, uid, journal.sequence_id.id)
496             if journal.centralisation:
497                 raise osv.except_osv(_('UserError'),
498                         _('Can not create invoice move on centralized journal'))
499
500             move = {'name': name, 'line_id': line, 'journal_id': journal_id}
501             period_id=inv.period_id and inv.period_id.id or False
502             if not period_id:
503                 period_ids= self.pool.get('account.period').search(cr,uid,[('date_start','<=',inv.date_invoice),('date_stop','>=',inv.date_invoice)])
504                 if len(period_ids):
505                     period_id=period_ids[0]
506             if period_id:
507                 move['period_id'] = period_id
508                 for i in line:
509                     i[2]['period_id'] = period_id
510             move_id = self.pool.get('account.move').create(cr, uid, move)
511             # make the invoice point to that move
512             self.write(cr, uid, [inv.id], {'move_id': move_id,'period_id':period_id})
513             self.pool.get('account.move').post(cr, uid, [move_id])
514         self._log_event(cr, uid, ids)
515         return True
516
517     def line_get_convert(self, cr, uid, x, part, date, context={}):
518         return {
519             'date':date,
520             'date_maturity': x.get('date_maturity', False),
521             'partner_id':part,
522             'name':x['name'][:64],
523             'debit':x['price']>0 and x['price'],
524             'credit':x['price']<0 and -x['price'],
525             'account_id':x['account_id'],
526             'analytic_lines':x.get('analytic_lines', []),
527             'amount_currency':x['price']>0 and abs(x.get('amount_currency', False)) or -abs(x.get('amount_currency', False)),
528             'currency_id':x.get('currency_id', False),
529             'tax_code_id': x.get('tax_code_id', False),
530             'tax_amount': x.get('tax_amount', False),
531             'ref':x.get('ref',False)
532         }
533
534     def action_number(self, cr, uid, ids, *args):
535         cr.execute('SELECT id, type, number, move_id, reference ' \
536                 'FROM account_invoice ' \
537                 'WHERE id IN ('+','.join(map(str,ids))+')')
538         for (id, invtype, number, move_id, reference) in cr.fetchall():
539             if not number:
540                 number = self.pool.get('ir.sequence').get(cr, uid,
541                         'account.invoice.' + invtype)
542                 if invtype in ('in_invoice', 'in_refund'):
543                     ref = reference
544                 else:
545                     ref = self._convert_ref(cr, uid, number)
546                 cr.execute('UPDATE account_invoice SET number=%s ' \
547                         'WHERE id=%d', (number, id))
548                 cr.execute('UPDATE account_move_line SET ref=%s ' \
549                         'WHERE move_id=%d AND (ref is null OR ref = \'\')',
550                         (ref, move_id))
551                 cr.execute('UPDATE account_analytic_line SET ref=%s ' \
552                         'FROM account_move_line ' \
553                         'WHERE account_move_line.move_id = %d ' \
554                             'AND account_analytic_line.move_id = account_move_line.id',
555                             (ref, move_id))
556         return True
557
558     def action_cancel(self, cr, uid, ids, *args):
559         account_move_obj = self.pool.get('account.move')
560         invoices = self.read(cr, uid, ids, ['move_id'])
561         for i in invoices:
562             if i['move_id']:
563                 account_move_obj.button_cancel(cr, uid, [i['move_id'][0]])
564                 # delete the move this invoice was pointing to
565                 # Note that the corresponding move_lines and move_reconciles
566                 # will be automatically deleted too
567                 account_move_obj.unlink(cr, uid, [i['move_id'][0]])
568         self.write(cr, uid, ids, {'state':'cancel', 'move_id':False})
569         self._log_event(cr, uid, ids,-1.0, 'Cancel Invoice')
570         return True
571
572     ###################
573
574     def list_distinct_taxes(self, cr, uid, ids):
575         invoices = self.browse(cr, uid, ids)
576         taxes = {}
577         for inv in invoices:
578             for tax in inv.tax_line:
579                 if not tax['name'] in taxes:
580                     taxes[tax['name']] = {'name': tax['name']}
581         return taxes.values()
582
583     def _log_event(self, cr, uid, ids, factor=1.0, name='Open Invoice'):
584         invs = self.read(cr, uid, ids, ['type','partner_id','amount_untaxed'])
585         for inv in invs:
586             part=inv['partner_id'] and inv['partner_id'][0]
587             pc = pr = 0.0
588             cr.execute('select sum(quantity*price_unit) from account_invoice_line where invoice_id=%d', (inv['id'],))
589             total = inv['amount_untaxed']
590             if inv['type'] in ('in_invoice','in_refund'):
591                 partnertype='supplier'
592                 eventtype = 'purchase'
593                 pc = total*factor
594             else:
595                 partnertype = 'customer'
596                 eventtype = 'sale'
597                 pr = total*factor
598             if self.pool.get('res.partner.event.type').check(cr, uid, 'invoice_open'):
599                 self.pool.get('res.partner.event').create(cr, uid, {'name':'Invoice: '+name, 'som':False, 'description':name+' '+str(inv['id']), 'document':name, 'partner_id':part, 'date':time.strftime('%Y-%m-%d %H:%M:%S'), 'canal_id':False, 'user_id':uid, 'partner_type':partnertype, 'probability':1.0, 'planned_revenue':pr, 'planned_cost':pc, 'type':eventtype})
600         return len(invs)
601
602     def name_get(self, cr, uid, ids, context={}):
603         if not len(ids):
604             return []
605         types = {
606                 'out_invoice': 'CI: ',
607                 'in_invoice': 'SI: ',
608                 'out_refund': 'OR: ',
609                 'in_refund': 'SR: ',
610                 }
611         return [(r['id'], types[r['type']]+(r['number'] or '')+' '+(r['name'] or '')) for r in self.read(cr, uid, ids, ['type', 'number', 'name'], context, load='_classic_write')]
612
613     def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
614         if not args:
615             args=[]
616         if not context:
617             context={}
618         ids = []
619         if name:
620             ids = self.search(cr, user, [('number','=',name)]+ args, limit=limit, context=context)
621         if not ids:
622             ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
623         return self.name_get(cr, user, ids, context)
624
625     def _refund_cleanup_lines(self, lines):
626         for line in lines:
627             del line['id']
628             del line['invoice_id']
629             if 'account_id' in line:
630                 line['account_id'] = line.get('account_id', False) and line['account_id'][0]
631             if 'product_id' in line:
632                 line['product_id'] = line.get('product_id', False) and line['product_id'][0]
633             if 'uos_id' in line:
634                 line['uos_id'] = line.get('uos_id', False) and line['uos_id'][0]
635             if 'invoice_line_tax_id' in line:
636                 line['invoice_line_tax_id'] = [(6,0, line.get('invoice_line_tax_id', [])) ]
637             if 'account_analytic_id' in line:
638                 line['account_analytic_id'] = line.get('account_analytic_id', False) and line['account_analytic_id'][0]
639         return map(lambda x: (0,0,x), lines)
640
641     def refund(self, cr, uid, ids):
642         invoices = self.read(cr, uid, ids, ['name', 'type', 'number', 'reference', 'comment', 'date_due', 'partner_id', 'address_contact_id', 'address_invoice_id', 'partner_contact', 'partner_insite', 'partner_ref', 'payment_term', 'account_id', 'currency_id', 'invoice_line', 'tax_line', 'journal_id'])
643
644         new_ids = []
645         for invoice in invoices:
646             del invoice['id']
647
648             type_dict = {
649                 'out_invoice': 'out_refund', # Customer Invoice
650                 'in_invoice': 'in_refund',   # Supplier Invoice
651                 'out_refund': 'out_invoice', # Customer Refund
652                 'in_refund': 'in_invoice',   # Supplier Refund
653             }
654
655
656             invoice_lines = self.pool.get('account.invoice.line').read(cr, uid, invoice['invoice_line'])
657             invoice_lines = self._refund_cleanup_lines(invoice_lines)
658
659             tax_lines = self.pool.get('account.invoice.tax').read(cr, uid, invoice['tax_line'])
660             tax_lines = filter(lambda l: l['manual'], tax_lines)
661             tax_lines = self._refund_cleanup_lines(tax_lines)
662
663             invoice.update({
664                 'type': type_dict[invoice['type']],
665                 'date_invoice': time.strftime('%Y-%m-%d'),
666                 'state': 'draft',
667                 'number': False,
668                 'invoice_line': invoice_lines,
669                 'tax_line': tax_lines
670             })
671
672             # take the id part of the tuple returned for many2one fields
673             for field in ('address_contact_id', 'address_invoice_id', 'partner_id',
674                     'account_id', 'currency_id', 'payment_term', 'journal_id'):
675                 invoice[field] = invoice[field] and invoice[field][0]
676
677             # create the new invoice
678             new_ids.append(self.create(cr, uid, invoice))
679         return new_ids
680
681     def pay_and_reconcile(self, cr, uid, ids, pay_amount, pay_account_id, period_id, pay_journal_id, writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context={}, name=''):
682         #TODO check if we can use different period for payment and the writeoff line
683         assert len(ids)==1, "Can only pay one invoice at a time"
684         invoice = self.browse(cr, uid, ids[0])
685         src_account_id = invoice.account_id.id
686         journal = self.pool.get('account.journal').browse(cr, uid, pay_journal_id)
687         if journal.sequence_id:
688             name = self.pool.get('ir.sequence').get_id(cr, uid, journal.sequence_id.id)
689         else:
690             raise osv.except_osv(_('No piece number !'), _('Can not create an automatic sequence for this piece !\n\nPut a sequence in the journal definition for automatic numbering or create a sequence manually for this piece.'))
691         # Take the seq as name for move
692         if journal.sequence_id:
693             seq = self.pool.get('ir.sequence').get_id(cr, uid, journal.sequence_id.id)
694         else:
695             raise osv.except_osv('No piece number !', 'Can not create an automatic sequence for this piece !\n\nPut a sequence in the journal definition for automatic numbering or create a sequence manually for this piece.')
696         types = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
697         direction = types[invoice.type]
698         #take the choosen date
699         if context.has_key('date_p') and context['date_p']:
700             date=context['date_p']
701         else:
702             date=time.strftime('%Y-%m-%d')
703         l1 = {
704             'name': name,
705             'debit': direction * pay_amount>0 and direction * pay_amount,
706             'credit': direction * pay_amount<0 and - direction * pay_amount,
707             'account_id': src_account_id,
708             'partner_id': invoice.partner_id.id,
709             'date': date,
710             'ref':invoice.number,
711         }
712         l2 = {
713             'name':name,
714             'debit': direction * pay_amount<0 and - direction * pay_amount,
715             'credit': direction * pay_amount>0 and direction * pay_amount,
716             'account_id': pay_account_id,
717             'partner_id': invoice.partner_id.id,
718             'date': date,
719             'ref':invoice.number,
720         }
721
722         lines = [(0, 0, l1), (0, 0, l2)]
723         move = {'name': seq, 'line_id': lines, 'journal_id': pay_journal_id, 'period_id': period_id}
724         move_id = self.pool.get('account.move').create(cr, uid, move)
725
726         line_ids = []
727         total = 0.0
728         line = self.pool.get('account.move.line')
729         cr.execute('select id from account_move_line where move_id in ('+str(move_id)+','+str(invoice.move_id.id)+')')
730         lines = line.browse(cr, uid, map(lambda x: x[0], cr.fetchall()) )
731         for l in lines:
732             if l.account_id.id==src_account_id:
733                 line_ids.append(l.id)
734                 total += (l.debit or 0.0) - (l.credit or 0.0)
735         if (not total) or writeoff_acc_id:
736             self.pool.get('account.move.line').reconcile(cr, uid, line_ids, 'manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context)
737         else:
738             self.pool.get('account.move.line').reconcile_partial(cr, uid, line_ids, 'manual', context)
739         return True
740 account_invoice()
741
742 class account_invoice_line(osv.osv):
743     def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
744         res = {}
745         for line in self.browse(cr, uid, ids):
746             res[line.id] = round(line.price_unit * line.quantity * (1-(line.discount or 0.0)/100.0),2)
747         return res
748
749     def _price_unit_default(self, cr, uid, context={}):
750         if 'check_total' in context:
751             t = context['check_total']
752             for l in context.get('invoice_line', {}):
753                 if len(l) >= 3 and l[2]:
754                     tax_obj = self.pool.get('account.tax')
755                     p = l[2].get('price_unit', 0) * (1-l[2].get('discount', 0)/100.0)
756                     t = t - (p * l[2].get('quantity'))
757                     taxes = l[2].get('invoice_line_tax_id')
758                     if len(taxes[0]) >= 3 and taxes[0][2]:
759                         taxes=tax_obj.browse(cr, uid, taxes[0][2])
760                         for tax in tax_obj.compute(cr, uid, taxes, p,l[2].get('quantity'), context.get('address_invoice_id', False), l[2].get('product_id', False), context.get('partner_id', False)):
761                             t = t - tax['amount']
762             return t
763         return 0
764
765     _name = "account.invoice.line"
766     _description = "Invoice line"
767     _columns = {
768         'name': fields.char('Description', size=256, required=True),
769         'invoice_id': fields.many2one('account.invoice', 'Invoice Ref', ondelete='cascade', select=True),
770         'uos_id': fields.many2one('product.uom', 'Unit', ondelete='set null'),
771         'product_id': fields.many2one('product.product', 'Product', ondelete='set null'),
772         'account_id': fields.many2one('account.account', 'Account', required=True, domain=[('type','<>','view'), ('type', '<>', 'closed')]),
773         'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
774         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',store=True),
775         'quantity': fields.float('Quantity', required=True),
776         'discount': fields.float('Discount (%)', digits=(16,2)),
777         'invoice_line_tax_id': fields.many2many('account.tax', 'account_invoice_line_tax', 'invoice_line_id', 'tax_id', 'Taxes', domain=[('parent_id','=',False)]),
778         'note': fields.text('Notes'),
779         'account_analytic_id':  fields.many2one('account.analytic.account', 'Analytic Account'),
780     }
781     _defaults = {
782         'quantity': lambda *a: 1,
783         'discount': lambda *a: 0.0,
784         'price_unit': _price_unit_default,
785     }
786
787     def product_id_change_unit_price_inv(self, cr, uid, tax_id, price_unit, qty, address_invoice_id, product, partner_id, context={}):
788         tax_obj = self.pool.get('account.tax')
789         if price_unit:
790             taxes = tax_obj.browse(cr, uid, tax_id)
791             for tax in tax_obj.compute_inv(cr, uid, taxes, price_unit, qty, address_invoice_id, product, partner_id):
792                 price_unit = price_unit - tax['amount']
793         return {'price_unit': price_unit,'invoice_line_tax_id': tax_id}
794
795     def product_id_change(self, cr, uid, ids, product, uom, qty=0, name='', type='out_invoice', partner_id=False, price_unit=False, address_invoice_id=False, context={}):
796         if not partner_id:
797             raise osv.except_osv(_('No Partner Defined !'),_("You must first select a partner !") )
798         if not product:
799             if type in ('in_invoice', 'in_refund'):
800                 return {'domain':{'product_uom':[]}}
801             else:
802                 return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
803         lang=False
804         context.update({'lang': lang})
805         res = self.pool.get('product.product').browse(cr, uid, product, context=context)
806         taxep=None
807         lang=self.pool.get('res.partner').read(cr, uid, [partner_id])[0]['lang']
808         tax_obj = self.pool.get('account.tax')
809         if type in ('out_invoice', 'out_refund'):
810             taxep = self.pool.get('res.partner').browse(cr, uid, partner_id).property_account_tax
811             if not taxep or not taxep.id:
812                 tax_id = map(lambda x: x.id, res.taxes_id)
813             else:
814                 tax_id = [taxep.id]
815                 for t in res.taxes_id:
816                     if not t.tax_group==taxep.tax_group:
817                         tax_id.append(t.id)
818         else:
819             taxep = self.pool.get('res.partner').browse(cr, uid, partner_id).property_account_supplier_tax
820             if not taxep or not taxep.id:
821                 tax_id = map(lambda x: x.id, res.supplier_taxes_id)
822             else:
823                 tax_id = [taxep.id]
824                 for t in res.supplier_taxes_id:
825                     if not t.tax_group==taxep.tax_group:
826                         tax_id.append(t.id)
827         if type in ('in_invoice', 'in_refund'):
828             result = self.product_id_change_unit_price_inv(cr, uid, tax_id, price_unit, qty, address_invoice_id, product, partner_id, context=context)
829         else:
830             result = {'price_unit': res.list_price, 'invoice_line_tax_id': tax_id}
831
832         if not name:
833             result['name'] = res.name
834
835         if type in ('out_invoice','out_refund'):
836             a =  res.product_tmpl_id.property_account_income.id
837             if not a:
838                 a = res.categ_id.property_account_income_categ.id
839         else:
840             a =  res.product_tmpl_id.property_account_expense.id
841             if not a:
842                 a = res.categ_id.property_account_expense_categ.id
843         if a:
844             result['account_id'] = a
845
846         domain = {}
847         result['uos_id'] = uom or res.uom_id.id or False
848         if result['uos_id']:
849             res2 = res.uom_id.category_id.id
850             if res2 :
851                 domain = {'uos_id':[('category_id','=',res2 )]}
852         return {'value':result, 'domain':domain}
853
854     def move_line_get(self, cr, uid, invoice_id, context={}):
855         res = []
856         tax_grouped = {}
857         tax_obj = self.pool.get('account.tax')
858         cur_obj = self.pool.get('res.currency')
859         ait_obj = self.pool.get('account.invoice.tax')
860         inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id)
861         company_currency = inv.company_id.currency_id.id
862         cur = inv.currency_id
863
864         for line in inv.invoice_line:
865             mres = self.move_line_get_item(cr, uid, line, context)
866             if not mres:
867                 continue
868             res.append(mres)
869             tax_code_found= False
870             for tax in tax_obj.compute(cr, uid, line.invoice_line_tax_id,
871                     (line.price_unit * (1.0 - (line['discount'] or 0.0) / 100.0)),
872                     line.quantity, inv.address_invoice_id.id, line.product_id,
873                     inv.partner_id):
874
875                 if inv.type in ('out_invoice', 'in_invoice'):
876                     tax_code_id = tax['base_code_id']
877                     tax_amount = line.price_subtotal * tax['base_sign']
878                 else:
879                     tax_code_id = tax['ref_base_code_id']
880                     tax_amount = line.price_subtotal * tax['ref_base_sign']
881
882                 if tax_code_found:
883                     if not tax_code_id:
884                         continue
885                     res.append(self.move_line_get_item(cr, uid, line, context))
886                     res[-1]['price'] = 0.0
887                     res[-1]['account_analytic_id'] = False
888                 elif not tax_code_id:
889                     continue
890                 tax_code_found = True
891
892                 res[-1]['tax_code_id'] = tax_code_id
893                 res[-1]['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, tax_amount, context={'date': inv.date_invoice})
894         return res
895
896     def move_line_get_item(self, cr, uid, line, context={}):
897         return {
898             'type':'src',
899             'name': line.name[:64],
900             'price_unit':line.price_unit,
901             'quantity':line.quantity,
902             'price':line.price_subtotal,
903             'account_id':line.account_id.id,
904             'product_id':line.product_id.id,
905             'uos_id':line.uos_id.id,
906             'account_analytic_id':line.account_analytic_id.id,
907         }
908     #
909     # Set the tax field according to the account and the partner
910     #
911     def onchange_account_id(self, cr, uid, ids, partner_id,account_id):
912         if not (partner_id and account_id):
913             return {}
914         taxes = self.pool.get('account.account').browse(cr, uid, account_id).tax_ids
915         taxep = self.pool.get('res.partner').browse(cr, uid, partner_id).property_account_tax
916         if not taxep.id:
917             return {'value': {'invoice_line_tax_id': map(lambda x: x.id, taxes or []) }}
918         res = [taxep.id]
919         for t in taxes:
920             if not t.tax_group==taxep.tax_group:
921                 res.append(t.id)
922         r = {'value':{'invoice_line_tax_id': res}}
923         return r
924 account_invoice_line()
925
926 class account_invoice_tax(osv.osv):
927     _name = "account.invoice.tax"
928     _description = "Invoice Tax"
929     _columns = {
930         'invoice_id': fields.many2one('account.invoice', 'Invoice Line', ondelete='cascade', select=True),
931         'name': fields.char('Tax Description', size=64, required=True),
932         'account_id': fields.many2one('account.account', 'Tax Account', required=True, domain=[('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')]),
933         'base': fields.float('Base', digits=(16,2)),
934         'amount': fields.float('Amount', digits=(16,2)),
935         'manual': fields.boolean('Manual'),
936         'sequence': fields.integer('Sequence'),
937
938         'base_code_id': fields.many2one('account.tax.code', 'Base Code'),
939         'base_amount': fields.float('Base Code Amount', digits=(16,2)),
940         'tax_code_id': fields.many2one('account.tax.code', 'Tax Code'),
941         'tax_amount': fields.float('Tax Code Amount', digits=(16,2)),
942     }
943     def base_change(self, cr, uid, ids, base):
944         return {'value': {'base_amount':base}}
945     def amount_change(self, cr, uid, ids, amount):
946         return {'value': {'tax_amount':amount}}
947     _order = 'sequence'
948     _defaults = {
949         'manual': lambda *a: 1,
950         'base_amount': lambda *a: 0.0,
951         'tax_amount': lambda *a: 0.0,
952     }
953     def compute(self, cr, uid, invoice_id):
954         tax_grouped = {}
955         tax_obj = self.pool.get('account.tax')
956         cur_obj = self.pool.get('res.currency')
957         inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id)
958         cur = inv.currency_id
959         company_currency = inv.company_id.currency_id.id
960
961         for line in inv.invoice_line:
962             for tax in tax_obj.compute(cr, uid, line.invoice_line_tax_id, (line.price_unit* (1-(line.discount or 0.0)/100.0)), line.quantity, inv.address_invoice_id.id, line.product_id, inv.partner_id):
963                 val={}
964                 val['invoice_id'] = inv.id
965                 val['name'] = tax['name']
966                 val['amount'] = cur_obj.round(cr, uid, cur, tax['amount'])
967                 val['manual'] = False
968                 val['sequence'] = tax['sequence']
969                 val['base'] = tax['price_unit'] * line['quantity']
970
971                 if inv.type in ('out_invoice','in_invoice'):
972                     val['base_code_id'] = tax['base_code_id']
973                     val['tax_code_id'] = tax['tax_code_id']
974                     val['base_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['base'] * tax['base_sign'], context={'date': inv.date_invoice})
975                     val['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['amount'] * tax['tax_sign'], context={'date': inv.date_invoice})
976                     val['account_id'] = tax['account_collected_id'] or line.account_id.id
977                 else:
978                     val['base_code_id'] = tax['ref_base_code_id']
979                     val['tax_code_id'] = tax['ref_tax_code_id']
980                     val['base_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['base'] * tax['ref_base_sign'], context={'date': inv.date_invoice})
981                     val['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['amount'] * tax['ref_tax_sign'], context={'date': inv.date_invoice})
982                     val['account_id'] = tax['account_paid_id'] or line.account_id.id
983
984                 key = (val['tax_code_id'], val['base_code_id'], val['account_id'])
985                 if not key in tax_grouped:
986                     tax_grouped[key] = val
987                 else:
988                     tax_grouped[key]['amount'] += val['amount']
989                     tax_grouped[key]['base'] += val['base']
990                     tax_grouped[key]['base_amount'] += val['base_amount']
991                     tax_grouped[key]['tax_amount'] += val['tax_amount']
992
993         return tax_grouped
994
995     def move_line_get(self, cr, uid, invoice_id):
996         res = []
997         cr.execute('SELECT * FROM account_invoice_tax WHERE invoice_id=%d', (invoice_id,))
998         for t in cr.dictfetchall():
999             if not t['amount'] \
1000                     and not t['tax_code_id'] \
1001                     and not t['tax_amount']:
1002                 continue
1003             res.append({
1004                 'type':'tax',
1005                 'name':t['name'],
1006                 'price_unit': t['amount'],
1007                 'quantity': 1,
1008                 'price': t['amount'] or 0.0,
1009                 'account_id': t['account_id'],
1010                 'tax_code_id': t['tax_code_id'],
1011                 'tax_amount': t['tax_amount']
1012             })
1013         return res
1014 account_invoice_tax()
1015
1016 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1017