[MERGE] forward port of branch 8.0 up to fd4fd35
[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')
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
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         if not self.reconciled and self.state == 'paid':
101             self.signal_workflow('open_test')
102
103     @api.model
104     def _get_reference_type(self):
105         return [('none', _('Free Reference'))]
106
107     @api.one
108     @api.depends(
109         'state', 'currency_id', 'invoice_line.price_subtotal',
110         'move_id.line_id.account_id.type',
111         'move_id.line_id.amount_residual',
112         'move_id.line_id.amount_residual_currency',
113         'move_id.line_id.currency_id',
114         'move_id.line_id.reconcile_partial_id.line_partial_ids.invoice.type',
115     )
116     def _compute_residual(self):
117         nb_inv_in_partial_rec = max_invoice_id = 0
118         self.residual = 0.0
119         for line in self.move_id.line_id:
120             if line.account_id.type in ('receivable', 'payable'):
121                 if line.currency_id == self.currency_id:
122                     self.residual += line.amount_residual_currency
123                 else:
124                     # ahem, shouldn't we use line.currency_id here?
125                     from_currency = line.company_id.currency_id.with_context(date=line.date)
126                     self.residual += from_currency.compute(line.amount_residual, self.currency_id)
127                 # we check if the invoice is partially reconciled and if there
128                 # are other invoices involved in this partial reconciliation
129                 for pline in line.reconcile_partial_id.line_partial_ids:
130                     if pline.invoice and self.type == pline.invoice.type:
131                         nb_inv_in_partial_rec += 1
132                         # store the max invoice id as for this invoice we will
133                         # make a balance instead of a simple division
134                         max_invoice_id = max(max_invoice_id, pline.invoice.id)
135         if nb_inv_in_partial_rec:
136             # if there are several invoices in a partial reconciliation, we
137             # split the residual by the number of invoices to have a sum of
138             # residual amounts that matches the partner balance
139             new_value = self.currency_id.round(self.residual / nb_inv_in_partial_rec)
140             if self.id == max_invoice_id:
141                 # if it's the last the invoice of the bunch of invoices
142                 # partially reconciled together, we make a balance to avoid
143                 # rounding errors
144                 self.residual = self.residual - ((nb_inv_in_partial_rec - 1) * new_value)
145             else:
146                 self.residual = new_value
147         # prevent the residual amount on the invoice to be less than 0
148         self.residual = max(self.residual, 0.0)
149
150     @api.one
151     @api.depends(
152         'move_id.line_id.account_id',
153         'move_id.line_id.reconcile_id.line_id',
154         'move_id.line_id.reconcile_partial_id.line_partial_ids',
155     )
156     def _compute_move_lines(self):
157         # Give Journal Items related to the payment reconciled to this invoice.
158         # Return partial and total payments related to the selected invoice.
159         self.move_lines = self.env['account.move.line']
160         if not self.move_id:
161             return
162         data_lines = self.move_id.line_id.filtered(lambda l: l.account_id == self.account_id)
163         partial_lines = self.env['account.move.line']
164         for data_line in data_lines:
165             if data_line.reconcile_id:
166                 lines = data_line.reconcile_id.line_id
167             elif data_line.reconcile_partial_id:
168                 lines = data_line.reconcile_partial_id.line_partial_ids
169             else:
170                 lines = self.env['account_move_line']
171             partial_lines += data_line
172             self.move_lines = lines - partial_lines
173
174     @api.one
175     @api.depends(
176         'move_id.line_id.reconcile_id.line_id',
177         'move_id.line_id.reconcile_partial_id.line_partial_ids',
178     )
179     def _compute_payments(self):
180         partial_lines = lines = self.env['account.move.line']
181         for line in self.move_id.line_id:
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(self._context,
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.id
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.today()
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(['journal_id'])
578                 type_label = next(t for t, label in field_desc['journal_id']['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     @staticmethod
649     def _convert_ref(ref):
650         return (ref or '').replace('/','')
651
652     @api.multi
653     def _get_analytic_lines(self):
654         """ Return a list of dict for creating analytic lines for self[0] """
655         company_currency = self.company_id.currency_id
656         sign = 1 if self.type in ('out_invoice', 'in_refund') else -1
657
658         iml = self.env['account.invoice.line'].move_line_get(self.id)
659         for il in iml:
660             if il['account_analytic_id']:
661                 if self.type in ('in_invoice', 'in_refund'):
662                     ref = self.reference
663                 else:
664                     ref = self._convert_ref(self.number)
665                 if not self.journal_id.analytic_journal_id:
666                     raise except_orm(_('No Analytic Journal!'),
667                         _("You have to define an analytic journal on the '%s' journal!") % (self.journal_id.name,))
668                 currency = self.currency_id.with_context(date=self.date_invoice)
669                 il['analytic_lines'] = [(0,0, {
670                     'name': il['name'],
671                     'date': self.date_invoice,
672                     'account_id': il['account_analytic_id'],
673                     'unit_amount': il['quantity'],
674                     'amount': currency.compute(il['price'], company_currency) * sign,
675                     'product_id': il['product_id'],
676                     'product_uom_id': il['uos_id'],
677                     'general_account_id': il['account_id'],
678                     'journal_id': self.journal_id.analytic_journal_id.id,
679                     'ref': ref,
680                 })]
681         return iml
682
683     @api.multi
684     def action_date_assign(self):
685         for inv in self:
686             res = inv.onchange_payment_term_date_invoice(inv.payment_term.id, inv.date_invoice)
687             if res and res.get('value'):
688                 inv.write(res['value'])
689         return True
690
691     @api.multi
692     def finalize_invoice_move_lines(self, move_lines):
693         """ finalize_invoice_move_lines(move_lines) -> move_lines
694
695             Hook method to be overridden in additional modules to verify and
696             possibly alter the move lines to be created by an invoice, for
697             special cases.
698             :param move_lines: list of dictionaries with the account.move.lines (as for create())
699             :return: the (possibly updated) final move_lines to create for this invoice
700         """
701         return move_lines
702
703     @api.multi
704     def check_tax_lines(self, compute_taxes):
705         account_invoice_tax = self.env['account.invoice.tax']
706         company_currency = self.company_id.currency_id
707         if not self.tax_line:
708             for tax in compute_taxes.values():
709                 account_invoice_tax.create(tax)
710         else:
711             tax_key = []
712             for tax in self.tax_line:
713                 if tax.manual:
714                     continue
715                 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
716                 tax_key.append(key)
717                 if key not in compute_taxes:
718                     raise except_orm(_('Warning!'), _('Global taxes defined, but they are not in invoice lines !'))
719                 base = compute_taxes[key]['base']
720                 if abs(base - tax.base) > company_currency.rounding:
721                     raise except_orm(_('Warning!'), _('Tax base different!\nClick on compute to update the tax base.'))
722             for key in compute_taxes:
723                 if key not in tax_key:
724                     raise except_orm(_('Warning!'), _('Taxes are missing!\nClick on compute button.'))
725
726     @api.multi
727     def compute_invoice_totals(self, company_currency, ref, invoice_move_lines):
728         total = 0
729         total_currency = 0
730         for line in invoice_move_lines:
731             if self.currency_id != company_currency:
732                 currency = self.currency_id.with_context(date=self.date_invoice or fields.Date.today())
733                 line['currency_id'] = currency.id
734                 line['amount_currency'] = line['price']
735                 line['price'] = currency.compute(line['price'], company_currency)
736             else:
737                 line['currency_id'] = False
738                 line['amount_currency'] = False
739             line['ref'] = ref
740             if self.type in ('out_invoice','in_refund'):
741                 total += line['price']
742                 total_currency += line['amount_currency'] or line['price']
743                 line['price'] = - line['price']
744             else:
745                 total -= line['price']
746                 total_currency -= line['amount_currency'] or line['price']
747         return total, total_currency, invoice_move_lines
748
749     def inv_line_characteristic_hashcode(self, invoice_line):
750         """Overridable hashcode generation for invoice lines. Lines having the same hashcode
751         will be grouped together if the journal has the 'group line' option. Of course a module
752         can add fields to invoice lines that would need to be tested too before merging lines
753         or not."""
754         return "%s-%s-%s-%s-%s" % (
755             invoice_line['account_id'],
756             invoice_line.get('tax_code_id', 'False'),
757             invoice_line.get('product_id', 'False'),
758             invoice_line.get('analytic_account_id', 'False'),
759             invoice_line.get('date_maturity', 'False'),
760         )
761
762     def group_lines(self, iml, line):
763         """Merge account move lines (and hence analytic lines) if invoice line hashcodes are equals"""
764         if self.journal_id.group_invoice_lines:
765             line2 = {}
766             for x, y, l in line:
767                 tmp = self.inv_line_characteristic_hashcode(l)
768                 if tmp in line2:
769                     am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
770                     line2[tmp]['debit'] = (am > 0) and am or 0.0
771                     line2[tmp]['credit'] = (am < 0) and -am or 0.0
772                     line2[tmp]['tax_amount'] += l['tax_amount']
773                     line2[tmp]['analytic_lines'] += l['analytic_lines']
774                 else:
775                     line2[tmp] = l
776             line = []
777             for key, val in line2.items():
778                 line.append((0,0,val))
779         return line
780
781     @api.multi
782     def action_move_create(self):
783         """ Creates invoice related analytics and financial move lines """
784         account_invoice_tax = self.env['account.invoice.tax']
785         account_move = self.env['account.move']
786
787         for inv in self:
788             if not inv.journal_id.sequence_id:
789                 raise except_orm(_('Error!'), _('Please define sequence on the journal related to this invoice.'))
790             if not inv.invoice_line:
791                 raise except_orm(_('No Invoice Lines!'), _('Please create some invoice lines.'))
792             if inv.move_id:
793                 continue
794
795             ctx = dict(self._context, lang=inv.partner_id.lang)
796             date_invoice = inv.date_invoice or fields.Date.context_today(self)
797
798             company_currency = inv.company_id.currency_id
799             # create the analytical lines, one move line per invoice line
800             iml = inv._get_analytic_lines()
801             # check if taxes are all computed
802             compute_taxes = account_invoice_tax.compute(inv)
803             inv.check_tax_lines(compute_taxes)
804
805             # I disabled the check_total feature
806             group_check_total = self.env.ref('account.group_supplier_inv_check_total')
807             if self.env.user in group_check_total.users:
808                 if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding / 2.0):
809                     raise except_orm(_('Bad Total!'), _('Please verify the price of the invoice!\nThe encoded total does not match the computed total.'))
810
811             if inv.payment_term:
812                 total_fixed = total_percent = 0
813                 for line in inv.payment_term.line_ids:
814                     if line.value == 'fixed':
815                         total_fixed += line.value_amount
816                     if line.value == 'procent':
817                         total_percent += line.value_amount
818                 total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
819                 if (total_fixed + total_percent) > 100:
820                     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'."))
821
822             # one move line per tax line
823             iml += account_invoice_tax.move_line_get(inv.id)
824
825             if inv.type in ('in_invoice', 'in_refund'):
826                 ref = inv.reference
827             else:
828                 ref = self._convert_ref(inv.number)
829
830             diff_currency = inv.currency_id != company_currency
831             # create one move line for the total and possibly adjust the other lines amount
832             total, total_currency, iml = inv.with_context(ctx).compute_invoice_totals(company_currency, ref, iml)
833
834             name = inv.name or inv.supplier_invoice_number or '/'
835             totlines = []
836             if inv.payment_term:
837                 totlines = inv.with_context(ctx).payment_term.compute(total, date_invoice)[0]
838             if totlines:
839                 res_amount_currency = total_currency
840                 ctx['date'] = date_invoice
841                 for i, t in enumerate(totlines):
842                     if inv.currency_id != company_currency:
843                         amount_currency = company_currency.with_context(ctx).compute(t[1], inv.currency_id)
844                     else:
845                         amount_currency = False
846
847                     # last line: add the diff
848                     res_amount_currency -= amount_currency or 0
849                     if i + 1 == len(totlines):
850                         amount_currency += res_amount_currency
851
852                     iml.append({
853                         'type': 'dest',
854                         'name': name,
855                         'price': t[1],
856                         'account_id': inv.account_id.id,
857                         'date_maturity': t[0],
858                         'amount_currency': diff_currency and amount_currency,
859                         'currency_id': diff_currency and inv.currency_id.id,
860                         'ref': ref,
861                     })
862             else:
863                 iml.append({
864                     'type': 'dest',
865                     'name': name,
866                     'price': total,
867                     'account_id': inv.account_id.id,
868                     'date_maturity': inv.date_due,
869                     'amount_currency': diff_currency and total_currency,
870                     'currency_id': diff_currency and inv.currency_id.id,
871                     'ref': ref
872                 })
873
874             date = date_invoice
875
876             part = self.env['res.partner']._find_accounting_partner(inv.partner_id)
877
878             line = [(0, 0, self.line_get_convert(l, part.id, date)) for l in iml]
879             line = inv.group_lines(iml, line)
880
881             journal = inv.journal_id.with_context(ctx)
882             if journal.centralisation:
883                 raise except_orm(_('User Error!'),
884                         _('You cannot create an invoice on a centralized journal. Uncheck the centralized counterpart box in the related journal from the configuration menu.'))
885
886             line = inv.finalize_invoice_move_lines(line)
887
888             move_vals = {
889                 'ref': inv.reference or inv.name,
890                 'line_id': line,
891                 'journal_id': journal.id,
892                 'date': date,
893                 'narration': inv.comment,
894                 'company_id': inv.company_id.id,
895             }
896             ctx['company_id'] = inv.company_id.id
897             period = inv.period_id
898             if not period:
899                 period = period.with_context(ctx).find(date_invoice)[:1]
900             if period:
901                 move_vals['period_id'] = period.id
902                 for i in line:
903                     i[2]['period_id'] = period.id
904
905             ctx['invoice'] = inv
906             move = account_move.with_context(ctx).create(move_vals)
907             # make the invoice point to that move
908             vals = {
909                 'date_invoice': date_invoice,
910                 'move_id': move.id,
911                 'period_id': period.id,
912                 'move_name': move.name,
913             }
914             inv.with_context(ctx).write(vals)
915             # Pass invoice in context in method post: used if you want to get the same
916             # account move reference when creating the same invoice after a cancelled one:
917             move.post()
918         self._log_event()
919         return True
920
921     @api.multi
922     def invoice_validate(self):
923         return self.write({'state': 'open'})
924
925     @api.model
926     def line_get_convert(self, line, part, date):
927         return {
928             'date_maturity': line.get('date_maturity', False),
929             'partner_id': part,
930             'name': line['name'][:64],
931             'date': date,
932             'debit': line['price']>0 and line['price'],
933             'credit': line['price']<0 and -line['price'],
934             'account_id': line['account_id'],
935             'analytic_lines': line.get('analytic_lines', []),
936             'amount_currency': line['price']>0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)),
937             'currency_id': line.get('currency_id', False),
938             'tax_code_id': line.get('tax_code_id', False),
939             'tax_amount': line.get('tax_amount', False),
940             'ref': line.get('ref', False),
941             'quantity': line.get('quantity',1.00),
942             'product_id': line.get('product_id', False),
943             'product_uom_id': line.get('uos_id', False),
944             'analytic_account_id': line.get('account_analytic_id', False),
945         }
946
947     @api.multi
948     def action_number(self):
949         #TODO: not correct fix but required a fresh values before reading it.
950         self.write({})
951
952         for inv in self:
953             self.write({'internal_number': inv.number})
954
955             if inv.type in ('in_invoice', 'in_refund'):
956                 if not inv.reference:
957                     ref = self._convert_ref(inv.number)
958                 else:
959                     ref = inv.reference
960             else:
961                 ref = self._convert_ref(inv.number)
962
963             self._cr.execute(""" UPDATE account_move SET ref=%s
964                            WHERE id=%s AND (ref IS NULL OR ref = '')""",
965                         (ref, inv.move_id.id))
966             self._cr.execute(""" UPDATE account_move_line SET ref=%s
967                            WHERE move_id=%s AND (ref IS NULL OR ref = '')""",
968                         (ref, inv.move_id.id))
969             self._cr.execute(""" UPDATE account_analytic_line SET ref=%s
970                            FROM account_move_line
971                            WHERE account_move_line.move_id = %s AND
972                                  account_analytic_line.move_id = account_move_line.id""",
973                         (ref, inv.move_id.id))
974             self.invalidate_cache()
975
976         return True
977
978     @api.multi
979     def action_cancel(self):
980         moves = self.env['account.move']
981         for inv in self:
982             if inv.move_id:
983                 moves += inv.move_id
984             if inv.payment_ids:
985                 for move_line in inv.payment_ids:
986                     if move_line.reconcile_partial_id.line_partial_ids:
987                         raise except_orm(_('Error!'), _('You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first.'))
988
989         # First, set the invoices as cancelled and detach the move ids
990         self.write({'state': 'cancel', 'move_id': False})
991         if moves:
992             # second, invalidate the move(s)
993             moves.button_cancel()
994             # delete the move this invoice was pointing to
995             # Note that the corresponding move_lines and move_reconciles
996             # will be automatically deleted too
997             moves.unlink()
998         self._log_event(-1.0, 'Cancel Invoice')
999         return True
1000
1001     ###################
1002
1003     @api.multi
1004     def _log_event(self, factor=1.0, name='Open Invoice'):
1005         #TODO: implement messages system
1006         return True
1007
1008     @api.multi
1009     def name_get(self):
1010         TYPES = {
1011             'out_invoice': _('Invoice'),
1012             'in_invoice': _('Supplier Invoice'),
1013             'out_refund': _('Refund'),
1014             'in_refund': _('Supplier Refund'),
1015         }
1016         result = []
1017         for inv in self:
1018             result.append((inv.id, "%s %s" % (inv.number or TYPES[inv.type], inv.name or '')))
1019         return result
1020
1021     @api.model
1022     def name_search(self, name, args=None, operator='ilike', limit=100):
1023         args = args or []
1024         recs = self.browse()
1025         if name:
1026             recs = self.search([('number', '=', name)] + args, limit=limit)
1027         if not recs:
1028             recs = self.search([('name', operator, name)] + args, limit=limit)
1029         return recs.name_get()
1030
1031     @api.model
1032     def _refund_cleanup_lines(self, lines):
1033         """ Convert records to dict of values suitable for one2many line creation
1034
1035             :param recordset lines: records to convert
1036             :return: list of command tuple for one2many line creation [(0, 0, dict of valueis), ...]
1037         """
1038         result = []
1039         for line in lines:
1040             values = {}
1041             for name, field in line._fields.iteritems():
1042                 if name in MAGIC_COLUMNS:
1043                     continue
1044                 elif field.type == 'many2one':
1045                     values[name] = line[name].id
1046                 elif field.type not in ['many2many', 'one2many']:
1047                     values[name] = line[name]
1048                 elif name == 'invoice_line_tax_id':
1049                     values[name] = [(6, 0, line[name].ids)]
1050             result.append((0, 0, values))
1051         return result
1052
1053     @api.model
1054     def _prepare_refund(self, invoice, date=None, period_id=None, description=None, journal_id=None):
1055         """ Prepare the dict of values to create the new refund from the invoice.
1056             This method may be overridden to implement custom
1057             refund generation (making sure to call super() to establish
1058             a clean extension chain).
1059
1060             :param record invoice: invoice to refund
1061             :param string date: refund creation date from the wizard
1062             :param integer period_id: force account.period from the wizard
1063             :param string description: description of the refund from the wizard
1064             :param integer journal_id: account.journal from the wizard
1065             :return: dict of value to create() the refund
1066         """
1067         values = {}
1068         for field in ['name', 'reference', 'comment', 'date_due', 'partner_id', 'company_id',
1069                 'account_id', 'currency_id', 'payment_term', 'user_id', 'fiscal_position']:
1070             if invoice._fields[field].type == 'many2one':
1071                 values[field] = invoice[field].id
1072             else:
1073                 values[field] = invoice[field] or False
1074
1075         values['invoice_line'] = self._refund_cleanup_lines(invoice.invoice_line)
1076
1077         tax_lines = filter(lambda l: l.manual, invoice.tax_line)
1078         values['tax_line'] = self._refund_cleanup_lines(tax_lines)
1079
1080         if journal_id:
1081             journal = self.env['account.journal'].browse(journal_id)
1082         elif invoice['type'] == 'in_invoice':
1083             journal = self.env['account.journal'].search([('type', '=', 'purchase_refund')], limit=1)
1084         else:
1085             journal = self.env['account.journal'].search([('type', '=', 'sale_refund')], limit=1)
1086         values['journal_id'] = journal.id
1087
1088         values['type'] = TYPE2REFUND[invoice['type']]
1089         values['date_invoice'] = date or fields.Date.today()
1090         values['state'] = 'draft'
1091         values['number'] = False
1092
1093         if period_id:
1094             values['period_id'] = period_id
1095         if description:
1096             values['name'] = description
1097         return values
1098
1099     @api.multi
1100     @api.returns('self')
1101     def refund(self, date=None, period_id=None, description=None, journal_id=None):
1102         new_invoices = self.browse()
1103         for invoice in self:
1104             # create the new invoice
1105             values = self._prepare_refund(invoice, date=date, period_id=period_id,
1106                                     description=description, journal_id=journal_id)
1107             new_invoices += self.create(values)
1108         return new_invoices
1109
1110     @api.v8
1111     def pay_and_reconcile(self, pay_amount, pay_account_id, period_id, pay_journal_id,
1112                           writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=''):
1113         # TODO check if we can use different period for payment and the writeoff line
1114         assert len(self)==1, "Can only pay one invoice at a time."
1115         # Take the seq as name for move
1116         SIGN = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1117         direction = SIGN[self.type]
1118         # take the chosen date
1119         date = self._context.get('date_p') or fields.Date.today()
1120
1121         # Take the amount in currency and the currency of the payment
1122         if self._context.get('amount_currency') and self._context.get('currency_id'):
1123             amount_currency = self._context['amount_currency']
1124             currency_id = self._context['currency_id']
1125         else:
1126             amount_currency = False
1127             currency_id = False
1128
1129         pay_journal = self.env['account.journal'].browse(pay_journal_id)
1130         if self.type in ('in_invoice', 'in_refund'):
1131             ref = self.reference
1132         else:
1133             ref = self._convert_ref(self.number)
1134         partner = self.partner_id._find_accounting_partner(self.partner_id)
1135         name = name or self.invoice_line.name or self.number
1136         # Pay attention to the sign for both debit/credit AND amount_currency
1137         l1 = {
1138             'name': name,
1139             'debit': direction * pay_amount > 0 and direction * pay_amount,
1140             'credit': direction * pay_amount < 0 and -direction * pay_amount,
1141             'account_id': self.account_id.id,
1142             'partner_id': partner.id,
1143             'ref': ref,
1144             'date': date,
1145             'currency_id': currency_id,
1146             'amount_currency': direction * (amount_currency or 0.0),
1147             'company_id': self.company_id.id,
1148         }
1149         l2 = {
1150             'name': name,
1151             'debit': direction * pay_amount < 0 and -direction * pay_amount,
1152             'credit': direction * pay_amount > 0 and direction * pay_amount,
1153             'account_id': pay_account_id,
1154             'partner_id': partner.id,
1155             'ref': ref,
1156             'date': date,
1157             'currency_id': currency_id,
1158             'amount_currency': -direction * (amount_currency or 0.0),
1159             'company_id': self.company_id.id,
1160         }
1161         move = self.env['account.move'].create({
1162             'ref': ref,
1163             'line_id': [(0, 0, l1), (0, 0, l2)],
1164             'journal_id': pay_journal_id,
1165             'period_id': period_id,
1166             'date': date,
1167         })
1168
1169         move_ids = (move | self.move_id).ids
1170         self._cr.execute("SELECT id FROM account_move_line WHERE move_id IN %s",
1171                          (tuple(move_ids),))
1172         lines = self.env['account.move.line'].browse([r[0] for r in self._cr.fetchall()])
1173         lines2rec = lines.browse()
1174         total = 0.0
1175         for line in itertools.chain(lines, self.payment_ids):
1176             if line.account_id == self.account_id:
1177                 lines2rec += line
1178                 total += (line.debit or 0.0) - (line.credit or 0.0)
1179
1180         inv_id, name = self.name_get()[0]
1181         if not round(total, self.env['decimal.precision'].precision_get('Account')) or writeoff_acc_id:
1182             lines2rec.reconcile('manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id)
1183         else:
1184             code = self.currency_id.symbol
1185             # TODO: use currency's formatting function
1186             msg = _("Invoice partially paid: %s%s of %s%s (%s%s remaining).") % \
1187                     (pay_amount, code, self.amount_total, code, total, code)
1188             self.message_post(body=msg)
1189             lines2rec.reconcile_partial('manual')
1190
1191         # Update the stored value (fields.function), so we write to trigger recompute
1192         return self.write({})
1193
1194     @api.v7
1195     def pay_and_reconcile(self, cr, uid, ids, pay_amount, pay_account_id, period_id, pay_journal_id,
1196                           writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context=None, name=''):
1197         recs = self.browse(cr, uid, ids, context)
1198         return recs.pay_and_reconcile(pay_amount, pay_account_id, period_id, pay_journal_id,
1199                     writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=name)
1200
1201 class account_invoice_line(models.Model):
1202     _name = "account.invoice.line"
1203     _description = "Invoice Line"
1204     _order = "invoice_id,sequence,id"
1205
1206     @api.one
1207     @api.depends('price_unit', 'discount', 'invoice_line_tax_id', 'quantity',
1208         'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id')
1209     def _compute_price(self):
1210         price = self.price_unit * (1 - (self.discount or 0.0) / 100.0)
1211         taxes = self.invoice_line_tax_id.compute_all(price, self.quantity, product=self.product_id, partner=self.invoice_id.partner_id)
1212         self.price_subtotal = taxes['total']
1213         if self.invoice_id:
1214             self.price_subtotal = self.invoice_id.currency_id.round(self.price_subtotal)
1215
1216     @api.model
1217     def _default_price_unit(self):
1218         if not self._context.get('check_total'):
1219             return 0
1220         total = self._context['check_total']
1221         for l in self._context.get('invoice_line', []):
1222             if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1223                 vals = l[2]
1224                 price = vals.get('price_unit', 0) * (1 - vals.get('discount', 0) / 100.0)
1225                 total = total - (price * vals.get('quantity'))
1226                 taxes = vals.get('invoice_line_tax_id')
1227                 if taxes and len(taxes[0]) >= 3 and taxes[0][2]:
1228                     taxes = self.env['account.tax'].browse(taxes[0][2])
1229                     tax_res = taxes.compute_all(price, vals.get('quantity'),
1230                         product=vals.get('product_id'), partner=self._context.get('partner_id'))
1231                     for tax in tax_res['taxes']:
1232                         total = total - tax['amount']
1233         return total
1234
1235     @api.model
1236     def _default_account(self):
1237         # XXX this gets the default account for the user's company,
1238         # it should get the default account for the invoice's company
1239         # however, the invoice's company does not reach this point
1240         if self._context.get('type') in ('out_invoice', 'out_refund'):
1241             return self.env['ir.property'].get('property_account_income_categ', 'product.category')
1242         else:
1243             return self.env['ir.property'].get('property_account_expense_categ', 'product.category')
1244
1245     name = fields.Text(string='Description', required=True)
1246     origin = fields.Char(string='Source Document',
1247         help="Reference of the document that produced this invoice.")
1248     sequence = fields.Integer(string='Sequence', default=10,
1249         help="Gives the sequence of this line when displaying the invoice.")
1250     invoice_id = fields.Many2one('account.invoice', string='Invoice Reference',
1251         ondelete='cascade', index=True)
1252     uos_id = fields.Many2one('product.uom', string='Unit of Measure',
1253         ondelete='set null', index=True)
1254     product_id = fields.Many2one('product.product', string='Product',
1255         ondelete='set null', index=True)
1256     account_id = fields.Many2one('account.account', string='Account',
1257         required=True, domain=[('type', 'not in', ['view', 'closed'])],
1258         default=_default_account,
1259         help="The income or expense account related to the selected product.")
1260     price_unit = fields.Float(string='Unit Price', required=True,
1261         digits= dp.get_precision('Product Price'),
1262         default=_default_price_unit)
1263     price_subtotal = fields.Float(string='Amount', digits= dp.get_precision('Account'),
1264         store=True, readonly=True, compute='_compute_price')
1265     quantity = fields.Float(string='Quantity', digits= dp.get_precision('Product Unit of Measure'),
1266         required=True, default=1)
1267     discount = fields.Float(string='Discount (%)', digits= dp.get_precision('Discount'),
1268         default=0.0)
1269     invoice_line_tax_id = fields.Many2many('account.tax',
1270         'account_invoice_line_tax', 'invoice_line_id', 'tax_id',
1271         string='Taxes', domain=[('parent_id', '=', False)])
1272     account_analytic_id = fields.Many2one('account.analytic.account',
1273         string='Analytic Account')
1274     company_id = fields.Many2one('res.company', string='Company',
1275         related='invoice_id.company_id', store=True, readonly=True)
1276     partner_id = fields.Many2one('res.partner', string='Partner',
1277         related='invoice_id.partner_id', store=True, readonly=True)
1278
1279     @api.model
1280     def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
1281         res = super(account_invoice_line, self).fields_view_get(
1282             view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
1283         if self._context.get('type'):
1284             doc = etree.XML(res['arch'])
1285             for node in doc.xpath("//field[@name='product_id']"):
1286                 if self._context['type'] in ('in_invoice', 'in_refund'):
1287                     node.set('domain', "[('purchase_ok', '=', True)]")
1288                 else:
1289                     node.set('domain', "[('sale_ok', '=', True)]")
1290             res['arch'] = etree.tostring(doc)
1291         return res
1292
1293     @api.multi
1294     def product_id_change(self, product, uom_id, qty=0, name='', type='out_invoice',
1295             partner_id=False, fposition_id=False, price_unit=False, currency_id=False,
1296             context=None, company_id=None):
1297         context = context or {}
1298         company_id = company_id if company_id is not None else context.get('company_id', False)
1299         self = self.with_context(company_id=company_id, force_company=company_id)
1300
1301         if not partner_id:
1302             raise except_orm(_('No Partner Defined!'), _("You must first select a partner!"))
1303         if not product:
1304             if type in ('in_invoice', 'in_refund'):
1305                 return {'value': {}, 'domain': {'product_uom': []}}
1306             else:
1307                 return {'value': {'price_unit': 0.0}, 'domain': {'product_uom': []}}
1308
1309         values = {}
1310
1311         part = self.env['res.partner'].browse(partner_id)
1312         fpos = self.env['account.fiscal.position'].browse(fposition_id)
1313
1314         if part.lang:
1315             self = self.with_context(lang=part.lang)
1316         product = self.env['product.product'].browse(product)
1317
1318         values['name'] = product.partner_ref
1319         if type in ('out_invoice', 'out_refund'):
1320             account = product.property_account_income or product.categ_id.property_account_income_categ
1321         else:
1322             account = product.property_account_expense or product.categ_id.property_account_expense_categ
1323         account = fpos.map_account(account)
1324         if account:
1325             values['account_id'] = account.id
1326
1327         if type in ('out_invoice', 'out_refund'):
1328             taxes = product.taxes_id or account.tax_ids
1329             if product.description_sale:
1330                 values['name'] += '\n' + product.description_sale
1331         else:
1332             taxes = product.supplier_taxes_id or account.tax_ids
1333             if product.description_purchase:
1334                 values['name'] += '\n' + product.description_purchase
1335
1336         taxes = fpos.map_tax(taxes)
1337         values['invoice_line_tax_id'] = taxes.ids
1338
1339         if type in ('in_invoice', 'in_refund'):
1340             values['price_unit'] = price_unit or product.standard_price
1341         else:
1342             values['price_unit'] = product.list_price
1343
1344         values['uos_id'] = uom_id or product.uom_id.id
1345         domain = {'uos_id': [('category_id', '=', product.uom_id.category_id.id)]}
1346
1347         company = self.env['res.company'].browse(company_id)
1348         currency = self.env['res.currency'].browse(currency_id)
1349
1350         if company and currency:
1351             if company.currency_id != currency:
1352                 if type in ('in_invoice', 'in_refund'):
1353                     values['price_unit'] = product.standard_price
1354                 values['price_unit'] = values['price_unit'] * currency.rate
1355
1356             if values['uos_id'] and values['uos_id'] != product.uom_id.id:
1357                 values['price_unit'] = self.env['product.uom']._compute_price(
1358                     product.uom_id.id, values['price_unit'], values['uos_id'])
1359
1360         return {'value': values, 'domain': domain}
1361
1362     @api.multi
1363     def uos_id_change(self, product, uom, qty=0, name='', type='out_invoice', partner_id=False,
1364             fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
1365         context = context or {}
1366         company_id = company_id if company_id != None else context.get('company_id', False)
1367         self = self.with_context(company_id=company_id)
1368
1369         result = self.product_id_change(
1370             product, uom, qty, name, type, partner_id, fposition_id, price_unit,
1371             currency_id, context=context, company_id=company_id,
1372         )
1373         warning = {}
1374         if not uom:
1375             result['value']['price_unit'] = 0.0
1376         if product and uom:
1377             prod = self.env['product.product'].browse(product)
1378             prod_uom = self.env['product.uom'].browse(uom)
1379             if prod.uom_id.category_id != prod_uom.category_id:
1380                 warning = {
1381                     'title': _('Warning!'),
1382                     'message': _('The selected unit of measure is not compatible with the unit of measure of the product.'),
1383                 }
1384                 result['value']['uos_id'] = prod.uom_id.id
1385         if warning:
1386             result['warning'] = warning
1387         return result
1388
1389     @api.model
1390     def move_line_get(self, invoice_id):
1391         inv = self.env['account.invoice'].browse(invoice_id)
1392         currency = inv.currency_id.with_context(date=inv.date_invoice)
1393         company_currency = inv.company_id.currency_id
1394
1395         res = []
1396         for line in inv.invoice_line:
1397             mres = self.move_line_get_item(line)
1398             if not mres:
1399                 continue
1400             res.append(mres)
1401             tax_code_found = False
1402             taxes = line.invoice_line_tax_id.compute_all(
1403                 (line.price_unit * (1.0 - (line.discount or 0.0) / 100.0)),
1404                 line.quantity, line.product_id, inv.partner_id)['taxes']
1405             for tax in taxes:
1406                 if inv.type in ('out_invoice', 'in_invoice'):
1407                     tax_code_id = tax['base_code_id']
1408                     tax_amount = line.price_subtotal * tax['base_sign']
1409                 else:
1410                     tax_code_id = tax['ref_base_code_id']
1411                     tax_amount = line.price_subtotal * tax['ref_base_sign']
1412
1413                 if tax_code_found:
1414                     if not tax_code_id:
1415                         continue
1416                     res.append(dict(mres))
1417                     res[-1]['price'] = 0.0
1418                     res[-1]['account_analytic_id'] = False
1419                 elif not tax_code_id:
1420                     continue
1421                 tax_code_found = True
1422
1423                 res[-1]['tax_code_id'] = tax_code_id
1424                 res[-1]['tax_amount'] = currency.compute(tax_amount, company_currency)
1425
1426         return res
1427
1428     @api.model
1429     def move_line_get_item(self, line):
1430         return {
1431             'type': 'src',
1432             'name': line.name.split('\n')[0][:64],
1433             'price_unit': line.price_unit,
1434             'quantity': line.quantity,
1435             'price': line.price_subtotal,
1436             'account_id': line.account_id.id,
1437             'product_id': line.product_id.id,
1438             'uos_id': line.uos_id.id,
1439             'account_analytic_id': line.account_analytic_id.id,
1440             'taxes': line.invoice_line_tax_id,
1441         }
1442
1443     #
1444     # Set the tax field according to the account and the fiscal position
1445     #
1446     @api.multi
1447     def onchange_account_id(self, product_id, partner_id, inv_type, fposition_id, account_id):
1448         if not account_id:
1449             return {}
1450         unique_tax_ids = []
1451         account = self.env['account.account'].browse(account_id)
1452         if not product_id:
1453             fpos = self.env['account.fiscal.position'].browse(fposition_id)
1454             unique_tax_ids = fpos.map_tax(account.tax_ids).ids
1455         else:
1456             product_change_result = self.product_id_change(product_id, False, type=inv_type,
1457                 partner_id=partner_id, fposition_id=fposition_id, company_id=account.company_id.id)
1458             if 'invoice_line_tax_id' in product_change_result.get('value', {}):
1459                 unique_tax_ids = product_change_result['value']['invoice_line_tax_id']
1460         return {'value': {'invoice_line_tax_id': unique_tax_ids}}
1461
1462
1463 class account_invoice_tax(models.Model):
1464     _name = "account.invoice.tax"
1465     _description = "Invoice Tax"
1466     _order = 'sequence'
1467
1468     @api.one
1469     @api.depends('base', 'base_amount', 'amount', 'tax_amount')
1470     def _compute_factors(self):
1471         self.factor_base = self.base_amount / self.base if self.base else 1.0
1472         self.factor_tax = self.tax_amount / self.amount if self.amount else 1.0
1473
1474     invoice_id = fields.Many2one('account.invoice', string='Invoice Line',
1475         ondelete='cascade', index=True)
1476     name = fields.Char(string='Tax Description',
1477         required=True)
1478     account_id = fields.Many2one('account.account', string='Tax Account',
1479         required=True, domain=[('type', 'not in', ['view', 'income', 'closed'])])
1480     account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic account')
1481     base = fields.Float(string='Base', digits=dp.get_precision('Account'))
1482     amount = fields.Float(string='Amount', digits=dp.get_precision('Account'))
1483     manual = fields.Boolean(string='Manual', default=True)
1484     sequence = fields.Integer(string='Sequence',
1485         help="Gives the sequence order when displaying a list of invoice tax.")
1486     base_code_id = fields.Many2one('account.tax.code', string='Base Code',
1487         help="The account basis of the tax declaration.")
1488     base_amount = fields.Float(string='Base Code Amount', digits=dp.get_precision('Account'),
1489         default=0.0)
1490     tax_code_id = fields.Many2one('account.tax.code', string='Tax Code',
1491         help="The tax basis of the tax declaration.")
1492     tax_amount = fields.Float(string='Tax Code Amount', digits=dp.get_precision('Account'),
1493         default=0.0)
1494
1495     company_id = fields.Many2one('res.company', string='Company',
1496         related='account_id.company_id', store=True, readonly=True)
1497     factor_base = fields.Float(string='Multipication factor for Base code',
1498         compute='_compute_factors')
1499     factor_tax = fields.Float(string='Multipication factor Tax code',
1500         compute='_compute_factors')
1501
1502     @api.multi
1503     def base_change(self, base, currency_id=False, company_id=False, date_invoice=False):
1504         factor = self.factor_base if self else 1
1505         company = self.env['res.company'].browse(company_id)
1506         if currency_id and company.currency_id:
1507             currency = self.env['res.currency'].browse(currency_id)
1508             currency = currency.with_context(date=date_invoice or fields.Date.today())
1509             base = currency.compute(base * factor, company.currency_id, round=False)
1510         return {'value': {'base_amount': base}}
1511
1512     @api.multi
1513     def amount_change(self, amount, currency_id=False, company_id=False, date_invoice=False):
1514         factor = self.factor_tax if self else 1
1515         company = self.env['res.company'].browse(company_id)
1516         if currency_id and company.currency_id:
1517             currency = self.env['res.currency'].browse(currency_id)
1518             currency = currency.with_context(date=date_invoice or fields.Date.today())
1519             amount = currency.compute(amount * factor, company.currency_id, round=False)
1520         return {'value': {'tax_amount': amount}}
1521
1522     @api.v8
1523     def compute(self, invoice):
1524         tax_grouped = {}
1525         currency = invoice.currency_id.with_context(date=invoice.date_invoice or fields.Date.today())
1526         company_currency = invoice.company_id.currency_id
1527         for line in invoice.invoice_line:
1528             taxes = line.invoice_line_tax_id.compute_all(
1529                 (line.price_unit * (1 - (line.discount or 0.0) / 100.0)),
1530                 line.quantity, line.product_id, invoice.partner_id)['taxes']
1531             for tax in taxes:
1532                 val = {
1533                     'invoice_id': invoice.id,
1534                     'name': tax['name'],
1535                     'amount': tax['amount'],
1536                     'manual': False,
1537                     'sequence': tax['sequence'],
1538                     'base': currency.round(tax['price_unit'] * line['quantity']),
1539                 }
1540                 if invoice.type in ('out_invoice','in_invoice'):
1541                     val['base_code_id'] = tax['base_code_id']
1542                     val['tax_code_id'] = tax['tax_code_id']
1543                     val['base_amount'] = currency.compute(val['base'] * tax['base_sign'], company_currency, round=False)
1544                     val['tax_amount'] = currency.compute(val['amount'] * tax['tax_sign'], company_currency, round=False)
1545                     val['account_id'] = tax['account_collected_id'] or line.account_id.id
1546                     val['account_analytic_id'] = tax['account_analytic_collected_id']
1547                 else:
1548                     val['base_code_id'] = tax['ref_base_code_id']
1549                     val['tax_code_id'] = tax['ref_tax_code_id']
1550                     val['base_amount'] = currency.compute(val['base'] * tax['ref_base_sign'], company_currency, round=False)
1551                     val['tax_amount'] = currency.compute(val['amount'] * tax['ref_tax_sign'], company_currency, round=False)
1552                     val['account_id'] = tax['account_paid_id'] or line.account_id.id
1553                     val['account_analytic_id'] = tax['account_analytic_paid_id']
1554
1555                 # If the taxes generate moves on the same financial account as the invoice line
1556                 # and no default analytic account is defined at the tax level, propagate the
1557                 # analytic account from the invoice line to the tax line. This is necessary
1558                 # in situations were (part of) the taxes cannot be reclaimed,
1559                 # to ensure the tax move is allocated to the proper analytic account.
1560                 if not val.get('account_analytic_id') and line.account_analytic_id and val['account_id'] == line.account_id.id:
1561                     val['account_analytic_id'] = line.account_analytic_id.id
1562
1563                 key = (val['tax_code_id'], val['base_code_id'], val['account_id'])
1564                 if not key in tax_grouped:
1565                     tax_grouped[key] = val
1566                 else:
1567                     tax_grouped[key]['base'] += val['base']
1568                     tax_grouped[key]['amount'] += val['amount']
1569                     tax_grouped[key]['base_amount'] += val['base_amount']
1570                     tax_grouped[key]['tax_amount'] += val['tax_amount']
1571
1572         for t in tax_grouped.values():
1573             t['base'] = currency.round(t['base'])
1574             t['amount'] = currency.round(t['amount'])
1575             t['base_amount'] = currency.round(t['base_amount'])
1576             t['tax_amount'] = currency.round(t['tax_amount'])
1577
1578         return tax_grouped
1579
1580     @api.v7
1581     def compute(self, cr, uid, invoice_id, context=None):
1582         recs = self.browse(cr, uid, [], context)
1583         invoice = recs.env['account.invoice'].browse(invoice_id)
1584         return recs.compute(invoice)
1585
1586     @api.model
1587     def move_line_get(self, invoice_id):
1588         res = []
1589         self._cr.execute(
1590             'SELECT * FROM account_invoice_tax WHERE invoice_id = %s',
1591             (invoice_id,)
1592         )
1593         for row in self._cr.dictfetchall():
1594             if not (row['amount'] or row['tax_code_id'] or row['tax_amount']):
1595                 continue
1596             res.append({
1597                 'type': 'tax',
1598                 'name': row['name'],
1599                 'price_unit': row['amount'],
1600                 'quantity': 1,
1601                 'price': row['amount'] or 0.0,
1602                 'account_id': row['account_id'],
1603                 'tax_code_id': row['tax_code_id'],
1604                 'tax_amount': row['tax_amount'],
1605                 'account_analytic_id': row['account_analytic_id'],
1606             })
1607         return res
1608
1609
1610 class res_partner(models.Model):
1611     # Inherits partner and adds invoice information in the partner form
1612     _inherit = 'res.partner'
1613
1614     invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices',
1615         readonly=True)
1616
1617     def _find_accounting_partner(self, partner):
1618         '''
1619         Find the partner for which the accounting entries will be created
1620         '''
1621         return partner.commercial_partner_id
1622
1623 class mail_compose_message(models.Model):
1624     _inherit = 'mail.compose.message'
1625
1626     @api.multi
1627     def send_mail(self):
1628         context = self._context
1629         if context.get('default_model') == 'account.invoice' and \
1630                 context.get('default_res_id') and context.get('mark_invoice_as_sent'):
1631             invoice = self.env['account.invoice'].browse(context['default_res_id'])
1632             invoice = invoice.with_context(mail_post_autofollow=True)
1633             invoice.write({'sent': True})
1634             invoice.message_post(body=_("Invoice sent"))
1635         return super(mail_compose_message, self).send_mail()
1636
1637 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: