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