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