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 journal_type = TYPE2JOURNAL.get(inv_type, 'sale')
91 journal = self.env['account.analytic.journal'].search([('type', '=', journal_type)], limit=1)
93 raise except_orm(_('No Analytic Journal!'),
94 _("You must define an analytic journal of type '%s'!") % (journal_type,))
98 @api.depends('account_id', 'move_id.line_id.account_id', 'move_id.line_id.reconcile_id')
99 def _compute_reconciled(self):
100 self.reconciled = self.test_paid()
103 def _get_reference_type(self):
104 return [('none', _('Free Reference'))]
108 'state', 'currency_id', 'invoice_line.price_subtotal',
109 'move_id.line_id.account_id.type',
110 'move_id.line_id.amount_residual',
111 'move_id.line_id.amount_residual_currency',
112 'move_id.line_id.currency_id',
113 'move_id.line_id.reconcile_partial_id.line_partial_ids.invoice.type',
115 def _compute_residual(self):
116 nb_inv_in_partial_rec = max_invoice_id = 0
118 for line in self.sudo().move_id.line_id:
119 if line.account_id.type in ('receivable', 'payable'):
120 if line.currency_id == self.currency_id:
121 self.residual += line.amount_residual_currency
123 # ahem, shouldn't we use line.currency_id here?
124 from_currency = line.company_id.currency_id.with_context(date=line.date)
125 self.residual += from_currency.compute(line.amount_residual, self.currency_id)
126 # we check if the invoice is partially reconciled and if there
127 # are other invoices involved in this partial reconciliation
128 for pline in line.reconcile_partial_id.line_partial_ids:
129 if pline.invoice and self.type == pline.invoice.type:
130 nb_inv_in_partial_rec += 1
131 # store the max invoice id as for this invoice we will
132 # make a balance instead of a simple division
133 max_invoice_id = max(max_invoice_id, pline.invoice.id)
134 if nb_inv_in_partial_rec:
135 # if there are several invoices in a partial reconciliation, we
136 # split the residual by the number of invoices to have a sum of
137 # residual amounts that matches the partner balance
138 new_value = self.currency_id.round(self.residual / nb_inv_in_partial_rec)
139 if self.id == max_invoice_id:
140 # if it's the last the invoice of the bunch of invoices
141 # partially reconciled together, we make a balance to avoid
143 self.residual = self.residual - ((nb_inv_in_partial_rec - 1) * new_value)
145 self.residual = new_value
146 # prevent the residual amount on the invoice to be less than 0
147 self.residual = max(self.residual, 0.0)
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
321 if context.get('active_model') == 'res.partner' and context.get('active_ids'):
322 partner = self.env['res.partner'].browse(context['active_ids'])[0]
324 view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.tree')]).id
326 elif view_type == 'form':
327 if partner.supplier and not partner.customer:
328 view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.supplier.form')]).id
329 elif partner.customer and not partner.supplier:
330 view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.form')]).id
332 res = super(account_invoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
334 # adapt selection of field journal_id
335 for field in res['fields']:
336 if field == 'journal_id' and type:
337 journal_select = self.env['account.journal']._name_search('', [('type', '=', type)], name_get_uid=1)
338 res['fields'][field]['selection'] = journal_select
340 doc = etree.XML(res['arch'])
342 if context.get('type'):
343 for node in doc.xpath("//field[@name='partner_bank_id']"):
344 if context['type'] == 'in_refund':
345 node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]")
346 elif context['type'] == 'out_refund':
347 node.set('domain', "[('partner_id', '=', partner_id)]")
349 if view_type == 'search':
350 if context.get('type') in ('out_invoice', 'out_refund'):
351 for node in doc.xpath("//group[@name='extended filter']"):
354 if view_type == 'tree':
355 partner_string = _('Customer')
356 if context.get('type') in ('in_invoice', 'in_refund'):
357 partner_string = _('Supplier')
358 for node in doc.xpath("//field[@name='reference']"):
359 node.set('invisible', '0')
360 for node in doc.xpath("//field[@name='partner_id']"):
361 node.set('string', partner_string)
363 res['arch'] = etree.tostring(doc)
367 def invoice_print(self):
368 """ Print the invoice and mark it as sent, so that we can see more
369 easily the next step of the workflow
371 assert len(self) == 1, 'This option should only be used for a single id at a time.'
373 return self.env['report'].get_action(self, 'account.report_invoice')
376 def action_invoice_sent(self):
377 """ Open a window to compose an email, with the edi invoice template
378 message loaded by default
380 assert len(self) == 1, 'This option should only be used for a single id at a time.'
381 template = self.env.ref('account.email_template_edi_invoice', False)
382 compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
384 default_model='account.invoice',
385 default_res_id=self.id,
386 default_use_template=bool(template),
387 default_template_id=template.id,
388 default_composition_mode='comment',
389 mark_invoice_as_sent=True,
392 'name': _('Compose Email'),
393 'type': 'ir.actions.act_window',
396 'res_model': 'mail.compose.message',
397 'views': [(compose_form.id, 'form')],
398 'view_id': compose_form.id,
404 def confirm_paid(self):
405 return self.write({'state': 'paid'})
410 if invoice.state not in ('draft', 'cancel'):
411 raise Warning(_('You cannot delete an invoice which is not draft or cancelled. You should refund it instead.'))
412 elif invoice.internal_number:
413 raise Warning(_('You cannot delete an invoice after it has been validated (and received a number). You can set it back to "Draft" state and modify its content, then re-confirm it.'))
414 return super(account_invoice, self).unlink()
417 def onchange_partner_id(self, type, partner_id, date_invoice=False,
418 payment_term=False, partner_bank_id=False, company_id=False):
420 payment_term_id = False
421 fiscal_position = False
425 p = self.env['res.partner'].browse(partner_id)
426 rec_account = p.property_account_receivable
427 pay_account = p.property_account_payable
429 if p.property_account_receivable.company_id and \
430 p.property_account_receivable.company_id.id != company_id and \
431 p.property_account_payable.company_id and \
432 p.property_account_payable.company_id.id != company_id:
433 prop = self.env['ir.property']
434 rec_dom = [('name', '=', 'property_account_receivable'), ('company_id', '=', company_id)]
435 pay_dom = [('name', '=', 'property_account_payable'), ('company_id', '=', company_id)]
436 res_dom = [('res_id', '=', 'res.partner,%s' % partner_id)]
437 rec_prop = prop.search(rec_dom + res_dom) or prop.search(rec_dom)
438 pay_prop = prop.search(pay_dom + res_dom) or prop.search(pay_dom)
439 rec_account = rec_prop.get_by_record(rec_prop)
440 pay_account = pay_prop.get_by_record(pay_prop)
441 if not rec_account and not pay_account:
442 action = self.env.ref('account.action_account_config')
443 msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
444 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
446 if type in ('out_invoice', 'out_refund'):
447 account_id = rec_account.id
448 payment_term_id = p.property_payment_term.id
450 account_id = pay_account.id
451 payment_term_id = p.property_supplier_payment_term.id
452 fiscal_position = p.property_account_position.id
453 bank_id = p.bank_ids and p.bank_ids[0].id or False
456 'account_id': account_id,
457 'payment_term': payment_term_id,
458 'fiscal_position': fiscal_position,
461 if type in ('in_invoice', 'in_refund'):
462 result['value']['partner_bank_id'] = bank_id
464 if payment_term != payment_term_id:
466 to_update = self.onchange_payment_term_date_invoice(payment_term_id, date_invoice)
467 result['value'].update(to_update.get('value', {}))
469 result['value']['date_due'] = False
471 if partner_bank_id != bank_id:
472 to_update = self.onchange_partner_bank(bank_id)
473 result['value'].update(to_update.get('value', {}))
478 def onchange_journal_id(self, journal_id=False):
480 journal = self.env['account.journal'].browse(journal_id)
483 'currency_id': journal.currency.id or journal.company_id.currency_id.id,
484 'company_id': journal.company_id.id,
490 def onchange_payment_term_date_invoice(self, payment_term_id, date_invoice):
492 date_invoice = fields.Date.context_today(self)
493 if not payment_term_id:
494 # To make sure the invoice due date should contain due date which is
495 # entered by user when there is no payment term defined
496 return {'value': {'date_due': self.date_due or date_invoice}}
497 pterm = self.env['account.payment.term'].browse(payment_term_id)
498 pterm_list = pterm.compute(value=1, date_ref=date_invoice)[0]
500 return {'value': {'date_due': max(line[0] for line in pterm_list)}}
502 raise except_orm(_('Insufficient Data!'),
503 _('The payment term of supplier does not have a payment term line.'))
506 def onchange_invoice_line(self, lines):
510 def onchange_partner_bank(self, partner_bank_id=False):
514 def onchange_company_id(self, company_id, part_id, type, invoice_line, currency_id):
515 # TODO: add the missing context parameter when forward-porting in trunk
516 # so we can remove this hack!
517 self = self.with_context(self.env['res.users'].context_get())
522 if company_id and part_id and type:
523 p = self.env['res.partner'].browse(part_id)
524 if p.property_account_payable and p.property_account_receivable and \
525 p.property_account_payable.company_id.id != company_id and \
526 p.property_account_receivable.company_id.id != company_id:
527 prop = self.env['ir.property']
528 rec_dom = [('name', '=', 'property_account_receivable'), ('company_id', '=', company_id)]
529 pay_dom = [('name', '=', 'property_account_payable'), ('company_id', '=', company_id)]
530 res_dom = [('res_id', '=', 'res.partner,%s' % part_id)]
531 rec_prop = prop.search(rec_dom + res_dom) or prop.search(rec_dom)
532 pay_prop = prop.search(pay_dom + res_dom) or prop.search(pay_dom)
533 rec_account = rec_prop.get_by_record(rec_prop)
534 pay_account = pay_prop.get_by_record(pay_prop)
535 if not rec_account and not pay_account:
536 action = self.env.ref('account.action_account_config')
537 msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
538 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
540 if type in ('out_invoice', 'out_refund'):
541 acc_id = rec_account.id
543 acc_id = pay_account.id
544 values= {'account_id': acc_id}
548 for line in self.invoice_line:
549 if not line.account_id:
551 if line.account_id.company_id.id == company_id:
553 accounts = self.env['account.account'].search([('name', '=', line.account_id.name), ('company_id', '=', company_id)])
555 action = self.env.ref('account.action_account_config')
556 msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
557 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
558 line.write({'account_id': accounts[-1].id})
560 for line_cmd in invoice_line or []:
561 if len(line_cmd) >= 3 and isinstance(line_cmd[2], dict):
562 line = self.env['account.account'].browse(line_cmd[2]['account_id'])
563 if line.company_id.id != company_id:
565 _('Configuration Error!'),
566 _("Invoice line account's company and invoice's company does not match.")
569 if company_id and type:
570 journal_type = TYPE2JOURNAL[type]
571 journals = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company_id)])
573 values['journal_id'] = journals[0].id
574 journal_defaults = self.env['ir.values'].get_defaults_dict('account.invoice', 'type=%s' % type)
575 if 'journal_id' in journal_defaults:
576 values['journal_id'] = journal_defaults['journal_id']
577 if not values.get('journal_id'):
578 field_desc = journals.fields_get(['type'])
579 type_label = next(t for t, label in field_desc['type']['selection'] if t == journal_type)
580 action = self.env.ref('account.action_account_journal_form')
581 msg = _('Cannot find any account journal of type "%s" for this company, You should create one.\n Please go to Journal Configuration') % type_label
582 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
583 domain = {'journal_id': [('id', 'in', journals.ids)]}
585 return {'value': values, 'domain': domain}
588 def action_cancel_draft(self):
589 # go from canceled state to draft state
590 self.write({'state': 'draft'})
591 self.delete_workflow()
592 self.create_workflow()
596 @api.returns('ir.ui.view')
597 def get_formview_id(self):
598 """ Update form view id of action to open the invoice """
599 if self.type == 'in_invoice':
600 return self.env.ref('account.invoice_supplier_form')
602 return self.env.ref('account.invoice_form')
605 def move_line_id_payment_get(self):
606 # return the move line ids with the same account as the invoice self
609 query = """ SELECT l.id
610 FROM account_move_line l, account_invoice i
611 WHERE i.id = %s AND l.move_id = i.move_id AND l.account_id = i.account_id
613 self._cr.execute(query, (self.id,))
614 return [row[0] for row in self._cr.fetchall()]
618 # check whether all corresponding account move lines are reconciled
619 line_ids = self.move_line_id_payment_get()
622 query = "SELECT reconcile_id FROM account_move_line WHERE id IN %s"
623 self._cr.execute(query, (tuple(line_ids),))
624 return all(row[0] for row in self._cr.fetchall())
627 def button_reset_taxes(self):
628 account_invoice_tax = self.env['account.invoice.tax']
629 ctx = dict(self._context)
631 self._cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (invoice.id,))
632 self.invalidate_cache()
633 partner = invoice.partner_id
635 ctx['lang'] = partner.lang
636 for taxe in account_invoice_tax.compute(invoice).values():
637 account_invoice_tax.create(taxe)
638 # dummy write on self to trigger recomputations
639 return self.with_context(ctx).write({'invoice_line': []})
642 def button_compute(self, set_total=False):
643 self.button_reset_taxes()
646 invoice.check_total = invoice.amount_total
650 def _get_analytic_lines(self):
651 """ Return a list of dict for creating analytic lines for self[0] """
652 company_currency = self.company_id.currency_id
653 sign = 1 if self.type in ('out_invoice', 'in_refund') else -1
655 iml = self.env['account.invoice.line'].move_line_get(self.id)
657 if il['account_analytic_id']:
658 if self.type in ('in_invoice', 'in_refund'):
662 if not self.journal_id.analytic_journal_id:
663 raise except_orm(_('No Analytic Journal!'),
664 _("You have to define an analytic journal on the '%s' journal!") % (self.journal_id.name,))
665 currency = self.currency_id.with_context(date=self.date_invoice)
666 il['analytic_lines'] = [(0,0, {
668 'date': self.date_invoice,
669 'account_id': il['account_analytic_id'],
670 'unit_amount': il['quantity'],
671 'amount': currency.compute(il['price'], company_currency) * sign,
672 'product_id': il['product_id'],
673 'product_uom_id': il['uos_id'],
674 'general_account_id': il['account_id'],
675 'journal_id': self.journal_id.analytic_journal_id.id,
681 def action_date_assign(self):
683 res = inv.onchange_payment_term_date_invoice(inv.payment_term.id, inv.date_invoice)
684 if res and res.get('value'):
685 inv.write(res['value'])
689 def finalize_invoice_move_lines(self, move_lines):
690 """ finalize_invoice_move_lines(move_lines) -> move_lines
692 Hook method to be overridden in additional modules to verify and
693 possibly alter the move lines to be created by an invoice, for
695 :param move_lines: list of dictionaries with the account.move.lines (as for create())
696 :return: the (possibly updated) final move_lines to create for this invoice
701 def check_tax_lines(self, compute_taxes):
702 account_invoice_tax = self.env['account.invoice.tax']
703 company_currency = self.company_id.currency_id
704 if not self.tax_line:
705 for tax in compute_taxes.values():
706 account_invoice_tax.create(tax)
709 precision = self.env['decimal.precision'].precision_get('Account')
710 for tax in self.tax_line:
713 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id, tax.account_analytic_id.id)
715 if key not in compute_taxes:
716 raise except_orm(_('Warning!'), _('Global taxes defined, but they are not in invoice lines !'))
717 base = compute_taxes[key]['base']
718 if float_compare(abs(base - tax.base), company_currency.rounding, precision_digits=precision) == 1:
719 raise except_orm(_('Warning!'), _('Tax base different!\nClick on compute to update the tax base.'))
720 for key in compute_taxes:
721 if key not in tax_key:
722 raise except_orm(_('Warning!'), _('Taxes are missing!\nClick on compute button.'))
725 def compute_invoice_totals(self, company_currency, ref, invoice_move_lines):
728 for line in invoice_move_lines:
729 if self.currency_id != company_currency:
730 currency = self.currency_id.with_context(date=self.date_invoice or fields.Date.context_today(self))
731 line['currency_id'] = currency.id
732 line['amount_currency'] = line['price']
733 line['price'] = currency.compute(line['price'], company_currency)
735 line['currency_id'] = False
736 line['amount_currency'] = False
738 if self.type in ('out_invoice','in_refund'):
739 total += line['price']
740 total_currency += line['amount_currency'] or line['price']
741 line['price'] = - line['price']
743 total -= line['price']
744 total_currency -= line['amount_currency'] or line['price']
745 return total, total_currency, invoice_move_lines
747 def inv_line_characteristic_hashcode(self, invoice_line):
748 """Overridable hashcode generation for invoice lines. Lines having the same hashcode
749 will be grouped together if the journal has the 'group line' option. Of course a module
750 can add fields to invoice lines that would need to be tested too before merging lines
752 return "%s-%s-%s-%s-%s" % (
753 invoice_line['account_id'],
754 invoice_line.get('tax_code_id', 'False'),
755 invoice_line.get('product_id', 'False'),
756 invoice_line.get('analytic_account_id', 'False'),
757 invoice_line.get('date_maturity', 'False'),
760 def group_lines(self, iml, line):
761 """Merge account move lines (and hence analytic lines) if invoice line hashcodes are equals"""
762 if self.journal_id.group_invoice_lines:
765 tmp = self.inv_line_characteristic_hashcode(l)
767 am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
768 line2[tmp]['debit'] = (am > 0) and am or 0.0
769 line2[tmp]['credit'] = (am < 0) and -am or 0.0
770 line2[tmp]['tax_amount'] += l['tax_amount']
771 line2[tmp]['analytic_lines'] += l['analytic_lines']
775 for key, val in line2.items():
776 line.append((0,0,val))
780 def action_move_create(self):
781 """ Creates invoice related analytics and financial move lines """
782 account_invoice_tax = self.env['account.invoice.tax']
783 account_move = self.env['account.move']
786 if not inv.journal_id.sequence_id:
787 raise except_orm(_('Error!'), _('Please define sequence on the journal related to this invoice.'))
788 if not inv.invoice_line:
789 raise except_orm(_('No Invoice Lines!'), _('Please create some invoice lines.'))
793 ctx = dict(self._context, lang=inv.partner_id.lang)
795 if not inv.date_invoice:
796 inv.with_context(ctx).write({'date_invoice': fields.Date.context_today(self)})
797 date_invoice = inv.date_invoice
799 company_currency = inv.company_id.currency_id
800 # create the analytical lines, one move line per invoice line
801 iml = inv._get_analytic_lines()
802 # check if taxes are all computed
803 compute_taxes = account_invoice_tax.compute(inv)
804 inv.check_tax_lines(compute_taxes)
806 # I disabled the check_total feature
807 if self.env['res.users'].has_group('account.group_supplier_inv_check_total'):
808 if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding / 2.0):
809 raise except_orm(_('Bad Total!'), _('Please verify the price of the invoice!\nThe encoded total does not match the computed total.'))
812 total_fixed = total_percent = 0
813 for line in inv.payment_term.line_ids:
814 if line.value == 'fixed':
815 total_fixed += line.value_amount
816 if line.value == 'procent':
817 total_percent += line.value_amount
818 total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
819 if (total_fixed + total_percent) > 100:
820 raise except_orm(_('Error!'), _("Cannot create the invoice.\nThe related payment term is probably misconfigured as it gives a computed amount greater than the total invoiced amount. In order to avoid rounding issues, the latest line of your payment term must be of type 'balance'."))
822 # one move line per tax line
823 iml += account_invoice_tax.move_line_get(inv.id)
825 if inv.type in ('in_invoice', 'in_refund'):
830 diff_currency = inv.currency_id != company_currency
831 # create one move line for the total and possibly adjust the other lines amount
832 total, total_currency, iml = inv.with_context(ctx).compute_invoice_totals(company_currency, ref, iml)
834 name = inv.name or inv.supplier_invoice_number or '/'
837 totlines = inv.with_context(ctx).payment_term.compute(total, date_invoice)[0]
839 res_amount_currency = total_currency
840 ctx['date'] = date_invoice
841 for i, t in enumerate(totlines):
842 if inv.currency_id != company_currency:
843 amount_currency = company_currency.with_context(ctx).compute(t[1], inv.currency_id)
845 amount_currency = False
847 # last line: add the diff
848 res_amount_currency -= amount_currency or 0
849 if i + 1 == len(totlines):
850 amount_currency += res_amount_currency
856 'account_id': inv.account_id.id,
857 'date_maturity': t[0],
858 'amount_currency': diff_currency and amount_currency,
859 'currency_id': diff_currency and inv.currency_id.id,
867 'account_id': inv.account_id.id,
868 'date_maturity': inv.date_due,
869 'amount_currency': diff_currency and total_currency,
870 'currency_id': diff_currency and inv.currency_id.id,
876 part = self.env['res.partner']._find_accounting_partner(inv.partner_id)
878 line = [(0, 0, self.line_get_convert(l, part.id, date)) for l in iml]
879 line = inv.group_lines(iml, line)
881 journal = inv.journal_id.with_context(ctx)
882 if journal.centralisation:
883 raise except_orm(_('User Error!'),
884 _('You cannot create an invoice on a centralized journal. Uncheck the centralized counterpart box in the related journal from the configuration menu.'))
886 line = inv.finalize_invoice_move_lines(line)
889 'ref': inv.reference or inv.name,
891 'journal_id': journal.id,
892 'date': inv.date_invoice,
893 'narration': inv.comment,
894 'company_id': inv.company_id.id,
896 ctx['company_id'] = inv.company_id.id
897 period = inv.period_id
899 period = period.with_context(ctx).find(date_invoice)[:1]
901 move_vals['period_id'] = period.id
903 i[2]['period_id'] = period.id
906 move = account_move.with_context(ctx).create(move_vals)
907 # make the invoice point to that move
910 'period_id': period.id,
911 'move_name': move.name,
913 inv.with_context(ctx).write(vals)
914 # Pass invoice in context in method post: used if you want to get the same
915 # account move reference when creating the same invoice after a cancelled one:
921 def invoice_validate(self):
922 return self.write({'state': 'open'})
925 def line_get_convert(self, line, part, date):
927 'date_maturity': line.get('date_maturity', False),
929 'name': line['name'][:64],
931 'debit': line['price']>0 and line['price'],
932 'credit': line['price']<0 and -line['price'],
933 'account_id': line['account_id'],
934 'analytic_lines': line.get('analytic_lines', []),
935 'amount_currency': line['price']>0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)),
936 'currency_id': line.get('currency_id', False),
937 'tax_code_id': line.get('tax_code_id', False),
938 'tax_amount': line.get('tax_amount', False),
939 'ref': line.get('ref', False),
940 'quantity': line.get('quantity',1.00),
941 'product_id': line.get('product_id', False),
942 'product_uom_id': line.get('uos_id', False),
943 'analytic_account_id': line.get('account_analytic_id', False),
947 def action_number(self):
948 #TODO: not correct fix but required a fresh values before reading it.
952 self.write({'internal_number': inv.number})
954 if inv.type in ('in_invoice', 'in_refund'):
955 if not inv.reference:
962 self._cr.execute(""" UPDATE account_move SET ref=%s
963 WHERE id=%s AND (ref IS NULL OR ref = '')""",
964 (ref, inv.move_id.id))
965 self._cr.execute(""" UPDATE account_move_line SET ref=%s
966 WHERE move_id=%s AND (ref IS NULL OR ref = '')""",
967 (ref, inv.move_id.id))
968 self._cr.execute(""" UPDATE account_analytic_line SET ref=%s
969 FROM account_move_line
970 WHERE account_move_line.move_id = %s AND
971 account_analytic_line.move_id = account_move_line.id""",
972 (ref, inv.move_id.id))
973 self.invalidate_cache()
978 def action_cancel(self):
979 moves = self.env['account.move']
984 for move_line in inv.payment_ids:
985 if move_line.reconcile_partial_id.line_partial_ids:
986 raise except_orm(_('Error!'), _('You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first.'))
988 # First, set the invoices as cancelled and detach the move ids
989 self.write({'state': 'cancel', 'move_id': False})
991 # second, invalidate the move(s)
992 moves.button_cancel()
993 # delete the move this invoice was pointing to
994 # Note that the corresponding move_lines and move_reconciles
995 # will be automatically deleted too
997 self._log_event(-1.0, 'Cancel Invoice')
1003 def _log_event(self, factor=1.0, name='Open Invoice'):
1004 #TODO: implement messages system
1010 'out_invoice': _('Invoice'),
1011 'in_invoice': _('Supplier Invoice'),
1012 'out_refund': _('Refund'),
1013 'in_refund': _('Supplier Refund'),
1017 result.append((inv.id, "%s %s" % (inv.number or TYPES[inv.type], inv.name or '')))
1021 def name_search(self, name, args=None, operator='ilike', limit=100):
1023 recs = self.browse()
1025 recs = self.search([('number', '=', name)] + args, limit=limit)
1027 recs = self.search([('name', operator, name)] + args, limit=limit)
1028 return recs.name_get()
1031 def _refund_cleanup_lines(self, lines):
1032 """ Convert records to dict of values suitable for one2many line creation
1034 :param recordset lines: records to convert
1035 :return: list of command tuple for one2many line creation [(0, 0, dict of valueis), ...]
1040 for name, field in line._fields.iteritems():
1041 if name in MAGIC_COLUMNS:
1043 elif field.type == 'many2one':
1044 values[name] = line[name].id
1045 elif field.type not in ['many2many', 'one2many']:
1046 values[name] = line[name]
1047 elif name == 'invoice_line_tax_id':
1048 values[name] = [(6, 0, line[name].ids)]
1049 result.append((0, 0, values))
1053 def _prepare_refund(self, invoice, date=None, period_id=None, description=None, journal_id=None):
1054 """ Prepare the dict of values to create the new refund from the invoice.
1055 This method may be overridden to implement custom
1056 refund generation (making sure to call super() to establish
1057 a clean extension chain).
1059 :param record invoice: invoice to refund
1060 :param string date: refund creation date from the wizard
1061 :param integer period_id: force account.period from the wizard
1062 :param string description: description of the refund from the wizard
1063 :param integer journal_id: account.journal from the wizard
1064 :return: dict of value to create() the refund
1067 for field in ['name', 'reference', 'comment', 'date_due', 'partner_id', 'company_id',
1068 'account_id', 'currency_id', 'payment_term', 'user_id', 'fiscal_position']:
1069 if invoice._fields[field].type == 'many2one':
1070 values[field] = invoice[field].id
1072 values[field] = invoice[field] or False
1074 values['invoice_line'] = self._refund_cleanup_lines(invoice.invoice_line)
1076 tax_lines = filter(lambda l: l.manual, invoice.tax_line)
1077 values['tax_line'] = self._refund_cleanup_lines(tax_lines)
1080 journal = self.env['account.journal'].browse(journal_id)
1081 elif invoice['type'] == 'in_invoice':
1082 journal = self.env['account.journal'].search([('type', '=', 'purchase_refund')], limit=1)
1084 journal = self.env['account.journal'].search([('type', '=', 'sale_refund')], limit=1)
1085 values['journal_id'] = journal.id
1087 values['type'] = TYPE2REFUND[invoice['type']]
1088 values['date_invoice'] = date or fields.Date.context_today(invoice)
1089 values['state'] = 'draft'
1090 values['number'] = False
1093 values['period_id'] = period_id
1095 values['name'] = description
1099 @api.returns('self')
1100 def refund(self, date=None, period_id=None, description=None, journal_id=None):
1101 new_invoices = self.browse()
1102 for invoice in self:
1103 # create the new invoice
1104 values = self._prepare_refund(invoice, date=date, period_id=period_id,
1105 description=description, journal_id=journal_id)
1106 new_invoices += self.create(values)
1110 def pay_and_reconcile(self, pay_amount, pay_account_id, period_id, pay_journal_id,
1111 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=''):
1112 # TODO check if we can use different period for payment and the writeoff line
1113 assert len(self)==1, "Can only pay one invoice at a time."
1114 # Take the seq as name for move
1115 SIGN = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1116 direction = SIGN[self.type]
1117 # take the chosen date
1118 date = self._context.get('date_p') or fields.Date.context_today(self)
1120 # Take the amount in currency and the currency of the payment
1121 if self._context.get('amount_currency') and self._context.get('currency_id'):
1122 amount_currency = self._context['amount_currency']
1123 currency_id = self._context['currency_id']
1125 amount_currency = False
1128 pay_journal = self.env['account.journal'].browse(pay_journal_id)
1129 if self.type in ('in_invoice', 'in_refund'):
1130 ref = self.reference
1133 partner = self.partner_id._find_accounting_partner(self.partner_id)
1134 name = name or self.invoice_line.name or self.number
1135 # Pay attention to the sign for both debit/credit AND amount_currency
1138 'debit': direction * pay_amount > 0 and direction * pay_amount,
1139 'credit': direction * pay_amount < 0 and -direction * pay_amount,
1140 'account_id': self.account_id.id,
1141 'partner_id': partner.id,
1144 'currency_id': currency_id,
1145 'amount_currency': direction * (amount_currency or 0.0),
1146 'company_id': self.company_id.id,
1150 'debit': direction * pay_amount < 0 and -direction * pay_amount,
1151 'credit': direction * pay_amount > 0 and direction * pay_amount,
1152 'account_id': pay_account_id,
1153 'partner_id': partner.id,
1156 'currency_id': currency_id,
1157 'amount_currency': -direction * (amount_currency or 0.0),
1158 'company_id': self.company_id.id,
1160 move = self.env['account.move'].create({
1162 'line_id': [(0, 0, l1), (0, 0, l2)],
1163 'journal_id': pay_journal_id,
1164 'period_id': period_id,
1168 move_ids = (move | self.move_id).ids
1169 self._cr.execute("SELECT id FROM account_move_line WHERE move_id IN %s",
1171 lines = self.env['account.move.line'].browse([r[0] for r in self._cr.fetchall()])
1172 lines2rec = lines.browse()
1174 for line in itertools.chain(lines, self.payment_ids):
1175 if line.account_id == self.account_id:
1177 total += (line.debit or 0.0) - (line.credit or 0.0)
1179 inv_id, name = self.name_get()[0]
1180 if not round(total, self.env['decimal.precision'].precision_get('Account')) or writeoff_acc_id:
1181 lines2rec.reconcile('manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id)
1183 code = self.currency_id.symbol
1184 # TODO: use currency's formatting function
1185 msg = _("Invoice partially paid: %s%s of %s%s (%s%s remaining).") % \
1186 (pay_amount, code, self.amount_total, code, total, code)
1187 self.message_post(body=msg)
1188 lines2rec.reconcile_partial('manual')
1190 # Update the stored value (fields.function), so we write to trigger recompute
1191 return self.write({})
1194 def pay_and_reconcile(self, cr, uid, ids, pay_amount, pay_account_id, period_id, pay_journal_id,
1195 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context=None, name=''):
1196 recs = self.browse(cr, uid, ids, context)
1197 return recs.pay_and_reconcile(pay_amount, pay_account_id, period_id, pay_journal_id,
1198 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=name)
1200 class account_invoice_line(models.Model):
1201 _name = "account.invoice.line"
1202 _description = "Invoice Line"
1203 _order = "invoice_id,sequence,id"
1206 @api.depends('price_unit', 'discount', 'invoice_line_tax_id', 'quantity',
1207 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id')
1208 def _compute_price(self):
1209 price = self.price_unit * (1 - (self.discount or 0.0) / 100.0)
1210 taxes = self.invoice_line_tax_id.compute_all(price, self.quantity, product=self.product_id, partner=self.invoice_id.partner_id)
1211 self.price_subtotal = taxes['total']
1213 self.price_subtotal = self.invoice_id.currency_id.round(self.price_subtotal)
1216 def _default_price_unit(self):
1217 if not self._context.get('check_total'):
1219 total = self._context['check_total']
1220 for l in self._context.get('invoice_line', []):
1221 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1223 price = vals.get('price_unit', 0) * (1 - vals.get('discount', 0) / 100.0)
1224 total = total - (price * vals.get('quantity'))
1225 taxes = vals.get('invoice_line_tax_id')
1226 if taxes and len(taxes[0]) >= 3 and taxes[0][2]:
1227 taxes = self.env['account.tax'].browse(taxes[0][2])
1228 tax_res = taxes.compute_all(price, vals.get('quantity'),
1229 product=vals.get('product_id'), partner=self._context.get('partner_id'))
1230 for tax in tax_res['taxes']:
1231 total = total - tax['amount']
1235 def _default_account(self):
1236 # XXX this gets the default account for the user's company,
1237 # it should get the default account for the invoice's company
1238 # however, the invoice's company does not reach this point
1239 if self._context.get('type') in ('out_invoice', 'out_refund'):
1240 return self.env['ir.property'].get('property_account_income_categ', 'product.category')
1242 return self.env['ir.property'].get('property_account_expense_categ', 'product.category')
1244 name = fields.Text(string='Description', required=True)
1245 origin = fields.Char(string='Source Document',
1246 help="Reference of the document that produced this invoice.")
1247 sequence = fields.Integer(string='Sequence', default=10,
1248 help="Gives the sequence of this line when displaying the invoice.")
1249 invoice_id = fields.Many2one('account.invoice', string='Invoice Reference',
1250 ondelete='cascade', index=True)
1251 uos_id = fields.Many2one('product.uom', string='Unit of Measure',
1252 ondelete='set null', index=True)
1253 product_id = fields.Many2one('product.product', string='Product',
1254 ondelete='set null', index=True)
1255 account_id = fields.Many2one('account.account', string='Account',
1256 required=True, domain=[('type', 'not in', ['view', 'closed'])],
1257 default=_default_account,
1258 help="The income or expense account related to the selected product.")
1259 price_unit = fields.Float(string='Unit Price', required=True,
1260 digits= dp.get_precision('Product Price'),
1261 default=_default_price_unit)
1262 price_subtotal = fields.Float(string='Amount', digits= dp.get_precision('Account'),
1263 store=True, readonly=True, compute='_compute_price')
1264 quantity = fields.Float(string='Quantity', digits= dp.get_precision('Product Unit of Measure'),
1265 required=True, default=1)
1266 discount = fields.Float(string='Discount (%)', digits= dp.get_precision('Discount'),
1268 invoice_line_tax_id = fields.Many2many('account.tax',
1269 'account_invoice_line_tax', 'invoice_line_id', 'tax_id',
1270 string='Taxes', domain=[('parent_id', '=', False)])
1271 account_analytic_id = fields.Many2one('account.analytic.account',
1272 string='Analytic Account')
1273 company_id = fields.Many2one('res.company', string='Company',
1274 related='invoice_id.company_id', store=True, readonly=True)
1275 partner_id = fields.Many2one('res.partner', string='Partner',
1276 related='invoice_id.partner_id', store=True, readonly=True)
1279 def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
1280 res = super(account_invoice_line, self).fields_view_get(
1281 view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
1282 if self._context.get('type'):
1283 doc = etree.XML(res['arch'])
1284 for node in doc.xpath("//field[@name='product_id']"):
1285 if self._context['type'] in ('in_invoice', 'in_refund'):
1286 node.set('domain', "[('purchase_ok', '=', True)]")
1288 node.set('domain', "[('sale_ok', '=', True)]")
1289 res['arch'] = etree.tostring(doc)
1293 def product_id_change(self, product, uom_id, qty=0, name='', type='out_invoice',
1294 partner_id=False, fposition_id=False, price_unit=False, currency_id=False,
1296 context = self._context
1297 company_id = company_id if company_id is not None else context.get('company_id', False)
1298 self = self.with_context(company_id=company_id, force_company=company_id)
1301 raise except_orm(_('No Partner Defined!'), _("You must first select a partner!"))
1303 if type in ('in_invoice', 'in_refund'):
1304 return {'value': {}, 'domain': {'product_uom': []}}
1306 return {'value': {'price_unit': 0.0}, 'domain': {'product_uom': []}}
1310 part = self.env['res.partner'].browse(partner_id)
1311 fpos = self.env['account.fiscal.position'].browse(fposition_id)
1314 self = self.with_context(lang=part.lang)
1315 product = self.env['product.product'].browse(product)
1317 values['name'] = product.partner_ref
1318 if type in ('out_invoice', 'out_refund'):
1319 account = product.property_account_income or product.categ_id.property_account_income_categ
1321 account = product.property_account_expense or product.categ_id.property_account_expense_categ
1322 account = fpos.map_account(account)
1324 values['account_id'] = account.id
1326 if type in ('out_invoice', 'out_refund'):
1327 taxes = product.taxes_id or account.tax_ids
1328 if product.description_sale:
1329 values['name'] += '\n' + product.description_sale
1331 taxes = product.supplier_taxes_id or account.tax_ids
1332 if product.description_purchase:
1333 values['name'] += '\n' + product.description_purchase
1335 taxes = fpos.map_tax(taxes)
1336 values['invoice_line_tax_id'] = taxes.ids
1338 if type in ('in_invoice', 'in_refund'):
1339 values['price_unit'] = price_unit or product.standard_price
1341 values['price_unit'] = product.list_price
1343 values['uos_id'] = uom_id or product.uom_id.id
1344 domain = {'uos_id': [('category_id', '=', product.uom_id.category_id.id)]}
1346 company = self.env['res.company'].browse(company_id)
1347 currency = self.env['res.currency'].browse(currency_id)
1349 if company and currency:
1350 if company.currency_id != currency:
1351 if type in ('in_invoice', 'in_refund'):
1352 values['price_unit'] = product.standard_price
1353 values['price_unit'] = values['price_unit'] * currency.rate
1355 if values['uos_id'] and values['uos_id'] != product.uom_id.id:
1356 values['price_unit'] = self.env['product.uom']._compute_price(
1357 product.uom_id.id, values['price_unit'], values['uos_id'])
1359 return {'value': values, 'domain': domain}
1362 def uos_id_change(self, product, uom, qty=0, name='', type='out_invoice', partner_id=False,
1363 fposition_id=False, price_unit=False, currency_id=False, company_id=None):
1364 context = self._context
1365 company_id = company_id if company_id != None else context.get('company_id', False)
1366 self = self.with_context(company_id=company_id)
1368 result = self.product_id_change(
1369 product, uom, qty, name, type, partner_id, fposition_id, price_unit,
1370 currency_id, company_id=company_id,
1374 result['value']['price_unit'] = 0.0
1376 prod = self.env['product.product'].browse(product)
1377 prod_uom = self.env['product.uom'].browse(uom)
1378 if prod.uom_id.category_id != prod_uom.category_id:
1380 'title': _('Warning!'),
1381 'message': _('The selected unit of measure is not compatible with the unit of measure of the product.'),
1383 result['value']['uos_id'] = prod.uom_id.id
1385 result['warning'] = warning
1389 def move_line_get(self, invoice_id):
1390 inv = self.env['account.invoice'].browse(invoice_id)
1391 currency = inv.currency_id.with_context(date=inv.date_invoice)
1392 company_currency = inv.company_id.currency_id
1395 for line in inv.invoice_line:
1396 mres = self.move_line_get_item(line)
1397 mres['invl_id'] = line.id
1399 tax_code_found = False
1400 taxes = line.invoice_line_tax_id.compute_all(
1401 (line.price_unit * (1.0 - (line.discount or 0.0) / 100.0)),
1402 line.quantity, line.product_id, inv.partner_id)['taxes']
1404 if inv.type in ('out_invoice', 'in_invoice'):
1405 tax_code_id = tax['base_code_id']
1406 tax_amount = line.price_subtotal * tax['base_sign']
1408 tax_code_id = tax['ref_base_code_id']
1409 tax_amount = line.price_subtotal * tax['ref_base_sign']
1414 res.append(dict(mres))
1415 res[-1]['price'] = 0.0
1416 res[-1]['account_analytic_id'] = False
1417 elif not tax_code_id:
1419 tax_code_found = True
1421 res[-1]['tax_code_id'] = tax_code_id
1422 res[-1]['tax_amount'] = currency.compute(tax_amount, company_currency)
1427 def move_line_get_item(self, line):
1430 'name': line.name.split('\n')[0][:64],
1431 'price_unit': line.price_unit,
1432 'quantity': line.quantity,
1433 'price': line.price_subtotal,
1434 'account_id': line.account_id.id,
1435 'product_id': line.product_id.id,
1436 'uos_id': line.uos_id.id,
1437 'account_analytic_id': line.account_analytic_id.id,
1438 'taxes': line.invoice_line_tax_id,
1442 # Set the tax field according to the account and the fiscal position
1445 def onchange_account_id(self, product_id, partner_id, inv_type, fposition_id, account_id):
1449 account = self.env['account.account'].browse(account_id)
1451 fpos = self.env['account.fiscal.position'].browse(fposition_id)
1452 unique_tax_ids = fpos.map_tax(account.tax_ids).ids
1454 product_change_result = self.product_id_change(product_id, False, type=inv_type,
1455 partner_id=partner_id, fposition_id=fposition_id, company_id=account.company_id.id)
1456 if 'invoice_line_tax_id' in product_change_result.get('value', {}):
1457 unique_tax_ids = product_change_result['value']['invoice_line_tax_id']
1458 return {'value': {'invoice_line_tax_id': unique_tax_ids}}
1461 class account_invoice_tax(models.Model):
1462 _name = "account.invoice.tax"
1463 _description = "Invoice Tax"
1467 @api.depends('base', 'base_amount', 'amount', 'tax_amount')
1468 def _compute_factors(self):
1469 self.factor_base = self.base_amount / self.base if self.base else 1.0
1470 self.factor_tax = self.tax_amount / self.amount if self.amount else 1.0
1472 invoice_id = fields.Many2one('account.invoice', string='Invoice Line',
1473 ondelete='cascade', index=True)
1474 name = fields.Char(string='Tax Description',
1476 account_id = fields.Many2one('account.account', string='Tax Account',
1477 required=True, domain=[('type', 'not in', ['view', 'income', 'closed'])])
1478 account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic account')
1479 base = fields.Float(string='Base', digits=dp.get_precision('Account'))
1480 amount = fields.Float(string='Amount', digits=dp.get_precision('Account'))
1481 manual = fields.Boolean(string='Manual', default=True)
1482 sequence = fields.Integer(string='Sequence',
1483 help="Gives the sequence order when displaying a list of invoice tax.")
1484 base_code_id = fields.Many2one('account.tax.code', string='Base Code',
1485 help="The account basis of the tax declaration.")
1486 base_amount = fields.Float(string='Base Code Amount', digits=dp.get_precision('Account'),
1488 tax_code_id = fields.Many2one('account.tax.code', string='Tax Code',
1489 help="The tax basis of the tax declaration.")
1490 tax_amount = fields.Float(string='Tax Code Amount', digits=dp.get_precision('Account'),
1493 company_id = fields.Many2one('res.company', string='Company',
1494 related='account_id.company_id', store=True, readonly=True)
1495 factor_base = fields.Float(string='Multipication factor for Base code',
1496 compute='_compute_factors')
1497 factor_tax = fields.Float(string='Multipication factor Tax code',
1498 compute='_compute_factors')
1501 def base_change(self, base, currency_id=False, company_id=False, date_invoice=False):
1502 factor = self.factor_base if self else 1
1503 company = self.env['res.company'].browse(company_id)
1504 if currency_id and company.currency_id:
1505 currency = self.env['res.currency'].browse(currency_id)
1506 currency = currency.with_context(date=date_invoice or fields.Date.context_today(self))
1507 base = currency.compute(base * factor, company.currency_id, round=False)
1508 return {'value': {'base_amount': base}}
1511 def amount_change(self, amount, currency_id=False, company_id=False, date_invoice=False):
1512 factor = self.factor_tax if self else 1
1513 company = self.env['res.company'].browse(company_id)
1514 if currency_id and company.currency_id:
1515 currency = self.env['res.currency'].browse(currency_id)
1516 currency = currency.with_context(date=date_invoice or fields.Date.context_today(self))
1517 amount = currency.compute(amount * factor, company.currency_id, round=False)
1518 return {'value': {'tax_amount': amount}}
1521 def compute(self, invoice):
1523 currency = invoice.currency_id.with_context(date=invoice.date_invoice or fields.Date.context_today(invoice))
1524 company_currency = invoice.company_id.currency_id
1525 for line in invoice.invoice_line:
1526 taxes = line.invoice_line_tax_id.compute_all(
1527 (line.price_unit * (1 - (line.discount or 0.0) / 100.0)),
1528 line.quantity, line.product_id, invoice.partner_id)['taxes']
1531 'invoice_id': invoice.id,
1532 'name': tax['name'],
1533 'amount': tax['amount'],
1535 'sequence': tax['sequence'],
1536 'base': currency.round(tax['price_unit'] * line['quantity']),
1538 if invoice.type in ('out_invoice','in_invoice'):
1539 val['base_code_id'] = tax['base_code_id']
1540 val['tax_code_id'] = tax['tax_code_id']
1541 val['base_amount'] = currency.compute(val['base'] * tax['base_sign'], company_currency, round=False)
1542 val['tax_amount'] = currency.compute(val['amount'] * tax['tax_sign'], company_currency, round=False)
1543 val['account_id'] = tax['account_collected_id'] or line.account_id.id
1544 val['account_analytic_id'] = tax['account_analytic_collected_id']
1546 val['base_code_id'] = tax['ref_base_code_id']
1547 val['tax_code_id'] = tax['ref_tax_code_id']
1548 val['base_amount'] = currency.compute(val['base'] * tax['ref_base_sign'], company_currency, round=False)
1549 val['tax_amount'] = currency.compute(val['amount'] * tax['ref_tax_sign'], company_currency, round=False)
1550 val['account_id'] = tax['account_paid_id'] or line.account_id.id
1551 val['account_analytic_id'] = tax['account_analytic_paid_id']
1553 key = (val['tax_code_id'], val['base_code_id'], val['account_id'], val['account_analytic_id'])
1554 if not key in tax_grouped:
1555 tax_grouped[key] = val
1557 tax_grouped[key]['base'] += val['base']
1558 tax_grouped[key]['amount'] += val['amount']
1559 tax_grouped[key]['base_amount'] += val['base_amount']
1560 tax_grouped[key]['tax_amount'] += val['tax_amount']
1562 for t in tax_grouped.values():
1563 t['base'] = currency.round(t['base'])
1564 t['amount'] = currency.round(t['amount'])
1565 t['base_amount'] = currency.round(t['base_amount'])
1566 t['tax_amount'] = currency.round(t['tax_amount'])
1571 def compute(self, cr, uid, invoice_id, context=None):
1572 recs = self.browse(cr, uid, [], context)
1573 invoice = recs.env['account.invoice'].browse(invoice_id)
1574 return recs.compute(invoice)
1577 def move_line_get(self, invoice_id):
1580 'SELECT * FROM account_invoice_tax WHERE invoice_id = %s',
1583 for row in self._cr.dictfetchall():
1584 if not (row['amount'] or row['tax_code_id'] or row['tax_amount']):
1588 'name': row['name'],
1589 'price_unit': row['amount'],
1591 'price': row['amount'] or 0.0,
1592 'account_id': row['account_id'],
1593 'tax_code_id': row['tax_code_id'],
1594 'tax_amount': row['tax_amount'],
1595 'account_analytic_id': row['account_analytic_id'],
1600 class res_partner(models.Model):
1601 # Inherits partner and adds invoice information in the partner form
1602 _inherit = 'res.partner'
1604 invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices',
1607 def _find_accounting_partner(self, partner):
1609 Find the partner for which the accounting entries will be created
1611 return partner.commercial_partner_id
1613 class mail_compose_message(models.Model):
1614 _inherit = 'mail.compose.message'
1617 def send_mail(self):
1618 context = self._context
1619 if context.get('default_model') == 'account.invoice' and \
1620 context.get('default_res_id') and context.get('mark_invoice_as_sent'):
1621 invoice = self.env['account.invoice'].browse(context['default_res_id'])
1622 invoice = invoice.with_context(mail_post_autofollow=True)
1623 invoice.write({'sent': True})
1624 invoice.message_post(body=_("Invoice sent"))
1625 return super(mail_compose_message, self).send_mail()
1627 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: