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