[REF] mail_thread: message_track now uses browse records instead of read results.
[odoo/odoo.git] / addons / account / account_invoice.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import time
23 from lxml import etree
24 import openerp.addons.decimal_precision as dp
25 import openerp.exceptions
26
27 from openerp.osv import fields, osv, orm
28 from openerp.tools.translate import _
29
30 class account_invoice(osv.osv):
31     def _amount_all(self, cr, uid, ids, name, args, context=None):
32         res = {}
33         for invoice in self.browse(cr, uid, ids, context=context):
34             res[invoice.id] = {
35                 'amount_untaxed': 0.0,
36                 'amount_tax': 0.0,
37                 'amount_total': 0.0
38             }
39             for line in invoice.invoice_line:
40                 res[invoice.id]['amount_untaxed'] += line.price_subtotal
41             for line in invoice.tax_line:
42                 res[invoice.id]['amount_tax'] += line.amount
43             res[invoice.id]['amount_total'] = res[invoice.id]['amount_tax'] + res[invoice.id]['amount_untaxed']
44         return res
45
46     def _get_journal(self, cr, uid, context=None):
47         if context is None:
48             context = {}
49         type_inv = context.get('type', 'out_invoice')
50         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
51         company_id = context.get('company_id', user.company_id.id)
52         type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale_refund', 'in_refund': 'purchase_refund'}
53         journal_obj = self.pool.get('account.journal')
54         domain = [('company_id', '=', company_id)]
55         if isinstance(type_inv, list):
56             domain.append(('type', 'in', [type2journal.get(type) for type in type_inv if type2journal.get(type)]))
57         else:
58             domain.append(('type', '=', type2journal.get(type_inv, 'sale')))
59         res = journal_obj.search(cr, uid, domain, limit=1)
60         return res and res[0] or False
61
62     def _get_currency(self, cr, uid, context=None):
63         res = False
64         journal_id = self._get_journal(cr, uid, context=context)
65         if journal_id:
66             journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
67             res = journal.currency and journal.currency.id or journal.company_id.currency_id.id
68         return res
69
70     def _get_journal_analytic(self, cr, uid, type_inv, context=None):
71         type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale', 'in_refund': 'purchase'}
72         tt = type2journal.get(type_inv, 'sale')
73         result = self.pool.get('account.analytic.journal').search(cr, uid, [('type','=',tt)], context=context)
74         if not result:
75             raise osv.except_osv(_('No Analytic Journal!'),_("You must define an analytic journal of type '%s'!") % (tt,))
76         return result[0]
77
78     def _get_type(self, cr, uid, context=None):
79         if context is None:
80             context = {}
81         return context.get('type', 'out_invoice')
82
83     def _reconciled(self, cr, uid, ids, name, args, context=None):
84         res = {}
85         for inv in self.browse(cr, uid, ids, context=context):
86             res[inv.id] = self.test_paid(cr, uid, [inv.id])
87             if not res[inv.id] and inv.state == 'paid':
88                 self.signal_open_test(cr, uid, [inv.id])
89         return res
90
91     def _get_reference_type(self, cr, uid, context=None):
92         return [('none', _('Free Reference'))]
93
94     def _amount_residual(self, cr, uid, ids, name, args, context=None):
95         """Function of the field residua. It computes the residual amount (balance) for each invoice"""
96         if context is None:
97             context = {}
98         ctx = context.copy()
99         result = {}
100         currency_obj = self.pool.get('res.currency')
101         for invoice in self.browse(cr, uid, ids, context=context):
102             nb_inv_in_partial_rec = max_invoice_id = 0
103             result[invoice.id] = 0.0
104             if invoice.move_id:
105                 for aml in invoice.move_id.line_id:
106                     if aml.account_id.type in ('receivable','payable'):
107                         if aml.currency_id and aml.currency_id.id == invoice.currency_id.id:
108                             result[invoice.id] += aml.amount_residual_currency
109                         else:
110                             ctx['date'] = aml.date
111                             result[invoice.id] += currency_obj.compute(cr, uid, aml.company_id.currency_id.id, invoice.currency_id.id, aml.amount_residual, context=ctx)
112
113                         if aml.reconcile_partial_id.line_partial_ids:
114                             #we check if the invoice is partially reconciled and if there are other invoices
115                             #involved in this partial reconciliation (and we sum these invoices)
116                             for line in aml.reconcile_partial_id.line_partial_ids:
117                                 if line.invoice:
118                                     nb_inv_in_partial_rec += 1
119                                     #store the max invoice id as for this invoice we will make a balance instead of a simple division
120                                     max_invoice_id = max(max_invoice_id, line.invoice.id)
121             if nb_inv_in_partial_rec:
122                 #if there are several invoices in a partial reconciliation, we split the residual by the number
123                 #of invoice to have a sum of residual amounts that matches the partner balance
124                 new_value = currency_obj.round(cr, uid, invoice.currency_id, result[invoice.id] / nb_inv_in_partial_rec)
125                 if invoice.id == max_invoice_id:
126                     #if it's the last the invoice of the bunch of invoices partially reconciled together, we make a
127                     #balance to avoid rounding errors
128                     result[invoice.id] = result[invoice.id] - ((nb_inv_in_partial_rec - 1) * new_value)
129                 else:
130                     result[invoice.id] = new_value
131
132             #prevent the residual amount on the invoice to be less than 0
133             result[invoice.id] = max(result[invoice.id], 0.0)            
134         return result
135
136     # Give Journal Items related to the payment reconciled to this invoice
137     # Return ids of partial and total payments related to the selected invoices
138     def _get_lines(self, cr, uid, ids, name, arg, context=None):
139         res = {}
140         for invoice in self.browse(cr, uid, ids, context=context):
141             id = invoice.id
142             res[id] = []
143             if not invoice.move_id:
144                 continue
145             data_lines = [x for x in invoice.move_id.line_id if x.account_id.id == invoice.account_id.id]
146             partial_ids = []
147             for line in data_lines:
148                 ids_line = []
149                 if line.reconcile_id:
150                     ids_line = line.reconcile_id.line_id
151                 elif line.reconcile_partial_id:
152                     ids_line = line.reconcile_partial_id.line_partial_ids
153                 l = map(lambda x: x.id, ids_line)
154                 partial_ids.append(line.id)
155                 res[id] =[x for x in l if x <> line.id and x not in partial_ids]
156         return res
157
158     def _get_invoice_line(self, cr, uid, ids, context=None):
159         result = {}
160         for line in self.pool.get('account.invoice.line').browse(cr, uid, ids, context=context):
161             result[line.invoice_id.id] = True
162         return result.keys()
163
164     def _get_invoice_tax(self, cr, uid, ids, context=None):
165         result = {}
166         for tax in self.pool.get('account.invoice.tax').browse(cr, uid, ids, context=context):
167             result[tax.invoice_id.id] = True
168         return result.keys()
169
170     def _compute_lines(self, cr, uid, ids, name, args, context=None):
171         result = {}
172         for invoice in self.browse(cr, uid, ids, context=context):
173             src = []
174             lines = []
175             if invoice.move_id:
176                 for m in invoice.move_id.line_id:
177                     temp_lines = []
178                     if m.reconcile_id:
179                         temp_lines = map(lambda x: x.id, m.reconcile_id.line_id)
180                     elif m.reconcile_partial_id:
181                         temp_lines = map(lambda x: x.id, m.reconcile_partial_id.line_partial_ids)
182                     lines += [x for x in temp_lines if x not in lines]
183                     src.append(m.id)
184
185             lines = filter(lambda x: x not in src, lines)
186             result[invoice.id] = lines
187         return result
188
189     def _get_invoice_from_line(self, cr, uid, ids, context=None):
190         move = {}
191         for line in self.pool.get('account.move.line').browse(cr, uid, ids, context=context):
192             if line.reconcile_partial_id:
193                 for line2 in line.reconcile_partial_id.line_partial_ids:
194                     move[line2.move_id.id] = True
195             if line.reconcile_id:
196                 for line2 in line.reconcile_id.line_id:
197                     move[line2.move_id.id] = True
198         invoice_ids = []
199         if move:
200             invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
201         return invoice_ids
202
203     def _get_invoice_from_reconcile(self, cr, uid, ids, context=None):
204         move = {}
205         for r in self.pool.get('account.move.reconcile').browse(cr, uid, ids, context=context):
206             for line in r.line_partial_ids:
207                 move[line.move_id.id] = True
208             for line in r.line_id:
209                 move[line.move_id.id] = True
210
211         invoice_ids = []
212         if move:
213             invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
214         return invoice_ids
215
216     _name = "account.invoice"
217     _inherit = ['mail.thread']
218     _description = 'Invoice'
219     _order = "id desc"
220     _track = {
221         'type': {
222         },
223         'state': {
224             'account.mt_invoice_paid': lambda self, cr, uid, obj, ctx=None: obj.state == 'paid' and obj.type in ('out_invoice', 'out_refund'),
225             'account.mt_invoice_validated': lambda self, cr, uid, obj, ctx=None: obj.state == 'open' and obj.type in ('out_invoice', 'out_refund'),
226         },
227     }
228     _columns = {
229         'name': fields.char('Description', size=64, select=True, readonly=True, states={'draft':[('readonly',False)]}),
230         'origin': fields.char('Source Document', size=64, help="Reference of the document that produced this invoice.", readonly=True, states={'draft':[('readonly',False)]}),
231         'supplier_invoice_number': fields.char('Supplier Invoice Number', size=64, help="The reference of this invoice as provided by the supplier.", readonly=True, states={'draft':[('readonly',False)]}),
232         'type': fields.selection([
233             ('out_invoice','Customer Invoice'),
234             ('in_invoice','Supplier Invoice'),
235             ('out_refund','Customer Refund'),
236             ('in_refund','Supplier Refund'),
237             ],'Type', readonly=True, select=True, change_default=True, track_visibility='always'),
238
239         'number': fields.related('move_id','name', type='char', readonly=True, size=64, relation='account.move', store=True, string='Number'),
240         'internal_number': fields.char('Invoice Number', size=32, readonly=True, help="Unique number of the invoice, computed automatically when the invoice is created."),
241         'reference': fields.char('Invoice Reference', size=64, help="The partner reference of this invoice."),
242         'reference_type': fields.selection(_get_reference_type, 'Payment Reference',
243             required=True, readonly=True, states={'draft':[('readonly',False)]}),
244         'comment': fields.text('Additional Information'),
245
246         'state': fields.selection([
247             ('draft','Draft'),
248             ('proforma','Pro-forma'),
249             ('proforma2','Pro-forma'),
250             ('open','Open'),
251             ('paid','Paid'),
252             ('cancel','Cancelled'),
253             ],'Status', select=True, readonly=True, track_visibility='onchange',
254             help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed Invoice. \
255             \n* The \'Pro-forma\' when invoice is in Pro-forma status,invoice does not have an invoice number. \
256             \n* The \'Open\' status is used when user create invoice,a invoice number is generated.Its in open status till user does not pay invoice. \
257             \n* The \'Paid\' status is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled. \
258             \n* The \'Cancelled\' status is used when user cancel invoice.'),
259         'sent': fields.boolean('Sent', readonly=True, help="It indicates that the invoice has been sent."),
260         'date_invoice': fields.date('Invoice Date', readonly=True, states={'draft':[('readonly',False)]}, select=True, help="Keep empty to use the current date"),
261         'date_due': fields.date('Due Date', readonly=True, states={'draft':[('readonly',False)]}, select=True,
262             help="If you use payment terms, the due date will be computed automatically at the generation "\
263                 "of accounting entries. The payment term may compute several due dates, for example 50% now and 50% in one month, but if you want to force a due date, make sure that the payment term is not set on the invoice. If you keep the payment term and the due date empty, it means direct payment."),
264         'partner_id': fields.many2one('res.partner', 'Partner', change_default=True, readonly=True, required=True, states={'draft':[('readonly',False)]}, track_visibility='always'),
265         'payment_term': fields.many2one('account.payment.term', 'Payment Terms',readonly=True, states={'draft':[('readonly',False)]},
266             help="If you use payment terms, the due date will be computed automatically at the generation "\
267                 "of accounting entries. If you keep the payment term and the due date empty, it means direct payment. "\
268                 "The payment term may compute several due dates, for example 50% now, 50% in one month."),
269         'period_id': fields.many2one('account.period', 'Force Period', domain=[('state','<>','done')], help="Keep empty to use the period of the validation(invoice) date.", readonly=True, states={'draft':[('readonly',False)]}),
270
271         'account_id': fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="The partner account used for this invoice."),
272         'invoice_line': fields.one2many('account.invoice.line', 'invoice_id', 'Invoice Lines', readonly=True, states={'draft':[('readonly',False)]}),
273         'tax_line': fields.one2many('account.invoice.tax', 'invoice_id', 'Tax Lines', readonly=True, states={'draft':[('readonly',False)]}),
274
275         'move_id': fields.many2one('account.move', 'Journal Entry', readonly=True, select=1, ondelete='restrict', help="Link to the automatically generated Journal Items."),
276         'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Subtotal', track_visibility='always',
277             store={
278                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
279                 'account.invoice.tax': (_get_invoice_tax, None, 20),
280                 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
281             },
282             multi='all'),
283         'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Tax',
284             store={
285                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
286                 'account.invoice.tax': (_get_invoice_tax, None, 20),
287                 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
288             },
289             multi='all'),
290         'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
291             store={
292                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
293                 'account.invoice.tax': (_get_invoice_tax, None, 20),
294                 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
295             },
296             multi='all'),
297         'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}, track_visibility='always'),
298         'journal_id': fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
299         'company_id': fields.many2one('res.company', 'Company', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
300         'check_total': fields.float('Verification Total', digits_compute=dp.get_precision('Account'), readonly=True, states={'draft':[('readonly',False)]}),
301         'reconciled': fields.function(_reconciled, string='Paid/Reconciled', type='boolean',
302             store={
303                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, None, 50), # Check if we can remove ?
304                 'account.move.line': (_get_invoice_from_line, None, 50),
305                 'account.move.reconcile': (_get_invoice_from_reconcile, None, 50),
306             }, help="It indicates that the invoice has been paid and the journal entry of the invoice has been reconciled with one or several journal entries of payment."),
307         'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account',
308             help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Supplier Refund, otherwise a Partner bank account number.', readonly=True, states={'draft':[('readonly',False)]}),
309         'move_lines':fields.function(_get_lines, type='many2many', relation='account.move.line', string='Entry Lines'),
310         'residual': fields.function(_amount_residual, digits_compute=dp.get_precision('Account'), string='Balance',
311             store={
312                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line','move_id'], 50),
313                 'account.invoice.tax': (_get_invoice_tax, None, 50),
314                 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 50),
315                 'account.move.line': (_get_invoice_from_line, None, 50),
316                 'account.move.reconcile': (_get_invoice_from_reconcile, None, 50),
317             },
318             help="Remaining amount due."),
319         'payment_ids': fields.function(_compute_lines, relation='account.move.line', type="many2many", string='Payments'),
320         'move_name': fields.char('Journal Entry', size=64, readonly=True, states={'draft':[('readonly',False)]}),
321         'user_id': fields.many2one('res.users', 'Salesperson', readonly=True, track_visibility='onchange', states={'draft':[('readonly',False)]}),
322         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position', readonly=True, states={'draft':[('readonly',False)]}),
323         'commercial_partner_id': fields.related('partner_id', 'commercial_partner_id', string='Commercial Entity', type='many2one',
324                                                 relation='res.partner', store=True, readonly=True,
325                                                 help="The commercial entity that will be used on Journal Entries for this invoice")
326     }
327     _defaults = {
328         'type': _get_type,
329         'state': 'draft',
330         'journal_id': _get_journal,
331         'currency_id': _get_currency,
332         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.invoice', context=c),
333         'reference_type': 'none',
334         'check_total': 0.0,
335         'internal_number': False,
336         'user_id': lambda s, cr, u, c: u,
337         'sent': False,
338     }
339     _sql_constraints = [
340         ('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
341     ]
342
343
344
345
346     def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
347         journal_obj = self.pool.get('account.journal')
348         if context is None:
349             context = {}
350
351         if context.get('active_model', '') in ['res.partner'] and context.get('active_ids', False) and context['active_ids']:
352             partner = self.pool[context['active_model']].read(cr, uid, context['active_ids'], ['supplier','customer'])[0]
353             if not view_type:
354                 view_id = self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'account.invoice.tree')])
355                 view_type = 'tree'
356             if view_type == 'form':
357                 if partner['supplier'] and not partner['customer']:
358                     view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.supplier.form')])
359                 elif partner['customer'] and not partner['supplier']:
360                     view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.form')])
361         if view_id and isinstance(view_id, (list, tuple)):
362             view_id = view_id[0]
363         res = super(account_invoice,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
364
365         type = context.get('journal_type', False)
366         for field in res['fields']:
367             if field == 'journal_id' and type:
368                 journal_select = journal_obj._name_search(cr, uid, '', [('type', '=', type)], context=context, limit=None, name_get_uid=1)
369                 res['fields'][field]['selection'] = journal_select
370
371         doc = etree.XML(res['arch'])
372
373         if context.get('type', False):
374             for node in doc.xpath("//field[@name='partner_bank_id']"):
375                 if context['type'] == 'in_refund':
376                     node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]")
377                 elif context['type'] == 'out_refund':
378                     node.set('domain', "[('partner_id', '=', partner_id)]")
379             res['arch'] = etree.tostring(doc)
380
381         if view_type == 'search':
382             if context.get('type', 'in_invoice') in ('out_invoice', 'out_refund'):
383                 for node in doc.xpath("//group[@name='extended filter']"):
384                     doc.remove(node)
385             res['arch'] = etree.tostring(doc)
386
387         if view_type == 'tree':
388             partner_string = _('Customer')
389             if context.get('type', 'out_invoice') in ('in_invoice', 'in_refund'):
390                 partner_string = _('Supplier')
391                 for node in doc.xpath("//field[@name='reference']"):
392                     node.set('invisible', '0')
393             for node in doc.xpath("//field[@name='partner_id']"):
394                 node.set('string', partner_string)
395             res['arch'] = etree.tostring(doc)
396         return res
397
398     def get_log_context(self, cr, uid, context=None):
399         if context is None:
400             context = {}
401         res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form')
402         view_id = res and res[1] or False
403         context['view_id'] = view_id
404         return context
405
406     def invoice_print(self, cr, uid, ids, context=None):
407         '''
408         This function prints the invoice and mark it as sent, so that we can see more easily the next step of the workflow
409         '''
410         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
411         self.write(cr, uid, ids, {'sent': True}, context=context)
412         datas = {
413              'ids': ids,
414              'model': 'account.invoice',
415              'form': self.read(cr, uid, ids[0], context=context)
416         }
417         return {
418             'type': 'ir.actions.report.xml',
419             'report_name': 'account.invoice',
420             'datas': datas,
421             'nodestroy' : True
422         }
423
424     def action_invoice_sent(self, cr, uid, ids, context=None):
425         '''
426         This function opens a window to compose an email, with the edi invoice template message loaded by default
427         '''
428         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
429         ir_model_data = self.pool.get('ir.model.data')
430         try:
431             template_id = ir_model_data.get_object_reference(cr, uid, 'account', 'email_template_edi_invoice')[1]
432         except ValueError:
433             template_id = False
434         try:
435             compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
436         except ValueError:
437             compose_form_id = False
438         ctx = dict(context)
439         ctx.update({
440             'default_model': 'account.invoice',
441             'default_res_id': ids[0],
442             'default_use_template': bool(template_id),
443             'default_template_id': template_id,
444             'default_composition_mode': 'comment',
445             'mark_invoice_as_sent': True,
446             })
447         return {
448             'name': _('Compose Email'),
449             'type': 'ir.actions.act_window',
450             'view_type': 'form',
451             'view_mode': 'form',
452             'res_model': 'mail.compose.message',
453             'views': [(compose_form_id, 'form')],
454             'view_id': compose_form_id,
455             'target': 'new',
456             'context': ctx,
457         }
458
459     def confirm_paid(self, cr, uid, ids, context=None):
460         if context is None:
461             context = {}
462         self.write(cr, uid, ids, {'state':'paid'}, context=context)
463         return True
464
465     def unlink(self, cr, uid, ids, context=None):
466         if context is None:
467             context = {}
468         invoices = self.read(cr, uid, ids, ['state','internal_number'], context=context)
469         unlink_ids = []
470
471         for t in invoices:
472             if t['state'] not in ('draft', 'cancel'):
473                 raise openerp.exceptions.Warning(_('You cannot delete an invoice which is not draft or cancelled. You should refund it instead.'))
474             elif t['internal_number']:
475                 raise openerp.exceptions.Warning(_('You cannot delete an invoice after it has been validated (and received a number).  You can set it back to "Draft" state and modify its content, then re-confirm it.'))
476             else:
477                 unlink_ids.append(t['id'])
478
479         osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
480         return True
481
482     def onchange_partner_id(self, cr, uid, ids, type, partner_id,\
483             date_invoice=False, payment_term=False, partner_bank_id=False, company_id=False):
484         partner_payment_term = False
485         acc_id = False
486         bank_id = False
487         fiscal_position = False
488
489         opt = [('uid', str(uid))]
490         if partner_id:
491
492             opt.insert(0, ('id', partner_id))
493             p = self.pool.get('res.partner').browse(cr, uid, partner_id)
494             if company_id:
495                 if (p.property_account_receivable.company_id and (p.property_account_receivable.company_id.id != company_id)) and (p.property_account_payable.company_id and (p.property_account_payable.company_id.id != company_id)):
496                     property_obj = self.pool.get('ir.property')
497                     rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
498                     pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
499                     if not rec_pro_id:
500                         rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('company_id','=',company_id)])
501                     if not pay_pro_id:
502                         pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('company_id','=',company_id)])
503                     rec_line_data = property_obj.read(cr,uid,rec_pro_id,['name','value_reference','res_id'])
504                     pay_line_data = property_obj.read(cr,uid,pay_pro_id,['name','value_reference','res_id'])
505                     rec_res_id = rec_line_data and rec_line_data[0].get('value_reference',False) and int(rec_line_data[0]['value_reference'].split(',')[1]) or False
506                     pay_res_id = pay_line_data and pay_line_data[0].get('value_reference',False) and int(pay_line_data[0]['value_reference'].split(',')[1]) or False
507                     if not rec_res_id and not pay_res_id:
508                         raise osv.except_osv(_('Configuration Error!'),
509                             _('Cannot find a chart of accounts for this company, you should create one.'))
510                     account_obj = self.pool.get('account.account')
511                     rec_obj_acc = account_obj.browse(cr, uid, [rec_res_id])
512                     pay_obj_acc = account_obj.browse(cr, uid, [pay_res_id])
513                     p.property_account_receivable = rec_obj_acc[0]
514                     p.property_account_payable = pay_obj_acc[0]
515
516             if type in ('out_invoice', 'out_refund'):
517                 acc_id = p.property_account_receivable.id
518                 partner_payment_term = p.property_payment_term and p.property_payment_term.id or False
519             else:
520                 acc_id = p.property_account_payable.id
521                 partner_payment_term = p.property_supplier_payment_term and p.property_supplier_payment_term.id or False
522             fiscal_position = p.property_account_position and p.property_account_position.id or False
523             if p.bank_ids:
524                 bank_id = p.bank_ids[0].id
525
526         result = {'value': {
527             'account_id': acc_id,
528             'payment_term': partner_payment_term,
529             'fiscal_position': fiscal_position
530             }
531         }
532
533         if type in ('in_invoice', 'in_refund'):
534             result['value']['partner_bank_id'] = bank_id
535
536         if payment_term != partner_payment_term:
537             if partner_payment_term:
538                 to_update = self.onchange_payment_term_date_invoice(
539                     cr, uid, ids, partner_payment_term, date_invoice)
540                 result['value'].update(to_update['value'])
541             else:
542                 result['value']['date_due'] = False
543
544         if partner_bank_id != bank_id:
545             to_update = self.onchange_partner_bank(cr, uid, ids, bank_id)
546             result['value'].update(to_update['value'])
547         return result
548
549     def onchange_journal_id(self, cr, uid, ids, journal_id=False, context=None):
550         result = {}
551         if journal_id:
552             journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
553             currency_id = journal.currency and journal.currency.id or journal.company_id.currency_id.id
554             company_id = journal.company_id.id
555             result = {'value': {
556                     'currency_id': currency_id,
557                     'company_id': company_id,
558                     }
559                 }
560         return result
561
562     def onchange_payment_term_date_invoice(self, cr, uid, ids, payment_term_id, date_invoice):
563         res = {}
564         if not date_invoice:
565             date_invoice = time.strftime('%Y-%m-%d')
566         if not payment_term_id:
567             return {'value':{'date_due': date_invoice}} #To make sure the invoice has a due date when no payment term
568         pterm_list = self.pool.get('account.payment.term').compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice)
569         if pterm_list:
570             pterm_list = [line[0] for line in pterm_list]
571             pterm_list.sort()
572             res = {'value':{'date_due': pterm_list[-1]}}
573         else:
574              raise osv.except_osv(_('Insufficient Data!'), _('The payment term of supplier does not have a payment term line.'))
575         return res
576
577     def onchange_invoice_line(self, cr, uid, ids, lines):
578         return {}
579
580     def onchange_partner_bank(self, cursor, user, ids, partner_bank_id=False):
581         return {'value': {}}
582
583     def onchange_company_id(self, cr, uid, ids, company_id, part_id, type, invoice_line, currency_id, context=None):
584         #TODO: add the missing context parameter when forward-porting in trunk so we can remove
585         #      this hack!
586         context = self.pool['res.users'].context_get(cr, uid)
587
588         val = {}
589         dom = {}
590         obj_journal = self.pool.get('account.journal')
591         account_obj = self.pool.get('account.account')
592         inv_line_obj = self.pool.get('account.invoice.line')
593
594         if company_id and part_id and type:
595             acc_id = False
596             partner_obj = self.pool.get('res.partner').browse(cr, uid, part_id, context=context)
597
598             if partner_obj.property_account_payable and partner_obj.property_account_receivable:
599                 if partner_obj.property_account_payable.company_id.id != company_id and partner_obj.property_account_receivable.company_id.id != company_id:
600                     property_obj = self.pool.get('ir.property')
601                     rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
602                     pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
603
604                     if not rec_pro_id:
605                         rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('company_id','=',company_id)])
606                     if not pay_pro_id:
607                         pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('company_id','=',company_id)])
608
609                     rec_line_data = property_obj.read(cr, uid, rec_pro_id, ['name','value_reference','res_id'])
610                     pay_line_data = property_obj.read(cr, uid, pay_pro_id, ['name','value_reference','res_id'])
611                     rec_res_id = rec_line_data and rec_line_data[0].get('value_reference',False) and int(rec_line_data[0]['value_reference'].split(',')[1]) or False
612                     pay_res_id = pay_line_data and pay_line_data[0].get('value_reference',False) and int(pay_line_data[0]['value_reference'].split(',')[1]) or False
613
614                     if not rec_res_id and not pay_res_id:
615                         raise self.pool.get('res.config.settings').get_config_warning(cr, _('Cannot find any chart of account: you can create a new one from %(menu:account.menu_account_config)s.'), context=context)
616
617                     if type in ('out_invoice', 'out_refund'):
618                         acc_id = rec_res_id
619                     else:
620                         acc_id = pay_res_id
621
622                     val= {'account_id': acc_id}
623             if ids:
624                 if company_id:
625                     inv_obj = self.browse(cr,uid,ids)
626                     for line in inv_obj[0].invoice_line:
627                         if line.account_id:
628                             if line.account_id.company_id.id != company_id:
629                                 result_id = account_obj.search(cr, uid, [('name','=',line.account_id.name),('company_id','=',company_id)])
630                                 if not result_id:
631                                     raise osv.except_osv(_('Configuration Error!'),
632                                         _('Cannot find a chart of account, you should create one from Settings\Configuration\Accounting menu.'))
633                                 inv_line_obj.write(cr, uid, [line.id], {'account_id': result_id[-1]})
634             else:
635                 if invoice_line:
636                     for inv_line in invoice_line:
637                         obj_l = account_obj.browse(cr, uid, inv_line[2]['account_id'])
638                         if obj_l.company_id.id != company_id:
639                             raise osv.except_osv(_('Configuration Error!'),
640                                 _('Invoice line account\'s company and invoice\'s company does not match.'))
641                         else:
642                             continue
643         if company_id and type:
644             journal_mapping = {
645                'out_invoice': 'sale',
646                'out_refund': 'sale_refund',
647                'in_refund': 'purchase_refund',
648                'in_invoice': 'purchase',
649             }
650             journal_type = journal_mapping[type]
651             journal_ids = obj_journal.search(cr, uid, [('company_id','=',company_id), ('type', '=', journal_type)])
652             if journal_ids:
653                 val['journal_id'] = journal_ids[0]
654             ir_values_obj = self.pool.get('ir.values')
655             res_journal_default = ir_values_obj.get(cr, uid, 'default', 'type=%s' % (type), ['account.invoice'])
656             for r in res_journal_default:
657                 if r[1] == 'journal_id' and r[2] in journal_ids:
658                     val['journal_id'] = r[2]
659             if not val.get('journal_id', False):
660                 journal_type_map = dict(obj_journal._columns['type'].selection)
661                 journal_type_label = self.pool['ir.translation']._get_source(cr, uid, None, ('code','selection'),
662                                                                              context.get('lang'),
663                                                                              journal_type_map.get(journal_type))
664                 raise osv.except_osv(_('Configuration Error!'),
665                                      _('Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.') % ('"%s"' % journal_type_label))
666             dom = {'journal_id':  [('id', 'in', journal_ids)]}
667         else:
668             journal_ids = obj_journal.search(cr, uid, [])
669
670         return {'value': val, 'domain': dom}
671
672     # go from canceled state to draft state
673     def action_cancel_draft(self, cr, uid, ids, *args):
674         self.write(cr, uid, ids, {'state':'draft'})
675         self.delete_workflow(cr, uid, ids)
676         self.create_workflow(cr, uid, ids)
677         return True
678
679     # ----------------------------------------
680     # Mail related methods
681     # ----------------------------------------
682
683     def _get_formview_action(self, cr, uid, id, context=None):
684         """ Update form view id of action to open the invoice """
685         action = super(account_invoice, self)._get_formview_action(cr, uid, id, context=context)
686         obj = self.browse(cr, uid, id, context=context)
687         if obj.type == 'in_invoice':
688             model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
689             action.update({
690                 'views': [(view_id, 'form')],
691                 })
692         else:
693             model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form')
694             action.update({
695                 'views': [(view_id, 'form')],
696                 })
697         return action
698
699     # Workflow stuff
700     #################
701
702     # return the ids of the move lines which has the same account than the invoice
703     # whose id is in ids
704     def move_line_id_payment_get(self, cr, uid, ids, *args):
705         if not ids: return []
706         result = self.move_line_id_payment_gets(cr, uid, ids, *args)
707         return result.get(ids[0], [])
708
709     def move_line_id_payment_gets(self, cr, uid, ids, *args):
710         res = {}
711         if not ids: return res
712         cr.execute('SELECT i.id, l.id '\
713                    'FROM account_move_line l '\
714                    'LEFT JOIN account_invoice i ON (i.move_id=l.move_id) '\
715                    'WHERE i.id IN %s '\
716                    'AND l.account_id=i.account_id',
717                    (tuple(ids),))
718         for r in cr.fetchall():
719             res.setdefault(r[0], [])
720             res[r[0]].append( r[1] )
721         return res
722
723     def copy(self, cr, uid, id, default=None, context=None):
724         default = default or {}
725         default.update({
726             'state':'draft',
727             'number':False,
728             'move_id':False,
729             'move_name':False,
730             'internal_number': False,
731             'period_id': False,
732             'sent': False,
733         })
734         if 'date_invoice' not in default:
735             default.update({
736                 'date_invoice':False
737             })
738         if 'date_due' not in default:
739             default.update({
740                 'date_due':False
741             })
742         return super(account_invoice, self).copy(cr, uid, id, default, context)
743
744     def test_paid(self, cr, uid, ids, *args):
745         res = self.move_line_id_payment_get(cr, uid, ids)
746         if not res:
747             return False
748         ok = True
749         for id in res:
750             cr.execute('select reconcile_id from account_move_line where id=%s', (id,))
751             ok = ok and  bool(cr.fetchone()[0])
752         return ok
753
754     def button_reset_taxes(self, cr, uid, ids, context=None):
755         if context is None:
756             context = {}
757         ctx = context.copy()
758         ait_obj = self.pool.get('account.invoice.tax')
759         for id in ids:
760             cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (id,))
761             partner = self.browse(cr, uid, id, context=ctx).partner_id
762             if partner.lang:
763                 ctx.update({'lang': partner.lang})
764             for taxe in ait_obj.compute(cr, uid, id, context=ctx).values():
765                 ait_obj.create(cr, uid, taxe)
766         # Update the stored value (fields.function), so we write to trigger recompute
767         self.pool.get('account.invoice').write(cr, uid, ids, {'invoice_line':[]}, context=ctx)
768         return True
769
770     def button_compute(self, cr, uid, ids, context=None, set_total=False):
771         self.button_reset_taxes(cr, uid, ids, context)
772         for inv in self.browse(cr, uid, ids, context=context):
773             if set_total:
774                 self.pool.get('account.invoice').write(cr, uid, [inv.id], {'check_total': inv.amount_total})
775         return True
776
777     def _convert_ref(self, cr, uid, ref):
778         return (ref or '').replace('/','')
779
780     def _get_analytic_lines(self, cr, uid, id, context=None):
781         if context is None:
782             context = {}
783         inv = self.browse(cr, uid, id)
784         cur_obj = self.pool.get('res.currency')
785
786         company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id
787         if inv.type in ('out_invoice', 'in_refund'):
788             sign = 1
789         else:
790             sign = -1
791
792         iml = self.pool.get('account.invoice.line').move_line_get(cr, uid, inv.id, context=context)
793         for il in iml:
794             if il['account_analytic_id']:
795                 if inv.type in ('in_invoice', 'in_refund'):
796                     ref = inv.reference
797                 else:
798                     ref = self._convert_ref(cr, uid, inv.number)
799                 if not inv.journal_id.analytic_journal_id:
800                     raise osv.except_osv(_('No Analytic Journal!'),_("You have to define an analytic journal on the '%s' journal!") % (inv.journal_id.name,))
801                 il['analytic_lines'] = [(0,0, {
802                     'name': il['name'],
803                     'date': inv['date_invoice'],
804                     'account_id': il['account_analytic_id'],
805                     'unit_amount': il['quantity'],
806                     'amount': cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, il['price'], context={'date': inv.date_invoice}) * sign,
807                     'product_id': il['product_id'],
808                     'product_uom_id': il['uos_id'],
809                     'general_account_id': il['account_id'],
810                     'journal_id': inv.journal_id.analytic_journal_id.id,
811                     'ref': ref,
812                 })]
813         return iml
814
815     def action_date_assign(self, cr, uid, ids, *args):
816         for inv in self.browse(cr, uid, ids):
817             res = self.onchange_payment_term_date_invoice(cr, uid, inv.id, inv.payment_term.id, inv.date_invoice)
818             if res and res['value']:
819                 self.write(cr, uid, [inv.id], res['value'])
820         return True
821
822     def finalize_invoice_move_lines(self, cr, uid, invoice_browse, move_lines):
823         """finalize_invoice_move_lines(cr, uid, invoice, move_lines) -> move_lines
824         Hook method to be overridden in additional modules to verify and possibly alter the
825         move lines to be created by an invoice, for special cases.
826         :param invoice_browse: browsable record of the invoice that is generating the move lines
827         :param move_lines: list of dictionaries with the account.move.lines (as for create())
828         :return: the (possibly updated) final move_lines to create for this invoice
829         """
830         return move_lines
831
832     def check_tax_lines(self, cr, uid, inv, compute_taxes, ait_obj):
833         company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id
834         if not inv.tax_line:
835             for tax in compute_taxes.values():
836                 ait_obj.create(cr, uid, tax)
837         else:
838             tax_key = []
839             for tax in inv.tax_line:
840                 if tax.manual:
841                     continue
842                 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id, tax.account_analytic_id.id)
843                 tax_key.append(key)
844                 if not key in compute_taxes:
845                     raise osv.except_osv(_('Warning!'), _('Global taxes defined, but they are not in invoice lines !'))
846                 base = compute_taxes[key]['base']
847                 if abs(base - tax.base) > company_currency.rounding:
848                     raise osv.except_osv(_('Warning!'), _('Tax base different!\nClick on compute to update the tax base.'))
849             for key in compute_taxes:
850                 if not key in tax_key:
851                     raise osv.except_osv(_('Warning!'), _('Taxes are missing!\nClick on compute button.'))
852
853     def compute_invoice_totals(self, cr, uid, inv, company_currency, ref, invoice_move_lines, context=None):
854         if context is None:
855             context={}
856         total = 0
857         total_currency = 0
858         cur_obj = self.pool.get('res.currency')
859         for i in invoice_move_lines:
860             if inv.currency_id.id != company_currency:
861                 context.update({'date': inv.date_invoice or time.strftime('%Y-%m-%d')})
862                 i['currency_id'] = inv.currency_id.id
863                 i['amount_currency'] = i['price']
864                 i['price'] = cur_obj.compute(cr, uid, inv.currency_id.id,
865                         company_currency, i['price'],
866                         context=context)
867             else:
868                 i['amount_currency'] = False
869                 i['currency_id'] = False
870             i['ref'] = ref
871             if inv.type in ('out_invoice','in_refund'):
872                 total += i['price']
873                 total_currency += i['amount_currency'] or i['price']
874                 i['price'] = - i['price']
875             else:
876                 total -= i['price']
877                 total_currency -= i['amount_currency'] or i['price']
878         return total, total_currency, invoice_move_lines
879
880     def inv_line_characteristic_hashcode(self, invoice, invoice_line):
881         """Overridable hashcode generation for invoice lines. Lines having the same hashcode
882         will be grouped together if the journal has the 'group line' option. Of course a module
883         can add fields to invoice lines that would need to be tested too before merging lines
884         or not."""
885         return "%s-%s-%s-%s-%s"%(
886             invoice_line['account_id'],
887             invoice_line.get('tax_code_id',"False"),
888             invoice_line.get('product_id',"False"),
889             invoice_line.get('analytic_account_id',"False"),
890             invoice_line.get('date_maturity',"False"))
891
892     def group_lines(self, cr, uid, iml, line, inv):
893         """Merge account move lines (and hence analytic lines) if invoice line hashcodes are equals"""
894         if inv.journal_id.group_invoice_lines:
895             line2 = {}
896             for x, y, l in line:
897                 tmp = self.inv_line_characteristic_hashcode(inv, l)
898
899                 if tmp in line2:
900                     am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
901                     line2[tmp]['debit'] = (am > 0) and am or 0.0
902                     line2[tmp]['credit'] = (am < 0) and -am or 0.0
903                     line2[tmp]['tax_amount'] += l['tax_amount']
904                     line2[tmp]['analytic_lines'] += l['analytic_lines']
905                 else:
906                     line2[tmp] = l
907             line = []
908             for key, val in line2.items():
909                 line.append((0,0,val))
910         return line
911
912     def action_move_create(self, cr, uid, ids, context=None):
913         """Creates invoice related analytics and financial move lines"""
914         ait_obj = self.pool.get('account.invoice.tax')
915         cur_obj = self.pool.get('res.currency')
916         period_obj = self.pool.get('account.period')
917         payment_term_obj = self.pool.get('account.payment.term')
918         journal_obj = self.pool.get('account.journal')
919         move_obj = self.pool.get('account.move')
920         if context is None:
921             context = {}
922         for inv in self.browse(cr, uid, ids, context=context):
923             if not inv.journal_id.sequence_id:
924                 raise osv.except_osv(_('Error!'), _('Please define sequence on the journal related to this invoice.'))
925             if not inv.invoice_line:
926                 raise osv.except_osv(_('No Invoice Lines!'), _('Please create some invoice lines.'))
927             if inv.move_id:
928                 continue
929
930             ctx = context.copy()
931             ctx.update({'lang': inv.partner_id.lang})
932             if not inv.date_invoice:
933                 self.write(cr, uid, [inv.id], {'date_invoice': fields.date.context_today(self,cr,uid,context=context)}, context=ctx)
934             company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id
935             # create the analytical lines
936             # one move line per invoice line
937             iml = self._get_analytic_lines(cr, uid, inv.id, context=ctx)
938             # check if taxes are all computed
939             compute_taxes = ait_obj.compute(cr, uid, inv.id, context=ctx)
940             self.check_tax_lines(cr, uid, inv, compute_taxes, ait_obj)
941
942             # I disabled the check_total feature
943             group_check_total_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'group_supplier_inv_check_total')[1]
944             group_check_total = self.pool.get('res.groups').browse(cr, uid, group_check_total_id, context=context)
945             if group_check_total and uid in [x.id for x in group_check_total.users]:
946                 if (inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0)):
947                     raise osv.except_osv(_('Bad Total!'), _('Please verify the price of the invoice!\nThe encoded total does not match the computed total.'))
948
949             if inv.payment_term:
950                 total_fixed = total_percent = 0
951                 for line in inv.payment_term.line_ids:
952                     if line.value == 'fixed':
953                         total_fixed += line.value_amount
954                     if line.value == 'procent':
955                         total_percent += line.value_amount
956                 total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
957                 if (total_fixed + total_percent) > 100:
958                     raise osv.except_osv(_('Error!'), _("Cannot create the invoice.\nThe related payment term is probably misconfigured as it gives a computed amount greater than the total invoiced amount. In order to avoid rounding issues, the latest line of your payment term must be of type 'balance'."))
959
960             # one move line per tax line
961             iml += ait_obj.move_line_get(cr, uid, inv.id)
962
963             entry_type = ''
964             if inv.type in ('in_invoice', 'in_refund'):
965                 ref = inv.reference
966                 entry_type = 'journal_pur_voucher'
967                 if inv.type == 'in_refund':
968                     entry_type = 'cont_voucher'
969             else:
970                 ref = self._convert_ref(cr, uid, inv.number)
971                 entry_type = 'journal_sale_vou'
972                 if inv.type == 'out_refund':
973                     entry_type = 'cont_voucher'
974
975             diff_currency_p = inv.currency_id.id <> company_currency
976             # create one move line for the total and possibly adjust the other lines amount
977             total = 0
978             total_currency = 0
979             total, total_currency, iml = self.compute_invoice_totals(cr, uid, inv, company_currency, ref, iml, context=ctx)
980             acc_id = inv.account_id.id
981
982             name = inv['name'] or inv['supplier_invoice_number'] or '/'
983             totlines = False
984             if inv.payment_term:
985                 totlines = payment_term_obj.compute(cr,
986                         uid, inv.payment_term.id, total, inv.date_invoice or False, context=ctx)
987             if totlines:
988                 res_amount_currency = total_currency
989                 i = 0
990                 ctx.update({'date': inv.date_invoice})
991                 for t in totlines:
992                     if inv.currency_id.id != company_currency:
993                         amount_currency = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, t[1], context=ctx)
994                     else:
995                         amount_currency = False
996
997                     # last line add the diff
998                     res_amount_currency -= amount_currency or 0
999                     i += 1
1000                     if i == len(totlines):
1001                         amount_currency += res_amount_currency
1002
1003                     iml.append({
1004                         'type': 'dest',
1005                         'name': name,
1006                         'price': t[1],
1007                         'account_id': acc_id,
1008                         'date_maturity': t[0],
1009                         'amount_currency': diff_currency_p \
1010                                 and amount_currency or False,
1011                         'currency_id': diff_currency_p \
1012                                 and inv.currency_id.id or False,
1013                         'ref': ref,
1014                     })
1015             else:
1016                 iml.append({
1017                     'type': 'dest',
1018                     'name': name,
1019                     'price': total,
1020                     'account_id': acc_id,
1021                     'date_maturity': inv.date_due or False,
1022                     'amount_currency': diff_currency_p \
1023                             and total_currency or False,
1024                     'currency_id': diff_currency_p \
1025                             and inv.currency_id.id or False,
1026                     'ref': ref
1027             })
1028
1029             date = inv.date_invoice or time.strftime('%Y-%m-%d')
1030
1031             part = self.pool.get("res.partner")._find_accounting_partner(inv.partner_id)
1032
1033             line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part.id, date, context=ctx)),iml)
1034
1035             line = self.group_lines(cr, uid, iml, line, inv)
1036
1037             journal_id = inv.journal_id.id
1038             journal = journal_obj.browse(cr, uid, journal_id, context=ctx)
1039             if journal.centralisation:
1040                 raise osv.except_osv(_('User Error!'),
1041                         _('You cannot create an invoice on a centralized journal. Uncheck the centralized counterpart box in the related journal from the configuration menu.'))
1042
1043             line = self.finalize_invoice_move_lines(cr, uid, inv, line)
1044
1045             move = {
1046                 'ref': inv.reference and inv.reference or inv.name,
1047                 'line_id': line,
1048                 'journal_id': journal_id,
1049                 'date': date,
1050                 'narration': inv.comment,
1051                 'company_id': inv.company_id.id,
1052             }
1053             period_id = inv.period_id and inv.period_id.id or False
1054             ctx.update(company_id=inv.company_id.id)
1055             if not period_id:
1056                 period_ids = period_obj.find(cr, uid, inv.date_invoice, context=ctx)
1057                 period_id = period_ids and period_ids[0] or False
1058             if period_id:
1059                 move['period_id'] = period_id
1060                 for i in line:
1061                     i[2]['period_id'] = period_id
1062
1063             ctx.update(invoice=inv)
1064             move_id = move_obj.create(cr, uid, move, context=ctx)
1065             new_move_name = move_obj.browse(cr, uid, move_id, context=ctx).name
1066             # make the invoice point to that move
1067             self.write(cr, uid, [inv.id], {'move_id': move_id,'period_id':period_id, 'move_name':new_move_name}, context=ctx)
1068             # Pass invoice in context in method post: used if you want to get the same
1069             # account move reference when creating the same invoice after a cancelled one:
1070             move_obj.post(cr, uid, [move_id], context=ctx)
1071         self._log_event(cr, uid, ids)
1072         return True
1073
1074     def invoice_validate(self, cr, uid, ids, context=None):
1075         self.write(cr, uid, ids, {'state':'open'}, context=context)
1076         return True
1077
1078     def line_get_convert(self, cr, uid, x, part, date, context=None):
1079         return {
1080             'date_maturity': x.get('date_maturity', False),
1081             'partner_id': part,
1082             'name': x['name'][:64],
1083             'date': date,
1084             'debit': x['price']>0 and x['price'],
1085             'credit': x['price']<0 and -x['price'],
1086             'account_id': x['account_id'],
1087             'analytic_lines': x.get('analytic_lines', []),
1088             'amount_currency': x['price']>0 and abs(x.get('amount_currency', False)) or -abs(x.get('amount_currency', False)),
1089             'currency_id': x.get('currency_id', False),
1090             'tax_code_id': x.get('tax_code_id', False),
1091             'tax_amount': x.get('tax_amount', False),
1092             'ref': x.get('ref', False),
1093             'quantity': x.get('quantity',1.00),
1094             'product_id': x.get('product_id', False),
1095             'product_uom_id': x.get('uos_id', False),
1096             'analytic_account_id': x.get('account_analytic_id', False),
1097         }
1098
1099     def action_number(self, cr, uid, ids, context=None):
1100         if context is None:
1101             context = {}
1102         #TODO: not correct fix but required a frech values before reading it.
1103         self.write(cr, uid, ids, {})
1104
1105         for obj_inv in self.browse(cr, uid, ids, context=context):
1106             invtype = obj_inv.type
1107             number = obj_inv.number
1108             move_id = obj_inv.move_id and obj_inv.move_id.id or False
1109             reference = obj_inv.reference or ''
1110
1111             self.write(cr, uid, ids, {'internal_number': number})
1112
1113             if invtype in ('in_invoice', 'in_refund'):
1114                 if not reference:
1115                     ref = self._convert_ref(cr, uid, number)
1116                 else:
1117                     ref = reference
1118             else:
1119                 ref = self._convert_ref(cr, uid, number)
1120
1121             cr.execute('UPDATE account_move SET ref=%s ' \
1122                     'WHERE id=%s AND (ref is null OR ref = \'\')',
1123                     (ref, move_id))
1124             cr.execute('UPDATE account_move_line SET ref=%s ' \
1125                     'WHERE move_id=%s AND (ref is null OR ref = \'\')',
1126                     (ref, move_id))
1127             cr.execute('UPDATE account_analytic_line SET ref=%s ' \
1128                     'FROM account_move_line ' \
1129                     'WHERE account_move_line.move_id = %s ' \
1130                         'AND account_analytic_line.move_id = account_move_line.id',
1131                         (ref, move_id))
1132         return True
1133
1134     def action_cancel(self, cr, uid, ids, context=None):
1135         if context is None:
1136             context = {}
1137         account_move_obj = self.pool.get('account.move')
1138         invoices = self.read(cr, uid, ids, ['move_id', 'payment_ids'])
1139         move_ids = [] # ones that we will need to remove
1140         for i in invoices:
1141             if i['move_id']:
1142                 move_ids.append(i['move_id'][0])
1143             if i['payment_ids']:
1144                 account_move_line_obj = self.pool.get('account.move.line')
1145                 pay_ids = account_move_line_obj.browse(cr, uid, i['payment_ids'])
1146                 for move_line in pay_ids:
1147                     if move_line.reconcile_partial_id and move_line.reconcile_partial_id.line_partial_ids:
1148                         raise osv.except_osv(_('Error!'), _('You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first.'))
1149
1150         # First, set the invoices as cancelled and detach the move ids
1151         self.write(cr, uid, ids, {'state':'cancel', 'move_id':False})
1152         if move_ids:
1153             # second, invalidate the move(s)
1154             account_move_obj.button_cancel(cr, uid, move_ids, context=context)
1155             # delete the move this invoice was pointing to
1156             # Note that the corresponding move_lines and move_reconciles
1157             # will be automatically deleted too
1158             account_move_obj.unlink(cr, uid, move_ids, context=context)
1159         self._log_event(cr, uid, ids, -1.0, 'Cancel Invoice')
1160         return True
1161
1162     ###################
1163
1164     def list_distinct_taxes(self, cr, uid, ids):
1165         invoices = self.browse(cr, uid, ids)
1166         taxes = {}
1167         for inv in invoices:
1168             for tax in inv.tax_line:
1169                 if not tax['name'] in taxes:
1170                     taxes[tax['name']] = {'name': tax['name']}
1171         return taxes.values()
1172
1173     def _log_event(self, cr, uid, ids, factor=1.0, name='Open Invoice'):
1174         #TODO: implement messages system
1175         return True
1176
1177     def name_get(self, cr, uid, ids, context=None):
1178         if not ids:
1179             return []
1180         types = {
1181                 'out_invoice': _('Invoice'),
1182                 'in_invoice': _('Supplier Invoice'),
1183                 'out_refund': _('Refund'),
1184                 'in_refund': _('Supplier Refund'),
1185                 }
1186         return [(r['id'], '%s %s' % (r['number'] or types[r['type']], r['name'] or '')) for r in self.read(cr, uid, ids, ['type', 'number', 'name'], context, load='_classic_write')]
1187
1188     def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
1189         if not args:
1190             args = []
1191         if context is None:
1192             context = {}
1193         ids = []
1194         if name:
1195             ids = self.search(cr, user, [('number','=',name)] + args, limit=limit, context=context)
1196         if not ids:
1197             ids = self.search(cr, user, [('name',operator,name)] + args, limit=limit, context=context)
1198         return self.name_get(cr, user, ids, context)
1199
1200     def _refund_cleanup_lines(self, cr, uid, lines, context=None):
1201         """Convert records to dict of values suitable for one2many line creation
1202
1203             :param list(browse_record) lines: records to convert
1204             :return: list of command tuple for one2many line creation [(0, 0, dict of valueis), ...]
1205         """
1206         clean_lines = []
1207         for line in lines:
1208             clean_line = {}
1209             for field in line._all_columns.keys():
1210                 if line._all_columns[field].column._type == 'many2one':
1211                     clean_line[field] = line[field].id
1212                 elif line._all_columns[field].column._type not in ['many2many','one2many']:
1213                     clean_line[field] = line[field]
1214                 elif field == 'invoice_line_tax_id':
1215                     tax_list = []
1216                     for tax in line[field]:
1217                         tax_list.append(tax.id)
1218                     clean_line[field] = [(6,0, tax_list)]
1219             clean_lines.append(clean_line)
1220         return map(lambda x: (0,0,x), clean_lines)
1221
1222     def _prepare_refund(self, cr, uid, invoice, date=None, period_id=None, description=None, journal_id=None, context=None):
1223         """Prepare the dict of values to create the new refund from the invoice.
1224             This method may be overridden to implement custom
1225             refund generation (making sure to call super() to establish
1226             a clean extension chain).
1227
1228             :param integer invoice_id: id of the invoice to refund
1229             :param dict invoice: read of the invoice to refund
1230             :param string date: refund creation date from the wizard
1231             :param integer period_id: force account.period from the wizard
1232             :param string description: description of the refund from the wizard
1233             :param integer journal_id: account.journal from the wizard
1234             :return: dict of value to create() the refund
1235         """
1236         obj_journal = self.pool.get('account.journal')
1237
1238         type_dict = {
1239             'out_invoice': 'out_refund', # Customer Invoice
1240             'in_invoice': 'in_refund',   # Supplier Invoice
1241             'out_refund': 'out_invoice', # Customer Refund
1242             'in_refund': 'in_invoice',   # Supplier Refund
1243         }
1244         invoice_data = {}
1245         for field in ['name', 'reference', 'comment', 'date_due', 'partner_id', 'company_id',
1246                 'account_id', 'currency_id', 'payment_term', 'user_id', 'fiscal_position']:
1247             if invoice._all_columns[field].column._type == 'many2one':
1248                 invoice_data[field] = invoice[field].id
1249             else:
1250                 invoice_data[field] = invoice[field] if invoice[field] else False
1251
1252         invoice_lines = self._refund_cleanup_lines(cr, uid, invoice.invoice_line, context=context)
1253
1254         tax_lines = filter(lambda l: l['manual'], invoice.tax_line)
1255         tax_lines = self._refund_cleanup_lines(cr, uid, tax_lines, context=context)
1256         if journal_id:
1257             refund_journal_ids = [journal_id]
1258         elif invoice['type'] == 'in_invoice':
1259             refund_journal_ids = obj_journal.search(cr, uid, [('type','=','purchase_refund')], context=context)
1260         else:
1261             refund_journal_ids = obj_journal.search(cr, uid, [('type','=','sale_refund')], context=context)
1262
1263         if not date:
1264             date = time.strftime('%Y-%m-%d')
1265         invoice_data.update({
1266             'type': type_dict[invoice['type']],
1267             'date_invoice': date,
1268             'state': 'draft',
1269             'number': False,
1270             'invoice_line': invoice_lines,
1271             'tax_line': tax_lines,
1272             'journal_id': refund_journal_ids and refund_journal_ids[0] or False,
1273         })
1274         if period_id:
1275             invoice_data['period_id'] = period_id
1276         if description:
1277             invoice_data['name'] = description
1278         return invoice_data
1279
1280     def refund(self, cr, uid, ids, date=None, period_id=None, description=None, journal_id=None, context=None):
1281         new_ids = []
1282         for invoice in self.browse(cr, uid, ids, context=context):
1283             invoice = self._prepare_refund(cr, uid, invoice,
1284                                                 date=date,
1285                                                 period_id=period_id,
1286                                                 description=description,
1287                                                 journal_id=journal_id,
1288                                                 context=context)
1289             # create the new invoice
1290             new_ids.append(self.create(cr, uid, invoice, context=context))
1291
1292         return new_ids
1293
1294     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=None, name=''):
1295         if context is None:
1296             context = {}
1297         #TODO check if we can use different period for payment and the writeoff line
1298         assert len(ids)==1, "Can only pay one invoice at a time."
1299         invoice = self.browse(cr, uid, ids[0], context=context)
1300         src_account_id = invoice.account_id.id
1301         # Take the seq as name for move
1302         types = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1303         direction = types[invoice.type]
1304         #take the choosen date
1305         if 'date_p' in context and context['date_p']:
1306             date=context['date_p']
1307         else:
1308             date=time.strftime('%Y-%m-%d')
1309
1310         # Take the amount in currency and the currency of the payment
1311         if 'amount_currency' in context and context['amount_currency'] and 'currency_id' in context and context['currency_id']:
1312             amount_currency = context['amount_currency']
1313             currency_id = context['currency_id']
1314         else:
1315             amount_currency = False
1316             currency_id = False
1317
1318         pay_journal = self.pool.get('account.journal').read(cr, uid, pay_journal_id, ['type'], context=context)
1319         if invoice.type in ('in_invoice', 'out_invoice'):
1320             if pay_journal['type'] == 'bank':
1321                 entry_type = 'bank_pay_voucher' # Bank payment
1322             else:
1323                 entry_type = 'pay_voucher' # Cash payment
1324         else:
1325             entry_type = 'cont_voucher'
1326         if invoice.type in ('in_invoice', 'in_refund'):
1327             ref = invoice.reference
1328         else:
1329             ref = self._convert_ref(cr, uid, invoice.number)
1330         partner = self.pool['res.partner']._find_accounting_partner(invoice.partner_id)
1331         # Pay attention to the sign for both debit/credit AND amount_currency
1332         l1 = {
1333             'debit': direction * pay_amount>0 and direction * pay_amount,
1334             'credit': direction * pay_amount<0 and - direction * pay_amount,
1335             'account_id': src_account_id,
1336             'partner_id': partner.id,
1337             'ref':ref,
1338             'date': date,
1339             'currency_id':currency_id,
1340             'amount_currency':amount_currency and direction * amount_currency or 0.0,
1341             'company_id': invoice.company_id.id,
1342         }
1343         l2 = {
1344             'debit': direction * pay_amount<0 and - direction * pay_amount,
1345             'credit': direction * pay_amount>0 and direction * pay_amount,
1346             'account_id': pay_account_id,
1347             'partner_id': partner.id,
1348             'ref':ref,
1349             'date': date,
1350             'currency_id':currency_id,
1351             'amount_currency':amount_currency and - direction * amount_currency or 0.0,
1352             'company_id': invoice.company_id.id,
1353         }
1354
1355         if not name:
1356             name = invoice.invoice_line and invoice.invoice_line[0].name or invoice.number
1357         l1['name'] = name
1358         l2['name'] = name
1359
1360         lines = [(0, 0, l1), (0, 0, l2)]
1361         move = {'ref': ref, 'line_id': lines, 'journal_id': pay_journal_id, 'period_id': period_id, 'date': date}
1362         move_id = self.pool.get('account.move').create(cr, uid, move, context=context)
1363
1364         line_ids = []
1365         total = 0.0
1366         line = self.pool.get('account.move.line')
1367         move_ids = [move_id,]
1368         if invoice.move_id:
1369             move_ids.append(invoice.move_id.id)
1370         cr.execute('SELECT id FROM account_move_line '\
1371                    'WHERE move_id IN %s',
1372                    ((move_id, invoice.move_id.id),))
1373         lines = line.browse(cr, uid, map(lambda x: x[0], cr.fetchall()) )
1374         for l in lines+invoice.payment_ids:
1375             if l.account_id.id == src_account_id:
1376                 line_ids.append(l.id)
1377                 total += (l.debit or 0.0) - (l.credit or 0.0)
1378
1379         inv_id, name = self.name_get(cr, uid, [invoice.id], context=context)[0]
1380         if (not round(total,self.pool.get('decimal.precision').precision_get(cr, uid, 'Account'))) or writeoff_acc_id:
1381             self.pool.get('account.move.line').reconcile(cr, uid, line_ids, 'manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context)
1382         else:
1383             code = invoice.currency_id.symbol
1384             # TODO: use currency's formatting function
1385             msg = _("Invoice partially paid: %s%s of %s%s (%s%s remaining).") % \
1386                     (pay_amount, code, invoice.amount_total, code, total, code)
1387             self.message_post(cr, uid, [inv_id], body=msg, context=context)
1388             self.pool.get('account.move.line').reconcile_partial(cr, uid, line_ids, 'manual', context)
1389
1390         # Update the stored value (fields.function), so we write to trigger recompute
1391         self.pool.get('account.invoice').write(cr, uid, ids, {}, context=context)
1392         return True
1393
1394
1395 class account_invoice_line(osv.osv):
1396
1397     def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict):
1398         res = {}
1399         tax_obj = self.pool.get('account.tax')
1400         cur_obj = self.pool.get('res.currency')
1401         for line in self.browse(cr, uid, ids):
1402             price = line.price_unit * (1-(line.discount or 0.0)/100.0)
1403             taxes = tax_obj.compute_all(cr, uid, line.invoice_line_tax_id, price, line.quantity, product=line.product_id, partner=line.invoice_id.partner_id)
1404             res[line.id] = taxes['total']
1405             if line.invoice_id:
1406                 cur = line.invoice_id.currency_id
1407                 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
1408         return res
1409
1410     def _price_unit_default(self, cr, uid, context=None):
1411         if context is None:
1412             context = {}
1413         if context.get('check_total', False):
1414             t = context['check_total']
1415             for l in context.get('invoice_line', {}):
1416                 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1417                     tax_obj = self.pool.get('account.tax')
1418                     p = l[2].get('price_unit', 0) * (1-l[2].get('discount', 0)/100.0)
1419                     t = t - (p * l[2].get('quantity'))
1420                     taxes = l[2].get('invoice_line_tax_id')
1421                     if len(taxes[0]) >= 3 and taxes[0][2]:
1422                         taxes = tax_obj.browse(cr, uid, list(taxes[0][2]))
1423                         for tax in tax_obj.compute_all(cr, uid, taxes, p,l[2].get('quantity'), l[2].get('product_id', False), context.get('partner_id', False))['taxes']:
1424                             t = t - tax['amount']
1425             return t
1426         return 0
1427
1428     _name = "account.invoice.line"
1429     _description = "Invoice Line"
1430     _columns = {
1431         'name': fields.text('Description', required=True),
1432         'origin': fields.char('Source Document', size=256, help="Reference of the document that produced this invoice."),
1433         'sequence': fields.integer('Sequence', help="Gives the sequence of this line when displaying the invoice."),
1434         'invoice_id': fields.many2one('account.invoice', 'Invoice Reference', ondelete='cascade', select=True),
1435         'uos_id': fields.many2one('product.uom', 'Unit of Measure', ondelete='set null', select=True),
1436         'product_id': fields.many2one('product.product', 'Product', ondelete='set null', select=True),
1437         'account_id': fields.many2one('account.account', 'Account', required=True, domain=[('type','<>','view'), ('type', '<>', 'closed')], help="The income or expense account related to the selected product."),
1438         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
1439         'price_subtotal': fields.function(_amount_line, string='Amount', type="float",
1440             digits_compute= dp.get_precision('Account'), store=True),
1441         'quantity': fields.float('Quantity', digits_compute= dp.get_precision('Product Unit of Measure'), required=True),
1442         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount')),
1443         'invoice_line_tax_id': fields.many2many('account.tax', 'account_invoice_line_tax', 'invoice_line_id', 'tax_id', 'Taxes', domain=[('parent_id','=',False)]),
1444         'account_analytic_id':  fields.many2one('account.analytic.account', 'Analytic Account'),
1445         'company_id': fields.related('invoice_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
1446         'partner_id': fields.related('invoice_id','partner_id',type='many2one',relation='res.partner',string='Partner',store=True)
1447     }
1448
1449     def _default_account_id(self, cr, uid, context=None):
1450         # XXX this gets the default account for the user's company,
1451         # it should get the default account for the invoice's company
1452         # however, the invoice's company does not reach this point
1453         if context is None:
1454             context = {}
1455         if context.get('type') in ('out_invoice','out_refund'):
1456             prop = self.pool.get('ir.property').get(cr, uid, 'property_account_income_categ', 'product.category', context=context)
1457         else:
1458             prop = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category', context=context)
1459         return prop and prop.id or False
1460
1461     _defaults = {
1462         'quantity': 1,
1463         'discount': 0.0,
1464         'price_unit': _price_unit_default,
1465         'account_id': _default_account_id,
1466     }
1467
1468     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
1469         if context is None:
1470             context = {}
1471         res = super(account_invoice_line,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
1472         if context.get('type', False):
1473             doc = etree.XML(res['arch'])
1474             for node in doc.xpath("//field[@name='product_id']"):
1475                 if context['type'] in ('in_invoice', 'in_refund'):
1476                     node.set('domain', "[('purchase_ok', '=', True)]")
1477                 else:
1478                     node.set('domain', "[('sale_ok', '=', True)]")
1479             res['arch'] = etree.tostring(doc)
1480         return res
1481
1482     def product_id_change(self, cr, uid, ids, product, uom_id, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
1483         if context is None:
1484             context = {}
1485         company_id = company_id if company_id != None else context.get('company_id',False)
1486         context = dict(context)
1487         context.update({'company_id': company_id, 'force_company': company_id})
1488         if not partner_id:
1489             raise osv.except_osv(_('No Partner Defined!'),_("You must first select a partner!") )
1490         if not product:
1491             if type in ('in_invoice', 'in_refund'):
1492                 return {'value': {}, 'domain':{'product_uom':[]}}
1493             else:
1494                 return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
1495         part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
1496         fpos_obj = self.pool.get('account.fiscal.position')
1497         fpos = fposition_id and fpos_obj.browse(cr, uid, fposition_id, context=context) or False
1498
1499         if part.lang:
1500             context.update({'lang': part.lang})
1501         result = {}
1502         res = self.pool.get('product.product').browse(cr, uid, product, context=context)
1503
1504         result['name'] = res.partner_ref
1505         if type in ('out_invoice','out_refund'):
1506             a = res.property_account_income.id
1507             if not a:
1508                 a = res.categ_id.property_account_income_categ.id
1509         else:
1510             a = res.property_account_expense.id
1511             if not a:
1512                 a = res.categ_id.property_account_expense_categ.id
1513         a = fpos_obj.map_account(cr, uid, fpos, a)
1514         if a:
1515             result['account_id'] = a
1516
1517         if type in ('out_invoice', 'out_refund'):
1518             taxes = res.taxes_id and res.taxes_id or (a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False)
1519             if res.description_sale:
1520                 result['name'] += '\n'+res.description_sale
1521         else:
1522             taxes = res.supplier_taxes_id and res.supplier_taxes_id or (a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False)
1523             if res.description_purchase:
1524                 result['name'] += '\n'+res.description_purchase
1525
1526         tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
1527
1528         if type in ('in_invoice', 'in_refund'):
1529             result.update( {'price_unit': price_unit or res.standard_price,'invoice_line_tax_id': tax_id} )
1530         else:
1531             result.update({'price_unit': res.list_price, 'invoice_line_tax_id': tax_id})
1532
1533         result['uos_id'] = uom_id or res.uom_id.id
1534
1535         domain = {'uos_id':[('category_id','=',res.uom_id.category_id.id)]}
1536
1537         res_final = {'value':result, 'domain':domain}
1538
1539         if not company_id or not currency_id:
1540             return res_final
1541
1542         company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
1543         currency = self.pool.get('res.currency').browse(cr, uid, currency_id, context=context)
1544
1545         if company.currency_id.id != currency.id:
1546             if type in ('in_invoice', 'in_refund'):
1547                 res_final['value']['price_unit'] = res.standard_price
1548             new_price = res_final['value']['price_unit'] * currency.rate
1549             res_final['value']['price_unit'] = new_price
1550
1551         if result['uos_id'] and result['uos_id'] != res.uom_id.id:
1552             selected_uom = self.pool.get('product.uom').browse(cr, uid, result['uos_id'], context=context)
1553             new_price = self.pool.get('product.uom')._compute_price(cr, uid, res.uom_id.id, res_final['value']['price_unit'], result['uos_id'])
1554             res_final['value']['price_unit'] = new_price
1555         return res_final
1556
1557     def uos_id_change(self, cr, uid, ids, product, uom, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
1558         if context is None:
1559             context = {}
1560         company_id = company_id if company_id != None else context.get('company_id',False)
1561         context = dict(context)
1562         context.update({'company_id': company_id})
1563         warning = {}
1564         res = self.product_id_change(cr, uid, ids, product, uom, qty, name, type, partner_id, fposition_id, price_unit, currency_id, context=context)
1565         if not uom:
1566             res['value']['price_unit'] = 0.0
1567         if product and uom:
1568             prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1569             prod_uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
1570             if prod.uom_id.category_id.id != prod_uom.category_id.id:
1571                 warning = {
1572                     'title': _('Warning!'),
1573                     'message': _('The selected unit of measure is not compatible with the unit of measure of the product.')
1574                 }
1575                 res['value'].update({'uos_id': prod.uom_id.id})
1576             return {'value': res['value'], 'warning': warning}
1577         return res
1578
1579     def move_line_get(self, cr, uid, invoice_id, context=None):
1580         res = []
1581         tax_obj = self.pool.get('account.tax')
1582         cur_obj = self.pool.get('res.currency')
1583         if context is None:
1584             context = {}
1585         inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1586         company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id
1587         for line in inv.invoice_line:
1588             mres = self.move_line_get_item(cr, uid, line, context)
1589             if not mres:
1590                 continue
1591             res.append(mres)
1592             tax_code_found= False
1593             for tax in tax_obj.compute_all(cr, uid, line.invoice_line_tax_id,
1594                     (line.price_unit * (1.0 - (line['discount'] or 0.0) / 100.0)),
1595                     line.quantity, line.product_id,
1596                     inv.partner_id)['taxes']:
1597
1598                 if inv.type in ('out_invoice', 'in_invoice'):
1599                     tax_code_id = tax['base_code_id']
1600                     tax_amount = line.price_subtotal * tax['base_sign']
1601                 else:
1602                     tax_code_id = tax['ref_base_code_id']
1603                     tax_amount = line.price_subtotal * tax['ref_base_sign']
1604
1605                 if tax_code_found:
1606                     if not tax_code_id:
1607                         continue
1608                     res.append(self.move_line_get_item(cr, uid, line, context))
1609                     res[-1]['price'] = 0.0
1610                     res[-1]['account_analytic_id'] = False
1611                 elif not tax_code_id:
1612                     continue
1613                 tax_code_found = True
1614
1615                 res[-1]['tax_code_id'] = tax_code_id
1616                 res[-1]['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, tax_amount, context={'date': inv.date_invoice})
1617         return res
1618
1619     def move_line_get_item(self, cr, uid, line, context=None):
1620         return {
1621             'type':'src',
1622             'name': line.name.split('\n')[0][:64],
1623             'price_unit':line.price_unit,
1624             'quantity':line.quantity,
1625             'price':line.price_subtotal,
1626             'account_id':line.account_id.id,
1627             'product_id':line.product_id.id,
1628             'uos_id':line.uos_id.id,
1629             'account_analytic_id':line.account_analytic_id.id,
1630             'taxes':line.invoice_line_tax_id,
1631         }
1632     #
1633     # Set the tax field according to the account and the fiscal position
1634     #
1635     def onchange_account_id(self, cr, uid, ids, product_id, partner_id, inv_type, fposition_id, account_id):
1636         if not account_id:
1637             return {}
1638         unique_tax_ids = []
1639         fpos = fposition_id and self.pool.get('account.fiscal.position').browse(cr, uid, fposition_id) or False
1640         account = self.pool.get('account.account').browse(cr, uid, account_id)
1641         if not product_id:
1642             taxes = account.tax_ids
1643             unique_tax_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
1644         else:
1645             product_change_result = self.product_id_change(cr, uid, ids, product_id, False, type=inv_type,
1646                 partner_id=partner_id, fposition_id=fposition_id,
1647                 company_id=account.company_id.id)
1648             if product_change_result and 'value' in product_change_result and 'invoice_line_tax_id' in product_change_result['value']:
1649                 unique_tax_ids = product_change_result['value']['invoice_line_tax_id']
1650         return {'value':{'invoice_line_tax_id': unique_tax_ids}}
1651
1652
1653 class account_invoice_tax(osv.osv):
1654     _name = "account.invoice.tax"
1655     _description = "Invoice Tax"
1656
1657     def _count_factor(self, cr, uid, ids, name, args, context=None):
1658         res = {}
1659         for invoice_tax in self.browse(cr, uid, ids, context=context):
1660             res[invoice_tax.id] = {
1661                 'factor_base': 1.0,
1662                 'factor_tax': 1.0,
1663             }
1664             if invoice_tax.amount <> 0.0:
1665                 factor_tax = invoice_tax.tax_amount / invoice_tax.amount
1666                 res[invoice_tax.id]['factor_tax'] = factor_tax
1667
1668             if invoice_tax.base <> 0.0:
1669                 factor_base = invoice_tax.base_amount / invoice_tax.base
1670                 res[invoice_tax.id]['factor_base'] = factor_base
1671
1672         return res
1673
1674     _columns = {
1675         'invoice_id': fields.many2one('account.invoice', 'Invoice Line', ondelete='cascade', select=True),
1676         'name': fields.char('Tax Description', size=64, required=True),
1677         'account_id': fields.many2one('account.account', 'Tax Account', required=True, domain=[('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')]),
1678         'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic account'),
1679         'base': fields.float('Base', digits_compute=dp.get_precision('Account')),
1680         'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
1681         'manual': fields.boolean('Manual'),
1682         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of invoice tax."),
1683         'base_code_id': fields.many2one('account.tax.code', 'Base Code', help="The account basis of the tax declaration."),
1684         'base_amount': fields.float('Base Code Amount', digits_compute=dp.get_precision('Account')),
1685         'tax_code_id': fields.many2one('account.tax.code', 'Tax Code', help="The tax basis of the tax declaration."),
1686         'tax_amount': fields.float('Tax Code Amount', digits_compute=dp.get_precision('Account')),
1687         'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1688         'factor_base': fields.function(_count_factor, string='Multipication factor for Base code', type='float', multi="all"),
1689         'factor_tax': fields.function(_count_factor, string='Multipication factor Tax code', type='float', multi="all")
1690     }
1691
1692     def base_change(self, cr, uid, ids, base, currency_id=False, company_id=False, date_invoice=False):
1693         cur_obj = self.pool.get('res.currency')
1694         company_obj = self.pool.get('res.company')
1695         company_currency = False
1696         factor = 1
1697         if ids:
1698             factor = self.read(cr, uid, ids[0], ['factor_base'])['factor_base']
1699         if company_id:
1700             company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1701         if currency_id and company_currency:
1702             base = cur_obj.compute(cr, uid, currency_id, company_currency, base*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1703         return {'value': {'base_amount':base}}
1704
1705     def amount_change(self, cr, uid, ids, amount, currency_id=False, company_id=False, date_invoice=False):
1706         cur_obj = self.pool.get('res.currency')
1707         company_obj = self.pool.get('res.company')
1708         company_currency = False
1709         factor = 1
1710         if ids:
1711             factor = self.read(cr, uid, ids[0], ['factor_tax'])['factor_tax']
1712         if company_id:
1713             company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1714         if currency_id and company_currency:
1715             amount = cur_obj.compute(cr, uid, currency_id, company_currency, amount*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1716         return {'value': {'tax_amount': amount}}
1717
1718     _order = 'sequence'
1719     _defaults = {
1720         'manual': 1,
1721         'base_amount': 0.0,
1722         'tax_amount': 0.0,
1723     }
1724     def compute(self, cr, uid, invoice_id, context=None):
1725         tax_grouped = {}
1726         tax_obj = self.pool.get('account.tax')
1727         cur_obj = self.pool.get('res.currency')
1728         inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1729         cur = inv.currency_id
1730         company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id
1731         for line in inv.invoice_line:
1732             for tax in tax_obj.compute_all(cr, uid, line.invoice_line_tax_id, (line.price_unit* (1-(line.discount or 0.0)/100.0)), line.quantity, line.product_id, inv.partner_id)['taxes']:
1733                 val={}
1734                 val['invoice_id'] = inv.id
1735                 val['name'] = tax['name']
1736                 val['amount'] = tax['amount']
1737                 val['manual'] = False
1738                 val['sequence'] = tax['sequence']
1739                 val['base'] = cur_obj.round(cr, uid, cur, tax['price_unit'] * line['quantity'])
1740
1741                 if inv.type in ('out_invoice','in_invoice'):
1742                     val['base_code_id'] = tax['base_code_id']
1743                     val['tax_code_id'] = tax['tax_code_id']
1744                     val['base_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['base'] * tax['base_sign'], context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1745                     val['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['amount'] * tax['tax_sign'], context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1746                     val['account_id'] = tax['account_collected_id'] or line.account_id.id
1747                     val['account_analytic_id'] = tax['account_analytic_collected_id']
1748                 else:
1749                     val['base_code_id'] = tax['ref_base_code_id']
1750                     val['tax_code_id'] = tax['ref_tax_code_id']
1751                     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 or time.strftime('%Y-%m-%d')}, round=False)
1752                     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 or time.strftime('%Y-%m-%d')}, round=False)
1753                     val['account_id'] = tax['account_paid_id'] or line.account_id.id
1754                     val['account_analytic_id'] = tax['account_analytic_paid_id']
1755
1756                 key = (val['tax_code_id'], val['base_code_id'], val['account_id'], val['account_analytic_id'])
1757                 if not key in tax_grouped:
1758                     tax_grouped[key] = val
1759                 else:
1760                     tax_grouped[key]['amount'] += val['amount']
1761                     tax_grouped[key]['base'] += val['base']
1762                     tax_grouped[key]['base_amount'] += val['base_amount']
1763                     tax_grouped[key]['tax_amount'] += val['tax_amount']
1764
1765         for t in tax_grouped.values():
1766             t['base'] = cur_obj.round(cr, uid, cur, t['base'])
1767             t['amount'] = cur_obj.round(cr, uid, cur, t['amount'])
1768             t['base_amount'] = cur_obj.round(cr, uid, cur, t['base_amount'])
1769             t['tax_amount'] = cur_obj.round(cr, uid, cur, t['tax_amount'])
1770         return tax_grouped
1771
1772     def move_line_get(self, cr, uid, invoice_id):
1773         res = []
1774         cr.execute('SELECT * FROM account_invoice_tax WHERE invoice_id=%s', (invoice_id,))
1775         for t in cr.dictfetchall():
1776             if not t['amount'] \
1777                     and not t['tax_code_id'] \
1778                     and not t['tax_amount']:
1779                 continue
1780             res.append({
1781                 'type':'tax',
1782                 'name':t['name'],
1783                 'price_unit': t['amount'],
1784                 'quantity': 1,
1785                 'price': t['amount'] or 0.0,
1786                 'account_id': t['account_id'],
1787                 'tax_code_id': t['tax_code_id'],
1788                 'tax_amount': t['tax_amount'],
1789                 'account_analytic_id': t['account_analytic_id'],
1790             })
1791         return res
1792
1793
1794 class res_partner(osv.osv):
1795     """ Inherits partner and adds invoice information in the partner form """
1796     _inherit = 'res.partner'
1797     _columns = {
1798         'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
1799     }
1800
1801     def _find_accounting_partner(self, partner):
1802         '''
1803         Find the partner for which the accounting entries will be created
1804         '''
1805         return partner.commercial_partner_id
1806
1807     def copy(self, cr, uid, id, default=None, context=None):
1808         default = default or {}
1809         default.update({'invoice_ids' : []})
1810         return super(res_partner, self).copy(cr, uid, id, default, context)
1811
1812
1813 class mail_compose_message(osv.Model):
1814     _inherit = 'mail.compose.message'
1815
1816     def send_mail(self, cr, uid, ids, context=None):
1817         context = context or {}
1818         if context.get('default_model') == 'account.invoice' and context.get('default_res_id') and context.get('mark_invoice_as_sent'):
1819             context = dict(context, mail_post_autofollow=True)
1820             self.pool.get('account.invoice').write(cr, uid, [context['default_res_id']], {'sent': True}, context=context)
1821             self.pool.get('account.invoice').message_post(cr, uid, [context['default_res_id']], body=_("Invoice sent"), context=context)
1822         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1823
1824 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: