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