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 group_check_total = self.env.ref('account.group_supplier_inv_check_total')
810 if self.env.user in group_check_total.users:
811 if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding / 2.0):
812 raise except_orm(_('Bad Total!'), _('Please verify the price of the invoice!\nThe encoded total does not match the computed total.'))
815 total_fixed = total_percent = 0
816 for line in inv.payment_term.line_ids:
817 if line.value == 'fixed':
818 total_fixed += line.value_amount
819 if line.value == 'procent':
820 total_percent += line.value_amount
821 total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
822 if (total_fixed + total_percent) > 100:
823 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'."))
825 # one move line per tax line
826 iml += account_invoice_tax.move_line_get(inv.id)
828 if inv.type in ('in_invoice', 'in_refund'):
831 ref = self._convert_ref(inv.number)
833 diff_currency = inv.currency_id != company_currency
834 # create one move line for the total and possibly adjust the other lines amount
835 total, total_currency, iml = inv.with_context(ctx).compute_invoice_totals(company_currency, ref, iml)
837 name = inv.name or inv.supplier_invoice_number or '/'
840 totlines = inv.with_context(ctx).payment_term.compute(total, date_invoice)[0]
842 res_amount_currency = total_currency
843 ctx['date'] = date_invoice
844 for i, t in enumerate(totlines):
845 if inv.currency_id != company_currency:
846 amount_currency = company_currency.with_context(ctx).compute(t[1], inv.currency_id)
848 amount_currency = False
850 # last line: add the diff
851 res_amount_currency -= amount_currency or 0
852 if i + 1 == len(totlines):
853 amount_currency += res_amount_currency
859 'account_id': inv.account_id.id,
860 'date_maturity': t[0],
861 'amount_currency': diff_currency and amount_currency,
862 'currency_id': diff_currency and inv.currency_id.id,
870 'account_id': inv.account_id.id,
871 'date_maturity': inv.date_due,
872 'amount_currency': diff_currency and total_currency,
873 'currency_id': diff_currency and inv.currency_id.id,
879 part = self.env['res.partner']._find_accounting_partner(inv.partner_id)
881 line = [(0, 0, self.line_get_convert(l, part.id, date)) for l in iml]
882 line = inv.group_lines(iml, line)
884 journal = inv.journal_id.with_context(ctx)
885 if journal.centralisation:
886 raise except_orm(_('User Error!'),
887 _('You cannot create an invoice on a centralized journal. Uncheck the centralized counterpart box in the related journal from the configuration menu.'))
889 line = inv.finalize_invoice_move_lines(line)
892 'ref': inv.reference or inv.name,
894 'journal_id': journal.id,
895 'date': inv.date_invoice,
896 'narration': inv.comment,
897 'company_id': inv.company_id.id,
899 ctx['company_id'] = inv.company_id.id
900 period = inv.period_id
902 period = period.with_context(ctx).find(date_invoice)[:1]
904 move_vals['period_id'] = period.id
906 i[2]['period_id'] = period.id
909 move = account_move.with_context(ctx).create(move_vals)
910 # make the invoice point to that move
913 'period_id': period.id,
914 'move_name': move.name,
916 inv.with_context(ctx).write(vals)
917 # Pass invoice in context in method post: used if you want to get the same
918 # account move reference when creating the same invoice after a cancelled one:
924 def invoice_validate(self):
925 return self.write({'state': 'open'})
928 def line_get_convert(self, line, part, date):
930 'date_maturity': line.get('date_maturity', False),
932 'name': line['name'][:64],
934 'debit': line['price']>0 and line['price'],
935 'credit': line['price']<0 and -line['price'],
936 'account_id': line['account_id'],
937 'analytic_lines': line.get('analytic_lines', []),
938 'amount_currency': line['price']>0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)),
939 'currency_id': line.get('currency_id', False),
940 'tax_code_id': line.get('tax_code_id', False),
941 'tax_amount': line.get('tax_amount', False),
942 'ref': line.get('ref', False),
943 'quantity': line.get('quantity',1.00),
944 'product_id': line.get('product_id', False),
945 'product_uom_id': line.get('uos_id', False),
946 'analytic_account_id': line.get('account_analytic_id', False),
950 def action_number(self):
951 #TODO: not correct fix but required a fresh values before reading it.
955 self.write({'internal_number': inv.number})
957 if inv.type in ('in_invoice', 'in_refund'):
958 if not inv.reference:
959 ref = self._convert_ref(inv.number)
963 ref = self._convert_ref(inv.number)
965 self._cr.execute(""" UPDATE account_move SET ref=%s
966 WHERE id=%s AND (ref IS NULL OR ref = '')""",
967 (ref, inv.move_id.id))
968 self._cr.execute(""" UPDATE account_move_line SET ref=%s
969 WHERE move_id=%s AND (ref IS NULL OR ref = '')""",
970 (ref, inv.move_id.id))
971 self._cr.execute(""" UPDATE account_analytic_line SET ref=%s
972 FROM account_move_line
973 WHERE account_move_line.move_id = %s AND
974 account_analytic_line.move_id = account_move_line.id""",
975 (ref, inv.move_id.id))
976 self.invalidate_cache()
981 def action_cancel(self):
982 moves = self.env['account.move']
987 for move_line in inv.payment_ids:
988 if move_line.reconcile_partial_id.line_partial_ids:
989 raise except_orm(_('Error!'), _('You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first.'))
991 # First, set the invoices as cancelled and detach the move ids
992 self.write({'state': 'cancel', 'move_id': False})
994 # second, invalidate the move(s)
995 moves.button_cancel()
996 # delete the move this invoice was pointing to
997 # Note that the corresponding move_lines and move_reconciles
998 # will be automatically deleted too
1000 self._log_event(-1.0, 'Cancel Invoice')
1006 def _log_event(self, factor=1.0, name='Open Invoice'):
1007 #TODO: implement messages system
1013 'out_invoice': _('Invoice'),
1014 'in_invoice': _('Supplier Invoice'),
1015 'out_refund': _('Refund'),
1016 'in_refund': _('Supplier Refund'),
1020 result.append((inv.id, "%s %s" % (inv.number or TYPES[inv.type], inv.name or '')))
1024 def name_search(self, name, args=None, operator='ilike', limit=100):
1026 recs = self.browse()
1028 recs = self.search([('number', '=', name)] + args, limit=limit)
1030 recs = self.search([('name', operator, name)] + args, limit=limit)
1031 return recs.name_get()
1034 def _refund_cleanup_lines(self, lines):
1035 """ Convert records to dict of values suitable for one2many line creation
1037 :param recordset lines: records to convert
1038 :return: list of command tuple for one2many line creation [(0, 0, dict of valueis), ...]
1043 for name, field in line._fields.iteritems():
1044 if name in MAGIC_COLUMNS:
1046 elif field.type == 'many2one':
1047 values[name] = line[name].id
1048 elif field.type not in ['many2many', 'one2many']:
1049 values[name] = line[name]
1050 elif name == 'invoice_line_tax_id':
1051 values[name] = [(6, 0, line[name].ids)]
1052 result.append((0, 0, values))
1056 def _prepare_refund(self, invoice, date=None, period_id=None, description=None, journal_id=None):
1057 """ Prepare the dict of values to create the new refund from the invoice.
1058 This method may be overridden to implement custom
1059 refund generation (making sure to call super() to establish
1060 a clean extension chain).
1062 :param record invoice: invoice to refund
1063 :param string date: refund creation date from the wizard
1064 :param integer period_id: force account.period from the wizard
1065 :param string description: description of the refund from the wizard
1066 :param integer journal_id: account.journal from the wizard
1067 :return: dict of value to create() the refund
1070 for field in ['name', 'reference', 'comment', 'date_due', 'partner_id', 'company_id',
1071 'account_id', 'currency_id', 'payment_term', 'user_id', 'fiscal_position']:
1072 if invoice._fields[field].type == 'many2one':
1073 values[field] = invoice[field].id
1075 values[field] = invoice[field] or False
1077 values['invoice_line'] = self._refund_cleanup_lines(invoice.invoice_line)
1079 tax_lines = filter(lambda l: l.manual, invoice.tax_line)
1080 values['tax_line'] = self._refund_cleanup_lines(tax_lines)
1083 journal = self.env['account.journal'].browse(journal_id)
1084 elif invoice['type'] == 'in_invoice':
1085 journal = self.env['account.journal'].search([('type', '=', 'purchase_refund')], limit=1)
1087 journal = self.env['account.journal'].search([('type', '=', 'sale_refund')], limit=1)
1088 values['journal_id'] = journal.id
1090 values['type'] = TYPE2REFUND[invoice['type']]
1091 values['date_invoice'] = date or fields.Date.context_today(invoice)
1092 values['state'] = 'draft'
1093 values['number'] = False
1096 values['period_id'] = period_id
1098 values['name'] = description
1102 @api.returns('self')
1103 def refund(self, date=None, period_id=None, description=None, journal_id=None):
1104 new_invoices = self.browse()
1105 for invoice in self:
1106 # create the new invoice
1107 values = self._prepare_refund(invoice, date=date, period_id=period_id,
1108 description=description, journal_id=journal_id)
1109 new_invoices += self.create(values)
1113 def pay_and_reconcile(self, pay_amount, pay_account_id, period_id, pay_journal_id,
1114 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=''):
1115 # TODO check if we can use different period for payment and the writeoff line
1116 assert len(self)==1, "Can only pay one invoice at a time."
1117 # Take the seq as name for move
1118 SIGN = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1119 direction = SIGN[self.type]
1120 # take the chosen date
1121 date = self._context.get('date_p') or fields.Date.context_today(self)
1123 # Take the amount in currency and the currency of the payment
1124 if self._context.get('amount_currency') and self._context.get('currency_id'):
1125 amount_currency = self._context['amount_currency']
1126 currency_id = self._context['currency_id']
1128 amount_currency = False
1131 pay_journal = self.env['account.journal'].browse(pay_journal_id)
1132 if self.type in ('in_invoice', 'in_refund'):
1133 ref = self.reference
1135 ref = self._convert_ref(self.number)
1136 partner = self.partner_id._find_accounting_partner(self.partner_id)
1137 name = name or self.invoice_line.name or self.number
1138 # Pay attention to the sign for both debit/credit AND amount_currency
1141 'debit': direction * pay_amount > 0 and direction * pay_amount,
1142 'credit': direction * pay_amount < 0 and -direction * pay_amount,
1143 'account_id': self.account_id.id,
1144 'partner_id': partner.id,
1147 'currency_id': currency_id,
1148 'amount_currency': direction * (amount_currency or 0.0),
1149 'company_id': self.company_id.id,
1153 'debit': direction * pay_amount < 0 and -direction * pay_amount,
1154 'credit': direction * pay_amount > 0 and direction * pay_amount,
1155 'account_id': pay_account_id,
1156 'partner_id': partner.id,
1159 'currency_id': currency_id,
1160 'amount_currency': -direction * (amount_currency or 0.0),
1161 'company_id': self.company_id.id,
1163 move = self.env['account.move'].create({
1165 'line_id': [(0, 0, l1), (0, 0, l2)],
1166 'journal_id': pay_journal_id,
1167 'period_id': period_id,
1171 move_ids = (move | self.move_id).ids
1172 self._cr.execute("SELECT id FROM account_move_line WHERE move_id IN %s",
1174 lines = self.env['account.move.line'].browse([r[0] for r in self._cr.fetchall()])
1175 lines2rec = lines.browse()
1177 for line in itertools.chain(lines, self.payment_ids):
1178 if line.account_id == self.account_id:
1180 total += (line.debit or 0.0) - (line.credit or 0.0)
1182 inv_id, name = self.name_get()[0]
1183 if not round(total, self.env['decimal.precision'].precision_get('Account')) or writeoff_acc_id:
1184 lines2rec.reconcile('manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id)
1186 code = self.currency_id.symbol
1187 # TODO: use currency's formatting function
1188 msg = _("Invoice partially paid: %s%s of %s%s (%s%s remaining).") % \
1189 (pay_amount, code, self.amount_total, code, total, code)
1190 self.message_post(body=msg)
1191 lines2rec.reconcile_partial('manual')
1193 # Update the stored value (fields.function), so we write to trigger recompute
1194 return self.write({})
1197 def pay_and_reconcile(self, cr, uid, ids, pay_amount, pay_account_id, period_id, pay_journal_id,
1198 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context=None, name=''):
1199 recs = self.browse(cr, uid, ids, context)
1200 return recs.pay_and_reconcile(pay_amount, pay_account_id, period_id, pay_journal_id,
1201 writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=name)
1203 class account_invoice_line(models.Model):
1204 _name = "account.invoice.line"
1205 _description = "Invoice Line"
1206 _order = "invoice_id,sequence,id"
1209 @api.depends('price_unit', 'discount', 'invoice_line_tax_id', 'quantity',
1210 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id')
1211 def _compute_price(self):
1212 price = self.price_unit * (1 - (self.discount or 0.0) / 100.0)
1213 taxes = self.invoice_line_tax_id.compute_all(price, self.quantity, product=self.product_id, partner=self.invoice_id.partner_id)
1214 self.price_subtotal = taxes['total']
1216 self.price_subtotal = self.invoice_id.currency_id.round(self.price_subtotal)
1219 def _default_price_unit(self):
1220 if not self._context.get('check_total'):
1222 total = self._context['check_total']
1223 for l in self._context.get('invoice_line', []):
1224 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1226 price = vals.get('price_unit', 0) * (1 - vals.get('discount', 0) / 100.0)
1227 total = total - (price * vals.get('quantity'))
1228 taxes = vals.get('invoice_line_tax_id')
1229 if taxes and len(taxes[0]) >= 3 and taxes[0][2]:
1230 taxes = self.env['account.tax'].browse(taxes[0][2])
1231 tax_res = taxes.compute_all(price, vals.get('quantity'),
1232 product=vals.get('product_id'), partner=self._context.get('partner_id'))
1233 for tax in tax_res['taxes']:
1234 total = total - tax['amount']
1238 def _default_account(self):
1239 # XXX this gets the default account for the user's company,
1240 # it should get the default account for the invoice's company
1241 # however, the invoice's company does not reach this point
1242 if self._context.get('type') in ('out_invoice', 'out_refund'):
1243 return self.env['ir.property'].get('property_account_income_categ', 'product.category')
1245 return self.env['ir.property'].get('property_account_expense_categ', 'product.category')
1247 name = fields.Text(string='Description', required=True)
1248 origin = fields.Char(string='Source Document',
1249 help="Reference of the document that produced this invoice.")
1250 sequence = fields.Integer(string='Sequence', default=10,
1251 help="Gives the sequence of this line when displaying the invoice.")
1252 invoice_id = fields.Many2one('account.invoice', string='Invoice Reference',
1253 ondelete='cascade', index=True)
1254 uos_id = fields.Many2one('product.uom', string='Unit of Measure',
1255 ondelete='set null', index=True)
1256 product_id = fields.Many2one('product.product', string='Product',
1257 ondelete='set null', index=True)
1258 account_id = fields.Many2one('account.account', string='Account',
1259 required=True, domain=[('type', 'not in', ['view', 'closed'])],
1260 default=_default_account,
1261 help="The income or expense account related to the selected product.")
1262 price_unit = fields.Float(string='Unit Price', required=True,
1263 digits= dp.get_precision('Product Price'),
1264 default=_default_price_unit)
1265 price_subtotal = fields.Float(string='Amount', digits= dp.get_precision('Account'),
1266 store=True, readonly=True, compute='_compute_price')
1267 quantity = fields.Float(string='Quantity', digits= dp.get_precision('Product Unit of Measure'),
1268 required=True, default=1)
1269 discount = fields.Float(string='Discount (%)', digits= dp.get_precision('Discount'),
1271 invoice_line_tax_id = fields.Many2many('account.tax',
1272 'account_invoice_line_tax', 'invoice_line_id', 'tax_id',
1273 string='Taxes', domain=[('parent_id', '=', False)])
1274 account_analytic_id = fields.Many2one('account.analytic.account',
1275 string='Analytic Account')
1276 company_id = fields.Many2one('res.company', string='Company',
1277 related='invoice_id.company_id', store=True, readonly=True)
1278 partner_id = fields.Many2one('res.partner', string='Partner',
1279 related='invoice_id.partner_id', store=True, readonly=True)
1282 def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
1283 res = super(account_invoice_line, self).fields_view_get(
1284 view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
1285 if self._context.get('type'):
1286 doc = etree.XML(res['arch'])
1287 for node in doc.xpath("//field[@name='product_id']"):
1288 if self._context['type'] in ('in_invoice', 'in_refund'):
1289 node.set('domain', "[('purchase_ok', '=', True)]")
1291 node.set('domain', "[('sale_ok', '=', True)]")
1292 res['arch'] = etree.tostring(doc)
1296 def product_id_change(self, product, uom_id, qty=0, name='', type='out_invoice',
1297 partner_id=False, fposition_id=False, price_unit=False, currency_id=False,
1298 context=None, company_id=None):
1299 context = context or {}
1300 company_id = company_id if company_id is not None else context.get('company_id', False)
1301 self = self.with_context(company_id=company_id, force_company=company_id)
1304 raise except_orm(_('No Partner Defined!'), _("You must first select a partner!"))
1306 if type in ('in_invoice', 'in_refund'):
1307 return {'value': {}, 'domain': {'product_uom': []}}
1309 return {'value': {'price_unit': 0.0}, 'domain': {'product_uom': []}}
1313 part = self.env['res.partner'].browse(partner_id)
1314 fpos = self.env['account.fiscal.position'].browse(fposition_id)
1317 self = self.with_context(lang=part.lang)
1318 product = self.env['product.product'].browse(product)
1320 values['name'] = product.partner_ref
1321 if type in ('out_invoice', 'out_refund'):
1322 account = product.property_account_income or product.categ_id.property_account_income_categ
1324 account = product.property_account_expense or product.categ_id.property_account_expense_categ
1325 account = fpos.map_account(account)
1327 values['account_id'] = account.id
1329 if type in ('out_invoice', 'out_refund'):
1330 taxes = product.taxes_id or account.tax_ids
1331 if product.description_sale:
1332 values['name'] += '\n' + product.description_sale
1334 taxes = product.supplier_taxes_id or account.tax_ids
1335 if product.description_purchase:
1336 values['name'] += '\n' + product.description_purchase
1338 taxes = fpos.map_tax(taxes)
1339 values['invoice_line_tax_id'] = taxes.ids
1341 if type in ('in_invoice', 'in_refund'):
1342 values['price_unit'] = price_unit or product.standard_price
1344 values['price_unit'] = product.list_price
1346 values['uos_id'] = uom_id or product.uom_id.id
1347 domain = {'uos_id': [('category_id', '=', product.uom_id.category_id.id)]}
1349 company = self.env['res.company'].browse(company_id)
1350 currency = self.env['res.currency'].browse(currency_id)
1352 if company and currency:
1353 if company.currency_id != currency:
1354 if type in ('in_invoice', 'in_refund'):
1355 values['price_unit'] = product.standard_price
1356 values['price_unit'] = values['price_unit'] * currency.rate
1358 if values['uos_id'] and values['uos_id'] != product.uom_id.id:
1359 values['price_unit'] = self.env['product.uom']._compute_price(
1360 product.uom_id.id, values['price_unit'], values['uos_id'])
1362 return {'value': values, 'domain': domain}
1365 def uos_id_change(self, product, uom, qty=0, name='', type='out_invoice', partner_id=False,
1366 fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
1367 context = context or {}
1368 company_id = company_id if company_id != None else context.get('company_id', False)
1369 self = self.with_context(company_id=company_id)
1371 result = self.product_id_change(
1372 product, uom, qty, name, type, partner_id, fposition_id, price_unit,
1373 currency_id, context=context, company_id=company_id,
1377 result['value']['price_unit'] = 0.0
1379 prod = self.env['product.product'].browse(product)
1380 prod_uom = self.env['product.uom'].browse(uom)
1381 if prod.uom_id.category_id != prod_uom.category_id:
1383 'title': _('Warning!'),
1384 'message': _('The selected unit of measure is not compatible with the unit of measure of the product.'),
1386 result['value']['uos_id'] = prod.uom_id.id
1388 result['warning'] = warning
1392 def move_line_get(self, invoice_id):
1393 inv = self.env['account.invoice'].browse(invoice_id)
1394 currency = inv.currency_id.with_context(date=inv.date_invoice)
1395 company_currency = inv.company_id.currency_id
1398 for line in inv.invoice_line:
1399 mres = self.move_line_get_item(line)
1400 mres['invl_id'] = line.id
1402 tax_code_found = False
1403 taxes = line.invoice_line_tax_id.compute_all(
1404 (line.price_unit * (1.0 - (line.discount or 0.0) / 100.0)),
1405 line.quantity, line.product_id, inv.partner_id)['taxes']
1407 if inv.type in ('out_invoice', 'in_invoice'):
1408 tax_code_id = tax['base_code_id']
1409 tax_amount = line.price_subtotal * tax['base_sign']
1411 tax_code_id = tax['ref_base_code_id']
1412 tax_amount = line.price_subtotal * tax['ref_base_sign']
1417 res.append(dict(mres))
1418 res[-1]['price'] = 0.0
1419 res[-1]['account_analytic_id'] = False
1420 elif not tax_code_id:
1422 tax_code_found = True
1424 res[-1]['tax_code_id'] = tax_code_id
1425 res[-1]['tax_amount'] = currency.compute(tax_amount, company_currency)
1430 def move_line_get_item(self, line):
1433 'name': line.name.split('\n')[0][:64],
1434 'price_unit': line.price_unit,
1435 'quantity': line.quantity,
1436 'price': line.price_subtotal,
1437 'account_id': line.account_id.id,
1438 'product_id': line.product_id.id,
1439 'uos_id': line.uos_id.id,
1440 'account_analytic_id': line.account_analytic_id.id,
1441 'taxes': line.invoice_line_tax_id,
1445 # Set the tax field according to the account and the fiscal position
1448 def onchange_account_id(self, product_id, partner_id, inv_type, fposition_id, account_id):
1452 account = self.env['account.account'].browse(account_id)
1454 fpos = self.env['account.fiscal.position'].browse(fposition_id)
1455 unique_tax_ids = fpos.map_tax(account.tax_ids).ids
1457 product_change_result = self.product_id_change(product_id, False, type=inv_type,
1458 partner_id=partner_id, fposition_id=fposition_id, company_id=account.company_id.id)
1459 if 'invoice_line_tax_id' in product_change_result.get('value', {}):
1460 unique_tax_ids = product_change_result['value']['invoice_line_tax_id']
1461 return {'value': {'invoice_line_tax_id': unique_tax_ids}}
1464 class account_invoice_tax(models.Model):
1465 _name = "account.invoice.tax"
1466 _description = "Invoice Tax"
1470 @api.depends('base', 'base_amount', 'amount', 'tax_amount')
1471 def _compute_factors(self):
1472 self.factor_base = self.base_amount / self.base if self.base else 1.0
1473 self.factor_tax = self.tax_amount / self.amount if self.amount else 1.0
1475 invoice_id = fields.Many2one('account.invoice', string='Invoice Line',
1476 ondelete='cascade', index=True)
1477 name = fields.Char(string='Tax Description',
1479 account_id = fields.Many2one('account.account', string='Tax Account',
1480 required=True, domain=[('type', 'not in', ['view', 'income', 'closed'])])
1481 account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic account')
1482 base = fields.Float(string='Base', digits=dp.get_precision('Account'))
1483 amount = fields.Float(string='Amount', digits=dp.get_precision('Account'))
1484 manual = fields.Boolean(string='Manual', default=True)
1485 sequence = fields.Integer(string='Sequence',
1486 help="Gives the sequence order when displaying a list of invoice tax.")
1487 base_code_id = fields.Many2one('account.tax.code', string='Base Code',
1488 help="The account basis of the tax declaration.")
1489 base_amount = fields.Float(string='Base Code Amount', digits=dp.get_precision('Account'),
1491 tax_code_id = fields.Many2one('account.tax.code', string='Tax Code',
1492 help="The tax basis of the tax declaration.")
1493 tax_amount = fields.Float(string='Tax Code Amount', digits=dp.get_precision('Account'),
1496 company_id = fields.Many2one('res.company', string='Company',
1497 related='account_id.company_id', store=True, readonly=True)
1498 factor_base = fields.Float(string='Multipication factor for Base code',
1499 compute='_compute_factors')
1500 factor_tax = fields.Float(string='Multipication factor Tax code',
1501 compute='_compute_factors')
1504 def base_change(self, base, currency_id=False, company_id=False, date_invoice=False):
1505 factor = self.factor_base if self else 1
1506 company = self.env['res.company'].browse(company_id)
1507 if currency_id and company.currency_id:
1508 currency = self.env['res.currency'].browse(currency_id)
1509 currency = currency.with_context(date=date_invoice or fields.Date.context_today(self))
1510 base = currency.compute(base * factor, company.currency_id, round=False)
1511 return {'value': {'base_amount': base}}
1514 def amount_change(self, amount, currency_id=False, company_id=False, date_invoice=False):
1515 factor = self.factor_tax if self else 1
1516 company = self.env['res.company'].browse(company_id)
1517 if currency_id and company.currency_id:
1518 currency = self.env['res.currency'].browse(currency_id)
1519 currency = currency.with_context(date=date_invoice or fields.Date.context_today(self))
1520 amount = currency.compute(amount * factor, company.currency_id, round=False)
1521 return {'value': {'tax_amount': amount}}
1524 def compute(self, invoice):
1526 currency = invoice.currency_id.with_context(date=invoice.date_invoice or fields.Date.context_today(invoice))
1527 company_currency = invoice.company_id.currency_id
1528 for line in invoice.invoice_line:
1529 taxes = line.invoice_line_tax_id.compute_all(
1530 (line.price_unit * (1 - (line.discount or 0.0) / 100.0)),
1531 line.quantity, line.product_id, invoice.partner_id)['taxes']
1534 'invoice_id': invoice.id,
1535 'name': tax['name'],
1536 'amount': tax['amount'],
1538 'sequence': tax['sequence'],
1539 'base': currency.round(tax['price_unit'] * line['quantity']),
1541 if invoice.type in ('out_invoice','in_invoice'):
1542 val['base_code_id'] = tax['base_code_id']
1543 val['tax_code_id'] = tax['tax_code_id']
1544 val['base_amount'] = currency.compute(val['base'] * tax['base_sign'], company_currency, round=False)
1545 val['tax_amount'] = currency.compute(val['amount'] * tax['tax_sign'], company_currency, round=False)
1546 val['account_id'] = tax['account_collected_id'] or line.account_id.id
1547 val['account_analytic_id'] = tax['account_analytic_collected_id']
1549 val['base_code_id'] = tax['ref_base_code_id']
1550 val['tax_code_id'] = tax['ref_tax_code_id']
1551 val['base_amount'] = currency.compute(val['base'] * tax['ref_base_sign'], company_currency, round=False)
1552 val['tax_amount'] = currency.compute(val['amount'] * tax['ref_tax_sign'], company_currency, round=False)
1553 val['account_id'] = tax['account_paid_id'] or line.account_id.id
1554 val['account_analytic_id'] = tax['account_analytic_paid_id']
1556 key = (val['tax_code_id'], val['base_code_id'], val['account_id'], val['account_analytic_id'])
1557 if not key in tax_grouped:
1558 tax_grouped[key] = val
1560 tax_grouped[key]['base'] += val['base']
1561 tax_grouped[key]['amount'] += val['amount']
1562 tax_grouped[key]['base_amount'] += val['base_amount']
1563 tax_grouped[key]['tax_amount'] += val['tax_amount']
1565 for t in tax_grouped.values():
1566 t['base'] = currency.round(t['base'])
1567 t['amount'] = currency.round(t['amount'])
1568 t['base_amount'] = currency.round(t['base_amount'])
1569 t['tax_amount'] = currency.round(t['tax_amount'])
1574 def compute(self, cr, uid, invoice_id, context=None):
1575 recs = self.browse(cr, uid, [], context)
1576 invoice = recs.env['account.invoice'].browse(invoice_id)
1577 return recs.compute(invoice)
1580 def move_line_get(self, invoice_id):
1583 'SELECT * FROM account_invoice_tax WHERE invoice_id = %s',
1586 for row in self._cr.dictfetchall():
1587 if not (row['amount'] or row['tax_code_id'] or row['tax_amount']):
1591 'name': row['name'],
1592 'price_unit': row['amount'],
1594 'price': row['amount'] or 0.0,
1595 'account_id': row['account_id'],
1596 'tax_code_id': row['tax_code_id'],
1597 'tax_amount': row['tax_amount'],
1598 'account_analytic_id': row['account_analytic_id'],
1603 class res_partner(models.Model):
1604 # Inherits partner and adds invoice information in the partner form
1605 _inherit = 'res.partner'
1607 invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices',
1610 def _find_accounting_partner(self, partner):
1612 Find the partner for which the accounting entries will be created
1614 return partner.commercial_partner_id
1616 class mail_compose_message(models.Model):
1617 _inherit = 'mail.compose.message'
1620 def send_mail(self):
1621 context = self._context
1622 if context.get('default_model') == 'account.invoice' and \
1623 context.get('default_res_id') and context.get('mark_invoice_as_sent'):
1624 invoice = self.env['account.invoice'].browse(context['default_res_id'])
1625 invoice = invoice.with_context(mail_post_autofollow=True)
1626 invoice.write({'sent': True})
1627 invoice.message_post(body=_("Invoice sent"))
1628 return super(mail_compose_message, self).send_mail()
1630 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: