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