1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
23 from lxml import etree
25 from openerp import models, fields, api, _
26 from openerp.exceptions import except_orm, Warning, RedirectWarning
27 import openerp.addons.decimal_precision as dp
29 # mapping invoice type to journal type
31 'out_invoice': 'sale',
32 'in_invoice': 'purchase',
33 'out_refund': 'sale_refund',
34 'in_refund': 'purchase_refund',
37 # mapping invoice type to refund type
39 'out_invoice': 'out_refund', # Customer Invoice
40 'in_invoice': 'in_refund', # Supplier Invoice
41 'out_refund': 'out_invoice', # Customer Refund
42 'in_refund': 'in_invoice', # Supplier Refund
45 MAGIC_COLUMNS = ('id', 'create_uid', 'create_date', 'write_uid', 'write_date')
48 class account_invoice(models.Model):
49 _name = "account.invoice"
50 _inherit = ['mail.thread']
51 _description = "Invoice"
52 _order = "number desc, id desc"
57 'account.mt_invoice_paid': lambda self, cr, uid, obj, ctx=None: obj.state == 'paid' and obj.type in ('out_invoice', 'out_refund'),
58 'account.mt_invoice_validated': lambda self, cr, uid, obj, ctx=None: obj.state == 'open' and obj.type in ('out_invoice', 'out_refund'),
63 @api.depends('invoice_line.price_subtotal', 'tax_line.amount')
64 def _compute_amount(self):
65 self.amount_untaxed = sum(line.price_subtotal for line in self.invoice_line)
66 self.amount_tax = sum(line.amount for line in self.tax_line)
67 self.amount_total = self.amount_untaxed + self.amount_tax
70 def _default_journal(self):
71 inv_type = self._context.get('type', 'out_invoice')
72 inv_types = inv_type if isinstance(inv_type, list) else [inv_type]
73 company_id = self._context.get('company_id', self.env.user.company_id.id)
75 ('type', 'in', filter(None, map(TYPE2JOURNAL.get, inv_types))),
76 ('company_id', '=', company_id),
78 return self.env['account.journal'].search(domain, limit=1)
81 def _default_currency(self):
82 journal = self._default_journal()
83 return journal.currency or journal.company_id.currency_id
86 @api.returns('account.analytic.journal', lambda r: r.id)
87 def _get_journal_analytic(self, inv_type):
88 """ Return the analytic journal corresponding to the given invoice type. """
89 journal_type = TYPE2JOURNAL.get(inv_type, 'sale')
90 journal = self.env['account.analytic.journal'].search([('type', '=', journal_type)], limit=1)
92 raise except_orm(_('No Analytic Journal!'),
93 _("You must define an analytic journal of type '%s'!") % (journal_type,))
97 @api.depends('account_id', 'move_id.line_id.account_id', 'move_id.line_id.reconcile_id')
98 def _compute_reconciled(self):
99 self.reconciled = self.test_paid()
102 def _get_reference_type(self):
103 return [('none', _('Free Reference'))]
107 'state', 'currency_id', 'invoice_line.price_subtotal',
108 'move_id.line_id.account_id.type',
109 'move_id.line_id.amount_residual',
110 'move_id.line_id.amount_residual_currency',
111 'move_id.line_id.currency_id',
112 'move_id.line_id.reconcile_partial_id.line_partial_ids.invoice.type',
114 def _compute_residual(self):
115 nb_inv_in_partial_rec = max_invoice_id = 0
117 for line in self.sudo().move_id.line_id:
118 if line.account_id.type in ('receivable', 'payable'):
119 if line.currency_id == self.currency_id:
120 self.residual += line.amount_residual_currency
122 # ahem, shouldn't we use line.currency_id here?
123 from_currency = line.company_id.currency_id.with_context(date=line.date)
124 self.residual += from_currency.compute(line.amount_residual, self.currency_id)
125 # we check if the invoice is partially reconciled and if there
126 # are other invoices involved in this partial reconciliation
127 for pline in line.reconcile_partial_id.line_partial_ids:
128 if pline.invoice and self.type == pline.invoice.type:
129 nb_inv_in_partial_rec += 1
130 # store the max invoice id as for this invoice we will
131 # make a balance instead of a simple division
132 max_invoice_id = max(max_invoice_id, pline.invoice.id)
133 if nb_inv_in_partial_rec:
134 # if there are several invoices in a partial reconciliation, we
135 # split the residual by the number of invoices to have a sum of
136 # residual amounts that matches the partner balance
137 new_value = self.currency_id.round(self.residual / nb_inv_in_partial_rec)
138 if self.id == max_invoice_id:
139 # if it's the last the invoice of the bunch of invoices
140 # partially reconciled together, we make a balance to avoid
142 self.residual = self.residual - ((nb_inv_in_partial_rec - 1) * new_value)
144 self.residual = new_value
145 # prevent the residual amount on the invoice to be less than 0
146 self.residual = max(self.residual, 0.0)
150 'move_id.line_id.account_id',
151 'move_id.line_id.reconcile_id.line_id',
152 'move_id.line_id.reconcile_partial_id.line_partial_ids',
154 def _compute_move_lines(self):
155 # Give Journal Items related to the payment reconciled to this invoice.
156 # Return partial and total payments related to the selected invoice.
157 self.move_lines = self.env['account.move.line']
160 data_lines = self.move_id.line_id.filtered(lambda l: l.account_id == self.account_id)
161 partial_lines = self.env['account.move.line']
162 for data_line in data_lines:
163 if data_line.reconcile_id:
164 lines = data_line.reconcile_id.line_id
165 elif data_line.reconcile_partial_id:
166 lines = data_line.reconcile_partial_id.line_partial_ids
168 lines = self.env['account_move_line']
169 partial_lines += data_line
170 self.move_lines = lines - partial_lines
174 'move_id.line_id.reconcile_id.line_id',
175 'move_id.line_id.reconcile_partial_id.line_partial_ids',
177 def _compute_payments(self):
178 partial_lines = lines = self.env['account.move.line']
179 for line in self.move_id.line_id:
180 if line.account_id != self.account_id:
182 if line.reconcile_id:
183 lines |= line.reconcile_id.line_id
184 elif line.reconcile_partial_id:
185 lines |= line.reconcile_partial_id.line_partial_ids
186 partial_lines += line
187 self.payment_ids = (lines - partial_lines).sorted()
189 name = fields.Char(string='Reference/Description', index=True,
190 readonly=True, states={'draft': [('readonly', False)]})
191 origin = fields.Char(string='Source Document',
192 help="Reference of the document that produced this invoice.",
193 readonly=True, states={'draft': [('readonly', False)]})
194 supplier_invoice_number = fields.Char(string='Supplier Invoice Number',
195 help="The reference of this invoice as provided by the supplier.",
196 readonly=True, states={'draft': [('readonly', False)]})
197 type = fields.Selection([
198 ('out_invoice','Customer Invoice'),
199 ('in_invoice','Supplier Invoice'),
200 ('out_refund','Customer Refund'),
201 ('in_refund','Supplier Refund'),
202 ], string='Type', readonly=True, index=True, change_default=True,
203 default=lambda self: self._context.get('type', 'out_invoice'),
204 track_visibility='always')
206 number = fields.Char(related='move_id.name', store=True, readonly=True, copy=False)
207 internal_number = fields.Char(string='Invoice Number', readonly=True,
208 default=False, copy=False,
209 help="Unique number of the invoice, computed automatically when the invoice is created.")
210 reference = fields.Char(string='Invoice Reference',
211 help="The partner reference of this invoice.")
212 reference_type = fields.Selection('_get_reference_type', string='Payment Reference',
213 required=True, readonly=True, states={'draft': [('readonly', False)]},
215 comment = fields.Text('Additional Information')
217 state = fields.Selection([
219 ('proforma','Pro-forma'),
220 ('proforma2','Pro-forma'),
223 ('cancel','Cancelled'),
224 ], string='Status', index=True, readonly=True, default='draft',
225 track_visibility='onchange', copy=False,
226 help=" * The 'Draft' status is used when a user is encoding a new and unconfirmed Invoice.\n"
227 " * The 'Pro-forma' when invoice is in Pro-forma status,invoice does not have an invoice number.\n"
228 " * The 'Open' status is used when user create invoice,a invoice number is generated.Its in open status till user does not pay invoice.\n"
229 " * The 'Paid' status is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled.\n"
230 " * The 'Cancelled' status is used when user cancel invoice.")
231 sent = fields.Boolean(readonly=True, default=False, copy=False,
232 help="It indicates that the invoice has been sent.")
233 date_invoice = fields.Date(string='Invoice Date',
234 readonly=True, states={'draft': [('readonly', False)]}, index=True,
235 help="Keep empty to use the current date", copy=False)
236 date_due = fields.Date(string='Due Date',
237 readonly=True, states={'draft': [('readonly', False)]}, index=True, copy=False,
238 help="If you use payment terms, the due date will be computed automatically at the generation "
239 "of accounting entries. The payment term may compute several due dates, for example 50% "
240 "now and 50% in one month, but if you want to force a due date, make sure that the payment "
241 "term is not set on the invoice. If you keep the payment term and the due date empty, it "
242 "means direct payment.")
243 partner_id = fields.Many2one('res.partner', string='Partner', change_default=True,
244 required=True, readonly=True, states={'draft': [('readonly', False)]},
245 track_visibility='always')
246 payment_term = fields.Many2one('account.payment.term', string='Payment Terms',
247 readonly=True, states={'draft': [('readonly', False)]},
248 help="If you use payment terms, the due date will be computed automatically at the generation "
249 "of accounting entries. If you keep the payment term and the due date empty, it means direct payment. "
250 "The payment term may compute several due dates, for example 50% now, 50% in one month.")
251 period_id = fields.Many2one('account.period', string='Force Period',
252 domain=[('state', '!=', 'done')], copy=False,
253 help="Keep empty to use the period of the validation(invoice) date.",
254 readonly=True, states={'draft': [('readonly', False)]})
256 account_id = fields.Many2one('account.account', string='Account',
257 required=True, readonly=True, states={'draft': [('readonly', False)]},
258 help="The partner account used for this invoice.")
259 invoice_line = fields.One2many('account.invoice.line', 'invoice_id', string='Invoice Lines',
260 readonly=True, states={'draft': [('readonly', False)]}, copy=True)
261 tax_line = fields.One2many('account.invoice.tax', 'invoice_id', string='Tax Lines',
262 readonly=True, states={'draft': [('readonly', False)]}, copy=True)
263 move_id = fields.Many2one('account.move', string='Journal Entry',
264 readonly=True, index=True, ondelete='restrict', copy=False,
265 help="Link to the automatically generated Journal Items.")
267 amount_untaxed = fields.Float(string='Subtotal', digits=dp.get_precision('Account'),
268 store=True, readonly=True, compute='_compute_amount', track_visibility='always')
269 amount_tax = fields.Float(string='Tax', digits=dp.get_precision('Account'),
270 store=True, readonly=True, compute='_compute_amount')
271 amount_total = fields.Float(string='Total', digits=dp.get_precision('Account'),
272 store=True, readonly=True, compute='_compute_amount')
274 currency_id = fields.Many2one('res.currency', string='Currency',
275 required=True, readonly=True, states={'draft': [('readonly', False)]},
276 default=_default_currency, track_visibility='always')
277 journal_id = fields.Many2one('account.journal', string='Journal',
278 required=True, readonly=True, states={'draft': [('readonly', False)]},
279 default=_default_journal,
280 domain="[('type', 'in', {'out_invoice': ['sale'], 'out_refund': ['sale_refund'], 'in_refund': ['purchase_refund'], 'in_invoice': ['purchase']}.get(type, [])), ('company_id', '=', company_id)]")
281 company_id = fields.Many2one('res.company', string='Company', change_default=True,
282 required=True, readonly=True, states={'draft': [('readonly', False)]},
283 default=lambda self: self.env['res.company']._company_default_get('account.invoice'))
284 check_total = fields.Float(string='Verification Total', digits=dp.get_precision('Account'),
285 readonly=True, states={'draft': [('readonly', False)]}, default=0.0)
287 reconciled = fields.Boolean(string='Paid/Reconciled',
288 store=True, readonly=True, compute='_compute_reconciled',
289 help="It indicates that the invoice has been paid and the journal entry of the invoice has been reconciled with one or several journal entries of payment.")
290 partner_bank_id = fields.Many2one('res.partner.bank', string='Bank Account',
291 help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Supplier Refund, otherwise a Partner bank account number.',
292 readonly=True, states={'draft': [('readonly', False)]})
294 move_lines = fields.Many2many('account.move.line', string='Entry Lines',
295 compute='_compute_move_lines')
296 residual = fields.Float(string='Balance', digits=dp.get_precision('Account'),
297 compute='_compute_residual', store=True,
298 help="Remaining amount due.")
299 payment_ids = fields.Many2many('account.move.line', string='Payments',
300 compute='_compute_payments')
301 move_name = fields.Char(string='Journal Entry', readonly=True,
302 states={'draft': [('readonly', False)]}, copy=False)
303 user_id = fields.Many2one('res.users', string='Salesperson', track_visibility='onchange',
304 readonly=True, states={'draft': [('readonly', False)]},
305 default=lambda self: self.env.user)
306 fiscal_position = fields.Many2one('account.fiscal.position', string='Fiscal Position',
307 readonly=True, states={'draft': [('readonly', False)]})
308 commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity',
309 related='partner_id.commercial_partner_id', store=True, readonly=True,
310 help="The commercial entity that will be used on Journal Entries for this invoice")
313 ('number_uniq', 'unique(number, company_id, journal_id, type)',
314 'Invoice Number must be unique per Company!'),
318 def fields_view_get(self, view_id=None, view_type=False, toolbar=False, submenu=False):
319 context = self._context
320 if context.get('active_model') == 'res.partner' and context.get('active_ids'):
321 partner = self.env['res.partner'].browse(context['active_ids'])[0]
323 view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.tree')]).id
325 elif view_type == 'form':
326 if partner.supplier and not partner.customer:
327 view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.supplier.form')]).id
328 elif partner.customer and not partner.supplier:
329 view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.form')]).id
331 res = super(account_invoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
333 # adapt selection of field journal_id
334 for field in res['fields']:
335 if field == 'journal_id' and type:
336 journal_select = self.env['account.journal']._name_search('', [('type', '=', type)], name_get_uid=1)
337 res['fields'][field]['selection'] = journal_select
339 doc = etree.XML(res['arch'])
341 if context.get('type'):
342 for node in doc.xpath("//field[@name='partner_bank_id']"):
343 if context['type'] == 'in_refund':
344 node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]")
345 elif context['type'] == 'out_refund':
346 node.set('domain', "[('partner_id', '=', partner_id)]")
348 if view_type == 'search':
349 if context.get('type') in ('out_invoice', 'out_refund'):
350 for node in doc.xpath("//group[@name='extended filter']"):
353 if view_type == 'tree':
354 partner_string = _('Customer')
355 if context.get('type') in ('in_invoice', 'in_refund'):
356 partner_string = _('Supplier')
357 for node in doc.xpath("//field[@name='reference']"):
358 node.set('invisible', '0')
359 for node in doc.xpath("//field[@name='partner_id']"):
360 node.set('string', partner_string)
362 res['arch'] = etree.tostring(doc)
366 def invoice_print(self):
367 """ Print the invoice and mark it as sent, so that we can see more
368 easily the next step of the workflow
370 assert len(self) == 1, 'This option should only be used for a single id at a time.'
372 return self.env['report'].get_action(self, 'account.report_invoice')
375 def action_invoice_sent(self):
376 """ Open a window to compose an email, with the edi invoice template
377 message loaded by default
379 assert len(self) == 1, 'This option should only be used for a single id at a time.'
380 template = self.env.ref('account.email_template_edi_invoice', False)
381 compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
383 default_model='account.invoice',
384 default_res_id=self.id,
385 default_use_template=bool(template),
386 default_template_id=template.id,
387 default_composition_mode='comment',
388 mark_invoice_as_sent=True,
391 'name': _('Compose Email'),
392 'type': 'ir.actions.act_window',
395 'res_model': 'mail.compose.message',
396 'views': [(compose_form.id, 'form')],
397 'view_id': compose_form.id,
403 def confirm_paid(self):
404 return self.write({'state': 'paid'})
409 if invoice.state not in ('draft', 'cancel'):
410 raise Warning(_('You cannot delete an invoice which is not draft or cancelled. You should refund it instead.'))
411 elif invoice.internal_number:
412 raise Warning(_('You cannot delete an invoice after it has been validated (and received a number). You can set it back to "Draft" state and modify its content, then re-confirm it.'))
413 return super(account_invoice, self).unlink()
416 def onchange_partner_id(self, type, partner_id, date_invoice=False,
417 payment_term=False, partner_bank_id=False, company_id=False):
419 payment_term_id = False
420 fiscal_position = False
424 p = self.env['res.partner'].browse(partner_id)
425 rec_account = p.property_account_receivable
426 pay_account = p.property_account_payable
428 if p.property_account_receivable.company_id and \
429 p.property_account_receivable.company_id.id != company_id and \
430 p.property_account_payable.company_id and \
431 p.property_account_payable.company_id.id != company_id:
432 prop = self.env['ir.property']
433 rec_dom = [('name', '=', 'property_account_receivable'), ('company_id', '=', company_id)]
434 pay_dom = [('name', '=', 'property_account_payable'), ('company_id', '=', company_id)]
435 res_dom = [('res_id', '=', 'res.partner,%s' % partner_id)]
436 rec_prop = prop.search(rec_dom + res_dom) or prop.search(rec_dom)
437 pay_prop = prop.search(pay_dom + res_dom) or prop.search(pay_dom)
438 rec_account = rec_prop.get_by_record(rec_prop)
439 pay_account = pay_prop.get_by_record(pay_prop)
440 if not rec_account and not pay_account:
441 action = self.env.ref('account.action_account_config')
442 msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
443 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
445 if type in ('out_invoice', 'out_refund'):
446 account_id = rec_account.id
447 payment_term_id = p.property_payment_term.id
449 account_id = pay_account.id
450 payment_term_id = p.property_supplier_payment_term.id
451 fiscal_position = p.property_account_position.id
452 bank_id = p.bank_ids.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.context_today(self)
492 if not payment_term_id:
493 # To make sure the invoice due date should contain due date which is
494 # entered by user when there is no payment term defined
495 return {'value': {'date_due': self.date_due or date_invoice}}
496 pterm = self.env['account.payment.term'].browse(payment_term_id)
497 pterm_list = pterm.compute(value=1, date_ref=date_invoice)[0]
499 return {'value': {'date_due': max(line[0] for line in pterm_list)}}
501 raise except_orm(_('Insufficient Data!'),
502 _('The payment term of supplier does not have a payment term line.'))
505 def onchange_invoice_line(self, lines):
509 def onchange_partner_bank(self, partner_bank_id=False):
513 def onchange_company_id(self, company_id, part_id, type, invoice_line, currency_id):
514 # TODO: add the missing context parameter when forward-porting in trunk
515 # so we can remove this hack!
516 self = self.with_context(self.env['res.users'].context_get())
521 if company_id and part_id and type:
522 p = self.env['res.partner'].browse(part_id)
523 if p.property_account_payable and p.property_account_receivable and \
524 p.property_account_payable.company_id.id != company_id and \
525 p.property_account_receivable.company_id.id != company_id:
526 prop = self.env['ir.property']
527 rec_dom = [('name', '=', 'property_account_receivable'), ('company_id', '=', company_id)]
528 pay_dom = [('name', '=', 'property_account_payable'), ('company_id', '=', company_id)]
529 res_dom = [('res_id', '=', 'res.partner,%s' % part_id)]
530 rec_prop = prop.search(rec_dom + res_dom) or prop.search(rec_dom)
531 pay_prop = prop.search(pay_dom + res_dom) or prop.search(pay_dom)
532 rec_account = rec_prop.get_by_record(rec_prop)
533 pay_account = pay_prop.get_by_record(pay_prop)
534 if not rec_account and not pay_account:
535 action = self.env.ref('account.action_account_config')
536 msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
537 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
539 if type in ('out_invoice', 'out_refund'):
540 acc_id = rec_account.id
542 acc_id = pay_account.id
543 values= {'account_id': acc_id}
547 for line in self.invoice_line:
548 if not line.account_id:
550 if line.account_id.company_id.id == company_id:
552 accounts = self.env['account.account'].search([('name', '=', line.account_id.name), ('company_id', '=', company_id)])
554 action = self.env.ref('account.action_account_config')
555 msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
556 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
557 line.write({'account_id': accounts[-1].id})
559 for line_cmd in invoice_line or []:
560 if len(line_cmd) >= 3 and isinstance(line_cmd[2], dict):
561 line = self.env['account.account'].browse(line_cmd[2]['account_id'])
562 if line.company_id.id != company_id:
564 _('Configuration Error!'),
565 _("Invoice line account's company and invoice's company does not match.")
568 if company_id and type:
569 journal_type = TYPE2JOURNAL[type]
570 journals = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company_id)])
572 values['journal_id'] = journals[0].id
573 journal_defaults = self.env['ir.values'].get_defaults_dict('account.invoice', 'type=%s' % type)
574 if 'journal_id' in journal_defaults:
575 values['journal_id'] = journal_defaults['journal_id']
576 if not values.get('journal_id'):
577 field_desc = journals.fields_get(['type'])
578 type_label = next(t for t, label in field_desc['type']['selection'] if t == journal_type)
579 action = self.env.ref('account.action_account_journal_form')
580 msg = _('Cannot find any account journal of type "%s" for this company, You should create one.\n Please go to Journal Configuration') % type_label
581 raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
582 domain = {'journal_id': [('id', 'in', journals.ids)]}
584 return {'value': values, 'domain': domain}
587 def action_cancel_draft(self):
588 # go from canceled state to draft state
589 self.write({'state': 'draft'})
590 self.delete_workflow()
591 self.create_workflow()
595 @api.returns('ir.ui.view')
596 def get_formview_id(self):
597 """ Update form view id of action to open the invoice """
598 if self.type == 'in_invoice':
599 return self.env.ref('account.invoice_supplier_form')
601 return self.env.ref('account.invoice_form')
604 def move_line_id_payment_get(self):
605 # return the move line ids with the same account as the invoice self
608 query = """ SELECT l.id
609 FROM account_move_line l, account_invoice i
610 WHERE i.id = %s AND l.move_id = i.move_id AND l.account_id = i.account_id
612 self._cr.execute(query, (self.id,))
613 return [row[0] for row in self._cr.fetchall()]
617 # check whether all corresponding account move lines are reconciled
618 line_ids = self.move_line_id_payment_get()
621 query = "SELECT reconcile_id FROM account_move_line WHERE id IN %s"
622 self._cr.execute(query, (tuple(line_ids),))
623 return all(row[0] for row in self._cr.fetchall())
626 def button_reset_taxes(self):
627 account_invoice_tax = self.env['account.invoice.tax']
628 ctx = dict(self._context)
630 self._cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (invoice.id,))
631 self.invalidate_cache()
632 partner = invoice.partner_id
634 ctx['lang'] = partner.lang
635 for taxe in account_invoice_tax.compute(invoice).values():
636 account_invoice_tax.create(taxe)
637 # dummy write on self to trigger recomputations
638 return self.with_context(ctx).write({'invoice_line': []})
641 def button_compute(self, set_total=False):
642 self.button_reset_taxes()
645 invoice.check_total = invoice.amount_total
649 def _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, tax.account_analytic_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.context_today(self))
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)
797 if not inv.date_invoice:
798 inv.with_context(ctx).write({'date_invoice': fields.Date.context_today(self)})
799 date_invoice = inv.date_invoice
801 company_currency = inv.company_id.currency_id
802 # create the analytical lines, one move line per invoice line
803 iml = inv._get_analytic_lines()
804 # check if taxes are all computed
805 compute_taxes = account_invoice_tax.compute(inv)
806 inv.check_tax_lines(compute_taxes)
808 # I disabled the check_total feature
809 if self.env['res.users'].has_group('account.group_supplier_inv_check_total'):
810 if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding / 2.0):
811 raise except_orm(_('Bad Total!'), _('Please verify the price of the invoice!\nThe encoded total does not match the computed total.'))
814 total_fixed = total_percent = 0
815 for line in inv.payment_term.line_ids:
816 if line.value == 'fixed':
817 total_fixed += line.value_amount
818 if line.value == 'procent':
819 total_percent += line.value_amount
820 total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
821 if (total_fixed + total_percent) > 100:
822 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'."))
824 # one move line per tax line
825 iml += account_invoice_tax.move_line_get(inv.id)
827 if inv.type in ('in_invoice', 'in_refund'):
830 ref = self._convert_ref(inv.number)
832 diff_currency = inv.currency_id != company_currency
833 # create one move line for the total and possibly adjust the other lines amount
834 total, total_currency, iml = inv.with_context(ctx).compute_invoice_totals(company_currency, ref, iml)
836 name = inv.name or inv.supplier_invoice_number or '/'
839 totlines = inv.with_context(ctx).payment_term.compute(total, date_invoice)[0]
841 res_amount_currency = total_currency
842 ctx['date'] = date_invoice
843 for i, t in enumerate(totlines):
844 if inv.currency_id != company_currency:
845 amount_currency = company_currency.with_context(ctx).compute(t[1], inv.currency_id)
847 amount_currency = False
849 # last line: add the diff
850 res_amount_currency -= amount_currency or 0
851 if i + 1 == len(totlines):
852 amount_currency += res_amount_currency
858 'account_id': inv.account_id.id,
859 'date_maturity': t[0],
860 'amount_currency': diff_currency and amount_currency,
861 'currency_id': diff_currency and inv.currency_id.id,
869 'account_id': inv.account_id.id,
870 'date_maturity': inv.date_due,
871 'amount_currency': diff_currency and total_currency,
872 'currency_id': diff_currency and inv.currency_id.id,
878 part = self.env['res.partner']._find_accounting_partner(inv.partner_id)
880 line = [(0, 0, self.line_get_convert(l, part.id, date)) for l in iml]
881 line = inv.group_lines(iml, line)
883 journal = inv.journal_id.with_context(ctx)
884 if journal.centralisation:
885 raise except_orm(_('User Error!'),
886 _('You cannot create an invoice on a centralized journal. Uncheck the centralized counterpart box in the related journal from the configuration menu.'))
888 line = inv.finalize_invoice_move_lines(line)
891 'ref': inv.reference or inv.name,
893 'journal_id': journal.id,
894 'date': inv.date_invoice,
895 'narration': inv.comment,
896 'company_id': inv.company_id.id,
898 ctx['company_id'] = inv.company_id.id
899 period = inv.period_id
901 period = period.with_context(ctx).find(date_invoice)[:1]
903 move_vals['period_id'] = period.id
905 i[2]['period_id'] = period.id
908 move = account_move.with_context(ctx).create(move_vals)
909 # make the invoice point to that move
912 'period_id': period.id,
913 'move_name': move.name,
915 inv.with_context(ctx).write(vals)
916 # Pass invoice in context in method post: used if you want to get the same
917 # account move reference when creating the same invoice after a cancelled one:
923 def invoice_validate(self):
924 return self.write({'state': 'open'})
927 def line_get_convert(self, line, part, date):
929 'date_maturity': line.get('date_maturity', False),
931 'name': line['name'][:64],
933 'debit': line['price']>0 and line['price'],
934 'credit': line['price']<0 and -line['price'],
935 'account_id': line['account_id'],
936 'analytic_lines': line.get('analytic_lines', []),
937 'amount_currency': line['price']>0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)),
938 'currency_id': line.get('currency_id', False),
939 'tax_code_id': line.get('tax_code_id', False),
940 'tax_amount': line.get('tax_amount', False),
941 'ref': line.get('ref', False),
942 'quantity': line.get('quantity',1.00),
943 'product_id': line.get('product_id', False),
944 'product_uom_id': line.get('uos_id', False),
945 'analytic_account_id': line.get('account_analytic_id', False),
949 def action_number(self):
950 #TODO: not correct fix but required a fresh values before reading it.
954 self.write({'internal_number': inv.number})
956 if inv.type in ('in_invoice', 'in_refund'):
957 if not inv.reference:
958 ref = self._convert_ref(inv.number)
962 ref = self._convert_ref(inv.number)
964 self._cr.execute(""" UPDATE account_move SET ref=%s
965 WHERE id=%s AND (ref IS NULL OR ref = '')""",
966 (ref, inv.move_id.id))
967 self._cr.execute(""" UPDATE account_move_line SET ref=%s
968 WHERE move_id=%s AND (ref IS NULL OR ref = '')""",
969 (ref, inv.move_id.id))
970 self._cr.execute(""" UPDATE account_analytic_line SET ref=%s
971 FROM account_move_line
972 WHERE account_move_line.move_id = %s AND
973 account_analytic_line.move_id = account_move_line.id""",
974 (ref, inv.move_id.id))
975 self.invalidate_cache()
980 def action_cancel(self):
981 moves = self.env['account.move']
986 for move_line in inv.payment_ids:
987 if move_line.reconcile_partial_id.line_partial_ids:
988 raise except_orm(_('Error!'), _('You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first.'))
990 # First, set the invoices as cancelled and detach the move ids
991 self.write({'state': 'cancel', 'move_id': False})
993 # second, invalidate the move(s)
994 moves.button_cancel()
995 # delete the move this invoice was pointing to
996 # Note that the corresponding move_lines and move_reconciles
997 # will be automatically deleted too
999 self._log_event(-1.0, 'Cancel Invoice')
1005 def _log_event(self, factor=1.0, name='Open Invoice'):
1006 #TODO: implement messages system
1012 'out_invoice': _('Invoice'),
1013 'in_invoice': _('Supplier Invoice'),
1014 'out_refund': _('Refund'),
1015 'in_refund': _('Supplier Refund'),
1019 result.append((inv.id, "%s %s" % (inv.number or TYPES[inv.type], inv.name or '')))
1023 def name_search(self, name, args=None, operator='ilike', limit=100):
1025 recs = self.browse()
1027 recs = self.search([('number', '=', name)] + args, limit=limit)
1029 recs = self.search([('name', operator, name)] + args, limit=limit)
1030 return recs.name_get()
1033 def _refund_cleanup_lines(self, lines):
1034 """ Convert records to dict of values suitable for one2many line creation
1036 :param recordset lines: records to convert
1037 :return: list of command tuple for one2many line creation [(0, 0, dict of valueis), ...]
1042 for name, field in line._fields.iteritems():
1043 if name in MAGIC_COLUMNS:
1045 elif field.type == 'many2one':
1046 values[name] = line[name].id
1047 elif field.type not in ['many2many', 'one2many']:
1048 values[name] = line[name]
1049 elif name == 'invoice_line_tax_id':
1050 values[name] = [(6, 0, line[name].ids)]
1051 result.append((0, 0, values))
1055 def _prepare_refund(self, invoice, date=None, period_id=None, description=None, journal_id=None):
1056 """ Prepare the dict of values to create the new refund from the invoice.
1057 This method may be overridden to implement custom
1058 refund generation (making sure to call super() to establish
1059 a clean extension chain).
1061 :param record invoice: invoice to refund
1062 :param string date: refund creation date from the wizard
1063 :param integer period_id: force account.period from the wizard
1064 :param string description: description of the refund from the wizard
1065 :param integer journal_id: account.journal from the wizard
1066 :return: dict of value to create() the refund
1069 for field in ['name', 'reference', 'comment', 'date_due', 'partner_id', 'company_id',
1070 'account_id', 'currency_id', 'payment_term', 'user_id', 'fiscal_position']:
1071 if invoice._fields[field].type == 'many2one':
1072 values[field] = invoice[field].id
1074 values[field] = invoice[field] or False
1076 values['invoice_line'] = self._refund_cleanup_lines(invoice.invoice_line)
1078 tax_lines = filter(lambda l: l.manual, invoice.tax_line)
1079 values['tax_line'] = self._refund_cleanup_lines(tax_lines)
1082 journal = self.env['account.journal'].browse(journal_id)
1083 elif invoice['type'] == 'in_invoice':
1084 journal = self.env['account.journal'].search([('type', '=', 'purchase_refund')], limit=1)
1086 journal = self.env['account.journal'].search([('type', '=', 'sale_refund')], limit=1)
1087 values['journal_id'] = journal.id
1089 values['type'] = TYPE2REFUND[invoice['type']]
1090 values['date_invoice'] = date or fields.Date.context_today(invoice)
1091 values['state'] = 'draft'
1092 values['number'] = False
1095 values['period_id'] = period_id
1097 values['name'] = description
1101 @api.returns('self')
1102 def refund(self, date=None, period_id=None, description=None, journal_id=None):
1103 new_invoices = self.browse()
1104 for invoice in self:
1105 # create the new invoice
1106 values = self._prepare_refund(invoice, date=date, period_id=period_id,
1107 description=description, journal_id=journal_id)
1108 new_invoices += self.create(values)
1112 def pay_and_reconcile(self, pay_amount, pay_account_id, period_id, pay_journal_id,
1113 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=''):
1114 # TODO check if we can use different period for payment and the writeoff line
1115 assert len(self)==1, "Can only pay one invoice at a time."
1116 # Take the seq as name for move
1117 SIGN = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1118 direction = SIGN[self.type]
1119 # take the chosen date
1120 date = self._context.get('date_p') or fields.Date.context_today(self)
1122 # Take the amount in currency and the currency of the payment
1123 if self._context.get('amount_currency') and self._context.get('currency_id'):
1124 amount_currency = self._context['amount_currency']
1125 currency_id = self._context['currency_id']
1127 amount_currency = False
1130 pay_journal = self.env['account.journal'].browse(pay_journal_id)
1131 if self.type in ('in_invoice', 'in_refund'):
1132 ref = self.reference
1134 ref = self._convert_ref(self.number)
1135 partner = self.partner_id._find_accounting_partner(self.partner_id)
1136 name = name or self.invoice_line.name or self.number
1137 # Pay attention to the sign for both debit/credit AND amount_currency
1140 'debit': direction * pay_amount > 0 and direction * pay_amount,
1141 'credit': direction * pay_amount < 0 and -direction * pay_amount,
1142 'account_id': self.account_id.id,
1143 'partner_id': partner.id,
1146 'currency_id': currency_id,
1147 'amount_currency': direction * (amount_currency or 0.0),
1148 'company_id': self.company_id.id,
1152 'debit': direction * pay_amount < 0 and -direction * pay_amount,
1153 'credit': direction * pay_amount > 0 and direction * pay_amount,
1154 'account_id': pay_account_id,
1155 'partner_id': partner.id,
1158 'currency_id': currency_id,
1159 'amount_currency': -direction * (amount_currency or 0.0),
1160 'company_id': self.company_id.id,
1162 move = self.env['account.move'].create({
1164 'line_id': [(0, 0, l1), (0, 0, l2)],
1165 'journal_id': pay_journal_id,
1166 'period_id': period_id,
1170 move_ids = (move | self.move_id).ids
1171 self._cr.execute("SELECT id FROM account_move_line WHERE move_id IN %s",
1173 lines = self.env['account.move.line'].browse([r[0] for r in self._cr.fetchall()])
1174 lines2rec = lines.browse()
1176 for line in itertools.chain(lines, self.payment_ids):
1177 if line.account_id == self.account_id:
1179 total += (line.debit or 0.0) - (line.credit or 0.0)
1181 inv_id, name = self.name_get()[0]
1182 if not round(total, self.env['decimal.precision'].precision_get('Account')) or writeoff_acc_id:
1183 lines2rec.reconcile('manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id)
1185 code = self.currency_id.symbol
1186 # TODO: use currency's formatting function
1187 msg = _("Invoice partially paid: %s%s of %s%s (%s%s remaining).") % \
1188 (pay_amount, code, self.amount_total, code, total, code)
1189 self.message_post(body=msg)
1190 lines2rec.reconcile_partial('manual')
1192 # Update the stored value (fields.function), so we write to trigger recompute
1193 return self.write({})
1196 def pay_and_reconcile(self, cr, uid, ids, pay_amount, pay_account_id, period_id, pay_journal_id,
1197 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context=None, name=''):
1198 recs = self.browse(cr, uid, ids, context)
1199 return recs.pay_and_reconcile(pay_amount, pay_account_id, period_id, pay_journal_id,
1200 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=name)
1202 class account_invoice_line(models.Model):
1203 _name = "account.invoice.line"
1204 _description = "Invoice Line"
1205 _order = "invoice_id,sequence,id"
1208 @api.depends('price_unit', 'discount', 'invoice_line_tax_id', 'quantity',
1209 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id')
1210 def _compute_price(self):
1211 price = self.price_unit * (1 - (self.discount or 0.0) / 100.0)
1212 taxes = self.invoice_line_tax_id.compute_all(price, self.quantity, product=self.product_id, partner=self.invoice_id.partner_id)
1213 self.price_subtotal = taxes['total']
1215 self.price_subtotal = self.invoice_id.currency_id.round(self.price_subtotal)
1218 def _default_price_unit(self):
1219 if not self._context.get('check_total'):
1221 total = self._context['check_total']
1222 for l in self._context.get('invoice_line', []):
1223 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1225 price = vals.get('price_unit', 0) * (1 - vals.get('discount', 0) / 100.0)
1226 total = total - (price * vals.get('quantity'))
1227 taxes = vals.get('invoice_line_tax_id')
1228 if taxes and len(taxes[0]) >= 3 and taxes[0][2]:
1229 taxes = self.env['account.tax'].browse(taxes[0][2])
1230 tax_res = taxes.compute_all(price, vals.get('quantity'),
1231 product=vals.get('product_id'), partner=self._context.get('partner_id'))
1232 for tax in tax_res['taxes']:
1233 total = total - tax['amount']
1237 def _default_account(self):
1238 # XXX this gets the default account for the user's company,
1239 # it should get the default account for the invoice's company
1240 # however, the invoice's company does not reach this point
1241 if self._context.get('type') in ('out_invoice', 'out_refund'):
1242 return self.env['ir.property'].get('property_account_income_categ', 'product.category')
1244 return self.env['ir.property'].get('property_account_expense_categ', 'product.category')
1246 name = fields.Text(string='Description', required=True)
1247 origin = fields.Char(string='Source Document',
1248 help="Reference of the document that produced this invoice.")
1249 sequence = fields.Integer(string='Sequence', default=10,
1250 help="Gives the sequence of this line when displaying the invoice.")
1251 invoice_id = fields.Many2one('account.invoice', string='Invoice Reference',
1252 ondelete='cascade', index=True)
1253 uos_id = fields.Many2one('product.uom', string='Unit of Measure',
1254 ondelete='set null', index=True)
1255 product_id = fields.Many2one('product.product', string='Product',
1256 ondelete='set null', index=True)
1257 account_id = fields.Many2one('account.account', string='Account',
1258 required=True, domain=[('type', 'not in', ['view', 'closed'])],
1259 default=_default_account,
1260 help="The income or expense account related to the selected product.")
1261 price_unit = fields.Float(string='Unit Price', required=True,
1262 digits= dp.get_precision('Product Price'),
1263 default=_default_price_unit)
1264 price_subtotal = fields.Float(string='Amount', digits= dp.get_precision('Account'),
1265 store=True, readonly=True, compute='_compute_price')
1266 quantity = fields.Float(string='Quantity', digits= dp.get_precision('Product Unit of Measure'),
1267 required=True, default=1)
1268 discount = fields.Float(string='Discount (%)', digits= dp.get_precision('Discount'),
1270 invoice_line_tax_id = fields.Many2many('account.tax',
1271 'account_invoice_line_tax', 'invoice_line_id', 'tax_id',
1272 string='Taxes', domain=[('parent_id', '=', False)])
1273 account_analytic_id = fields.Many2one('account.analytic.account',
1274 string='Analytic Account')
1275 company_id = fields.Many2one('res.company', string='Company',
1276 related='invoice_id.company_id', store=True, readonly=True)
1277 partner_id = fields.Many2one('res.partner', string='Partner',
1278 related='invoice_id.partner_id', store=True, readonly=True)
1281 def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
1282 res = super(account_invoice_line, self).fields_view_get(
1283 view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
1284 if self._context.get('type'):
1285 doc = etree.XML(res['arch'])
1286 for node in doc.xpath("//field[@name='product_id']"):
1287 if self._context['type'] in ('in_invoice', 'in_refund'):
1288 node.set('domain', "[('purchase_ok', '=', True)]")
1290 node.set('domain', "[('sale_ok', '=', True)]")
1291 res['arch'] = etree.tostring(doc)
1295 def product_id_change(self, product, uom_id, qty=0, name='', type='out_invoice',
1296 partner_id=False, fposition_id=False, price_unit=False, currency_id=False,
1297 context=None, company_id=None):
1298 context = context or {}
1299 company_id = company_id if company_id is not None else context.get('company_id', False)
1300 self = self.with_context(company_id=company_id, force_company=company_id)
1303 raise except_orm(_('No Partner Defined!'), _("You must first select a partner!"))
1305 if type in ('in_invoice', 'in_refund'):
1306 return {'value': {}, 'domain': {'product_uom': []}}
1308 return {'value': {'price_unit': 0.0}, 'domain': {'product_uom': []}}
1312 part = self.env['res.partner'].browse(partner_id)
1313 fpos = self.env['account.fiscal.position'].browse(fposition_id)
1316 self = self.with_context(lang=part.lang)
1317 product = self.env['product.product'].browse(product)
1319 values['name'] = product.partner_ref
1320 if type in ('out_invoice', 'out_refund'):
1321 account = product.property_account_income or product.categ_id.property_account_income_categ
1323 account = product.property_account_expense or product.categ_id.property_account_expense_categ
1324 account = fpos.map_account(account)
1326 values['account_id'] = account.id
1328 if type in ('out_invoice', 'out_refund'):
1329 taxes = product.taxes_id or account.tax_ids
1330 if product.description_sale:
1331 values['name'] += '\n' + product.description_sale
1333 taxes = product.supplier_taxes_id or account.tax_ids
1334 if product.description_purchase:
1335 values['name'] += '\n' + product.description_purchase
1337 taxes = fpos.map_tax(taxes)
1338 values['invoice_line_tax_id'] = taxes.ids
1340 if type in ('in_invoice', 'in_refund'):
1341 values['price_unit'] = price_unit or product.standard_price
1343 values['price_unit'] = product.list_price
1345 values['uos_id'] = uom_id or product.uom_id.id
1346 domain = {'uos_id': [('category_id', '=', product.uom_id.category_id.id)]}
1348 company = self.env['res.company'].browse(company_id)
1349 currency = self.env['res.currency'].browse(currency_id)
1351 if company and currency:
1352 if company.currency_id != currency:
1353 if type in ('in_invoice', 'in_refund'):
1354 values['price_unit'] = product.standard_price
1355 values['price_unit'] = values['price_unit'] * currency.rate
1357 if values['uos_id'] and values['uos_id'] != product.uom_id.id:
1358 values['price_unit'] = self.env['product.uom']._compute_price(
1359 product.uom_id.id, values['price_unit'], values['uos_id'])
1361 return {'value': values, 'domain': domain}
1364 def uos_id_change(self, product, uom, qty=0, name='', type='out_invoice', partner_id=False,
1365 fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
1366 context = context or {}
1367 company_id = company_id if company_id != None else context.get('company_id', False)
1368 self = self.with_context(company_id=company_id)
1370 result = self.product_id_change(
1371 product, uom, qty, name, type, partner_id, fposition_id, price_unit,
1372 currency_id, context=context, company_id=company_id,
1376 result['value']['price_unit'] = 0.0
1378 prod = self.env['product.product'].browse(product)
1379 prod_uom = self.env['product.uom'].browse(uom)
1380 if prod.uom_id.category_id != prod_uom.category_id:
1382 'title': _('Warning!'),
1383 'message': _('The selected unit of measure is not compatible with the unit of measure of the product.'),
1385 result['value']['uos_id'] = prod.uom_id.id
1387 result['warning'] = warning
1391 def move_line_get(self, invoice_id):
1392 inv = self.env['account.invoice'].browse(invoice_id)
1393 currency = inv.currency_id.with_context(date=inv.date_invoice)
1394 company_currency = inv.company_id.currency_id
1397 for line in inv.invoice_line:
1398 mres = self.move_line_get_item(line)
1399 mres['invl_id'] = line.id
1401 tax_code_found = False
1402 taxes = line.invoice_line_tax_id.compute_all(
1403 (line.price_unit * (1.0 - (line.discount or 0.0) / 100.0)),
1404 line.quantity, line.product_id, inv.partner_id)['taxes']
1406 if inv.type in ('out_invoice', 'in_invoice'):
1407 tax_code_id = tax['base_code_id']
1408 tax_amount = line.price_subtotal * tax['base_sign']
1410 tax_code_id = tax['ref_base_code_id']
1411 tax_amount = line.price_subtotal * tax['ref_base_sign']
1416 res.append(dict(mres))
1417 res[-1]['price'] = 0.0
1418 res[-1]['account_analytic_id'] = False
1419 elif not tax_code_id:
1421 tax_code_found = True
1423 res[-1]['tax_code_id'] = tax_code_id
1424 res[-1]['tax_amount'] = currency.compute(tax_amount, company_currency)
1429 def move_line_get_item(self, line):
1432 'name': line.name.split('\n')[0][:64],
1433 'price_unit': line.price_unit,
1434 'quantity': line.quantity,
1435 'price': line.price_subtotal,
1436 'account_id': line.account_id.id,
1437 'product_id': line.product_id.id,
1438 'uos_id': line.uos_id.id,
1439 'account_analytic_id': line.account_analytic_id.id,
1440 'taxes': line.invoice_line_tax_id,
1444 # Set the tax field according to the account and the fiscal position
1447 def onchange_account_id(self, product_id, partner_id, inv_type, fposition_id, account_id):
1451 account = self.env['account.account'].browse(account_id)
1453 fpos = self.env['account.fiscal.position'].browse(fposition_id)
1454 unique_tax_ids = fpos.map_tax(account.tax_ids).ids
1456 product_change_result = self.product_id_change(product_id, False, type=inv_type,
1457 partner_id=partner_id, fposition_id=fposition_id, company_id=account.company_id.id)
1458 if 'invoice_line_tax_id' in product_change_result.get('value', {}):
1459 unique_tax_ids = product_change_result['value']['invoice_line_tax_id']
1460 return {'value': {'invoice_line_tax_id': unique_tax_ids}}
1463 class account_invoice_tax(models.Model):
1464 _name = "account.invoice.tax"
1465 _description = "Invoice Tax"
1469 @api.depends('base', 'base_amount', 'amount', 'tax_amount')
1470 def _compute_factors(self):
1471 self.factor_base = self.base_amount / self.base if self.base else 1.0
1472 self.factor_tax = self.tax_amount / self.amount if self.amount else 1.0
1474 invoice_id = fields.Many2one('account.invoice', string='Invoice Line',
1475 ondelete='cascade', index=True)
1476 name = fields.Char(string='Tax Description',
1478 account_id = fields.Many2one('account.account', string='Tax Account',
1479 required=True, domain=[('type', 'not in', ['view', 'income', 'closed'])])
1480 account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic account')
1481 base = fields.Float(string='Base', digits=dp.get_precision('Account'))
1482 amount = fields.Float(string='Amount', digits=dp.get_precision('Account'))
1483 manual = fields.Boolean(string='Manual', default=True)
1484 sequence = fields.Integer(string='Sequence',
1485 help="Gives the sequence order when displaying a list of invoice tax.")
1486 base_code_id = fields.Many2one('account.tax.code', string='Base Code',
1487 help="The account basis of the tax declaration.")
1488 base_amount = fields.Float(string='Base Code Amount', digits=dp.get_precision('Account'),
1490 tax_code_id = fields.Many2one('account.tax.code', string='Tax Code',
1491 help="The tax basis of the tax declaration.")
1492 tax_amount = fields.Float(string='Tax Code Amount', digits=dp.get_precision('Account'),
1495 company_id = fields.Many2one('res.company', string='Company',
1496 related='account_id.company_id', store=True, readonly=True)
1497 factor_base = fields.Float(string='Multipication factor for Base code',
1498 compute='_compute_factors')
1499 factor_tax = fields.Float(string='Multipication factor Tax code',
1500 compute='_compute_factors')
1503 def base_change(self, base, currency_id=False, company_id=False, date_invoice=False):
1504 factor = self.factor_base if self else 1
1505 company = self.env['res.company'].browse(company_id)
1506 if currency_id and company.currency_id:
1507 currency = self.env['res.currency'].browse(currency_id)
1508 currency = currency.with_context(date=date_invoice or fields.Date.context_today(self))
1509 base = currency.compute(base * factor, company.currency_id, round=False)
1510 return {'value': {'base_amount': base}}
1513 def amount_change(self, amount, currency_id=False, company_id=False, date_invoice=False):
1514 factor = self.factor_tax if self else 1
1515 company = self.env['res.company'].browse(company_id)
1516 if currency_id and company.currency_id:
1517 currency = self.env['res.currency'].browse(currency_id)
1518 currency = currency.with_context(date=date_invoice or fields.Date.context_today(self))
1519 amount = currency.compute(amount * factor, company.currency_id, round=False)
1520 return {'value': {'tax_amount': amount}}
1523 def compute(self, invoice):
1525 currency = invoice.currency_id.with_context(date=invoice.date_invoice or fields.Date.context_today(invoice))
1526 company_currency = invoice.company_id.currency_id
1527 for line in invoice.invoice_line:
1528 taxes = line.invoice_line_tax_id.compute_all(
1529 (line.price_unit * (1 - (line.discount or 0.0) / 100.0)),
1530 line.quantity, line.product_id, invoice.partner_id)['taxes']
1533 'invoice_id': invoice.id,
1534 'name': tax['name'],
1535 'amount': tax['amount'],
1537 'sequence': tax['sequence'],
1538 'base': currency.round(tax['price_unit'] * line['quantity']),
1540 if invoice.type in ('out_invoice','in_invoice'):
1541 val['base_code_id'] = tax['base_code_id']
1542 val['tax_code_id'] = tax['tax_code_id']
1543 val['base_amount'] = currency.compute(val['base'] * tax['base_sign'], company_currency, round=False)
1544 val['tax_amount'] = currency.compute(val['amount'] * tax['tax_sign'], company_currency, round=False)
1545 val['account_id'] = tax['account_collected_id'] or line.account_id.id
1546 val['account_analytic_id'] = tax['account_analytic_collected_id']
1548 val['base_code_id'] = tax['ref_base_code_id']
1549 val['tax_code_id'] = tax['ref_tax_code_id']
1550 val['base_amount'] = currency.compute(val['base'] * tax['ref_base_sign'], company_currency, round=False)
1551 val['tax_amount'] = currency.compute(val['amount'] * tax['ref_tax_sign'], company_currency, round=False)
1552 val['account_id'] = tax['account_paid_id'] or line.account_id.id
1553 val['account_analytic_id'] = tax['account_analytic_paid_id']
1555 key = (val['tax_code_id'], val['base_code_id'], val['account_id'], val['account_analytic_id'])
1556 if not key in tax_grouped:
1557 tax_grouped[key] = val
1559 tax_grouped[key]['base'] += val['base']
1560 tax_grouped[key]['amount'] += val['amount']
1561 tax_grouped[key]['base_amount'] += val['base_amount']
1562 tax_grouped[key]['tax_amount'] += val['tax_amount']
1564 for t in tax_grouped.values():
1565 t['base'] = currency.round(t['base'])
1566 t['amount'] = currency.round(t['amount'])
1567 t['base_amount'] = currency.round(t['base_amount'])
1568 t['tax_amount'] = currency.round(t['tax_amount'])
1573 def compute(self, cr, uid, invoice_id, context=None):
1574 recs = self.browse(cr, uid, [], context)
1575 invoice = recs.env['account.invoice'].browse(invoice_id)
1576 return recs.compute(invoice)
1579 def move_line_get(self, invoice_id):
1582 'SELECT * FROM account_invoice_tax WHERE invoice_id = %s',
1585 for row in self._cr.dictfetchall():
1586 if not (row['amount'] or row['tax_code_id'] or row['tax_amount']):
1590 'name': row['name'],
1591 'price_unit': row['amount'],
1593 'price': row['amount'] or 0.0,
1594 'account_id': row['account_id'],
1595 'tax_code_id': row['tax_code_id'],
1596 'tax_amount': row['tax_amount'],
1597 'account_analytic_id': row['account_analytic_id'],
1602 class res_partner(models.Model):
1603 # Inherits partner and adds invoice information in the partner form
1604 _inherit = 'res.partner'
1606 invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices',
1609 def _find_accounting_partner(self, partner):
1611 Find the partner for which the accounting entries will be created
1613 return partner.commercial_partner_id
1615 class mail_compose_message(models.Model):
1616 _inherit = 'mail.compose.message'
1619 def send_mail(self):
1620 context = self._context
1621 if context.get('default_model') == 'account.invoice' and \
1622 context.get('default_res_id') and context.get('mark_invoice_as_sent'):
1623 invoice = self.env['account.invoice'].browse(context['default_res_id'])
1624 invoice = invoice.with_context(mail_post_autofollow=True)
1625 invoice.write({'sent': True})
1626 invoice.message_post(body=_("Invoice sent"))
1627 return super(mail_compose_message, self).send_mail()
1629 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: