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 # Fixes the fact that move_id.line_id.amount_residual, being not stored and old API, doesn't trigger recomputation
112 'move_id.line_id.reconcile_id',
113 'move_id.line_id.amount_residual_currency',
114 'move_id.line_id.currency_id',
115 'move_id.line_id.reconcile_partial_id.line_partial_ids.invoice.type',
117 # An invoice's residual amount is the sum of its unreconciled move lines and,
118 # for partially reconciled move lines, their residual amount divided by the
119 # number of times this reconciliation is used in an invoice (so we split
120 # the residual amount between all invoice)
121 def _compute_residual(self):
123 # Each partial reconciliation is considered only once for each invoice it appears into,
124 # and its residual amount is divided by this number of invoices
125 partial_reconciliations_done = []
126 for line in self.sudo().move_id.line_id:
127 if line.account_id.type not in ('receivable', 'payable'):
129 if line.reconcile_partial_id and line.reconcile_partial_id.id in partial_reconciliations_done:
131 # Get the correct line residual amount
132 if line.currency_id == self.currency_id:
133 line_amount = line.currency_id and line.amount_residual_currency or line.amount_residual
135 from_currency = line.company_id.currency_id.with_context(date=line.date)
136 line_amount = from_currency.compute(line.amount_residual, self.currency_id)
137 # For partially reconciled lines, split the residual amount
138 if line.reconcile_partial_id:
139 partial_reconciliation_invoices = set()
140 for pline in line.reconcile_partial_id.line_partial_ids:
141 if pline.invoice and self.type == pline.invoice.type:
142 partial_reconciliation_invoices.update([pline.invoice.id])
143 line_amount = self.currency_id.round(line_amount / len(partial_reconciliation_invoices))
144 partial_reconciliations_done.append(line.reconcile_partial_id.id)
145 self.residual += line_amount
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
321 def get_view_id(xid, name):
323 return self.env['ir.model.data'].xmlid_to_res_id('account.' + xid, raise_if_not_found=True)
326 return self.env['ir.ui.view'].search([('name', '=', name)], limit=1).id
328 return False # view not found
330 if context.get('active_model') == 'res.partner' and context.get('active_ids'):
331 partner = self.env['res.partner'].browse(context['active_ids'])[0]
333 view_id = get_view_id('invoice_tree', 'account.invoice.tree')
335 elif view_type == 'form':
336 if partner.supplier and not partner.customer:
337 view_id = get_view_id('invoice_supplier_form', 'account.invoice.supplier.form')
338 elif partner.customer and not partner.supplier:
339 view_id = get_view_id('invoice_form', 'account.invoice.form')
341 res = super(account_invoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
343 # adapt selection of field journal_id
344 for field in res['fields']:
345 if field == 'journal_id' and type:
346 journal_select = self.env['account.journal']._name_search('', [('type', '=', type)], name_get_uid=1)
347 res['fields'][field]['selection'] = journal_select
349 doc = etree.XML(res['arch'])
351 if context.get('type'):
352 for node in doc.xpath("//field[@name='partner_bank_id']"):
353 if context['type'] == 'in_refund':
354 node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]")
355 elif context['type'] == 'out_refund':
356 node.set('domain', "[('partner_id', '=', partner_id)]")
358 if view_type == 'search':
359 if context.get('type') in ('out_invoice', 'out_refund'):
360 for node in doc.xpath("//group[@name='extended filter']"):
363 if view_type == 'tree':
364 partner_string = _('Customer')
365 if context.get('type') in ('in_invoice', 'in_refund'):
366 partner_string = _('Supplier')
367 for node in doc.xpath("//field[@name='reference']"):
368 node.set('invisible', '0')
369 for node in doc.xpath("//field[@name='partner_id']"):
370 node.set('string', partner_string)
372 res['arch'] = etree.tostring(doc)
376 def invoice_print(self):
377 """ Print the invoice and mark it as sent, so that we can see more
378 easily the next step of the workflow
380 assert len(self) == 1, 'This option should only be used for a single id at a time.'
382 return self.env['report'].get_action(self, 'account.report_invoice')
385 def action_invoice_sent(self):
386 """ Open a window to compose an email, with the edi invoice template
387 message loaded by default
389 assert len(self) == 1, 'This option should only be used for a single id at a time.'
390 template = self.env.ref('account.email_template_edi_invoice', False)
391 compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
393 default_model='account.invoice',
394 default_res_id=self.id,
395 default_use_template=bool(template),
396 default_template_id=template.id,
397 default_composition_mode='comment',
398 mark_invoice_as_sent=True,
401 'name': _('Compose Email'),
402 'type': 'ir.actions.act_window',
405 'res_model': 'mail.compose.message',
406 'views': [(compose_form.id, 'form')],
407 'view_id': compose_form.id,
413 def confirm_paid(self):
414 return self.write({'state': 'paid'})
419 if invoice.state not in ('draft', 'cancel'):
420 raise Warning(_('You cannot delete an invoice which is not draft or cancelled. You should refund it instead.'))
421 elif invoice.internal_number:
422 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.'))
423 return super(account_invoice, self).unlink()
426 def onchange_partner_id(self, type, partner_id, date_invoice=False,
427 payment_term=False, partner_bank_id=False, company_id=False):
429 payment_term_id = False
430 fiscal_position = False
434 p = self.env['res.partner'].browse(partner_id)
435 rec_account = p.property_account_receivable
436 pay_account = p.property_account_payable
438 if p.property_account_receivable.company_id and \
439 p.property_account_receivable.company_id.id != company_id and \
440 p.property_account_payable.company_id and \
441 p.property_account_payable.company_id.id != company_id:
442 prop = self.env['ir.property']
443 rec_dom = [('name', '=', 'property_account_receivable'), ('company_id', '=', company_id)]
444 pay_dom = [('name', '=', 'property_account_payable'), ('company_id', '=', company_id)]
445 res_dom = [('res_id', '=', 'res.partner,%s' % partner_id)]
446 rec_prop = prop.search(rec_dom + res_dom) or prop.search(rec_dom)
447 pay_prop = prop.search(pay_dom + res_dom) or prop.search(pay_dom)
448 rec_account = rec_prop.get_by_record(rec_prop)
449 pay_account = pay_prop.get_by_record(pay_prop)
450 if not rec_account and not pay_account:
451 action = self.env.ref('account.action_account_config')
452 msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
453 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
455 if type in ('out_invoice', 'out_refund'):
456 account_id = rec_account.id
457 payment_term_id = p.property_payment_term.id
459 account_id = pay_account.id
460 payment_term_id = p.property_supplier_payment_term.id
461 fiscal_position = p.property_account_position.id
462 bank_id = p.bank_ids and p.bank_ids[0].id or False
465 'account_id': account_id,
466 'payment_term': payment_term_id,
467 'fiscal_position': fiscal_position,
470 if type in ('in_invoice', 'in_refund'):
471 result['value']['partner_bank_id'] = bank_id
473 if payment_term != payment_term_id:
475 to_update = self.onchange_payment_term_date_invoice(payment_term_id, date_invoice)
476 result['value'].update(to_update.get('value', {}))
478 result['value']['date_due'] = False
480 if partner_bank_id != bank_id:
481 to_update = self.onchange_partner_bank(bank_id)
482 result['value'].update(to_update.get('value', {}))
487 def onchange_journal_id(self, journal_id=False):
489 journal = self.env['account.journal'].browse(journal_id)
492 'currency_id': journal.currency.id or journal.company_id.currency_id.id,
493 'company_id': journal.company_id.id,
499 def onchange_payment_term_date_invoice(self, payment_term_id, date_invoice):
501 date_invoice = fields.Date.context_today(self)
502 if not payment_term_id:
503 # To make sure the invoice due date should contain due date which is
504 # entered by user when there is no payment term defined
505 return {'value': {'date_due': self.date_due or date_invoice}}
506 pterm = self.env['account.payment.term'].browse(payment_term_id)
507 pterm_list = pterm.compute(value=1, date_ref=date_invoice)[0]
509 return {'value': {'date_due': max(line[0] for line in pterm_list)}}
511 raise except_orm(_('Insufficient Data!'),
512 _('The payment term of supplier does not have a payment term line.'))
515 def onchange_invoice_line(self, lines):
519 def onchange_partner_bank(self, partner_bank_id=False):
523 def onchange_company_id(self, company_id, part_id, type, invoice_line, currency_id):
524 # TODO: add the missing context parameter when forward-porting in trunk
525 # so we can remove this hack!
526 self = self.with_context(self.env['res.users'].context_get())
531 if company_id and part_id and type:
532 p = self.env['res.partner'].browse(part_id)
533 if p.property_account_payable and p.property_account_receivable and \
534 p.property_account_payable.company_id.id != company_id and \
535 p.property_account_receivable.company_id.id != company_id:
536 prop = self.env['ir.property']
537 rec_dom = [('name', '=', 'property_account_receivable'), ('company_id', '=', company_id)]
538 pay_dom = [('name', '=', 'property_account_payable'), ('company_id', '=', company_id)]
539 res_dom = [('res_id', '=', 'res.partner,%s' % part_id)]
540 rec_prop = prop.search(rec_dom + res_dom) or prop.search(rec_dom)
541 pay_prop = prop.search(pay_dom + res_dom) or prop.search(pay_dom)
542 rec_account = rec_prop.get_by_record(rec_prop)
543 pay_account = pay_prop.get_by_record(pay_prop)
544 if not rec_account and not pay_account:
545 action = self.env.ref('account.action_account_config')
546 msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
547 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
549 if type in ('out_invoice', 'out_refund'):
550 acc_id = rec_account.id
552 acc_id = pay_account.id
553 values= {'account_id': acc_id}
557 for line in self.invoice_line:
558 if not line.account_id:
560 if line.account_id.company_id.id == company_id:
562 accounts = self.env['account.account'].search([('name', '=', line.account_id.name), ('company_id', '=', company_id)])
564 action = self.env.ref('account.action_account_config')
565 msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
566 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
567 line.write({'account_id': accounts[-1].id})
569 for line_cmd in invoice_line or []:
570 if len(line_cmd) >= 3 and isinstance(line_cmd[2], dict):
571 line = self.env['account.account'].browse(line_cmd[2]['account_id'])
572 if line.company_id.id != company_id:
574 _('Configuration Error!'),
575 _("Invoice line account's company and invoice's company does not match.")
578 if company_id and type:
579 journal_type = TYPE2JOURNAL[type]
580 journals = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company_id)])
582 values['journal_id'] = journals[0].id
583 journal_defaults = self.env['ir.values'].get_defaults_dict('account.invoice', 'type=%s' % type)
584 if 'journal_id' in journal_defaults:
585 values['journal_id'] = journal_defaults['journal_id']
586 if not values.get('journal_id'):
587 field_desc = journals.fields_get(['type'])
588 type_label = next(t for t, label in field_desc['type']['selection'] if t == journal_type)
589 action = self.env.ref('account.action_account_journal_form')
590 msg = _('Cannot find any account journal of type "%s" for this company, You should create one.\n Please go to Journal Configuration') % type_label
591 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
592 domain = {'journal_id': [('id', 'in', journals.ids)]}
594 return {'value': values, 'domain': domain}
597 def action_cancel_draft(self):
598 # go from canceled state to draft state
599 self.write({'state': 'draft'})
600 self.delete_workflow()
601 self.create_workflow()
605 @api.returns('ir.ui.view')
606 def get_formview_id(self):
607 """ Update form view id of action to open the invoice """
608 if self.type == 'in_invoice':
609 return self.env.ref('account.invoice_supplier_form')
611 return self.env.ref('account.invoice_form')
614 def move_line_id_payment_get(self):
615 # return the move line ids with the same account as the invoice self
618 query = """ SELECT l.id
619 FROM account_move_line l, account_invoice i
620 WHERE i.id = %s AND l.move_id = i.move_id AND l.account_id = i.account_id
622 self._cr.execute(query, (self.id,))
623 return [row[0] for row in self._cr.fetchall()]
627 # check whether all corresponding account move lines are reconciled
628 line_ids = self.move_line_id_payment_get()
631 query = "SELECT reconcile_id FROM account_move_line WHERE id IN %s"
632 self._cr.execute(query, (tuple(line_ids),))
633 return all(row[0] for row in self._cr.fetchall())
636 def button_reset_taxes(self):
637 account_invoice_tax = self.env['account.invoice.tax']
638 ctx = dict(self._context)
640 self._cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (invoice.id,))
641 self.invalidate_cache()
642 partner = invoice.partner_id
644 ctx['lang'] = partner.lang
645 for taxe in account_invoice_tax.compute(invoice).values():
646 account_invoice_tax.create(taxe)
647 # dummy write on self to trigger recomputations
648 return self.with_context(ctx).write({'invoice_line': []})
651 def button_compute(self, set_total=False):
652 self.button_reset_taxes()
655 invoice.check_total = invoice.amount_total
659 def _get_analytic_lines(self):
660 """ Return a list of dict for creating analytic lines for self[0] """
661 company_currency = self.company_id.currency_id
662 sign = 1 if self.type in ('out_invoice', 'in_refund') else -1
664 iml = self.env['account.invoice.line'].move_line_get(self.id)
666 if il['account_analytic_id']:
667 if self.type in ('in_invoice', 'in_refund'):
671 if not self.journal_id.analytic_journal_id:
672 raise except_orm(_('No Analytic Journal!'),
673 _("You have to define an analytic journal on the '%s' journal!") % (self.journal_id.name,))
674 currency = self.currency_id.with_context(date=self.date_invoice)
675 il['analytic_lines'] = [(0,0, {
677 'date': self.date_invoice,
678 'account_id': il['account_analytic_id'],
679 'unit_amount': il['quantity'],
680 'amount': currency.compute(il['price'], company_currency) * sign,
681 'product_id': il['product_id'],
682 'product_uom_id': il['uos_id'],
683 'general_account_id': il['account_id'],
684 'journal_id': self.journal_id.analytic_journal_id.id,
690 def action_date_assign(self):
692 res = inv.onchange_payment_term_date_invoice(inv.payment_term.id, inv.date_invoice)
693 if res and res.get('value'):
694 inv.write(res['value'])
698 def finalize_invoice_move_lines(self, move_lines):
699 """ finalize_invoice_move_lines(move_lines) -> move_lines
701 Hook method to be overridden in additional modules to verify and
702 possibly alter the move lines to be created by an invoice, for
704 :param move_lines: list of dictionaries with the account.move.lines (as for create())
705 :return: the (possibly updated) final move_lines to create for this invoice
710 def check_tax_lines(self, compute_taxes):
711 account_invoice_tax = self.env['account.invoice.tax']
712 company_currency = self.company_id.currency_id
713 if not self.tax_line:
714 for tax in compute_taxes.values():
715 account_invoice_tax.create(tax)
718 precision = self.env['decimal.precision'].precision_get('Account')
719 for tax in self.tax_line:
722 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
724 if key not in compute_taxes:
725 raise except_orm(_('Warning!'), _('Global taxes defined, but they are not in invoice lines !'))
726 base = compute_taxes[key]['base']
727 if float_compare(abs(base - tax.base), company_currency.rounding, precision_digits=precision) == 1:
728 raise except_orm(_('Warning!'), _('Tax base different!\nClick on compute to update the tax base.'))
729 for key in compute_taxes:
730 if key not in tax_key:
731 raise except_orm(_('Warning!'), _('Taxes are missing!\nClick on compute button.'))
734 def compute_invoice_totals(self, company_currency, ref, invoice_move_lines):
737 for line in invoice_move_lines:
738 if self.currency_id != company_currency:
739 currency = self.currency_id.with_context(date=self.date_invoice or fields.Date.context_today(self))
740 line['currency_id'] = currency.id
741 line['amount_currency'] = line['price']
742 line['price'] = currency.compute(line['price'], company_currency)
744 line['currency_id'] = False
745 line['amount_currency'] = False
747 if self.type in ('out_invoice','in_refund'):
748 total += line['price']
749 total_currency += line['amount_currency'] or line['price']
750 line['price'] = - line['price']
752 total -= line['price']
753 total_currency -= line['amount_currency'] or line['price']
754 return total, total_currency, invoice_move_lines
756 def inv_line_characteristic_hashcode(self, invoice_line):
757 """Overridable hashcode generation for invoice lines. Lines having the same hashcode
758 will be grouped together if the journal has the 'group line' option. Of course a module
759 can add fields to invoice lines that would need to be tested too before merging lines
761 return "%s-%s-%s-%s-%s" % (
762 invoice_line['account_id'],
763 invoice_line.get('tax_code_id', 'False'),
764 invoice_line.get('product_id', 'False'),
765 invoice_line.get('analytic_account_id', 'False'),
766 invoice_line.get('date_maturity', 'False'),
769 def group_lines(self, iml, line):
770 """Merge account move lines (and hence analytic lines) if invoice line hashcodes are equals"""
771 if self.journal_id.group_invoice_lines:
774 tmp = self.inv_line_characteristic_hashcode(l)
776 am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
777 line2[tmp]['debit'] = (am > 0) and am or 0.0
778 line2[tmp]['credit'] = (am < 0) and -am or 0.0
779 line2[tmp]['tax_amount'] += l['tax_amount']
780 line2[tmp]['analytic_lines'] += l['analytic_lines']
784 for key, val in line2.items():
785 line.append((0,0,val))
789 def action_move_create(self):
790 """ Creates invoice related analytics and financial move lines """
791 account_invoice_tax = self.env['account.invoice.tax']
792 account_move = self.env['account.move']
795 if not inv.journal_id.sequence_id:
796 raise except_orm(_('Error!'), _('Please define sequence on the journal related to this invoice.'))
797 if not inv.invoice_line:
798 raise except_orm(_('No Invoice Lines!'), _('Please create some invoice lines.'))
802 ctx = dict(self._context, lang=inv.partner_id.lang)
804 if not inv.date_invoice:
805 inv.with_context(ctx).write({'date_invoice': fields.Date.context_today(self)})
806 date_invoice = inv.date_invoice
808 company_currency = inv.company_id.currency_id
809 # create the analytical lines, one move line per invoice line
810 iml = inv._get_analytic_lines()
811 # check if taxes are all computed
812 compute_taxes = account_invoice_tax.compute(inv)
813 inv.check_tax_lines(compute_taxes)
815 # I disabled the check_total feature
816 if self.env['res.users'].has_group('account.group_supplier_inv_check_total'):
817 if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding / 2.0):
818 raise except_orm(_('Bad Total!'), _('Please verify the price of the invoice!\nThe encoded total does not match the computed total.'))
821 total_fixed = total_percent = 0
822 for line in inv.payment_term.line_ids:
823 if line.value == 'fixed':
824 total_fixed += line.value_amount
825 if line.value == 'procent':
826 total_percent += line.value_amount
827 total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
828 if (total_fixed + total_percent) > 100:
829 raise except_orm(_('Error!'), _("Cannot create the invoice.\nThe related payment term is probably misconfigured as it gives a computed amount greater than the total invoiced amount. In order to avoid rounding issues, the latest line of your payment term must be of type 'balance'."))
831 # one move line per tax line
832 iml += account_invoice_tax.move_line_get(inv.id)
834 if inv.type in ('in_invoice', 'in_refund'):
839 diff_currency = inv.currency_id != company_currency
840 # create one move line for the total and possibly adjust the other lines amount
841 total, total_currency, iml = inv.with_context(ctx).compute_invoice_totals(company_currency, ref, iml)
843 name = inv.name or inv.supplier_invoice_number or '/'
846 totlines = inv.with_context(ctx).payment_term.compute(total, date_invoice)[0]
848 res_amount_currency = total_currency
849 ctx['date'] = date_invoice
850 for i, t in enumerate(totlines):
851 if inv.currency_id != company_currency:
852 amount_currency = company_currency.with_context(ctx).compute(t[1], inv.currency_id)
854 amount_currency = False
856 # last line: add the diff
857 res_amount_currency -= amount_currency or 0
858 if i + 1 == len(totlines):
859 amount_currency += res_amount_currency
865 'account_id': inv.account_id.id,
866 'date_maturity': t[0],
867 'amount_currency': diff_currency and amount_currency,
868 'currency_id': diff_currency and inv.currency_id.id,
876 'account_id': inv.account_id.id,
877 'date_maturity': inv.date_due,
878 'amount_currency': diff_currency and total_currency,
879 'currency_id': diff_currency and inv.currency_id.id,
885 part = self.env['res.partner']._find_accounting_partner(inv.partner_id)
887 line = [(0, 0, self.line_get_convert(l, part.id, date)) for l in iml]
888 line = inv.group_lines(iml, line)
890 journal = inv.journal_id.with_context(ctx)
891 if journal.centralisation:
892 raise except_orm(_('User Error!'),
893 _('You cannot create an invoice on a centralized journal. Uncheck the centralized counterpart box in the related journal from the configuration menu.'))
895 line = inv.finalize_invoice_move_lines(line)
898 'ref': inv.reference or inv.name,
900 'journal_id': journal.id,
901 'date': inv.date_invoice,
902 'narration': inv.comment,
903 'company_id': inv.company_id.id,
905 ctx['company_id'] = inv.company_id.id
906 period = inv.period_id
908 period = period.with_context(ctx).find(date_invoice)[:1]
910 move_vals['period_id'] = period.id
912 i[2]['period_id'] = period.id
915 move = account_move.with_context(ctx).create(move_vals)
916 # make the invoice point to that move
919 'period_id': period.id,
920 'move_name': move.name,
922 inv.with_context(ctx).write(vals)
923 # Pass invoice in context in method post: used if you want to get the same
924 # account move reference when creating the same invoice after a cancelled one:
930 def invoice_validate(self):
931 return self.write({'state': 'open'})
934 def line_get_convert(self, line, part, date):
936 'date_maturity': line.get('date_maturity', False),
938 'name': line['name'][:64],
940 'debit': line['price']>0 and line['price'],
941 'credit': line['price']<0 and -line['price'],
942 'account_id': line['account_id'],
943 'analytic_lines': line.get('analytic_lines', []),
944 'amount_currency': line['price']>0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)),
945 'currency_id': line.get('currency_id', False),
946 'tax_code_id': line.get('tax_code_id', False),
947 'tax_amount': line.get('tax_amount', False),
948 'ref': line.get('ref', False),
949 'quantity': line.get('quantity',1.00),
950 'product_id': line.get('product_id', False),
951 'product_uom_id': line.get('uos_id', False),
952 'analytic_account_id': line.get('account_analytic_id', False),
956 def action_number(self):
957 #TODO: not correct fix but required a fresh values before reading it.
961 self.write({'internal_number': inv.number})
963 if inv.type in ('in_invoice', 'in_refund'):
964 if not inv.reference:
971 self._cr.execute(""" UPDATE account_move SET ref=%s
972 WHERE id=%s AND (ref IS NULL OR ref = '')""",
973 (ref, inv.move_id.id))
974 self._cr.execute(""" UPDATE account_move_line SET ref=%s
975 WHERE move_id=%s AND (ref IS NULL OR ref = '')""",
976 (ref, inv.move_id.id))
977 self._cr.execute(""" UPDATE account_analytic_line SET ref=%s
978 FROM account_move_line
979 WHERE account_move_line.move_id = %s AND
980 account_analytic_line.move_id = account_move_line.id""",
981 (ref, inv.move_id.id))
982 self.invalidate_cache()
987 def action_cancel(self):
988 moves = self.env['account.move']
993 for move_line in inv.payment_ids:
994 if move_line.reconcile_partial_id.line_partial_ids:
995 raise except_orm(_('Error!'), _('You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first.'))
997 # First, set the invoices as cancelled and detach the move ids
998 self.write({'state': 'cancel', 'move_id': False})
1000 # second, invalidate the move(s)
1001 moves.button_cancel()
1002 # delete the move this invoice was pointing to
1003 # Note that the corresponding move_lines and move_reconciles
1004 # will be automatically deleted too
1006 self._log_event(-1.0, 'Cancel Invoice')
1012 def _log_event(self, factor=1.0, name='Open Invoice'):
1013 #TODO: implement messages system
1019 'out_invoice': _('Invoice'),
1020 'in_invoice': _('Supplier Invoice'),
1021 'out_refund': _('Refund'),
1022 'in_refund': _('Supplier Refund'),
1026 result.append((inv.id, "%s %s" % (inv.number or TYPES[inv.type], inv.name or '')))
1030 def name_search(self, name, args=None, operator='ilike', limit=100):
1032 recs = self.browse()
1034 recs = self.search([('number', '=', name)] + args, limit=limit)
1036 recs = self.search([('name', operator, name)] + args, limit=limit)
1037 return recs.name_get()
1040 def _refund_cleanup_lines(self, lines):
1041 """ Convert records to dict of values suitable for one2many line creation
1043 :param recordset lines: records to convert
1044 :return: list of command tuple for one2many line creation [(0, 0, dict of valueis), ...]
1049 for name, field in line._fields.iteritems():
1050 if name in MAGIC_COLUMNS:
1052 elif field.type == 'many2one':
1053 values[name] = line[name].id
1054 elif field.type not in ['many2many', 'one2many']:
1055 values[name] = line[name]
1056 elif name == 'invoice_line_tax_id':
1057 values[name] = [(6, 0, line[name].ids)]
1058 result.append((0, 0, values))
1062 def _prepare_refund(self, invoice, date=None, period_id=None, description=None, journal_id=None):
1063 """ Prepare the dict of values to create the new refund from the invoice.
1064 This method may be overridden to implement custom
1065 refund generation (making sure to call super() to establish
1066 a clean extension chain).
1068 :param record invoice: invoice to refund
1069 :param string date: refund creation date from the wizard
1070 :param integer period_id: force account.period from the wizard
1071 :param string description: description of the refund from the wizard
1072 :param integer journal_id: account.journal from the wizard
1073 :return: dict of value to create() the refund
1076 for field in ['name', 'reference', 'comment', 'date_due', 'partner_id', 'company_id',
1077 'account_id', 'currency_id', 'payment_term', 'user_id', 'fiscal_position']:
1078 if invoice._fields[field].type == 'many2one':
1079 values[field] = invoice[field].id
1081 values[field] = invoice[field] or False
1083 values['invoice_line'] = self._refund_cleanup_lines(invoice.invoice_line)
1085 tax_lines = filter(lambda l: l.manual, invoice.tax_line)
1086 values['tax_line'] = self._refund_cleanup_lines(tax_lines)
1089 journal = self.env['account.journal'].browse(journal_id)
1090 elif invoice['type'] == 'in_invoice':
1091 journal = self.env['account.journal'].search([('type', '=', 'purchase_refund')], limit=1)
1093 journal = self.env['account.journal'].search([('type', '=', 'sale_refund')], limit=1)
1094 values['journal_id'] = journal.id
1096 values['type'] = TYPE2REFUND[invoice['type']]
1097 values['date_invoice'] = date or fields.Date.context_today(invoice)
1098 values['state'] = 'draft'
1099 values['number'] = False
1102 values['period_id'] = period_id
1104 values['name'] = description
1108 @api.returns('self')
1109 def refund(self, date=None, period_id=None, description=None, journal_id=None):
1110 new_invoices = self.browse()
1111 for invoice in self:
1112 # create the new invoice
1113 values = self._prepare_refund(invoice, date=date, period_id=period_id,
1114 description=description, journal_id=journal_id)
1115 new_invoices += self.create(values)
1119 def pay_and_reconcile(self, pay_amount, pay_account_id, period_id, pay_journal_id,
1120 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=''):
1121 # TODO check if we can use different period for payment and the writeoff line
1122 assert len(self)==1, "Can only pay one invoice at a time."
1123 # Take the seq as name for move
1124 SIGN = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1125 direction = SIGN[self.type]
1126 # take the chosen date
1127 date = self._context.get('date_p') or fields.Date.context_today(self)
1129 # Take the amount in currency and the currency of the payment
1130 if self._context.get('amount_currency') and self._context.get('currency_id'):
1131 amount_currency = self._context['amount_currency']
1132 currency_id = self._context['currency_id']
1134 amount_currency = False
1137 pay_journal = self.env['account.journal'].browse(pay_journal_id)
1138 if self.type in ('in_invoice', 'in_refund'):
1139 ref = self.reference
1142 partner = self.partner_id._find_accounting_partner(self.partner_id)
1143 name = name or self.invoice_line.name or self.number
1144 # Pay attention to the sign for both debit/credit AND amount_currency
1147 'debit': direction * pay_amount > 0 and direction * pay_amount,
1148 'credit': direction * pay_amount < 0 and -direction * pay_amount,
1149 'account_id': self.account_id.id,
1150 'partner_id': partner.id,
1153 'currency_id': currency_id,
1154 'amount_currency': direction * (amount_currency or 0.0),
1155 'company_id': self.company_id.id,
1159 'debit': direction * pay_amount < 0 and -direction * pay_amount,
1160 'credit': direction * pay_amount > 0 and direction * pay_amount,
1161 'account_id': pay_account_id,
1162 'partner_id': partner.id,
1165 'currency_id': currency_id,
1166 'amount_currency': -direction * (amount_currency or 0.0),
1167 'company_id': self.company_id.id,
1169 move = self.env['account.move'].create({
1171 'line_id': [(0, 0, l1), (0, 0, l2)],
1172 'journal_id': pay_journal_id,
1173 'period_id': period_id,
1177 move_ids = (move | self.move_id).ids
1178 self._cr.execute("SELECT id FROM account_move_line WHERE move_id IN %s",
1180 lines = self.env['account.move.line'].browse([r[0] for r in self._cr.fetchall()])
1181 lines2rec = lines.browse()
1183 for line in itertools.chain(lines, self.payment_ids):
1184 if line.account_id == self.account_id:
1186 total += (line.debit or 0.0) - (line.credit or 0.0)
1188 inv_id, name = self.name_get()[0]
1189 if not round(total, self.env['decimal.precision'].precision_get('Account')) or writeoff_acc_id:
1190 lines2rec.reconcile('manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id)
1192 code = self.currency_id.symbol
1193 # TODO: use currency's formatting function
1194 msg = _("Invoice partially paid: %s%s of %s%s (%s%s remaining).") % \
1195 (pay_amount, code, self.amount_total, code, total, code)
1196 self.message_post(body=msg)
1197 lines2rec.reconcile_partial('manual')
1199 # Update the stored value (fields.function), so we write to trigger recompute
1200 return self.write({})
1203 def pay_and_reconcile(self, cr, uid, ids, pay_amount, pay_account_id, period_id, pay_journal_id,
1204 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context=None, name=''):
1205 recs = self.browse(cr, uid, ids, context)
1206 return recs.pay_and_reconcile(pay_amount, pay_account_id, period_id, pay_journal_id,
1207 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=name)
1209 class account_invoice_line(models.Model):
1210 _name = "account.invoice.line"
1211 _description = "Invoice Line"
1212 _order = "invoice_id,sequence,id"
1215 @api.depends('price_unit', 'discount', 'invoice_line_tax_id', 'quantity',
1216 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id')
1217 def _compute_price(self):
1218 price = self.price_unit * (1 - (self.discount or 0.0) / 100.0)
1219 taxes = self.invoice_line_tax_id.compute_all(price, self.quantity, product=self.product_id, partner=self.invoice_id.partner_id)
1220 self.price_subtotal = taxes['total']
1222 self.price_subtotal = self.invoice_id.currency_id.round(self.price_subtotal)
1225 def _default_price_unit(self):
1226 if not self._context.get('check_total'):
1228 total = self._context['check_total']
1229 for l in self._context.get('invoice_line', []):
1230 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1232 price = vals.get('price_unit', 0) * (1 - vals.get('discount', 0) / 100.0)
1233 total = total - (price * vals.get('quantity'))
1234 taxes = vals.get('invoice_line_tax_id')
1235 if taxes and len(taxes[0]) >= 3 and taxes[0][2]:
1236 taxes = self.env['account.tax'].browse(taxes[0][2])
1237 tax_res = taxes.compute_all(price, vals.get('quantity'),
1238 product=vals.get('product_id'), partner=self._context.get('partner_id'))
1239 for tax in tax_res['taxes']:
1240 total = total - tax['amount']
1244 def _default_account(self):
1245 # XXX this gets the default account for the user's company,
1246 # it should get the default account for the invoice's company
1247 # however, the invoice's company does not reach this point
1248 if self._context.get('type') in ('out_invoice', 'out_refund'):
1249 return self.env['ir.property'].get('property_account_income_categ', 'product.category')
1251 return self.env['ir.property'].get('property_account_expense_categ', 'product.category')
1253 name = fields.Text(string='Description', required=True)
1254 origin = fields.Char(string='Source Document',
1255 help="Reference of the document that produced this invoice.")
1256 sequence = fields.Integer(string='Sequence', default=10,
1257 help="Gives the sequence of this line when displaying the invoice.")
1258 invoice_id = fields.Many2one('account.invoice', string='Invoice Reference',
1259 ondelete='cascade', index=True)
1260 uos_id = fields.Many2one('product.uom', string='Unit of Measure',
1261 ondelete='set null', index=True)
1262 product_id = fields.Many2one('product.product', string='Product',
1263 ondelete='set null', index=True)
1264 account_id = fields.Many2one('account.account', string='Account',
1265 required=True, domain=[('type', 'not in', ['view', 'closed'])],
1266 default=_default_account,
1267 help="The income or expense account related to the selected product.")
1268 price_unit = fields.Float(string='Unit Price', required=True,
1269 digits= dp.get_precision('Product Price'),
1270 default=_default_price_unit)
1271 price_subtotal = fields.Float(string='Amount', digits= dp.get_precision('Account'),
1272 store=True, readonly=True, compute='_compute_price')
1273 quantity = fields.Float(string='Quantity', digits= dp.get_precision('Product Unit of Measure'),
1274 required=True, default=1)
1275 discount = fields.Float(string='Discount (%)', digits= dp.get_precision('Discount'),
1277 invoice_line_tax_id = fields.Many2many('account.tax',
1278 'account_invoice_line_tax', 'invoice_line_id', 'tax_id',
1279 string='Taxes', domain=[('parent_id', '=', False)])
1280 account_analytic_id = fields.Many2one('account.analytic.account',
1281 string='Analytic Account')
1282 company_id = fields.Many2one('res.company', string='Company',
1283 related='invoice_id.company_id', store=True, readonly=True)
1284 partner_id = fields.Many2one('res.partner', string='Partner',
1285 related='invoice_id.partner_id', store=True, readonly=True)
1288 def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
1289 res = super(account_invoice_line, self).fields_view_get(
1290 view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
1291 if self._context.get('type'):
1292 doc = etree.XML(res['arch'])
1293 for node in doc.xpath("//field[@name='product_id']"):
1294 if self._context['type'] in ('in_invoice', 'in_refund'):
1295 node.set('domain', "[('purchase_ok', '=', True)]")
1297 node.set('domain', "[('sale_ok', '=', True)]")
1298 res['arch'] = etree.tostring(doc)
1302 def product_id_change(self, product, uom_id, qty=0, name='', type='out_invoice',
1303 partner_id=False, fposition_id=False, price_unit=False, currency_id=False,
1305 context = self._context
1306 company_id = company_id if company_id is not None else context.get('company_id', False)
1307 self = self.with_context(company_id=company_id, force_company=company_id)
1310 raise except_orm(_('No Partner Defined!'), _("You must first select a partner!"))
1312 if type in ('in_invoice', 'in_refund'):
1313 return {'value': {}, 'domain': {'product_uom': []}}
1315 return {'value': {'price_unit': 0.0}, 'domain': {'product_uom': []}}
1319 part = self.env['res.partner'].browse(partner_id)
1320 fpos = self.env['account.fiscal.position'].browse(fposition_id)
1323 self = self.with_context(lang=part.lang)
1324 product = self.env['product.product'].browse(product)
1326 values['name'] = product.partner_ref
1327 if type in ('out_invoice', 'out_refund'):
1328 account = product.property_account_income or product.categ_id.property_account_income_categ
1330 account = product.property_account_expense or product.categ_id.property_account_expense_categ
1331 account = fpos.map_account(account)
1333 values['account_id'] = account.id
1335 if type in ('out_invoice', 'out_refund'):
1336 taxes = product.taxes_id or account.tax_ids
1337 if product.description_sale:
1338 values['name'] += '\n' + product.description_sale
1340 taxes = product.supplier_taxes_id or account.tax_ids
1341 if product.description_purchase:
1342 values['name'] += '\n' + product.description_purchase
1344 taxes = fpos.map_tax(taxes)
1345 values['invoice_line_tax_id'] = taxes.ids
1347 if type in ('in_invoice', 'in_refund'):
1348 values['price_unit'] = price_unit or product.standard_price
1350 values['price_unit'] = product.list_price
1352 values['uos_id'] = uom_id or product.uom_id.id
1353 domain = {'uos_id': [('category_id', '=', product.uom_id.category_id.id)]}
1355 company = self.env['res.company'].browse(company_id)
1356 currency = self.env['res.currency'].browse(currency_id)
1358 if company and currency:
1359 if company.currency_id != currency:
1360 if type in ('in_invoice', 'in_refund'):
1361 values['price_unit'] = product.standard_price
1362 values['price_unit'] = values['price_unit'] * currency.rate
1364 if values['uos_id'] and values['uos_id'] != product.uom_id.id:
1365 values['price_unit'] = self.env['product.uom']._compute_price(
1366 product.uom_id.id, values['price_unit'], values['uos_id'])
1368 return {'value': values, 'domain': domain}
1371 def uos_id_change(self, product, uom, qty=0, name='', type='out_invoice', partner_id=False,
1372 fposition_id=False, price_unit=False, currency_id=False, company_id=None):
1373 context = self._context
1374 company_id = company_id if company_id != None else context.get('company_id', False)
1375 self = self.with_context(company_id=company_id)
1377 result = self.product_id_change(
1378 product, uom, qty, name, type, partner_id, fposition_id, price_unit,
1379 currency_id, company_id=company_id,
1383 result['value']['price_unit'] = 0.0
1385 prod = self.env['product.product'].browse(product)
1386 prod_uom = self.env['product.uom'].browse(uom)
1387 if prod.uom_id.category_id != prod_uom.category_id:
1389 'title': _('Warning!'),
1390 'message': _('The selected unit of measure is not compatible with the unit of measure of the product.'),
1392 result['value']['uos_id'] = prod.uom_id.id
1394 result['warning'] = warning
1398 def move_line_get(self, invoice_id):
1399 inv = self.env['account.invoice'].browse(invoice_id)
1400 currency = inv.currency_id.with_context(date=inv.date_invoice)
1401 company_currency = inv.company_id.currency_id
1404 for line in inv.invoice_line:
1405 mres = self.move_line_get_item(line)
1406 mres['invl_id'] = line.id
1408 tax_code_found = False
1409 taxes = line.invoice_line_tax_id.compute_all(
1410 (line.price_unit * (1.0 - (line.discount or 0.0) / 100.0)),
1411 line.quantity, line.product_id, inv.partner_id)['taxes']
1413 if inv.type in ('out_invoice', 'in_invoice'):
1414 tax_code_id = tax['base_code_id']
1415 tax_amount = line.price_subtotal * tax['base_sign']
1417 tax_code_id = tax['ref_base_code_id']
1418 tax_amount = line.price_subtotal * tax['ref_base_sign']
1423 res.append(dict(mres))
1424 res[-1]['price'] = 0.0
1425 res[-1]['account_analytic_id'] = False
1426 elif not tax_code_id:
1428 tax_code_found = True
1430 res[-1]['tax_code_id'] = tax_code_id
1431 res[-1]['tax_amount'] = currency.compute(tax_amount, company_currency)
1436 def move_line_get_item(self, line):
1439 'name': line.name.split('\n')[0][:64],
1440 'price_unit': line.price_unit,
1441 'quantity': line.quantity,
1442 'price': line.price_subtotal,
1443 'account_id': line.account_id.id,
1444 'product_id': line.product_id.id,
1445 'uos_id': line.uos_id.id,
1446 'account_analytic_id': line.account_analytic_id.id,
1447 'taxes': line.invoice_line_tax_id,
1451 # Set the tax field according to the account and the fiscal position
1454 def onchange_account_id(self, product_id, partner_id, inv_type, fposition_id, account_id):
1458 account = self.env['account.account'].browse(account_id)
1460 fpos = self.env['account.fiscal.position'].browse(fposition_id)
1461 unique_tax_ids = fpos.map_tax(account.tax_ids).ids
1463 product_change_result = self.product_id_change(product_id, False, type=inv_type,
1464 partner_id=partner_id, fposition_id=fposition_id, company_id=account.company_id.id)
1465 if 'invoice_line_tax_id' in product_change_result.get('value', {}):
1466 unique_tax_ids = product_change_result['value']['invoice_line_tax_id']
1467 return {'value': {'invoice_line_tax_id': unique_tax_ids}}
1470 class account_invoice_tax(models.Model):
1471 _name = "account.invoice.tax"
1472 _description = "Invoice Tax"
1476 @api.depends('base', 'base_amount', 'amount', 'tax_amount')
1477 def _compute_factors(self):
1478 self.factor_base = self.base_amount / self.base if self.base else 1.0
1479 self.factor_tax = self.tax_amount / self.amount if self.amount else 1.0
1481 invoice_id = fields.Many2one('account.invoice', string='Invoice Line',
1482 ondelete='cascade', index=True)
1483 name = fields.Char(string='Tax Description',
1485 account_id = fields.Many2one('account.account', string='Tax Account',
1486 required=True, domain=[('type', 'not in', ['view', 'income', 'closed'])])
1487 account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic account')
1488 base = fields.Float(string='Base', digits=dp.get_precision('Account'))
1489 amount = fields.Float(string='Amount', digits=dp.get_precision('Account'))
1490 manual = fields.Boolean(string='Manual', default=True)
1491 sequence = fields.Integer(string='Sequence',
1492 help="Gives the sequence order when displaying a list of invoice tax.")
1493 base_code_id = fields.Many2one('account.tax.code', string='Base Code',
1494 help="The account basis of the tax declaration.")
1495 base_amount = fields.Float(string='Base Code Amount', digits=dp.get_precision('Account'),
1497 tax_code_id = fields.Many2one('account.tax.code', string='Tax Code',
1498 help="The tax basis of the tax declaration.")
1499 tax_amount = fields.Float(string='Tax Code Amount', digits=dp.get_precision('Account'),
1502 company_id = fields.Many2one('res.company', string='Company',
1503 related='account_id.company_id', store=True, readonly=True)
1504 factor_base = fields.Float(string='Multipication factor for Base code',
1505 compute='_compute_factors')
1506 factor_tax = fields.Float(string='Multipication factor Tax code',
1507 compute='_compute_factors')
1510 def base_change(self, base, currency_id=False, company_id=False, date_invoice=False):
1511 factor = self.factor_base if self else 1
1512 company = self.env['res.company'].browse(company_id)
1513 if currency_id and company.currency_id:
1514 currency = self.env['res.currency'].browse(currency_id)
1515 currency = currency.with_context(date=date_invoice or fields.Date.context_today(self))
1516 base = currency.compute(base * factor, company.currency_id, round=False)
1517 return {'value': {'base_amount': base}}
1520 def amount_change(self, amount, currency_id=False, company_id=False, date_invoice=False):
1521 factor = self.factor_tax if self else 1
1522 company = self.env['res.company'].browse(company_id)
1523 if currency_id and company.currency_id:
1524 currency = self.env['res.currency'].browse(currency_id)
1525 currency = currency.with_context(date=date_invoice or fields.Date.context_today(self))
1526 amount = currency.compute(amount * factor, company.currency_id, round=False)
1527 return {'value': {'tax_amount': amount}}
1530 def compute(self, invoice):
1532 currency = invoice.currency_id.with_context(date=invoice.date_invoice or fields.Date.context_today(invoice))
1533 company_currency = invoice.company_id.currency_id
1534 for line in invoice.invoice_line:
1535 taxes = line.invoice_line_tax_id.compute_all(
1536 (line.price_unit * (1 - (line.discount or 0.0) / 100.0)),
1537 line.quantity, line.product_id, invoice.partner_id)['taxes']
1540 'invoice_id': invoice.id,
1541 'name': tax['name'],
1542 'amount': tax['amount'],
1544 'sequence': tax['sequence'],
1545 'base': currency.round(tax['price_unit'] * line['quantity']),
1547 if invoice.type in ('out_invoice','in_invoice'):
1548 val['base_code_id'] = tax['base_code_id']
1549 val['tax_code_id'] = tax['tax_code_id']
1550 val['base_amount'] = currency.compute(val['base'] * tax['base_sign'], company_currency, round=False)
1551 val['tax_amount'] = currency.compute(val['amount'] * tax['tax_sign'], company_currency, round=False)
1552 val['account_id'] = tax['account_collected_id'] or line.account_id.id
1553 val['account_analytic_id'] = tax['account_analytic_collected_id']
1555 val['base_code_id'] = tax['ref_base_code_id']
1556 val['tax_code_id'] = tax['ref_tax_code_id']
1557 val['base_amount'] = currency.compute(val['base'] * tax['ref_base_sign'], company_currency, round=False)
1558 val['tax_amount'] = currency.compute(val['amount'] * tax['ref_tax_sign'], company_currency, round=False)
1559 val['account_id'] = tax['account_paid_id'] or line.account_id.id
1560 val['account_analytic_id'] = tax['account_analytic_paid_id']
1562 # If the taxes generate moves on the same financial account as the invoice line
1563 # and no default analytic account is defined at the tax level, propagate the
1564 # analytic account from the invoice line to the tax line. This is necessary
1565 # in situations were (part of) the taxes cannot be reclaimed,
1566 # to ensure the tax move is allocated to the proper analytic account.
1567 if not val.get('account_analytic_id') and line.account_analytic_id and val['account_id'] == line.account_id.id:
1568 val['account_analytic_id'] = line.account_analytic_id.id
1570 key = (val['tax_code_id'], val['base_code_id'], val['account_id'])
1571 if not key in tax_grouped:
1572 tax_grouped[key] = val
1574 tax_grouped[key]['base'] += val['base']
1575 tax_grouped[key]['amount'] += val['amount']
1576 tax_grouped[key]['base_amount'] += val['base_amount']
1577 tax_grouped[key]['tax_amount'] += val['tax_amount']
1579 for t in tax_grouped.values():
1580 t['base'] = currency.round(t['base'])
1581 t['amount'] = currency.round(t['amount'])
1582 t['base_amount'] = currency.round(t['base_amount'])
1583 t['tax_amount'] = currency.round(t['tax_amount'])
1588 def compute(self, cr, uid, invoice_id, context=None):
1589 recs = self.browse(cr, uid, [], context)
1590 invoice = recs.env['account.invoice'].browse(invoice_id)
1591 return recs.compute(invoice)
1594 def move_line_get(self, invoice_id):
1597 'SELECT * FROM account_invoice_tax WHERE invoice_id = %s',
1600 for row in self._cr.dictfetchall():
1601 if not (row['amount'] or row['tax_code_id'] or row['tax_amount']):
1605 'name': row['name'],
1606 'price_unit': row['amount'],
1608 'price': row['amount'] or 0.0,
1609 'account_id': row['account_id'],
1610 'tax_code_id': row['tax_code_id'],
1611 'tax_amount': row['tax_amount'],
1612 'account_analytic_id': row['account_analytic_id'],
1617 class res_partner(models.Model):
1618 # Inherits partner and adds invoice information in the partner form
1619 _inherit = 'res.partner'
1621 invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices',
1624 def _find_accounting_partner(self, partner):
1626 Find the partner for which the accounting entries will be created
1628 return partner.commercial_partner_id
1630 class mail_compose_message(models.Model):
1631 _inherit = 'mail.compose.message'
1634 def send_mail(self):
1635 context = self._context
1636 if context.get('default_model') == 'account.invoice' and \
1637 context.get('default_res_id') and context.get('mark_invoice_as_sent'):
1638 invoice = self.env['account.invoice'].browse(context['default_res_id'])
1639 invoice = invoice.with_context(mail_post_autofollow=True)
1640 invoice.write({'sent': True})
1641 invoice.message_post(body=_("Invoice sent"))
1642 return super(mail_compose_message, self).send_mail()
1644 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: