1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
23 from lxml import etree
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
30 # mapping invoice type to journal type
32 'out_invoice': 'sale',
33 'in_invoice': 'purchase',
34 'out_refund': 'sale_refund',
35 'in_refund': 'purchase_refund',
38 # mapping invoice type to refund type
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
46 MAGIC_COLUMNS = ('id', 'create_uid', 'create_date', 'write_uid', 'write_date')
49 class account_invoice(models.Model):
50 _name = "account.invoice"
51 _inherit = ['mail.thread']
52 _description = "Invoice"
53 _order = "number desc, id desc"
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'),
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
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)
76 ('type', 'in', filter(None, map(TYPE2JOURNAL.get, inv_types))),
77 ('company_id', '=', company_id),
79 return self.env['account.journal'].search(domain, limit=1)
82 def _default_currency(self):
83 journal = self._default_journal()
84 return journal.currency or journal.company_id.currency_id
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)
94 raise except_orm(_('No Analytic Journal!'),
95 _("You must define an analytic journal of type '%s'!") % (journal_type,))
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()
104 def _get_reference_type(self):
105 return [('none', _('Free Reference'))]
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',
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):
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'):
130 if line.reconcile_partial_id and line.reconcile_partial_id.id in partial_reconciliations_done:
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
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)
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',
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']
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
169 lines = self.env['account.move.line']
170 partial_lines += data_line
171 self.move_lines = lines - partial_lines
175 'move_id.line_id.reconcile_id.line_id',
176 'move_id.line_id.reconcile_partial_id.line_partial_ids',
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:
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()
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')
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)]},
216 comment = fields.Text('Additional Information')
218 state = fields.Selection([
220 ('proforma','Pro-forma'),
221 ('proforma2','Pro-forma'),
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)]})
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.")
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')
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)
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)]})
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")
314 ('number_uniq', 'unique(number, company_id, journal_id, type)',
315 'Invoice Number must be unique per Company!'),
319 def fields_view_get(self, view_id=None, view_type=False, toolbar=False, submenu=False):
320 context = self._context
322 def get_view_id(xid, name):
324 return self.env['ir.model.data'].xmlid_to_res_id('account.' + xid, raise_if_not_found=True)
327 return self.env['ir.ui.view'].search([('name', '=', name)], limit=1).id
329 return False # view not found
331 if context.get('active_model') == 'res.partner' and context.get('active_ids'):
332 partner = self.env['res.partner'].browse(context['active_ids'])[0]
334 view_id = get_view_id('invoice_tree', 'account.invoice.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')
342 res = super(account_invoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
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
350 doc = etree.XML(res['arch'])
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)]")
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']"):
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)
373 res['arch'] = etree.tostring(doc)
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
381 assert len(self) == 1, 'This option should only be used for a single id at a time.'
383 return self.env['report'].get_action(self, 'account.report_invoice')
386 def action_invoice_sent(self):
387 """ Open a window to compose an email, with the edi invoice template
388 message loaded by default
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)
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,
402 'name': _('Compose Email'),
403 'type': 'ir.actions.act_window',
406 'res_model': 'mail.compose.message',
407 'views': [(compose_form.id, 'form')],
408 'view_id': compose_form.id,
414 def confirm_paid(self):
415 return self.write({'state': 'paid'})
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()
427 def onchange_partner_id(self, type, partner_id, date_invoice=False,
428 payment_term=False, partner_bank_id=False, company_id=False):
430 payment_term_id = False
431 fiscal_position = False
435 p = self.env['res.partner'].browse(partner_id)
436 rec_account = p.property_account_receivable
437 pay_account = p.property_account_payable
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'))
456 if type in ('out_invoice', 'out_refund'):
457 account_id = rec_account.id
458 payment_term_id = p.property_payment_term.id
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
466 'account_id': account_id,
467 'payment_term': payment_term_id,
468 'fiscal_position': fiscal_position,
471 if type in ('in_invoice', 'in_refund'):
472 result['value']['partner_bank_id'] = bank_id
474 if payment_term != 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', {}))
479 result['value']['date_due'] = False
481 if partner_bank_id != bank_id:
482 to_update = self.onchange_partner_bank(bank_id)
483 result['value'].update(to_update.get('value', {}))
488 def onchange_journal_id(self, journal_id=False):
490 journal = self.env['account.journal'].browse(journal_id)
493 'currency_id': journal.currency.id or journal.company_id.currency_id.id,
494 'company_id': journal.company_id.id,
500 def onchange_payment_term_date_invoice(self, payment_term_id, 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]
510 return {'value': {'date_due': max(line[0] for line in pterm_list)}}
512 raise except_orm(_('Insufficient Data!'),
513 _('The payment term of supplier does not have a payment term line.'))
516 def onchange_invoice_line(self, lines):
520 def onchange_partner_bank(self, partner_bank_id=False):
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())
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'))
550 if type in ('out_invoice', 'out_refund'):
551 acc_id = rec_account.id
553 acc_id = pay_account.id
554 values= {'account_id': acc_id}
558 for line in self.invoice_line:
559 if not line.account_id:
561 if line.account_id.company_id.id == company_id:
563 accounts = self.env['account.account'].search([('name', '=', line.account_id.name), ('company_id', '=', company_id)])
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})
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:
575 _('Configuration Error!'),
576 _("Invoice line account's company and invoice's company does not match.")
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)])
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)]}
595 return {'value': values, 'domain': domain}
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()
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')
612 return self.env.ref('account.invoice_form')
615 def move_line_id_payment_get(self):
616 # return the move line ids with the same account as the invoice self
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
623 self._cr.execute(query, (self.id,))
624 return [row[0] for row in self._cr.fetchall()]
628 # check whether all corresponding account move lines are reconciled
629 line_ids = self.move_line_id_payment_get()
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())
637 def button_reset_taxes(self):
638 account_invoice_tax = self.env['account.invoice.tax']
639 ctx = dict(self._context)
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
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': []})
652 def button_compute(self, set_total=False):
653 self.button_reset_taxes()
656 invoice.check_total = invoice.amount_total
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
665 iml = self.env['account.invoice.line'].move_line_get(self.id)
667 if il['account_analytic_id']:
668 if self.type in ('in_invoice', 'in_refund'):
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, {
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,
691 def action_date_assign(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'])
699 def finalize_invoice_move_lines(self, move_lines):
700 """ finalize_invoice_move_lines(move_lines) -> move_lines
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
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
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)
719 precision = self.env['decimal.precision'].precision_get('Account')
720 for tax in self.tax_line:
723 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
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.'))
735 def compute_invoice_totals(self, company_currency, ref, invoice_move_lines):
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)
745 line['currency_id'] = False
746 line['amount_currency'] = False
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']
753 total -= line['price']
754 total_currency -= line['amount_currency'] or line['price']
755 return total, total_currency, invoice_move_lines
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
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'),
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:
775 tmp = self.inv_line_characteristic_hashcode(l)
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']
785 for key, val in line2.items():
786 line.append((0,0,val))
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']
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.'))
803 ctx = dict(self._context, lang=inv.partner_id.lang)
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
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)
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.'))
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'."))
832 # one move line per tax line
833 iml += account_invoice_tax.move_line_get(inv.id)
835 if inv.type in ('in_invoice', 'in_refund'):
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)
844 name = inv.name or inv.supplier_invoice_number or '/'
847 totlines = inv.with_context(ctx).payment_term.compute(total, date_invoice)[0]
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)
855 amount_currency = False
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
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,
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,
886 part = self.env['res.partner']._find_accounting_partner(inv.partner_id)
888 line = [(0, 0, self.line_get_convert(l, part.id, date)) for l in iml]
889 line = inv.group_lines(iml, line)
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.'))
896 line = inv.finalize_invoice_move_lines(line)
899 'ref': inv.reference or inv.name,
901 'journal_id': journal.id,
902 'date': inv.date_invoice,
903 'narration': inv.comment,
904 'company_id': inv.company_id.id,
906 ctx['company_id'] = inv.company_id.id
907 period = inv.period_id
909 period = period.with_context(ctx).find(date_invoice)[:1]
911 move_vals['period_id'] = period.id
913 i[2]['period_id'] = period.id
916 move = account_move.with_context(ctx).create(move_vals)
917 # make the invoice point to that move
920 'period_id': period.id,
921 'move_name': move.name,
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:
931 def invoice_validate(self):
932 return self.write({'state': 'open'})
935 def line_get_convert(self, line, part, date):
937 'date_maturity': line.get('date_maturity', False),
939 'name': line['name'][:64],
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),
957 def action_number(self):
958 #TODO: not correct fix but required a fresh values before reading it.
962 self.write({'internal_number': inv.number})
964 if inv.type in ('in_invoice', 'in_refund'):
965 if not inv.reference:
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()
988 def action_cancel(self):
989 moves = self.env['account.move']
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.'))
998 # First, set the invoices as cancelled and detach the move ids
999 self.write({'state': 'cancel', 'move_id': False})
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
1007 self._log_event(-1.0, 'Cancel Invoice')
1013 def _log_event(self, factor=1.0, name='Open Invoice'):
1014 #TODO: implement messages system
1020 'out_invoice': _('Invoice'),
1021 'in_invoice': _('Supplier Invoice'),
1022 'out_refund': _('Refund'),
1023 'in_refund': _('Supplier Refund'),
1027 result.append((inv.id, "%s %s" % (inv.number or TYPES[inv.type], inv.name or '')))
1031 def name_search(self, name, args=None, operator='ilike', limit=100):
1033 recs = self.browse()
1035 recs = self.search([('number', '=', name)] + args, limit=limit)
1037 recs = self.search([('name', operator, name)] + args, limit=limit)
1038 return recs.name_get()
1041 def _refund_cleanup_lines(self, lines):
1042 """ Convert records to dict of values suitable for one2many line creation
1044 :param recordset lines: records to convert
1045 :return: list of command tuple for one2many line creation [(0, 0, dict of valueis), ...]
1050 for name, field in line._fields.iteritems():
1051 if name in MAGIC_COLUMNS:
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))
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).
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
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
1082 values[field] = invoice[field] or False
1084 values['invoice_line'] = self._refund_cleanup_lines(invoice.invoice_line)
1086 tax_lines = filter(lambda l: l.manual, invoice.tax_line)
1087 values['tax_line'] = self._refund_cleanup_lines(tax_lines)
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)
1094 journal = self.env['account.journal'].search([('type', '=', 'sale_refund')], limit=1)
1095 values['journal_id'] = journal.id
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
1103 values['period_id'] = period_id
1105 values['name'] = description
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)
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)
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']
1135 amount_currency = False
1138 pay_journal = self.env['account.journal'].browse(pay_journal_id)
1139 if self.type in ('in_invoice', 'in_refund'):
1140 ref = self.reference
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
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,
1154 'currency_id': currency_id,
1155 'amount_currency': direction * (amount_currency or 0.0),
1156 'company_id': self.company_id.id,
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,
1166 'currency_id': currency_id,
1167 'amount_currency': -direction * (amount_currency or 0.0),
1168 'company_id': self.company_id.id,
1170 move = self.env['account.move'].create({
1172 'line_id': [(0, 0, l1), (0, 0, l2)],
1173 'journal_id': pay_journal_id,
1174 'period_id': period_id,
1178 move_ids = (move | self.move_id).ids
1179 self._cr.execute("SELECT id FROM account_move_line WHERE move_id IN %s",
1181 lines = self.env['account.move.line'].browse([r[0] for r in self._cr.fetchall()])
1182 lines2rec = lines.browse()
1184 for line in itertools.chain(lines, self.payment_ids):
1185 if line.account_id == self.account_id:
1187 total += (line.debit or 0.0) - (line.credit or 0.0)
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)
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')
1200 # Update the stored value (fields.function), so we write to trigger recompute
1201 return self.write({})
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)
1210 class account_invoice_line(models.Model):
1211 _name = "account.invoice.line"
1212 _description = "Invoice Line"
1213 _order = "invoice_id,sequence,id"
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']
1223 self.price_subtotal = self.invoice_id.currency_id.round(self.price_subtotal)
1226 def _default_price_unit(self):
1227 if not self._context.get('check_total'):
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]:
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']
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')
1252 return self.env['ir.property'].get('property_account_expense_categ', 'product.category')
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'),
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)
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)]")
1298 node.set('domain', "[('sale_ok', '=', True)]")
1299 res['arch'] = etree.tostring(doc)
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,
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)
1311 raise except_orm(_('No Partner Defined!'), _("You must first select a partner!"))
1313 if type in ('in_invoice', 'in_refund'):
1314 return {'value': {}, 'domain': {'product_uom': []}}
1316 return {'value': {'price_unit': 0.0}, 'domain': {'product_uom': []}}
1320 part = self.env['res.partner'].browse(partner_id)
1321 fpos = self.env['account.fiscal.position'].browse(fposition_id)
1324 self = self.with_context(lang=part.lang)
1325 product = self.env['product.product'].browse(product)
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
1331 account = product.property_account_expense or product.categ_id.property_account_expense_categ
1332 account = fpos.map_account(account)
1334 values['account_id'] = account.id
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
1341 taxes = product.supplier_taxes_id or account.tax_ids
1342 if product.description_purchase:
1343 values['name'] += '\n' + product.description_purchase
1345 taxes = fpos.map_tax(taxes)
1346 values['invoice_line_tax_id'] = taxes.ids
1348 if type in ('in_invoice', 'in_refund'):
1349 values['price_unit'] = price_unit or product.standard_price
1351 values['price_unit'] = product.list_price
1353 values['uos_id'] = uom_id or product.uom_id.id
1354 domain = {'uos_id': [('category_id', '=', product.uom_id.category_id.id)]}
1356 company = self.env['res.company'].browse(company_id)
1357 currency = self.env['res.currency'].browse(currency_id)
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
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'])
1369 return {'value': values, 'domain': domain}
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)
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,
1384 result['value']['price_unit'] = 0.0
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:
1390 'title': _('Warning!'),
1391 'message': _('The selected unit of measure is not compatible with the unit of measure of the product.'),
1393 result['value']['uos_id'] = prod.uom_id.id
1395 result['warning'] = warning
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
1405 for line in inv.invoice_line:
1406 mres = self.move_line_get_item(line)
1407 mres['invl_id'] = line.id
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']
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']
1418 tax_code_id = tax['ref_base_code_id']
1419 tax_amount = line.price_subtotal * tax['ref_base_sign']
1424 res.append(dict(mres))
1425 res[-1]['price'] = 0.0
1426 res[-1]['account_analytic_id'] = False
1427 elif not tax_code_id:
1429 tax_code_found = True
1431 res[-1]['tax_code_id'] = tax_code_id
1432 res[-1]['tax_amount'] = currency.compute(tax_amount, company_currency)
1437 def move_line_get_item(self, line):
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,
1452 # Set the tax field according to the account and the fiscal position
1455 def onchange_account_id(self, product_id, partner_id, inv_type, fposition_id, account_id):
1459 account = self.env['account.account'].browse(account_id)
1461 fpos = self.env['account.fiscal.position'].browse(fposition_id)
1462 unique_tax_ids = fpos.map_tax(account.tax_ids).ids
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}}
1471 class account_invoice_tax(models.Model):
1472 _name = "account.invoice.tax"
1473 _description = "Invoice Tax"
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
1482 invoice_id = fields.Many2one('account.invoice', string='Invoice Line',
1483 ondelete='cascade', index=True)
1484 name = fields.Char(string='Tax Description',
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'),
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'),
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')
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}}
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}}
1531 def compute(self, invoice):
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']
1541 'invoice_id': invoice.id,
1542 'name': tax['name'],
1543 'amount': tax['amount'],
1545 'sequence': tax['sequence'],
1546 'base': currency.round(tax['price_unit'] * line['quantity']),
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']
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']
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
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
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']
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'])
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)
1595 def move_line_get(self, invoice_id):
1598 'SELECT * FROM account_invoice_tax WHERE invoice_id = %s',
1601 for row in self._cr.dictfetchall():
1602 if not (row['amount'] or row['tax_code_id'] or row['tax_amount']):
1606 'name': row['name'],
1607 'price_unit': row['amount'],
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'],
1618 class res_partner(models.Model):
1619 # Inherits partner and adds invoice information in the partner form
1620 _inherit = 'res.partner'
1622 invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices',
1623 readonly=True, copy=False)
1625 def _find_accounting_partner(self, partner):
1627 Find the partner for which the accounting entries will be created
1629 return partner.commercial_partner_id
1631 class mail_compose_message(models.Model):
1632 _inherit = 'mail.compose.message'
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()
1645 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: