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 import openerp.addons.decimal_precision as dp
29 # mapping invoice type to journal type
31 'out_invoice': 'sale',
32 'in_invoice': 'purchase',
33 'out_refund': 'sale_refund',
34 'in_refund': 'purchase_refund',
37 # mapping invoice type to refund type
39 'out_invoice': 'out_refund', # Customer Invoice
40 'in_invoice': 'in_refund', # Supplier Invoice
41 'out_refund': 'out_invoice', # Customer Refund
42 'in_refund': 'in_invoice', # Supplier Refund
45 MAGIC_COLUMNS = ('id', 'create_uid', 'create_date', 'write_uid', 'write_date')
48 class account_invoice(models.Model):
49 _name = "account.invoice"
50 _inherit = ['mail.thread']
51 _description = "Invoice"
52 _order = "number desc, id desc"
57 'account.mt_invoice_paid': lambda self, cr, uid, obj, ctx=None: obj.state == 'paid' and obj.type in ('out_invoice', 'out_refund'),
58 'account.mt_invoice_validated': lambda self, cr, uid, obj, ctx=None: obj.state == 'open' and obj.type in ('out_invoice', 'out_refund'),
63 @api.depends('invoice_line.price_subtotal', 'tax_line.amount')
64 def _compute_amount(self):
65 self.amount_untaxed = sum(line.price_subtotal for line in self.invoice_line)
66 self.amount_tax = sum(line.amount for line in self.tax_line)
67 self.amount_total = self.amount_untaxed + self.amount_tax
70 def _default_journal(self):
71 inv_type = self._context.get('type', 'out_invoice')
72 inv_types = inv_type if isinstance(inv_type, list) else [inv_type]
73 company_id = self._context.get('company_id', self.env.user.company_id.id)
75 ('type', 'in', filter(None, map(TYPE2JOURNAL.get, inv_types))),
76 ('company_id', '=', company_id),
78 return self.env['account.journal'].search(domain, limit=1)
81 def _default_currency(self):
82 journal = self._default_journal()
83 return journal.currency or journal.company_id.currency_id
86 @api.returns('account.analytic.journal', lambda r: r.id)
87 def _get_journal_analytic(self, inv_type):
88 """ Return the analytic journal corresponding to the given invoice type. """
89 journal_type = TYPE2JOURNAL.get(inv_type, 'sale')
90 journal = self.env['account.analytic.journal'].search([('type', '=', journal_type)], limit=1)
92 raise except_orm(_('No Analytic Journal!'),
93 _("You must define an analytic journal of type '%s'!") % (journal_type,))
97 @api.depends('account_id', 'move_id.line_id.account_id', 'move_id.line_id.reconcile_id')
98 def _compute_reconciled(self):
99 self.reconciled = self.test_paid()
102 def _get_reference_type(self):
103 return [('none', _('Free Reference'))]
107 'state', 'currency_id', 'invoice_line.price_subtotal',
108 'move_id.line_id.account_id.type',
109 'move_id.line_id.amount_residual',
110 'move_id.line_id.amount_residual_currency',
111 'move_id.line_id.currency_id',
112 'move_id.line_id.reconcile_partial_id.line_partial_ids.invoice.type',
114 def _compute_residual(self):
115 nb_inv_in_partial_rec = max_invoice_id = 0
117 for line in self.sudo().move_id.line_id:
118 if line.account_id.type in ('receivable', 'payable'):
119 if line.currency_id == self.currency_id:
120 self.residual += line.amount_residual_currency
122 # ahem, shouldn't we use line.currency_id here?
123 from_currency = line.company_id.currency_id.with_context(date=line.date)
124 self.residual += from_currency.compute(line.amount_residual, self.currency_id)
125 # we check if the invoice is partially reconciled and if there
126 # are other invoices involved in this partial reconciliation
127 for pline in line.reconcile_partial_id.line_partial_ids:
128 if pline.invoice and self.type == pline.invoice.type:
129 nb_inv_in_partial_rec += 1
130 # store the max invoice id as for this invoice we will
131 # make a balance instead of a simple division
132 max_invoice_id = max(max_invoice_id, pline.invoice.id)
133 if nb_inv_in_partial_rec:
134 # if there are several invoices in a partial reconciliation, we
135 # split the residual by the number of invoices to have a sum of
136 # residual amounts that matches the partner balance
137 new_value = self.currency_id.round(self.residual / nb_inv_in_partial_rec)
138 if self.id == max_invoice_id:
139 # if it's the last the invoice of the bunch of invoices
140 # partially reconciled together, we make a balance to avoid
142 self.residual = self.residual - ((nb_inv_in_partial_rec - 1) * new_value)
144 self.residual = new_value
145 # prevent the residual amount on the invoice to be less than 0
146 self.residual = max(self.residual, 0.0)
150 'move_id.line_id.account_id',
151 'move_id.line_id.reconcile_id.line_id',
152 'move_id.line_id.reconcile_partial_id.line_partial_ids',
154 def _compute_move_lines(self):
155 # Give Journal Items related to the payment reconciled to this invoice.
156 # Return partial and total payments related to the selected invoice.
157 self.move_lines = self.env['account.move.line']
160 data_lines = self.move_id.line_id.filtered(lambda l: l.account_id == self.account_id)
161 partial_lines = self.env['account.move.line']
162 for data_line in data_lines:
163 if data_line.reconcile_id:
164 lines = data_line.reconcile_id.line_id
165 elif data_line.reconcile_partial_id:
166 lines = data_line.reconcile_partial_id.line_partial_ids
168 lines = self.env['account.move.line']
169 partial_lines += data_line
170 self.move_lines = lines - partial_lines
174 'move_id.line_id.reconcile_id.line_id',
175 'move_id.line_id.reconcile_partial_id.line_partial_ids',
177 def _compute_payments(self):
178 partial_lines = lines = self.env['account.move.line']
179 for line in self.move_id.line_id:
180 if line.account_id != self.account_id:
182 if line.reconcile_id:
183 lines |= line.reconcile_id.line_id
184 elif line.reconcile_partial_id:
185 lines |= line.reconcile_partial_id.line_partial_ids
186 partial_lines += line
187 self.payment_ids = (lines - partial_lines).sorted()
189 name = fields.Char(string='Reference/Description', index=True,
190 readonly=True, states={'draft': [('readonly', False)]})
191 origin = fields.Char(string='Source Document',
192 help="Reference of the document that produced this invoice.",
193 readonly=True, states={'draft': [('readonly', False)]})
194 supplier_invoice_number = fields.Char(string='Supplier Invoice Number',
195 help="The reference of this invoice as provided by the supplier.",
196 readonly=True, states={'draft': [('readonly', False)]})
197 type = fields.Selection([
198 ('out_invoice','Customer Invoice'),
199 ('in_invoice','Supplier Invoice'),
200 ('out_refund','Customer Refund'),
201 ('in_refund','Supplier Refund'),
202 ], string='Type', readonly=True, index=True, change_default=True,
203 default=lambda self: self._context.get('type', 'out_invoice'),
204 track_visibility='always')
206 number = fields.Char(related='move_id.name', store=True, readonly=True, copy=False)
207 internal_number = fields.Char(string='Invoice Number', readonly=True,
208 default=False, copy=False,
209 help="Unique number of the invoice, computed automatically when the invoice is created.")
210 reference = fields.Char(string='Invoice Reference',
211 help="The partner reference of this invoice.")
212 reference_type = fields.Selection('_get_reference_type', string='Payment Reference',
213 required=True, readonly=True, states={'draft': [('readonly', False)]},
215 comment = fields.Text('Additional Information')
217 state = fields.Selection([
219 ('proforma','Pro-forma'),
220 ('proforma2','Pro-forma'),
223 ('cancel','Cancelled'),
224 ], string='Status', index=True, readonly=True, default='draft',
225 track_visibility='onchange', copy=False,
226 help=" * The 'Draft' status is used when a user is encoding a new and unconfirmed Invoice.\n"
227 " * The 'Pro-forma' when invoice is in Pro-forma status,invoice does not have an invoice number.\n"
228 " * The 'Open' status is used when user create invoice,a invoice number is generated.Its in open status till user does not pay invoice.\n"
229 " * The 'Paid' status is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled.\n"
230 " * The 'Cancelled' status is used when user cancel invoice.")
231 sent = fields.Boolean(readonly=True, default=False, copy=False,
232 help="It indicates that the invoice has been sent.")
233 date_invoice = fields.Date(string='Invoice Date',
234 readonly=True, states={'draft': [('readonly', False)]}, index=True,
235 help="Keep empty to use the current date", copy=False)
236 date_due = fields.Date(string='Due Date',
237 readonly=True, states={'draft': [('readonly', False)]}, index=True, copy=False,
238 help="If you use payment terms, the due date will be computed automatically at the generation "
239 "of accounting entries. The payment term may compute several due dates, for example 50% "
240 "now and 50% in one month, but if you want to force a due date, make sure that the payment "
241 "term is not set on the invoice. If you keep the payment term and the due date empty, it "
242 "means direct payment.")
243 partner_id = fields.Many2one('res.partner', string='Partner', change_default=True,
244 required=True, readonly=True, states={'draft': [('readonly', False)]},
245 track_visibility='always')
246 payment_term = fields.Many2one('account.payment.term', string='Payment Terms',
247 readonly=True, states={'draft': [('readonly', False)]},
248 help="If you use payment terms, the due date will be computed automatically at the generation "
249 "of accounting entries. If you keep the payment term and the due date empty, it means direct payment. "
250 "The payment term may compute several due dates, for example 50% now, 50% in one month.")
251 period_id = fields.Many2one('account.period', string='Force Period',
252 domain=[('state', '!=', 'done')], copy=False,
253 help="Keep empty to use the period of the validation(invoice) date.",
254 readonly=True, states={'draft': [('readonly', False)]})
256 account_id = fields.Many2one('account.account', string='Account',
257 required=True, readonly=True, states={'draft': [('readonly', False)]},
258 help="The partner account used for this invoice.")
259 invoice_line = fields.One2many('account.invoice.line', 'invoice_id', string='Invoice Lines',
260 readonly=True, states={'draft': [('readonly', False)]}, copy=True)
261 tax_line = fields.One2many('account.invoice.tax', 'invoice_id', string='Tax Lines',
262 readonly=True, states={'draft': [('readonly', False)]}, copy=True)
263 move_id = fields.Many2one('account.move', string='Journal Entry',
264 readonly=True, index=True, ondelete='restrict', copy=False,
265 help="Link to the automatically generated Journal Items.")
267 amount_untaxed = fields.Float(string='Subtotal', digits=dp.get_precision('Account'),
268 store=True, readonly=True, compute='_compute_amount', track_visibility='always')
269 amount_tax = fields.Float(string='Tax', digits=dp.get_precision('Account'),
270 store=True, readonly=True, compute='_compute_amount')
271 amount_total = fields.Float(string='Total', digits=dp.get_precision('Account'),
272 store=True, readonly=True, compute='_compute_amount')
274 currency_id = fields.Many2one('res.currency', string='Currency',
275 required=True, readonly=True, states={'draft': [('readonly', False)]},
276 default=_default_currency, track_visibility='always')
277 journal_id = fields.Many2one('account.journal', string='Journal',
278 required=True, readonly=True, states={'draft': [('readonly', False)]},
279 default=_default_journal,
280 domain="[('type', 'in', {'out_invoice': ['sale'], 'out_refund': ['sale_refund'], 'in_refund': ['purchase_refund'], 'in_invoice': ['purchase']}.get(type, [])), ('company_id', '=', company_id)]")
281 company_id = fields.Many2one('res.company', string='Company', change_default=True,
282 required=True, readonly=True, states={'draft': [('readonly', False)]},
283 default=lambda self: self.env['res.company']._company_default_get('account.invoice'))
284 check_total = fields.Float(string='Verification Total', digits=dp.get_precision('Account'),
285 readonly=True, states={'draft': [('readonly', False)]}, default=0.0)
287 reconciled = fields.Boolean(string='Paid/Reconciled',
288 store=True, readonly=True, compute='_compute_reconciled',
289 help="It indicates that the invoice has been paid and the journal entry of the invoice has been reconciled with one or several journal entries of payment.")
290 partner_bank_id = fields.Many2one('res.partner.bank', string='Bank Account',
291 help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Supplier Refund, otherwise a Partner bank account number.',
292 readonly=True, states={'draft': [('readonly', False)]})
294 move_lines = fields.Many2many('account.move.line', string='Entry Lines',
295 compute='_compute_move_lines')
296 residual = fields.Float(string='Balance', digits=dp.get_precision('Account'),
297 compute='_compute_residual', store=True,
298 help="Remaining amount due.")
299 payment_ids = fields.Many2many('account.move.line', string='Payments',
300 compute='_compute_payments')
301 move_name = fields.Char(string='Journal Entry', readonly=True,
302 states={'draft': [('readonly', False)]}, copy=False)
303 user_id = fields.Many2one('res.users', string='Salesperson', track_visibility='onchange',
304 readonly=True, states={'draft': [('readonly', False)]},
305 default=lambda self: self.env.user)
306 fiscal_position = fields.Many2one('account.fiscal.position', string='Fiscal Position',
307 readonly=True, states={'draft': [('readonly', False)]})
308 commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity',
309 related='partner_id.commercial_partner_id', store=True, readonly=True,
310 help="The commercial entity that will be used on Journal Entries for this invoice")
313 ('number_uniq', 'unique(number, company_id, journal_id, type)',
314 'Invoice Number must be unique per Company!'),
318 def fields_view_get(self, view_id=None, view_type=False, toolbar=False, submenu=False):
319 context = self._context
320 if context.get('active_model') == 'res.partner' and context.get('active_ids'):
321 partner = self.env['res.partner'].browse(context['active_ids'])[0]
323 view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.tree')]).id
325 elif view_type == 'form':
326 if partner.supplier and not partner.customer:
327 view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.supplier.form')]).id
328 elif partner.customer and not partner.supplier:
329 view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.form')]).id
331 res = super(account_invoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
333 # adapt selection of field journal_id
334 for field in res['fields']:
335 if field == 'journal_id' and type:
336 journal_select = self.env['account.journal']._name_search('', [('type', '=', type)], name_get_uid=1)
337 res['fields'][field]['selection'] = journal_select
339 doc = etree.XML(res['arch'])
341 if context.get('type'):
342 for node in doc.xpath("//field[@name='partner_bank_id']"):
343 if context['type'] == 'in_refund':
344 node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]")
345 elif context['type'] == 'out_refund':
346 node.set('domain', "[('partner_id', '=', partner_id)]")
348 if view_type == 'search':
349 if context.get('type') in ('out_invoice', 'out_refund'):
350 for node in doc.xpath("//group[@name='extended filter']"):
353 if view_type == 'tree':
354 partner_string = _('Customer')
355 if context.get('type') in ('in_invoice', 'in_refund'):
356 partner_string = _('Supplier')
357 for node in doc.xpath("//field[@name='reference']"):
358 node.set('invisible', '0')
359 for node in doc.xpath("//field[@name='partner_id']"):
360 node.set('string', partner_string)
362 res['arch'] = etree.tostring(doc)
366 def invoice_print(self):
367 """ Print the invoice and mark it as sent, so that we can see more
368 easily the next step of the workflow
370 assert len(self) == 1, 'This option should only be used for a single id at a time.'
372 return self.env['report'].get_action(self, 'account.report_invoice')
375 def action_invoice_sent(self):
376 """ Open a window to compose an email, with the edi invoice template
377 message loaded by default
379 assert len(self) == 1, 'This option should only be used for a single id at a time.'
380 template = self.env.ref('account.email_template_edi_invoice', False)
381 compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
383 default_model='account.invoice',
384 default_res_id=self.id,
385 default_use_template=bool(template),
386 default_template_id=template.id,
387 default_composition_mode='comment',
388 mark_invoice_as_sent=True,
391 'name': _('Compose Email'),
392 'type': 'ir.actions.act_window',
395 'res_model': 'mail.compose.message',
396 'views': [(compose_form.id, 'form')],
397 'view_id': compose_form.id,
403 def confirm_paid(self):
404 return self.write({'state': 'paid'})
409 if invoice.state not in ('draft', 'cancel'):
410 raise Warning(_('You cannot delete an invoice which is not draft or cancelled. You should refund it instead.'))
411 elif invoice.internal_number:
412 raise Warning(_('You cannot delete an invoice after it has been validated (and received a number). You can set it back to "Draft" state and modify its content, then re-confirm it.'))
413 return super(account_invoice, self).unlink()
416 def onchange_partner_id(self, type, partner_id, date_invoice=False,
417 payment_term=False, partner_bank_id=False, company_id=False):
419 payment_term_id = False
420 fiscal_position = False
424 p = self.env['res.partner'].browse(partner_id)
425 rec_account = p.property_account_receivable
426 pay_account = p.property_account_payable
428 if p.property_account_receivable.company_id and \
429 p.property_account_receivable.company_id.id != company_id and \
430 p.property_account_payable.company_id and \
431 p.property_account_payable.company_id.id != company_id:
432 prop = self.env['ir.property']
433 rec_dom = [('name', '=', 'property_account_receivable'), ('company_id', '=', company_id)]
434 pay_dom = [('name', '=', 'property_account_payable'), ('company_id', '=', company_id)]
435 res_dom = [('res_id', '=', 'res.partner,%s' % partner_id)]
436 rec_prop = prop.search(rec_dom + res_dom) or prop.search(rec_dom)
437 pay_prop = prop.search(pay_dom + res_dom) or prop.search(pay_dom)
438 rec_account = rec_prop.get_by_record(rec_prop)
439 pay_account = pay_prop.get_by_record(pay_prop)
440 if not rec_account and not pay_account:
441 action = self.env.ref('account.action_account_config')
442 msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
443 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
445 if type in ('out_invoice', 'out_refund'):
446 account_id = rec_account.id
447 payment_term_id = p.property_payment_term.id
449 account_id = pay_account.id
450 payment_term_id = p.property_supplier_payment_term.id
451 fiscal_position = p.property_account_position.id
452 bank_id = p.bank_ids and p.bank_ids[0].id or False
455 'account_id': account_id,
456 'payment_term': payment_term_id,
457 'fiscal_position': fiscal_position,
460 if type in ('in_invoice', 'in_refund'):
461 result['value']['partner_bank_id'] = bank_id
463 if payment_term != payment_term_id:
465 to_update = self.onchange_payment_term_date_invoice(payment_term_id, date_invoice)
466 result['value'].update(to_update.get('value', {}))
468 result['value']['date_due'] = False
470 if partner_bank_id != bank_id:
471 to_update = self.onchange_partner_bank(bank_id)
472 result['value'].update(to_update.get('value', {}))
477 def onchange_journal_id(self, journal_id=False):
479 journal = self.env['account.journal'].browse(journal_id)
482 'currency_id': journal.currency.id or journal.company_id.currency_id.id,
483 'company_id': journal.company_id.id,
489 def onchange_payment_term_date_invoice(self, payment_term_id, date_invoice):
491 date_invoice = fields.Date.context_today(self)
492 if not payment_term_id:
493 # To make sure the invoice due date should contain due date which is
494 # entered by user when there is no payment term defined
495 return {'value': {'date_due': self.date_due or date_invoice}}
496 pterm = self.env['account.payment.term'].browse(payment_term_id)
497 pterm_list = pterm.compute(value=1, date_ref=date_invoice)[0]
499 return {'value': {'date_due': max(line[0] for line in pterm_list)}}
501 raise except_orm(_('Insufficient Data!'),
502 _('The payment term of supplier does not have a payment term line.'))
505 def onchange_invoice_line(self, lines):
509 def onchange_partner_bank(self, partner_bank_id=False):
513 def onchange_company_id(self, company_id, part_id, type, invoice_line, currency_id):
514 # TODO: add the missing context parameter when forward-porting in trunk
515 # so we can remove this hack!
516 self = self.with_context(self.env['res.users'].context_get())
521 if company_id and part_id and type:
522 p = self.env['res.partner'].browse(part_id)
523 if p.property_account_payable and p.property_account_receivable and \
524 p.property_account_payable.company_id.id != company_id and \
525 p.property_account_receivable.company_id.id != company_id:
526 prop = self.env['ir.property']
527 rec_dom = [('name', '=', 'property_account_receivable'), ('company_id', '=', company_id)]
528 pay_dom = [('name', '=', 'property_account_payable'), ('company_id', '=', company_id)]
529 res_dom = [('res_id', '=', 'res.partner,%s' % part_id)]
530 rec_prop = prop.search(rec_dom + res_dom) or prop.search(rec_dom)
531 pay_prop = prop.search(pay_dom + res_dom) or prop.search(pay_dom)
532 rec_account = rec_prop.get_by_record(rec_prop)
533 pay_account = pay_prop.get_by_record(pay_prop)
534 if not rec_account and not pay_account:
535 action = self.env.ref('account.action_account_config')
536 msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
537 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
539 if type in ('out_invoice', 'out_refund'):
540 acc_id = rec_account.id
542 acc_id = pay_account.id
543 values= {'account_id': acc_id}
547 for line in self.invoice_line:
548 if not line.account_id:
550 if line.account_id.company_id.id == company_id:
552 accounts = self.env['account.account'].search([('name', '=', line.account_id.name), ('company_id', '=', company_id)])
554 action = self.env.ref('account.action_account_config')
555 msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
556 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
557 line.write({'account_id': accounts[-1].id})
559 for line_cmd in invoice_line or []:
560 if len(line_cmd) >= 3 and isinstance(line_cmd[2], dict):
561 line = self.env['account.account'].browse(line_cmd[2]['account_id'])
562 if line.company_id.id != company_id:
564 _('Configuration Error!'),
565 _("Invoice line account's company and invoice's company does not match.")
568 if company_id and type:
569 journal_type = TYPE2JOURNAL[type]
570 journals = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company_id)])
572 values['journal_id'] = journals[0].id
573 journal_defaults = self.env['ir.values'].get_defaults_dict('account.invoice', 'type=%s' % type)
574 if 'journal_id' in journal_defaults:
575 values['journal_id'] = journal_defaults['journal_id']
576 if not values.get('journal_id'):
577 field_desc = journals.fields_get(['type'])
578 type_label = next(t for t, label in field_desc['type']['selection'] if t == journal_type)
579 action = self.env.ref('account.action_account_journal_form')
580 msg = _('Cannot find any account journal of type "%s" for this company, You should create one.\n Please go to Journal Configuration') % type_label
581 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
582 domain = {'journal_id': [('id', 'in', journals.ids)]}
584 return {'value': values, 'domain': domain}
587 def action_cancel_draft(self):
588 # go from canceled state to draft state
589 self.write({'state': 'draft'})
590 self.delete_workflow()
591 self.create_workflow()
595 @api.returns('ir.ui.view')
596 def get_formview_id(self):
597 """ Update form view id of action to open the invoice """
598 if self.type == 'in_invoice':
599 return self.env.ref('account.invoice_supplier_form')
601 return self.env.ref('account.invoice_form')
604 def move_line_id_payment_get(self):
605 # return the move line ids with the same account as the invoice self
608 query = """ SELECT l.id
609 FROM account_move_line l, account_invoice i
610 WHERE i.id = %s AND l.move_id = i.move_id AND l.account_id = i.account_id
612 self._cr.execute(query, (self.id,))
613 return [row[0] for row in self._cr.fetchall()]
617 # check whether all corresponding account move lines are reconciled
618 line_ids = self.move_line_id_payment_get()
621 query = "SELECT reconcile_id FROM account_move_line WHERE id IN %s"
622 self._cr.execute(query, (tuple(line_ids),))
623 return all(row[0] for row in self._cr.fetchall())
626 def button_reset_taxes(self):
627 account_invoice_tax = self.env['account.invoice.tax']
628 ctx = dict(self._context)
630 self._cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (invoice.id,))
631 self.invalidate_cache()
632 partner = invoice.partner_id
634 ctx['lang'] = partner.lang
635 for taxe in account_invoice_tax.compute(invoice).values():
636 account_invoice_tax.create(taxe)
637 # dummy write on self to trigger recomputations
638 return self.with_context(ctx).write({'invoice_line': []})
641 def button_compute(self, set_total=False):
642 self.button_reset_taxes()
645 invoice.check_total = invoice.amount_total
649 def _get_analytic_lines(self):
650 """ Return a list of dict for creating analytic lines for self[0] """
651 company_currency = self.company_id.currency_id
652 sign = 1 if self.type in ('out_invoice', 'in_refund') else -1
654 iml = self.env['account.invoice.line'].move_line_get(self.id)
656 if il['account_analytic_id']:
657 if self.type in ('in_invoice', 'in_refund'):
661 if not self.journal_id.analytic_journal_id:
662 raise except_orm(_('No Analytic Journal!'),
663 _("You have to define an analytic journal on the '%s' journal!") % (self.journal_id.name,))
664 currency = self.currency_id.with_context(date=self.date_invoice)
665 il['analytic_lines'] = [(0,0, {
667 'date': self.date_invoice,
668 'account_id': il['account_analytic_id'],
669 'unit_amount': il['quantity'],
670 'amount': currency.compute(il['price'], company_currency) * sign,
671 'product_id': il['product_id'],
672 'product_uom_id': il['uos_id'],
673 'general_account_id': il['account_id'],
674 'journal_id': self.journal_id.analytic_journal_id.id,
680 def action_date_assign(self):
682 res = inv.onchange_payment_term_date_invoice(inv.payment_term.id, inv.date_invoice)
683 if res and res.get('value'):
684 inv.write(res['value'])
688 def finalize_invoice_move_lines(self, move_lines):
689 """ finalize_invoice_move_lines(move_lines) -> move_lines
691 Hook method to be overridden in additional modules to verify and
692 possibly alter the move lines to be created by an invoice, for
694 :param move_lines: list of dictionaries with the account.move.lines (as for create())
695 :return: the (possibly updated) final move_lines to create for this invoice
700 def check_tax_lines(self, compute_taxes):
701 account_invoice_tax = self.env['account.invoice.tax']
702 company_currency = self.company_id.currency_id
703 if not self.tax_line:
704 for tax in compute_taxes.values():
705 account_invoice_tax.create(tax)
708 for tax in self.tax_line:
711 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
713 if key not in compute_taxes:
714 raise except_orm(_('Warning!'), _('Global taxes defined, but they are not in invoice lines !'))
715 base = compute_taxes[key]['base']
716 if abs(base - tax.base) > company_currency.rounding:
717 raise except_orm(_('Warning!'), _('Tax base different!\nClick on compute to update the tax base.'))
718 for key in compute_taxes:
719 if key not in tax_key:
720 raise except_orm(_('Warning!'), _('Taxes are missing!\nClick on compute button.'))
723 def compute_invoice_totals(self, company_currency, ref, invoice_move_lines):
726 for line in invoice_move_lines:
727 if self.currency_id != company_currency:
728 currency = self.currency_id.with_context(date=self.date_invoice or fields.Date.context_today(self))
729 line['currency_id'] = currency.id
730 line['amount_currency'] = line['price']
731 line['price'] = currency.compute(line['price'], company_currency)
733 line['currency_id'] = False
734 line['amount_currency'] = False
736 if self.type in ('out_invoice','in_refund'):
737 total += line['price']
738 total_currency += line['amount_currency'] or line['price']
739 line['price'] = - line['price']
741 total -= line['price']
742 total_currency -= line['amount_currency'] or line['price']
743 return total, total_currency, invoice_move_lines
745 def inv_line_characteristic_hashcode(self, invoice_line):
746 """Overridable hashcode generation for invoice lines. Lines having the same hashcode
747 will be grouped together if the journal has the 'group line' option. Of course a module
748 can add fields to invoice lines that would need to be tested too before merging lines
750 return "%s-%s-%s-%s-%s" % (
751 invoice_line['account_id'],
752 invoice_line.get('tax_code_id', 'False'),
753 invoice_line.get('product_id', 'False'),
754 invoice_line.get('analytic_account_id', 'False'),
755 invoice_line.get('date_maturity', 'False'),
758 def group_lines(self, iml, line):
759 """Merge account move lines (and hence analytic lines) if invoice line hashcodes are equals"""
760 if self.journal_id.group_invoice_lines:
763 tmp = self.inv_line_characteristic_hashcode(l)
765 am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
766 line2[tmp]['debit'] = (am > 0) and am or 0.0
767 line2[tmp]['credit'] = (am < 0) and -am or 0.0
768 line2[tmp]['tax_amount'] += l['tax_amount']
769 line2[tmp]['analytic_lines'] += l['analytic_lines']
773 for key, val in line2.items():
774 line.append((0,0,val))
778 def action_move_create(self):
779 """ Creates invoice related analytics and financial move lines """
780 account_invoice_tax = self.env['account.invoice.tax']
781 account_move = self.env['account.move']
784 if not inv.journal_id.sequence_id:
785 raise except_orm(_('Error!'), _('Please define sequence on the journal related to this invoice.'))
786 if not inv.invoice_line:
787 raise except_orm(_('No Invoice Lines!'), _('Please create some invoice lines.'))
791 ctx = dict(self._context, lang=inv.partner_id.lang)
793 if not inv.date_invoice:
794 inv.with_context(ctx).write({'date_invoice': fields.Date.context_today(self)})
795 date_invoice = inv.date_invoice
797 company_currency = inv.company_id.currency_id
798 # create the analytical lines, one move line per invoice line
799 iml = inv._get_analytic_lines()
800 # check if taxes are all computed
801 compute_taxes = account_invoice_tax.compute(inv)
802 inv.check_tax_lines(compute_taxes)
804 # I disabled the check_total feature
805 if self.env['res.users'].has_group('account.group_supplier_inv_check_total'):
806 if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding / 2.0):
807 raise except_orm(_('Bad Total!'), _('Please verify the price of the invoice!\nThe encoded total does not match the computed total.'))
810 total_fixed = total_percent = 0
811 for line in inv.payment_term.line_ids:
812 if line.value == 'fixed':
813 total_fixed += line.value_amount
814 if line.value == 'procent':
815 total_percent += (line.value_amount/100.0)
816 total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
817 if (total_fixed + total_percent) > 100:
818 raise except_orm(_('Error!'), _("Cannot create the invoice.\nThe related payment term is probably misconfigured as it gives a computed amount greater than the total invoiced amount. In order to avoid rounding issues, the latest line of your payment term must be of type 'balance'."))
820 # one move line per tax line
821 iml += account_invoice_tax.move_line_get(inv.id)
823 if inv.type in ('in_invoice', 'in_refund'):
828 diff_currency = inv.currency_id != company_currency
829 # create one move line for the total and possibly adjust the other lines amount
830 total, total_currency, iml = inv.with_context(ctx).compute_invoice_totals(company_currency, ref, iml)
832 name = inv.name or inv.supplier_invoice_number or '/'
835 totlines = inv.with_context(ctx).payment_term.compute(total, date_invoice)[0]
837 res_amount_currency = total_currency
838 ctx['date'] = date_invoice
839 for i, t in enumerate(totlines):
840 if inv.currency_id != company_currency:
841 amount_currency = company_currency.with_context(ctx).compute(t[1], inv.currency_id)
843 amount_currency = False
845 # last line: add the diff
846 res_amount_currency -= amount_currency or 0
847 if i + 1 == len(totlines):
848 amount_currency += res_amount_currency
854 'account_id': inv.account_id.id,
855 'date_maturity': t[0],
856 'amount_currency': diff_currency and amount_currency,
857 'currency_id': diff_currency and inv.currency_id.id,
865 'account_id': inv.account_id.id,
866 'date_maturity': inv.date_due,
867 'amount_currency': diff_currency and total_currency,
868 'currency_id': diff_currency and inv.currency_id.id,
874 part = self.env['res.partner']._find_accounting_partner(inv.partner_id)
876 line = [(0, 0, self.line_get_convert(l, part.id, date)) for l in iml]
877 line = inv.group_lines(iml, line)
879 journal = inv.journal_id.with_context(ctx)
880 if journal.centralisation:
881 raise except_orm(_('User Error!'),
882 _('You cannot create an invoice on a centralized journal. Uncheck the centralized counterpart box in the related journal from the configuration menu.'))
884 line = inv.finalize_invoice_move_lines(line)
887 'ref': inv.reference or inv.name,
889 'journal_id': journal.id,
890 'date': inv.date_invoice,
891 'narration': inv.comment,
892 'company_id': inv.company_id.id,
894 ctx['company_id'] = inv.company_id.id
895 period = inv.period_id
897 period = period.with_context(ctx).find(date_invoice)[:1]
899 move_vals['period_id'] = period.id
901 i[2]['period_id'] = period.id
904 move = account_move.with_context(ctx).create(move_vals)
905 # make the invoice point to that move
908 'period_id': period.id,
909 'move_name': move.name,
911 inv.with_context(ctx).write(vals)
912 # Pass invoice in context in method post: used if you want to get the same
913 # account move reference when creating the same invoice after a cancelled one:
919 def invoice_validate(self):
920 return self.write({'state': 'open'})
923 def line_get_convert(self, line, part, date):
925 'date_maturity': line.get('date_maturity', False),
927 'name': line['name'][:64],
929 'debit': line['price']>0 and line['price'],
930 'credit': line['price']<0 and -line['price'],
931 'account_id': line['account_id'],
932 'analytic_lines': line.get('analytic_lines', []),
933 'amount_currency': line['price']>0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)),
934 'currency_id': line.get('currency_id', False),
935 'tax_code_id': line.get('tax_code_id', False),
936 'tax_amount': line.get('tax_amount', False),
937 'ref': line.get('ref', False),
938 'quantity': line.get('quantity',1.00),
939 'product_id': line.get('product_id', False),
940 'product_uom_id': line.get('uos_id', False),
941 'analytic_account_id': line.get('account_analytic_id', False),
945 def action_number(self):
946 #TODO: not correct fix but required a fresh values before reading it.
950 self.write({'internal_number': inv.number})
952 if inv.type in ('in_invoice', 'in_refund'):
953 if not inv.reference:
960 self._cr.execute(""" UPDATE account_move SET ref=%s
961 WHERE id=%s AND (ref IS NULL OR ref = '')""",
962 (ref, inv.move_id.id))
963 self._cr.execute(""" UPDATE account_move_line SET ref=%s
964 WHERE move_id=%s AND (ref IS NULL OR ref = '')""",
965 (ref, inv.move_id.id))
966 self._cr.execute(""" UPDATE account_analytic_line SET ref=%s
967 FROM account_move_line
968 WHERE account_move_line.move_id = %s AND
969 account_analytic_line.move_id = account_move_line.id""",
970 (ref, inv.move_id.id))
971 self.invalidate_cache()
976 def action_cancel(self):
977 moves = self.env['account.move']
982 for move_line in inv.payment_ids:
983 if move_line.reconcile_partial_id.line_partial_ids:
984 raise except_orm(_('Error!'), _('You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first.'))
986 # First, set the invoices as cancelled and detach the move ids
987 self.write({'state': 'cancel', 'move_id': False})
989 # second, invalidate the move(s)
990 moves.button_cancel()
991 # delete the move this invoice was pointing to
992 # Note that the corresponding move_lines and move_reconciles
993 # will be automatically deleted too
995 self._log_event(-1.0, 'Cancel Invoice')
1001 def _log_event(self, factor=1.0, name='Open Invoice'):
1002 #TODO: implement messages system
1008 'out_invoice': _('Invoice'),
1009 'in_invoice': _('Supplier Invoice'),
1010 'out_refund': _('Refund'),
1011 'in_refund': _('Supplier Refund'),
1015 result.append((inv.id, "%s %s" % (inv.number or TYPES[inv.type], inv.name or '')))
1019 def name_search(self, name, args=None, operator='ilike', limit=100):
1021 recs = self.browse()
1023 recs = self.search([('number', '=', name)] + args, limit=limit)
1025 recs = self.search([('name', operator, name)] + args, limit=limit)
1026 return recs.name_get()
1029 def _refund_cleanup_lines(self, lines):
1030 """ Convert records to dict of values suitable for one2many line creation
1032 :param recordset lines: records to convert
1033 :return: list of command tuple for one2many line creation [(0, 0, dict of valueis), ...]
1038 for name, field in line._fields.iteritems():
1039 if name in MAGIC_COLUMNS:
1041 elif field.type == 'many2one':
1042 values[name] = line[name].id
1043 elif field.type not in ['many2many', 'one2many']:
1044 values[name] = line[name]
1045 elif name == 'invoice_line_tax_id':
1046 values[name] = [(6, 0, line[name].ids)]
1047 result.append((0, 0, values))
1051 def _prepare_refund(self, invoice, date=None, period_id=None, description=None, journal_id=None):
1052 """ Prepare the dict of values to create the new refund from the invoice.
1053 This method may be overridden to implement custom
1054 refund generation (making sure to call super() to establish
1055 a clean extension chain).
1057 :param record invoice: invoice to refund
1058 :param string date: refund creation date from the wizard
1059 :param integer period_id: force account.period from the wizard
1060 :param string description: description of the refund from the wizard
1061 :param integer journal_id: account.journal from the wizard
1062 :return: dict of value to create() the refund
1065 for field in ['name', 'reference', 'comment', 'date_due', 'partner_id', 'company_id',
1066 'account_id', 'currency_id', 'payment_term', 'user_id', 'fiscal_position']:
1067 if invoice._fields[field].type == 'many2one':
1068 values[field] = invoice[field].id
1070 values[field] = invoice[field] or False
1072 values['invoice_line'] = self._refund_cleanup_lines(invoice.invoice_line)
1074 tax_lines = filter(lambda l: l.manual, invoice.tax_line)
1075 values['tax_line'] = self._refund_cleanup_lines(tax_lines)
1078 journal = self.env['account.journal'].browse(journal_id)
1079 elif invoice['type'] == 'in_invoice':
1080 journal = self.env['account.journal'].search([('type', '=', 'purchase_refund')], limit=1)
1082 journal = self.env['account.journal'].search([('type', '=', 'sale_refund')], limit=1)
1083 values['journal_id'] = journal.id
1085 values['type'] = TYPE2REFUND[invoice['type']]
1086 values['date_invoice'] = date or fields.Date.context_today(invoice)
1087 values['state'] = 'draft'
1088 values['number'] = False
1091 values['period_id'] = period_id
1093 values['name'] = description
1097 @api.returns('self')
1098 def refund(self, date=None, period_id=None, description=None, journal_id=None):
1099 new_invoices = self.browse()
1100 for invoice in self:
1101 # create the new invoice
1102 values = self._prepare_refund(invoice, date=date, period_id=period_id,
1103 description=description, journal_id=journal_id)
1104 new_invoices += self.create(values)
1108 def pay_and_reconcile(self, pay_amount, pay_account_id, period_id, pay_journal_id,
1109 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=''):
1110 # TODO check if we can use different period for payment and the writeoff line
1111 assert len(self)==1, "Can only pay one invoice at a time."
1112 # Take the seq as name for move
1113 SIGN = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1114 direction = SIGN[self.type]
1115 # take the chosen date
1116 date = self._context.get('date_p') or fields.Date.context_today(self)
1118 # Take the amount in currency and the currency of the payment
1119 if self._context.get('amount_currency') and self._context.get('currency_id'):
1120 amount_currency = self._context['amount_currency']
1121 currency_id = self._context['currency_id']
1123 amount_currency = False
1126 pay_journal = self.env['account.journal'].browse(pay_journal_id)
1127 if self.type in ('in_invoice', 'in_refund'):
1128 ref = self.reference
1131 partner = self.partner_id._find_accounting_partner(self.partner_id)
1132 name = name or self.invoice_line.name or self.number
1133 # Pay attention to the sign for both debit/credit AND amount_currency
1136 'debit': direction * pay_amount > 0 and direction * pay_amount,
1137 'credit': direction * pay_amount < 0 and -direction * pay_amount,
1138 'account_id': self.account_id.id,
1139 'partner_id': partner.id,
1142 'currency_id': currency_id,
1143 'amount_currency': direction * (amount_currency or 0.0),
1144 'company_id': self.company_id.id,
1148 'debit': direction * pay_amount < 0 and -direction * pay_amount,
1149 'credit': direction * pay_amount > 0 and direction * pay_amount,
1150 'account_id': pay_account_id,
1151 'partner_id': partner.id,
1154 'currency_id': currency_id,
1155 'amount_currency': -direction * (amount_currency or 0.0),
1156 'company_id': self.company_id.id,
1158 move = self.env['account.move'].create({
1160 'line_id': [(0, 0, l1), (0, 0, l2)],
1161 'journal_id': pay_journal_id,
1162 'period_id': period_id,
1166 move_ids = (move | self.move_id).ids
1167 self._cr.execute("SELECT id FROM account_move_line WHERE move_id IN %s",
1169 lines = self.env['account.move.line'].browse([r[0] for r in self._cr.fetchall()])
1170 lines2rec = lines.browse()
1172 for line in itertools.chain(lines, self.payment_ids):
1173 if line.account_id == self.account_id:
1175 total += (line.debit or 0.0) - (line.credit or 0.0)
1177 inv_id, name = self.name_get()[0]
1178 if not round(total, self.env['decimal.precision'].precision_get('Account')) or writeoff_acc_id:
1179 lines2rec.reconcile('manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id)
1181 code = self.currency_id.symbol
1182 # TODO: use currency's formatting function
1183 msg = _("Invoice partially paid: %s%s of %s%s (%s%s remaining).") % \
1184 (pay_amount, code, self.amount_total, code, total, code)
1185 self.message_post(body=msg)
1186 lines2rec.reconcile_partial('manual')
1188 # Update the stored value (fields.function), so we write to trigger recompute
1189 return self.write({})
1192 def pay_and_reconcile(self, cr, uid, ids, pay_amount, pay_account_id, period_id, pay_journal_id,
1193 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context=None, name=''):
1194 recs = self.browse(cr, uid, ids, context)
1195 return recs.pay_and_reconcile(pay_amount, pay_account_id, period_id, pay_journal_id,
1196 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=name)
1198 class account_invoice_line(models.Model):
1199 _name = "account.invoice.line"
1200 _description = "Invoice Line"
1201 _order = "invoice_id,sequence,id"
1204 @api.depends('price_unit', 'discount', 'invoice_line_tax_id', 'quantity',
1205 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id')
1206 def _compute_price(self):
1207 price = self.price_unit * (1 - (self.discount or 0.0) / 100.0)
1208 taxes = self.invoice_line_tax_id.compute_all(price, self.quantity, product=self.product_id, partner=self.invoice_id.partner_id)
1209 self.price_subtotal = taxes['total']
1211 self.price_subtotal = self.invoice_id.currency_id.round(self.price_subtotal)
1214 def _default_price_unit(self):
1215 if not self._context.get('check_total'):
1217 total = self._context['check_total']
1218 for l in self._context.get('invoice_line', []):
1219 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1221 price = vals.get('price_unit', 0) * (1 - vals.get('discount', 0) / 100.0)
1222 total = total - (price * vals.get('quantity'))
1223 taxes = vals.get('invoice_line_tax_id')
1224 if taxes and len(taxes[0]) >= 3 and taxes[0][2]:
1225 taxes = self.env['account.tax'].browse(taxes[0][2])
1226 tax_res = taxes.compute_all(price, vals.get('quantity'),
1227 product=vals.get('product_id'), partner=self._context.get('partner_id'))
1228 for tax in tax_res['taxes']:
1229 total = total - tax['amount']
1233 def _default_account(self):
1234 # XXX this gets the default account for the user's company,
1235 # it should get the default account for the invoice's company
1236 # however, the invoice's company does not reach this point
1237 if self._context.get('type') in ('out_invoice', 'out_refund'):
1238 return self.env['ir.property'].get('property_account_income_categ', 'product.category')
1240 return self.env['ir.property'].get('property_account_expense_categ', 'product.category')
1242 name = fields.Text(string='Description', required=True)
1243 origin = fields.Char(string='Source Document',
1244 help="Reference of the document that produced this invoice.")
1245 sequence = fields.Integer(string='Sequence', default=10,
1246 help="Gives the sequence of this line when displaying the invoice.")
1247 invoice_id = fields.Many2one('account.invoice', string='Invoice Reference',
1248 ondelete='cascade', index=True)
1249 uos_id = fields.Many2one('product.uom', string='Unit of Measure',
1250 ondelete='set null', index=True)
1251 product_id = fields.Many2one('product.product', string='Product',
1252 ondelete='set null', index=True)
1253 account_id = fields.Many2one('account.account', string='Account',
1254 required=True, domain=[('type', 'not in', ['view', 'closed'])],
1255 default=_default_account,
1256 help="The income or expense account related to the selected product.")
1257 price_unit = fields.Float(string='Unit Price', required=True,
1258 digits= dp.get_precision('Product Price'),
1259 default=_default_price_unit)
1260 price_subtotal = fields.Float(string='Amount', digits= dp.get_precision('Account'),
1261 store=True, readonly=True, compute='_compute_price')
1262 quantity = fields.Float(string='Quantity', digits= dp.get_precision('Product Unit of Measure'),
1263 required=True, default=1)
1264 discount = fields.Float(string='Discount (%)', digits= dp.get_precision('Discount'),
1266 invoice_line_tax_id = fields.Many2many('account.tax',
1267 'account_invoice_line_tax', 'invoice_line_id', 'tax_id',
1268 string='Taxes', domain=[('parent_id', '=', False)])
1269 account_analytic_id = fields.Many2one('account.analytic.account',
1270 string='Analytic Account')
1271 company_id = fields.Many2one('res.company', string='Company',
1272 related='invoice_id.company_id', store=True, readonly=True)
1273 partner_id = fields.Many2one('res.partner', string='Partner',
1274 related='invoice_id.partner_id', store=True, readonly=True)
1277 def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
1278 res = super(account_invoice_line, self).fields_view_get(
1279 view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
1280 if self._context.get('type'):
1281 doc = etree.XML(res['arch'])
1282 for node in doc.xpath("//field[@name='product_id']"):
1283 if self._context['type'] in ('in_invoice', 'in_refund'):
1284 node.set('domain', "[('purchase_ok', '=', True)]")
1286 node.set('domain', "[('sale_ok', '=', True)]")
1287 res['arch'] = etree.tostring(doc)
1291 def product_id_change(self, product, uom_id, qty=0, name='', type='out_invoice',
1292 partner_id=False, fposition_id=False, price_unit=False, currency_id=False,
1294 context = self._context
1295 company_id = company_id if company_id is not None else context.get('company_id', False)
1296 self = self.with_context(company_id=company_id, force_company=company_id)
1299 raise except_orm(_('No Partner Defined!'), _("You must first select a partner!"))
1301 if type in ('in_invoice', 'in_refund'):
1302 return {'value': {}, 'domain': {'product_uom': []}}
1304 return {'value': {'price_unit': 0.0}, 'domain': {'product_uom': []}}
1308 part = self.env['res.partner'].browse(partner_id)
1309 fpos = self.env['account.fiscal.position'].browse(fposition_id)
1312 self = self.with_context(lang=part.lang)
1313 product = self.env['product.product'].browse(product)
1315 values['name'] = product.partner_ref
1316 if type in ('out_invoice', 'out_refund'):
1317 account = product.property_account_income or product.categ_id.property_account_income_categ
1319 account = product.property_account_expense or product.categ_id.property_account_expense_categ
1320 account = fpos.map_account(account)
1322 values['account_id'] = account.id
1324 if type in ('out_invoice', 'out_refund'):
1325 taxes = product.taxes_id or account.tax_ids
1326 if product.description_sale:
1327 values['name'] += '\n' + product.description_sale
1329 taxes = product.supplier_taxes_id or account.tax_ids
1330 if product.description_purchase:
1331 values['name'] += '\n' + product.description_purchase
1333 taxes = fpos.map_tax(taxes)
1334 values['invoice_line_tax_id'] = taxes.ids
1336 if type in ('in_invoice', 'in_refund'):
1337 values['price_unit'] = price_unit or product.standard_price
1339 values['price_unit'] = product.list_price
1341 values['uos_id'] = uom_id or product.uom_id.id
1342 domain = {'uos_id': [('category_id', '=', product.uom_id.category_id.id)]}
1344 company = self.env['res.company'].browse(company_id)
1345 currency = self.env['res.currency'].browse(currency_id)
1347 if company and currency:
1348 if company.currency_id != currency:
1349 if type in ('in_invoice', 'in_refund'):
1350 values['price_unit'] = product.standard_price
1351 values['price_unit'] = values['price_unit'] * currency.rate
1353 if values['uos_id'] and values['uos_id'] != product.uom_id.id:
1354 values['price_unit'] = self.env['product.uom']._compute_price(
1355 product.uom_id.id, values['price_unit'], values['uos_id'])
1357 return {'value': values, 'domain': domain}
1360 def uos_id_change(self, product, uom, qty=0, name='', type='out_invoice', partner_id=False,
1361 fposition_id=False, price_unit=False, currency_id=False, company_id=None):
1362 context = self._context
1363 company_id = company_id if company_id != None else context.get('company_id', False)
1364 self = self.with_context(company_id=company_id)
1366 result = self.product_id_change(
1367 product, uom, qty, name, type, partner_id, fposition_id, price_unit,
1368 currency_id, company_id=company_id,
1372 result['value']['price_unit'] = 0.0
1374 prod = self.env['product.product'].browse(product)
1375 prod_uom = self.env['product.uom'].browse(uom)
1376 if prod.uom_id.category_id != prod_uom.category_id:
1378 'title': _('Warning!'),
1379 'message': _('The selected unit of measure is not compatible with the unit of measure of the product.'),
1381 result['value']['uos_id'] = prod.uom_id.id
1383 result['warning'] = warning
1387 def move_line_get(self, invoice_id):
1388 inv = self.env['account.invoice'].browse(invoice_id)
1389 currency = inv.currency_id.with_context(date=inv.date_invoice)
1390 company_currency = inv.company_id.currency_id
1393 for line in inv.invoice_line:
1394 mres = self.move_line_get_item(line)
1395 mres['invl_id'] = line.id
1397 tax_code_found = False
1398 taxes = line.invoice_line_tax_id.compute_all(
1399 (line.price_unit * (1.0 - (line.discount or 0.0) / 100.0)),
1400 line.quantity, line.product_id, inv.partner_id)['taxes']
1402 if inv.type in ('out_invoice', 'in_invoice'):
1403 tax_code_id = tax['base_code_id']
1404 tax_amount = line.price_subtotal * tax['base_sign']
1406 tax_code_id = tax['ref_base_code_id']
1407 tax_amount = line.price_subtotal * tax['ref_base_sign']
1412 res.append(dict(mres))
1413 res[-1]['price'] = 0.0
1414 res[-1]['account_analytic_id'] = False
1415 elif not tax_code_id:
1417 tax_code_found = True
1419 res[-1]['tax_code_id'] = tax_code_id
1420 res[-1]['tax_amount'] = currency.compute(tax_amount, company_currency)
1425 def move_line_get_item(self, line):
1428 'name': line.name.split('\n')[0][:64],
1429 'price_unit': line.price_unit,
1430 'quantity': line.quantity,
1431 'price': line.price_subtotal,
1432 'account_id': line.account_id.id,
1433 'product_id': line.product_id.id,
1434 'uos_id': line.uos_id.id,
1435 'account_analytic_id': line.account_analytic_id.id,
1436 'taxes': line.invoice_line_tax_id,
1440 # Set the tax field according to the account and the fiscal position
1443 def onchange_account_id(self, product_id, partner_id, inv_type, fposition_id, account_id):
1447 account = self.env['account.account'].browse(account_id)
1449 fpos = self.env['account.fiscal.position'].browse(fposition_id)
1450 unique_tax_ids = fpos.map_tax(account.tax_ids).ids
1452 product_change_result = self.product_id_change(product_id, False, type=inv_type,
1453 partner_id=partner_id, fposition_id=fposition_id, company_id=account.company_id.id)
1454 if 'invoice_line_tax_id' in product_change_result.get('value', {}):
1455 unique_tax_ids = product_change_result['value']['invoice_line_tax_id']
1456 return {'value': {'invoice_line_tax_id': unique_tax_ids}}
1459 class account_invoice_tax(models.Model):
1460 _name = "account.invoice.tax"
1461 _description = "Invoice Tax"
1465 @api.depends('base', 'base_amount', 'amount', 'tax_amount')
1466 def _compute_factors(self):
1467 self.factor_base = self.base_amount / self.base if self.base else 1.0
1468 self.factor_tax = self.tax_amount / self.amount if self.amount else 1.0
1470 invoice_id = fields.Many2one('account.invoice', string='Invoice Line',
1471 ondelete='cascade', index=True)
1472 name = fields.Char(string='Tax Description',
1474 account_id = fields.Many2one('account.account', string='Tax Account',
1475 required=True, domain=[('type', 'not in', ['view', 'income', 'closed'])])
1476 account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic account')
1477 base = fields.Float(string='Base', digits=dp.get_precision('Account'))
1478 amount = fields.Float(string='Amount', digits=dp.get_precision('Account'))
1479 manual = fields.Boolean(string='Manual', default=True)
1480 sequence = fields.Integer(string='Sequence',
1481 help="Gives the sequence order when displaying a list of invoice tax.")
1482 base_code_id = fields.Many2one('account.tax.code', string='Base Code',
1483 help="The account basis of the tax declaration.")
1484 base_amount = fields.Float(string='Base Code Amount', digits=dp.get_precision('Account'),
1486 tax_code_id = fields.Many2one('account.tax.code', string='Tax Code',
1487 help="The tax basis of the tax declaration.")
1488 tax_amount = fields.Float(string='Tax Code Amount', digits=dp.get_precision('Account'),
1491 company_id = fields.Many2one('res.company', string='Company',
1492 related='account_id.company_id', store=True, readonly=True)
1493 factor_base = fields.Float(string='Multipication factor for Base code',
1494 compute='_compute_factors')
1495 factor_tax = fields.Float(string='Multipication factor Tax code',
1496 compute='_compute_factors')
1499 def base_change(self, base, currency_id=False, company_id=False, date_invoice=False):
1500 factor = self.factor_base if self else 1
1501 company = self.env['res.company'].browse(company_id)
1502 if currency_id and company.currency_id:
1503 currency = self.env['res.currency'].browse(currency_id)
1504 currency = currency.with_context(date=date_invoice or fields.Date.context_today(self))
1505 base = currency.compute(base * factor, company.currency_id, round=False)
1506 return {'value': {'base_amount': base}}
1509 def amount_change(self, amount, currency_id=False, company_id=False, date_invoice=False):
1510 factor = self.factor_tax if self else 1
1511 company = self.env['res.company'].browse(company_id)
1512 if currency_id and company.currency_id:
1513 currency = self.env['res.currency'].browse(currency_id)
1514 currency = currency.with_context(date=date_invoice or fields.Date.context_today(self))
1515 amount = currency.compute(amount * factor, company.currency_id, round=False)
1516 return {'value': {'tax_amount': amount}}
1519 def compute(self, invoice):
1521 currency = invoice.currency_id.with_context(date=invoice.date_invoice or fields.Date.context_today(invoice))
1522 company_currency = invoice.company_id.currency_id
1523 for line in invoice.invoice_line:
1524 taxes = line.invoice_line_tax_id.compute_all(
1525 (line.price_unit * (1 - (line.discount or 0.0) / 100.0)),
1526 line.quantity, line.product_id, invoice.partner_id)['taxes']
1529 'invoice_id': invoice.id,
1530 'name': tax['name'],
1531 'amount': tax['amount'],
1533 'sequence': tax['sequence'],
1534 'base': currency.round(tax['price_unit'] * line['quantity']),
1536 if invoice.type in ('out_invoice','in_invoice'):
1537 val['base_code_id'] = tax['base_code_id']
1538 val['tax_code_id'] = tax['tax_code_id']
1539 val['base_amount'] = currency.compute(val['base'] * tax['base_sign'], company_currency, round=False)
1540 val['tax_amount'] = currency.compute(val['amount'] * tax['tax_sign'], company_currency, round=False)
1541 val['account_id'] = tax['account_collected_id'] or line.account_id.id
1542 val['account_analytic_id'] = tax['account_analytic_collected_id']
1544 val['base_code_id'] = tax['ref_base_code_id']
1545 val['tax_code_id'] = tax['ref_tax_code_id']
1546 val['base_amount'] = currency.compute(val['base'] * tax['ref_base_sign'], company_currency, round=False)
1547 val['tax_amount'] = currency.compute(val['amount'] * tax['ref_tax_sign'], company_currency, round=False)
1548 val['account_id'] = tax['account_paid_id'] or line.account_id.id
1549 val['account_analytic_id'] = tax['account_analytic_paid_id']
1551 # If the taxes generate moves on the same financial account as the invoice line
1552 # and no default analytic account is defined at the tax level, propagate the
1553 # analytic account from the invoice line to the tax line. This is necessary
1554 # in situations were (part of) the taxes cannot be reclaimed,
1555 # to ensure the tax move is allocated to the proper analytic account.
1556 if not val.get('account_analytic_id') and line.account_analytic_id and val['account_id'] == line.account_id.id:
1557 val['account_analytic_id'] = line.account_analytic_id.id
1559 key = (val['tax_code_id'], val['base_code_id'], val['account_id'])
1560 if not key in tax_grouped:
1561 tax_grouped[key] = val
1563 tax_grouped[key]['base'] += val['base']
1564 tax_grouped[key]['amount'] += val['amount']
1565 tax_grouped[key]['base_amount'] += val['base_amount']
1566 tax_grouped[key]['tax_amount'] += val['tax_amount']
1568 for t in tax_grouped.values():
1569 t['base'] = currency.round(t['base'])
1570 t['amount'] = currency.round(t['amount'])
1571 t['base_amount'] = currency.round(t['base_amount'])
1572 t['tax_amount'] = currency.round(t['tax_amount'])
1577 def compute(self, cr, uid, invoice_id, context=None):
1578 recs = self.browse(cr, uid, [], context)
1579 invoice = recs.env['account.invoice'].browse(invoice_id)
1580 return recs.compute(invoice)
1583 def move_line_get(self, invoice_id):
1586 'SELECT * FROM account_invoice_tax WHERE invoice_id = %s',
1589 for row in self._cr.dictfetchall():
1590 if not (row['amount'] or row['tax_code_id'] or row['tax_amount']):
1594 'name': row['name'],
1595 'price_unit': row['amount'],
1597 'price': row['amount'] or 0.0,
1598 'account_id': row['account_id'],
1599 'tax_code_id': row['tax_code_id'],
1600 'tax_amount': row['tax_amount'],
1601 'account_analytic_id': row['account_analytic_id'],
1606 class res_partner(models.Model):
1607 # Inherits partner and adds invoice information in the partner form
1608 _inherit = 'res.partner'
1610 invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices',
1613 def _find_accounting_partner(self, partner):
1615 Find the partner for which the accounting entries will be created
1617 return partner.commercial_partner_id
1619 class mail_compose_message(models.Model):
1620 _inherit = 'mail.compose.message'
1623 def send_mail(self):
1624 context = self._context
1625 if context.get('default_model') == 'account.invoice' and \
1626 context.get('default_res_id') and context.get('mark_invoice_as_sent'):
1627 invoice = self.env['account.invoice'].browse(context['default_res_id'])
1628 invoice = invoice.with_context(mail_post_autofollow=True)
1629 invoice.write({'sent': True})
1630 invoice.message_post(body=_("Invoice sent"))
1631 return super(mail_compose_message, self).send_mail()
1633 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: