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