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.osv import fields, osv
26 import openerp.addons.decimal_precision as dp
27 from openerp.tools.translate import _
28 from openerp.tools import float_compare
29 from openerp.report import report_sxw
32 class res_currency(osv.osv):
33 _inherit = "res.currency"
35 def _get_current_rate(self, cr, uid, ids, raise_on_no_rate=True, context=None):
38 res = super(res_currency, self)._get_current_rate(cr, uid, ids, raise_on_no_rate, context=context)
39 if context.get('voucher_special_currency') in ids and context.get('voucher_special_currency_rate'):
40 res[context.get('voucher_special_currency')] = context.get('voucher_special_currency_rate')
44 class account_voucher(osv.osv):
45 def _check_paid(self, cr, uid, ids, name, args, context=None):
47 for voucher in self.browse(cr, uid, ids, context=context):
48 res[voucher.id] = any([((line.account_id.type, 'in', ('receivable', 'payable')) and line.reconcile_id) for line in voucher.move_ids])
51 def _get_type(self, cr, uid, context=None):
54 return context.get('type', False)
56 def _get_period(self, cr, uid, context=None):
57 if context is None: context = {}
58 if context.get('period_id', False):
59 return context.get('period_id')
60 periods = self.pool.get('account.period').find(cr, uid, context=context)
61 return periods and periods[0] or False
63 def _make_journal_search(self, cr, uid, ttype, context=None):
64 journal_pool = self.pool.get('account.journal')
65 return journal_pool.search(cr, uid, [('type', '=', ttype)], limit=1)
67 def _get_journal(self, cr, uid, context=None):
68 if context is None: context = {}
69 invoice_pool = self.pool.get('account.invoice')
70 journal_pool = self.pool.get('account.journal')
71 if context.get('invoice_id', False):
72 currency_id = invoice_pool.browse(cr, uid, context['invoice_id'], context=context).currency_id.id
73 journal_id = journal_pool.search(cr, uid, [('currency', '=', currency_id)], limit=1)
74 return journal_id and journal_id[0] or False
75 if context.get('journal_id', False):
76 return context.get('journal_id')
77 if not context.get('journal_id', False) and context.get('search_default_journal_id', False):
78 return context.get('search_default_journal_id')
80 ttype = context.get('type', 'bank')
81 if ttype in ('payment', 'receipt'):
83 res = self._make_journal_search(cr, uid, ttype, context=context)
84 return res and res[0] or False
86 def _get_tax(self, cr, uid, context=None):
87 if context is None: context = {}
88 journal_pool = self.pool.get('account.journal')
89 journal_id = context.get('journal_id', False)
91 ttype = context.get('type', 'bank')
92 res = journal_pool.search(cr, uid, [('type', '=', ttype)], limit=1)
99 journal = journal_pool.browse(cr, uid, journal_id, context=context)
100 account_id = journal.default_credit_account_id or journal.default_debit_account_id
101 if account_id and account_id.tax_ids:
102 tax_id = account_id.tax_ids[0].id
106 def _get_payment_rate_currency(self, cr, uid, context=None):
108 Return the default value for field payment_rate_currency_id: the currency of the journal
109 if there is one, otherwise the currency of the user's company
111 if context is None: context = {}
112 journal_pool = self.pool.get('account.journal')
113 journal_id = context.get('journal_id', False)
115 journal = journal_pool.browse(cr, uid, journal_id, context=context)
117 return journal.currency.id
118 #no journal given in the context, use company currency as default
119 return self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
121 def _get_currency(self, cr, uid, context=None):
122 if context is None: context = {}
123 journal_pool = self.pool.get('account.journal')
124 journal_id = context.get('journal_id', False)
126 journal = journal_pool.browse(cr, uid, journal_id, context=context)
128 return journal.currency.id
129 return self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
131 def _get_partner(self, cr, uid, context=None):
132 if context is None: context = {}
133 return context.get('partner_id', False)
135 def _get_reference(self, cr, uid, context=None):
136 if context is None: context = {}
137 return context.get('reference', False)
139 def _get_narration(self, cr, uid, context=None):
140 if context is None: context = {}
141 return context.get('narration', False)
143 def _get_amount(self, cr, uid, context=None):
146 return context.get('amount', 0.0)
148 def name_get(self, cr, uid, ids, context=None):
151 if context is None: context = {}
152 return [(r['id'], (r['number'] or _('Voucher'))) for r in self.read(cr, uid, ids, ['number'], context, load='_classic_write')]
154 def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
155 mod_obj = self.pool.get('ir.model.data')
156 if context is None: context = {}
158 if view_type == 'form':
159 if not view_id and context.get('invoice_type'):
160 if context.get('invoice_type') in ('out_invoice', 'out_refund'):
161 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_receipt_form')
163 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_payment_form')
164 result = result and result[1] or False
166 if not view_id and context.get('line_type'):
167 if context.get('line_type') == 'customer':
168 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_receipt_form')
170 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_payment_form')
171 result = result and result[1] or False
174 res = super(account_voucher, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
175 doc = etree.XML(res['arch'])
177 if context.get('type', 'sale') in ('purchase', 'payment'):
178 nodes = doc.xpath("//field[@name='partner_id']")
180 node.set('context', "{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1}")
181 if context.get('invoice_type','') in ('in_invoice', 'in_refund'):
182 node.set('string', _("Supplier"))
183 res['arch'] = etree.tostring(doc)
186 def _compute_writeoff_amount(self, cr, uid, line_dr_ids, line_cr_ids, amount, type):
188 sign = type == 'payment' and -1 or 1
189 for l in line_dr_ids:
190 if isinstance(l, dict):
192 for l in line_cr_ids:
193 if isinstance(l, dict):
194 credit += l['amount']
195 return amount - sign * (credit - debit)
197 def onchange_line_ids(self, cr, uid, ids, line_dr_ids, line_cr_ids, amount, voucher_currency, type, context=None):
198 context = context or {}
199 if not line_dr_ids and not line_cr_ids:
200 return {'value':{'writeoff_amount': 0.0}}
201 # resolve lists of commands into lists of dicts
202 line_dr_ids = self.resolve_2many_commands(cr, uid, 'line_dr_ids', line_dr_ids, ['amount'], context)
203 line_cr_ids = self.resolve_2many_commands(cr, uid, 'line_cr_ids', line_cr_ids, ['amount'], context)
204 #compute the field is_multi_currency that is used to hide/display options linked to secondary currency on the voucher
205 is_multi_currency = False
206 #loop on the voucher lines to see if one of these has a secondary currency. If yes, we need to see the options
207 for voucher_line in line_dr_ids+line_cr_ids:
208 line_id = voucher_line.get('id') and self.pool.get('account.voucher.line').browse(cr, uid, voucher_line['id'], context=context).move_line_id.id or voucher_line.get('move_line_id')
209 if line_id and self.pool.get('account.move.line').browse(cr, uid, line_id, context=context).currency_id:
210 is_multi_currency = True
212 return {'value': {'writeoff_amount': self._compute_writeoff_amount(cr, uid, line_dr_ids, line_cr_ids, amount, type), 'is_multi_currency': is_multi_currency}}
214 def _get_journal_currency(self, cr, uid, ids, name, args, context=None):
216 for voucher in self.browse(cr, uid, ids, context=context):
217 res[voucher.id] = voucher.journal_id.currency and voucher.journal_id.currency.id or voucher.company_id.currency_id.id
220 def _get_writeoff_amount(self, cr, uid, ids, name, args, context=None):
221 if not ids: return {}
222 currency_obj = self.pool.get('res.currency')
225 for voucher in self.browse(cr, uid, ids, context=context):
226 sign = voucher.type == 'payment' and -1 or 1
227 for l in voucher.line_dr_ids:
229 for l in voucher.line_cr_ids:
231 currency = voucher.currency_id or voucher.company_id.currency_id
232 res[voucher.id] = currency_obj.round(cr, uid, currency, voucher.amount - sign * (credit - debit))
235 def _paid_amount_in_company_currency(self, cr, uid, ids, name, args, context=None):
240 for v in self.browse(cr, uid, ids, context=context):
241 ctx.update({'date': v.date})
242 #make a new call to browse in order to have the right date in the context, to get the right currency rate
243 voucher = self.browse(cr, uid, v.id, context=ctx)
245 'voucher_special_currency': voucher.payment_rate_currency_id and voucher.payment_rate_currency_id.id or False,
246 'voucher_special_currency_rate': voucher.currency_id.rate * voucher.payment_rate,})
247 res[voucher.id] = self.pool.get('res.currency').compute(cr, uid, voucher.currency_id.id, voucher.company_id.currency_id.id, voucher.amount, context=ctx)
250 def _get_currency_help_label(self, cr, uid, currency_id, payment_rate, payment_rate_currency_id, context=None):
252 This function builds a string to help the users to understand the behavior of the payment rate fields they can specify on the voucher.
253 This string is only used to improve the usability in the voucher form view and has no other effect.
255 :param currency_id: the voucher currency
256 :type currency_id: integer
257 :param payment_rate: the value of the payment_rate field of the voucher
258 :type payment_rate: float
259 :param payment_rate_currency_id: the value of the payment_rate_currency_id field of the voucher
260 :type payment_rate_currency_id: integer
261 :return: translated string giving a tip on what's the effect of the current payment rate specified
264 rml_parser = report_sxw.rml_parse(cr, uid, 'currency_help_label', context=context)
265 currency_pool = self.pool.get('res.currency')
266 currency_str = payment_rate_str = ''
268 currency_str = rml_parser.formatLang(1, currency_obj=currency_pool.browse(cr, uid, currency_id, context=context))
269 if payment_rate_currency_id:
270 payment_rate_str = rml_parser.formatLang(payment_rate, currency_obj=currency_pool.browse(cr, uid, payment_rate_currency_id, context=context))
271 currency_help_label = _('At the operation date, the exchange rate was\n%s = %s') % (currency_str, payment_rate_str)
272 return currency_help_label
274 def _fnct_currency_help_label(self, cr, uid, ids, name, args, context=None):
276 for voucher in self.browse(cr, uid, ids, context=context):
277 res[voucher.id] = self._get_currency_help_label(cr, uid, voucher.currency_id.id, voucher.payment_rate, voucher.payment_rate_currency_id.id, context=context)
280 _name = 'account.voucher'
281 _description = 'Accounting Voucher'
282 _inherit = ['mail.thread']
283 _order = "date desc, id desc"
284 # _rec_name = 'number'
287 'account_voucher.mt_voucher_state_change': lambda self, cr, uid, obj, ctx=None: True,
292 'type':fields.selection([
294 ('purchase','Purchase'),
295 ('payment','Payment'),
296 ('receipt','Receipt'),
297 ],'Default Type', readonly=True, states={'draft':[('readonly',False)]}),
298 'name':fields.char('Memo', readonly=True, states={'draft':[('readonly',False)]}),
299 'date':fields.date('Date', readonly=True, select=True, states={'draft':[('readonly',False)]},
300 help="Effective date for accounting entries", copy=False),
301 'journal_id':fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
302 'account_id':fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}),
303 'line_ids':fields.one2many('account.voucher.line', 'voucher_id', 'Voucher Lines',
304 readonly=True, copy=True,
305 states={'draft':[('readonly',False)]}),
306 'line_cr_ids':fields.one2many('account.voucher.line','voucher_id','Credits',
307 domain=[('type','=','cr')], context={'default_type':'cr'}, readonly=True, states={'draft':[('readonly',False)]}),
308 'line_dr_ids':fields.one2many('account.voucher.line','voucher_id','Debits',
309 domain=[('type','=','dr')], context={'default_type':'dr'}, readonly=True, states={'draft':[('readonly',False)]}),
310 'period_id': fields.many2one('account.period', 'Period', required=True, readonly=True, states={'draft':[('readonly',False)]}),
311 'narration':fields.text('Notes', readonly=True, states={'draft':[('readonly',False)]}),
312 'currency_id': fields.function(_get_journal_currency, type='many2one', relation='res.currency', string='Currency', readonly=True, required=True),
313 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
314 'state':fields.selection(
316 ('cancel','Cancelled'),
317 ('proforma','Pro-forma'),
319 ], 'Status', readonly=True, track_visibility='onchange', copy=False,
320 help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed Voucher. \
321 \n* The \'Pro-forma\' when voucher is in Pro-forma status,voucher does not have an voucher number. \
322 \n* The \'Posted\' status is used when user create voucher,a voucher number is generated and voucher entries are created in account \
323 \n* The \'Cancelled\' status is used when user cancel voucher.'),
324 'amount': fields.float('Total', digits_compute=dp.get_precision('Account'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
325 'tax_amount':fields.float('Tax Amount', digits_compute=dp.get_precision('Account'), readonly=True),
326 'reference': fields.char('Ref #', readonly=True, states={'draft':[('readonly',False)]},
327 help="Transaction reference number.", copy=False),
328 'number': fields.char('Number', readonly=True, copy=False),
329 'move_id':fields.many2one('account.move', 'Account Entry', copy=False),
330 'move_ids': fields.related('move_id','line_id', type='one2many', relation='account.move.line', string='Journal Items', readonly=True),
331 'partner_id':fields.many2one('res.partner', 'Partner', change_default=1, readonly=True, states={'draft':[('readonly',False)]}),
332 'audit': fields.related('move_id','to_check', type='boolean', help='Check this box if you are unsure of that journal entry and if you want to note it as \'to be reviewed\' by an accounting expert.', relation='account.move', string='To Review'),
333 'paid': fields.function(_check_paid, string='Paid', type='boolean', help="The Voucher has been totally paid."),
334 'pay_now':fields.selection([
335 ('pay_now','Pay Directly'),
336 ('pay_later','Pay Later or Group Funds'),
337 ],'Payment', select=True, readonly=True, states={'draft':[('readonly',False)]}),
338 'tax_id': fields.many2one('account.tax', 'Tax', readonly=True, states={'draft':[('readonly',False)]}, domain=[('price_include','=', False)], help="Only for tax excluded from price"),
339 'pre_line':fields.boolean('Previous Payments ?', required=False),
340 'date_due': fields.date('Due Date', readonly=True, select=True, states={'draft':[('readonly',False)]}),
341 'payment_option':fields.selection([
342 ('without_writeoff', 'Keep Open'),
343 ('with_writeoff', 'Reconcile Payment Balance'),
344 ], 'Payment Difference', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="This field helps you to choose what you want to do with the eventual difference between the paid amount and the sum of allocated amounts. You can either choose to keep open this difference on the partner's account, or reconcile it with the payment(s)"),
345 'writeoff_acc_id': fields.many2one('account.account', 'Counterpart Account', readonly=True, states={'draft': [('readonly', False)]}),
346 'comment': fields.char('Counterpart Comment', required=True, readonly=True, states={'draft': [('readonly', False)]}),
347 'analytic_id': fields.many2one('account.analytic.account','Write-Off Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
348 'writeoff_amount': fields.function(_get_writeoff_amount, string='Difference Amount', type='float', readonly=True, help="Computed as the difference between the amount stated in the voucher and the sum of allocation on the voucher lines."),
349 'payment_rate_currency_id': fields.many2one('res.currency', 'Payment Rate Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}),
350 'payment_rate': fields.float('Exchange Rate', digits=(12,6), required=True, readonly=True, states={'draft': [('readonly', False)]},
351 help='The specific rate that will be used, in this voucher, between the selected currency (in \'Payment Rate Currency\' field) and the voucher currency.'),
352 'paid_amount_in_company_currency': fields.function(_paid_amount_in_company_currency, string='Paid Amount in Company Currency', type='float', readonly=True),
353 'is_multi_currency': fields.boolean('Multi Currency Voucher', help='Fields with internal purpose only that depicts if the voucher is a multi currency one or not'),
354 'currency_help_label': fields.function(_fnct_currency_help_label, type='text', string="Helping Sentence", help="This sentence helps you to know how to specify the payment rate by giving you the direct effect it has"),
357 'period_id': _get_period,
358 'partner_id': _get_partner,
359 'journal_id':_get_journal,
360 'currency_id': _get_currency,
361 'reference': _get_reference,
362 'narration':_get_narration,
363 'amount': _get_amount,
366 'pay_now': 'pay_now',
368 'date': lambda *a: time.strftime('%Y-%m-%d'),
369 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.voucher',context=c),
371 'payment_option': 'without_writeoff',
372 'comment': _('Write-Off'),
374 'payment_rate_currency_id': _get_payment_rate_currency,
377 def compute_tax(self, cr, uid, ids, context=None):
378 tax_pool = self.pool.get('account.tax')
379 partner_pool = self.pool.get('res.partner')
380 position_pool = self.pool.get('account.fiscal.position')
381 voucher_line_pool = self.pool.get('account.voucher.line')
382 voucher_pool = self.pool.get('account.voucher')
383 if context is None: context = {}
385 for voucher in voucher_pool.browse(cr, uid, ids, context=context):
387 for line in voucher.line_ids:
388 voucher_amount += line.untax_amount or line.amount
389 line.amount = line.untax_amount or line.amount
390 voucher_line_pool.write(cr, uid, [line.id], {'amount':line.amount, 'untax_amount':line.untax_amount})
392 if not voucher.tax_id:
393 self.write(cr, uid, [voucher.id], {'amount':voucher_amount, 'tax_amount':0.0})
396 tax = [tax_pool.browse(cr, uid, voucher.tax_id.id, context=context)]
397 partner = partner_pool.browse(cr, uid, voucher.partner_id.id, context=context) or False
398 taxes = position_pool.map_tax(cr, uid, partner and partner.property_account_position or False, tax)
399 tax = tax_pool.browse(cr, uid, taxes, context=context)
401 total = voucher_amount
404 if not tax[0].price_include:
405 for line in voucher.line_ids:
406 for tax_line in tax_pool.compute_all(cr, uid, tax, line.amount, 1).get('taxes', []):
407 total_tax += tax_line.get('amount', 0.0)
410 for line in voucher.line_ids:
414 for tax_line in tax_pool.compute_all(cr, uid, tax, line.untax_amount or line.amount, 1).get('taxes', []):
415 line_tax += tax_line.get('amount', 0.0)
416 line_total += tax_line.get('price_unit')
417 total_tax += line_tax
418 untax_amount = line.untax_amount or line.amount
419 voucher_line_pool.write(cr, uid, [line.id], {'amount':line_total, 'untax_amount':untax_amount})
421 self.write(cr, uid, [voucher.id], {'amount':total, 'tax_amount':total_tax})
424 def onchange_price(self, cr, uid, ids, line_ids, tax_id, partner_id=False, context=None):
425 context = context or {}
426 tax_pool = self.pool.get('account.tax')
427 partner_pool = self.pool.get('res.partner')
428 position_pool = self.pool.get('account.fiscal.position')
437 # resolve the list of commands into a list of dicts
438 line_ids = self.resolve_2many_commands(cr, uid, 'line_ids', line_ids, ['amount'], context)
441 for line in line_ids:
443 line_amount = line.get('amount',0.0)
446 tax = [tax_pool.browse(cr, uid, tax_id, context=context)]
448 partner = partner_pool.browse(cr, uid, partner_id, context=context) or False
449 taxes = position_pool.map_tax(cr, uid, partner and partner.property_account_position or False, tax)
450 tax = tax_pool.browse(cr, uid, taxes, context=context)
452 if not tax[0].price_include:
453 for tax_line in tax_pool.compute_all(cr, uid, tax, line_amount, 1).get('taxes', []):
454 total_tax += tax_line.get('amount')
456 voucher_total += line_amount
457 total = voucher_total + total_tax
460 'amount': total or voucher_total,
461 'tax_amount': total_tax
467 def onchange_term_id(self, cr, uid, ids, term_id, amount):
468 term_pool = self.pool.get('account.payment.term')
471 default = {'date_due':False}
472 if term_id and amount:
473 terms = term_pool.compute(cr, uid, term_id, amount)
475 due_date = terms[-1][0]
479 return {'value':default}
481 def onchange_journal_voucher(self, cr, uid, ids, line_ids=False, tax_id=False, price=0.0, partner_id=False, journal_id=False, ttype=False, company_id=False, context=None):
483 Returns a dict that contains new values and context
485 @param partner_id: latest value from user input for field partner_id
486 @param args: other arguments
487 @param context: context arguments, like lang, time zone
489 @return: Returns a dict which contains new values, and context
495 if not partner_id or not journal_id:
498 partner_pool = self.pool.get('res.partner')
499 journal_pool = self.pool.get('account.journal')
501 journal = journal_pool.browse(cr, uid, journal_id, context=context)
502 partner = partner_pool.browse(cr, uid, partner_id, context=context)
505 if journal.type in ('sale','sale_refund'):
506 account_id = partner.property_account_receivable.id
508 elif journal.type in ('purchase', 'purchase_refund','expense'):
509 account_id = partner.property_account_payable.id
512 if not journal.default_credit_account_id or not journal.default_debit_account_id:
513 raise osv.except_osv(_('Error!'), _('Please define default credit/debit accounts on the journal "%s".') % (journal.name))
514 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
517 default['value']['account_id'] = account_id
518 default['value']['type'] = ttype or tr_type
520 vals = self.onchange_journal(cr, uid, ids, journal_id, line_ids, tax_id, partner_id, time.strftime('%Y-%m-%d'), price, ttype, company_id, context)
521 default['value'].update(vals.get('value'))
525 def onchange_rate(self, cr, uid, ids, rate, amount, currency_id, payment_rate_currency_id, company_id, context=None):
526 res = {'value': {'paid_amount_in_company_currency': amount, 'currency_help_label': self._get_currency_help_label(cr, uid, currency_id, rate, payment_rate_currency_id, context=context)}}
527 if rate and amount and currency_id:
528 company_currency = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id
529 #context should contain the date, the payment currency and the payment rate specified on the voucher
530 amount_in_company_currency = self.pool.get('res.currency').compute(cr, uid, currency_id, company_currency.id, amount, context=context)
531 res['value']['paid_amount_in_company_currency'] = amount_in_company_currency
534 def onchange_amount(self, cr, uid, ids, amount, rate, partner_id, journal_id, currency_id, ttype, date, payment_rate_currency_id, company_id, context=None):
538 ctx.update({'date': date})
539 #read the voucher rate with the right date in the context
540 currency_id = currency_id or self.pool.get('res.company').browse(cr, uid, company_id, context=ctx).currency_id.id
541 voucher_rate = self.pool.get('res.currency').read(cr, uid, [currency_id], ['rate'], context=ctx)[0]['rate']
543 'voucher_special_currency': payment_rate_currency_id,
544 'voucher_special_currency_rate': rate * voucher_rate})
545 res = self.recompute_voucher_lines(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=ctx)
546 vals = self.onchange_rate(cr, uid, ids, rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
547 for key in vals.keys():
548 res[key].update(vals[key])
551 def recompute_payment_rate(self, cr, uid, ids, vals, currency_id, date, ttype, journal_id, amount, context=None):
554 #on change of the journal, we need to set also the default value for payment_rate and payment_rate_currency_id
555 currency_obj = self.pool.get('res.currency')
556 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
557 company_id = journal.company_id.id
559 currency_id = currency_id or journal.company_id.currency_id.id
560 payment_rate_currency_id = currency_id
562 ctx.update({'date': date})
564 if ttype == 'receipt':
565 o2m_to_loop = 'line_cr_ids'
566 elif ttype == 'payment':
567 o2m_to_loop = 'line_dr_ids'
568 if o2m_to_loop and 'value' in vals and o2m_to_loop in vals['value']:
569 for voucher_line in vals['value'][o2m_to_loop]:
570 if not isinstance(voucher_line, dict):
572 if voucher_line['currency_id'] != currency_id:
573 # we take as default value for the payment_rate_currency_id, the currency of the first invoice that
574 # is not in the voucher currency
575 payment_rate_currency_id = voucher_line['currency_id']
576 tmp = currency_obj.browse(cr, uid, payment_rate_currency_id, context=ctx).rate
577 payment_rate = tmp / currency_obj.browse(cr, uid, currency_id, context=ctx).rate
579 vals['value'].update({
580 'payment_rate': payment_rate,
581 'currency_id': currency_id,
582 'payment_rate_currency_id': payment_rate_currency_id
584 #read the voucher rate with the right date in the context
585 voucher_rate = self.pool.get('res.currency').read(cr, uid, [currency_id], ['rate'], context=ctx)[0]['rate']
587 'voucher_special_currency_rate': payment_rate * voucher_rate,
588 'voucher_special_currency': payment_rate_currency_id})
589 res = self.onchange_rate(cr, uid, ids, payment_rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
590 for key in res.keys():
591 vals[key].update(res[key])
594 def basic_onchange_partner(self, cr, uid, ids, partner_id, journal_id, ttype, context=None):
595 partner_pool = self.pool.get('res.partner')
596 journal_pool = self.pool.get('account.journal')
597 res = {'value': {'account_id': False}}
598 if not partner_id or not journal_id:
601 journal = journal_pool.browse(cr, uid, journal_id, context=context)
602 partner = partner_pool.browse(cr, uid, partner_id, context=context)
604 if journal.type in ('sale','sale_refund'):
605 account_id = partner.property_account_receivable.id
606 elif journal.type in ('purchase', 'purchase_refund','expense'):
607 account_id = partner.property_account_payable.id
609 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
611 res['value']['account_id'] = account_id
614 def onchange_partner_id(self, cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=None):
619 #TODO: comment me and use me directly in the sales/purchases views
620 res = self.basic_onchange_partner(cr, uid, ids, partner_id, journal_id, ttype, context=context)
621 if ttype in ['sale', 'purchase']:
624 # not passing the payment_rate currency and the payment_rate in the context but it's ok because they are reset in recompute_payment_rate
625 ctx.update({'date': date})
626 vals = self.recompute_voucher_lines(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=ctx)
627 vals2 = self.recompute_payment_rate(cr, uid, ids, vals, currency_id, date, ttype, journal_id, amount, context=context)
628 for key in vals.keys():
629 res[key].update(vals[key])
630 for key in vals2.keys():
631 res[key].update(vals2[key])
632 #TODO: can probably be removed now
633 #TODO: onchange_partner_id() should not returns [pre_line, line_dr_ids, payment_rate...] for type sale, and not
634 # [pre_line, line_cr_ids, payment_rate...] for type purchase.
635 # We should definitively split account.voucher object in two and make distinct on_change functions. In the
636 # meanwhile, bellow lines must be there because the fields aren't present in the view, what crashes if the
637 # onchange returns a value for them
639 del(res['value']['line_dr_ids'])
640 del(res['value']['pre_line'])
641 del(res['value']['payment_rate'])
642 elif ttype == 'purchase':
643 del(res['value']['line_cr_ids'])
644 del(res['value']['pre_line'])
645 del(res['value']['payment_rate'])
648 def recompute_voucher_lines(self, cr, uid, ids, partner_id, journal_id, price, currency_id, ttype, date, context=None):
650 Returns a dict that contains new values and context
652 @param partner_id: latest value from user input for field partner_id
653 @param args: other arguments
654 @param context: context arguments, like lang, time zone
656 @return: Returns a dict which contains new values, and context
658 def _remove_noise_in_o2m():
659 """if the line is partially reconciled, then we must pay attention to display it only once and
661 This function returns True if the line is considered as noise and should not be displayed
663 if line.reconcile_partial_id:
664 if currency_id == line.currency_id.id:
665 if line.amount_residual_currency <= 0:
668 if line.amount_residual <= 0:
674 context_multi_currency = context.copy()
676 currency_pool = self.pool.get('res.currency')
677 move_line_pool = self.pool.get('account.move.line')
678 partner_pool = self.pool.get('res.partner')
679 journal_pool = self.pool.get('account.journal')
680 line_pool = self.pool.get('account.voucher.line')
684 'value': {'line_dr_ids': [], 'line_cr_ids': [], 'pre_line': False},
687 # drop existing lines
688 line_ids = ids and line_pool.search(cr, uid, [('voucher_id', '=', ids[0])])
689 for line in line_pool.browse(cr, uid, line_ids, context=context):
690 if line.type == 'cr':
691 default['value']['line_cr_ids'].append((2, line.id))
693 default['value']['line_dr_ids'].append((2, line.id))
695 if not partner_id or not journal_id:
698 journal = journal_pool.browse(cr, uid, journal_id, context=context)
699 partner = partner_pool.browse(cr, uid, partner_id, context=context)
700 currency_id = currency_id or journal.company_id.currency_id.id
705 if context.get('account_id'):
706 account_type = self.pool['account.account'].browse(cr, uid, context['account_id'], context=context).type
707 if ttype == 'payment':
709 account_type = 'payable'
710 total_debit = price or 0.0
712 total_credit = price or 0.0
714 account_type = 'receivable'
716 if not context.get('move_line_ids', False):
717 ids = move_line_pool.search(cr, uid, [('state','=','valid'), ('account_id.type', '=', account_type), ('reconcile_id', '=', False), ('partner_id', '=', partner_id)], context=context)
719 ids = context['move_line_ids']
720 invoice_id = context.get('invoice_id', False)
721 company_currency = journal.company_id.currency_id.id
722 move_lines_found = []
724 #order the lines by most old first
726 account_move_lines = move_line_pool.browse(cr, uid, ids, context=context)
728 #compute the total debit/credit and look for a matching open amount or invoice
729 for line in account_move_lines:
730 if _remove_noise_in_o2m():
734 if line.invoice.id == invoice_id:
735 #if the invoice linked to the voucher line is equal to the invoice_id in context
736 #then we assign the amount on that line, whatever the other voucher lines
737 move_lines_found.append(line.id)
738 elif currency_id == company_currency:
739 #otherwise treatments is the same but with other field names
740 if line.amount_residual == price:
741 #if the amount residual is equal the amount voucher, we assign it to that voucher
742 #line, whatever the other voucher lines
743 move_lines_found.append(line.id)
745 #otherwise we will split the voucher amount on each line (by most old first)
746 total_credit += line.credit or 0.0
747 total_debit += line.debit or 0.0
748 elif currency_id == line.currency_id.id:
749 if line.amount_residual_currency == price:
750 move_lines_found.append(line.id)
752 total_credit += line.credit and line.amount_currency or 0.0
753 total_debit += line.debit and line.amount_currency or 0.0
755 remaining_amount = price
756 #voucher line creation
757 for line in account_move_lines:
759 if _remove_noise_in_o2m():
762 if line.currency_id and currency_id == line.currency_id.id:
763 amount_original = abs(line.amount_currency)
764 amount_unreconciled = abs(line.amount_residual_currency)
766 #always use the amount booked in the company currency as the basis of the conversion into the voucher currency
767 amount_original = currency_pool.compute(cr, uid, company_currency, currency_id, line.credit or line.debit or 0.0, context=context_multi_currency)
768 amount_unreconciled = currency_pool.compute(cr, uid, company_currency, currency_id, abs(line.amount_residual), context=context_multi_currency)
769 line_currency_id = line.currency_id and line.currency_id.id or company_currency
771 'name':line.move_id.name,
772 'type': line.credit and 'dr' or 'cr',
773 'move_line_id':line.id,
774 'account_id':line.account_id.id,
775 'amount_original': amount_original,
776 'amount': (line.id in move_lines_found) and min(abs(remaining_amount), amount_unreconciled) or 0.0,
777 'date_original':line.date,
778 'date_due':line.date_maturity,
779 'amount_unreconciled': amount_unreconciled,
780 'currency_id': line_currency_id,
782 remaining_amount -= rs['amount']
783 #in case a corresponding move_line hasn't been found, we now try to assign the voucher amount
784 #on existing invoices: we split voucher amount by most old first, but only for lines in the same currency
785 if not move_lines_found:
786 if currency_id == line_currency_id:
788 amount = min(amount_unreconciled, abs(total_debit))
789 rs['amount'] = amount
790 total_debit -= amount
792 amount = min(amount_unreconciled, abs(total_credit))
793 rs['amount'] = amount
794 total_credit -= amount
796 if rs['amount_unreconciled'] == rs['amount']:
797 rs['reconcile'] = True
799 if rs['type'] == 'cr':
800 default['value']['line_cr_ids'].append(rs)
802 default['value']['line_dr_ids'].append(rs)
804 if len(default['value']['line_cr_ids']) > 0:
805 default['value']['pre_line'] = 1
806 elif len(default['value']['line_dr_ids']) > 0:
807 default['value']['pre_line'] = 1
808 default['value']['writeoff_amount'] = self._compute_writeoff_amount(cr, uid, default['value']['line_dr_ids'], default['value']['line_cr_ids'], price, ttype)
811 def onchange_payment_rate_currency(self, cr, uid, ids, currency_id, payment_rate, payment_rate_currency_id, date, amount, company_id, context=None):
816 #set the default payment rate of the voucher and compute the paid amount in company currency
818 ctx.update({'date': date})
819 #read the voucher rate with the right date in the context
820 voucher_rate = self.pool.get('res.currency').read(cr, uid, [currency_id], ['rate'], context=ctx)[0]['rate']
822 'voucher_special_currency_rate': payment_rate * voucher_rate,
823 'voucher_special_currency': payment_rate_currency_id})
824 vals = self.onchange_rate(cr, uid, ids, payment_rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
825 for key in vals.keys():
826 res[key].update(vals[key])
829 def onchange_date(self, cr, uid, ids, date, currency_id, payment_rate_currency_id, amount, company_id, context=None):
831 @param date: latest value from user input for field date
832 @param args: other arguments
833 @param context: context arguments, like lang, time zone
834 @return: Returns a dict which contains new values, and context
839 #set the period of the voucher
840 period_pool = self.pool.get('account.period')
841 currency_obj = self.pool.get('res.currency')
843 ctx.update({'company_id': company_id, 'account_period_prefer_normal': True})
844 voucher_currency_id = currency_id or self.pool.get('res.company').browse(cr, uid, company_id, context=ctx).currency_id.id
845 pids = period_pool.find(cr, uid, date, context=ctx)
847 res['value'].update({'period_id':pids[0]})
848 if payment_rate_currency_id:
849 ctx.update({'date': date})
851 if payment_rate_currency_id != currency_id:
852 tmp = currency_obj.browse(cr, uid, payment_rate_currency_id, context=ctx).rate
853 payment_rate = tmp / currency_obj.browse(cr, uid, voucher_currency_id, context=ctx).rate
854 vals = self.onchange_payment_rate_currency(cr, uid, ids, voucher_currency_id, payment_rate, payment_rate_currency_id, date, amount, company_id, context=context)
855 vals['value'].update({'payment_rate': payment_rate})
856 for key in vals.keys():
857 res[key].update(vals[key])
860 def onchange_journal(self, cr, uid, ids, journal_id, line_ids, tax_id, partner_id, date, amount, ttype, company_id, context=None):
865 journal_pool = self.pool.get('account.journal')
866 journal = journal_pool.browse(cr, uid, journal_id, context=context)
867 account_id = journal.default_credit_account_id or journal.default_debit_account_id
869 if account_id and account_id.tax_ids:
870 tax_id = account_id.tax_ids[0].id
873 if ttype in ('sale', 'purchase'):
874 vals = self.onchange_price(cr, uid, ids, line_ids, tax_id, partner_id, context)
875 vals['value'].update({'tax_id':tax_id,'amount': amount})
878 currency_id = journal.currency.id
880 currency_id = journal.company_id.currency_id.id
881 vals['value'].update({'currency_id': currency_id})
882 #in case we want to register the payment directly from an invoice, it's confusing to allow to switch the journal
883 #without seeing that the amount is expressed in the journal currency, and not in the invoice currency. So to avoid
884 #this common mistake, we simply reset the amount to 0 if the currency is not the invoice currency.
885 if context.get('payment_expected_currency') and currency_id != context.get('payment_expected_currency'):
886 vals['value']['amount'] = 0
889 res = self.onchange_partner_id(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context)
890 for key in res.keys():
891 vals[key].update(res[key])
894 def button_proforma_voucher(self, cr, uid, ids, context=None):
895 self.signal_workflow(cr, uid, ids, 'proforma_voucher')
896 return {'type': 'ir.actions.act_window_close'}
898 def proforma_voucher(self, cr, uid, ids, context=None):
899 self.action_move_line_create(cr, uid, ids, context=context)
902 def action_cancel_draft(self, cr, uid, ids, context=None):
903 self.create_workflow(cr, uid, ids)
904 self.write(cr, uid, ids, {'state':'draft'})
907 def cancel_voucher(self, cr, uid, ids, context=None):
908 reconcile_pool = self.pool.get('account.move.reconcile')
909 move_pool = self.pool.get('account.move')
910 move_line_pool = self.pool.get('account.move.line')
911 for voucher in self.browse(cr, uid, ids, context=context):
912 # refresh to make sure you don't unlink an already removed move
914 for line in voucher.move_ids:
915 # refresh to make sure you don't unreconcile an already unreconciled entry
917 if line.reconcile_id:
918 move_lines = [move_line.id for move_line in line.reconcile_id.line_id]
919 move_lines.remove(line.id)
920 reconcile_pool.unlink(cr, uid, [line.reconcile_id.id])
921 if len(move_lines) >= 2:
922 move_line_pool.reconcile_partial(cr, uid, move_lines, 'auto',context=context)
924 move_pool.button_cancel(cr, uid, [voucher.move_id.id])
925 move_pool.unlink(cr, uid, [voucher.move_id.id])
930 self.write(cr, uid, ids, res)
933 def unlink(self, cr, uid, ids, context=None):
934 for t in self.read(cr, uid, ids, ['state'], context=context):
935 if t['state'] not in ('draft', 'cancel'):
936 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete voucher(s) which are already opened or paid.'))
937 return super(account_voucher, self).unlink(cr, uid, ids, context=context)
939 def onchange_payment(self, cr, uid, ids, pay_now, journal_id, partner_id, ttype='sale'):
944 partner_pool = self.pool.get('res.partner')
945 journal_pool = self.pool.get('account.journal')
946 if pay_now == 'pay_later':
947 partner = partner_pool.browse(cr, uid, partner_id)
948 journal = journal_pool.browse(cr, uid, journal_id)
949 if journal.type in ('sale','sale_refund'):
950 account_id = partner.property_account_receivable.id
951 elif journal.type in ('purchase', 'purchase_refund','expense'):
952 account_id = partner.property_account_payable.id
954 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
956 res['account_id'] = account_id
959 def _sel_context(self, cr, uid, voucher_id, context=None):
961 Select the context to use accordingly if it needs to be multicurrency or not.
963 :param voucher_id: Id of the actual voucher
964 :return: The returned context will be the same as given in parameter if the voucher currency is the same
965 than the company currency, otherwise it's a copy of the parameter with an extra key 'date' containing
966 the date of the voucher.
969 company_currency = self._get_company_currency(cr, uid, voucher_id, context)
970 current_currency = self._get_current_currency(cr, uid, voucher_id, context)
971 if current_currency <> company_currency:
972 context_multi_currency = context.copy()
973 voucher = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context)
974 context_multi_currency.update({'date': voucher.date})
975 return context_multi_currency
978 def first_move_line_get(self, cr, uid, voucher_id, move_id, company_currency, current_currency, context=None):
980 Return a dict to be use to create the first account move line of given voucher.
982 :param voucher_id: Id of voucher what we are creating account_move.
983 :param move_id: Id of account move where this line will be added.
984 :param company_currency: id of currency of the company to which the voucher belong
985 :param current_currency: id of currency of the voucher
986 :return: mapping between fieldname and value of account move line to create
989 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
991 # TODO: is there any other alternative then the voucher type ??
992 # ANSWER: We can have payment and receipt "In Advance".
993 # TODO: Make this logic available.
994 # -for sale, purchase we have but for the payment and receipt we do not have as based on the bank/cash journal we can not know its payment or receipt
995 if voucher.type in ('purchase', 'payment'):
996 credit = voucher.paid_amount_in_company_currency
997 elif voucher.type in ('sale', 'receipt'):
998 debit = voucher.paid_amount_in_company_currency
999 if debit < 0: credit = -debit; debit = 0.0
1000 if credit < 0: debit = -credit; credit = 0.0
1001 sign = debit - credit < 0 and -1 or 1
1002 #set the first line of the voucher
1004 'name': voucher.name or '/',
1007 'account_id': voucher.account_id.id,
1009 'journal_id': voucher.journal_id.id,
1010 'period_id': voucher.period_id.id,
1011 'partner_id': voucher.partner_id.id,
1012 'currency_id': company_currency <> current_currency and current_currency or False,
1013 'amount_currency': (sign * abs(voucher.amount) # amount < 0 for refunds
1014 if company_currency != current_currency else 0.0),
1015 'date': voucher.date,
1016 'date_maturity': voucher.date_due
1020 def account_move_get(self, cr, uid, voucher_id, context=None):
1022 This method prepare the creation of the account move related to the given voucher.
1024 :param voucher_id: Id of voucher for which we are creating account_move.
1025 :return: mapping between fieldname and value of account move to create
1028 seq_obj = self.pool.get('ir.sequence')
1029 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1031 name = voucher.number
1032 elif voucher.journal_id.sequence_id:
1033 if not voucher.journal_id.sequence_id.active:
1034 raise osv.except_osv(_('Configuration Error !'),
1035 _('Please activate the sequence of selected journal !'))
1037 c.update({'fiscalyear_id': voucher.period_id.fiscalyear_id.id})
1038 name = seq_obj.next_by_id(cr, uid, voucher.journal_id.sequence_id.id, context=c)
1040 raise osv.except_osv(_('Error!'),
1041 _('Please define a sequence on the journal.'))
1042 if not voucher.reference:
1043 ref = name.replace('/','')
1045 ref = voucher.reference
1049 'journal_id': voucher.journal_id.id,
1050 'narration': voucher.narration,
1051 'date': voucher.date,
1053 'period_id': voucher.period_id.id,
1057 def _get_exchange_lines(self, cr, uid, line, move_id, amount_residual, company_currency, current_currency, context=None):
1059 Prepare the two lines in company currency due to currency rate difference.
1061 :param line: browse record of the voucher.line for which we want to create currency rate difference accounting
1063 :param move_id: Account move wher the move lines will be.
1064 :param amount_residual: Amount to be posted.
1065 :param company_currency: id of currency of the company to which the voucher belong
1066 :param current_currency: id of currency of the voucher
1067 :return: the account move line and its counterpart to create, depicted as mapping between fieldname and value
1068 :rtype: tuple of dict
1070 if amount_residual > 0:
1071 account_id = line.voucher_id.company_id.expense_currency_exchange_account_id
1073 model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'account', 'action_account_form')
1074 msg = _("You should configure the 'Loss Exchange Rate Account' to manage automatically the booking of accounting entries related to differences between exchange rates.")
1075 raise openerp.exceptions.RedirectWarning(msg, action_id, _('Go to the configuration panel'))
1077 account_id = line.voucher_id.company_id.income_currency_exchange_account_id
1079 model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'account', 'action_account_form')
1080 msg = _("You should configure the 'Gain Exchange Rate Account' to manage automatically the booking of accounting entries related to differences between exchange rates.")
1081 raise openerp.exceptions.RedirectWarning(msg, action_id, _('Go to the configuration panel'))
1082 # Even if the amount_currency is never filled, we need to pass the foreign currency because otherwise
1083 # the receivable/payable account may have a secondary currency, which render this field mandatory
1084 if line.account_id.currency_id:
1085 account_currency_id = line.account_id.currency_id.id
1087 account_currency_id = company_currency <> current_currency and current_currency or False
1089 'journal_id': line.voucher_id.journal_id.id,
1090 'period_id': line.voucher_id.period_id.id,
1091 'name': _('change')+': '+(line.name or '/'),
1092 'account_id': line.account_id.id,
1094 'partner_id': line.voucher_id.partner_id.id,
1095 'currency_id': account_currency_id,
1096 'amount_currency': 0.0,
1098 'credit': amount_residual > 0 and amount_residual or 0.0,
1099 'debit': amount_residual < 0 and -amount_residual or 0.0,
1100 'date': line.voucher_id.date,
1102 move_line_counterpart = {
1103 'journal_id': line.voucher_id.journal_id.id,
1104 'period_id': line.voucher_id.period_id.id,
1105 'name': _('change')+': '+(line.name or '/'),
1106 'account_id': account_id.id,
1108 'amount_currency': 0.0,
1109 'partner_id': line.voucher_id.partner_id.id,
1110 'currency_id': account_currency_id,
1112 'debit': amount_residual > 0 and amount_residual or 0.0,
1113 'credit': amount_residual < 0 and -amount_residual or 0.0,
1114 'date': line.voucher_id.date,
1116 return (move_line, move_line_counterpart)
1118 def _convert_amount(self, cr, uid, amount, voucher_id, context=None):
1120 This function convert the amount given in company currency. It takes either the rate in the voucher (if the
1121 payment_rate_currency_id is relevant) either the rate encoded in the system.
1123 :param amount: float. The amount to convert
1124 :param voucher: id of the voucher on which we want the conversion
1125 :param context: to context to use for the conversion. It may contain the key 'date' set to the voucher date
1126 field in order to select the good rate to use.
1127 :return: the amount in the currency of the voucher's company
1132 currency_obj = self.pool.get('res.currency')
1133 voucher = self.browse(cr, uid, voucher_id, context=context)
1134 return currency_obj.compute(cr, uid, voucher.currency_id.id, voucher.company_id.currency_id.id, amount, context=context)
1136 def voucher_move_line_create(self, cr, uid, voucher_id, line_total, move_id, company_currency, current_currency, context=None):
1138 Create one account move line, on the given account move, per voucher line where amount is not 0.0.
1139 It returns Tuple with tot_line what is total of difference between debit and credit and
1140 a list of lists with ids to be reconciled with this format (total_deb_cred,list_of_lists).
1142 :param voucher_id: Voucher id what we are working with
1143 :param line_total: Amount of the first line, which correspond to the amount we should totally split among all voucher lines.
1144 :param move_id: Account move wher those lines will be joined.
1145 :param company_currency: id of currency of the company to which the voucher belong
1146 :param current_currency: id of currency of the voucher
1147 :return: Tuple build as (remaining amount not allocated on voucher lines, list of account_move_line created in this method)
1148 :rtype: tuple(float, list of int)
1152 move_line_obj = self.pool.get('account.move.line')
1153 currency_obj = self.pool.get('res.currency')
1154 tax_obj = self.pool.get('account.tax')
1155 tot_line = line_total
1158 date = self.read(cr, uid, [voucher_id], ['date'], context=context)[0]['date']
1159 ctx = context.copy()
1160 ctx.update({'date': date})
1161 voucher = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context=ctx)
1162 voucher_currency = voucher.journal_id.currency or voucher.company_id.currency_id
1164 'voucher_special_currency_rate': voucher_currency.rate * voucher.payment_rate ,
1165 'voucher_special_currency': voucher.payment_rate_currency_id and voucher.payment_rate_currency_id.id or False,})
1166 prec = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')
1167 for line in voucher.line_ids:
1168 #create one move line per voucher line where amount is not 0.0
1169 # AND (second part of the clause) only if the original move line was not having debit = credit = 0 (which is a legal value)
1170 if not line.amount and not (line.move_line_id and not float_compare(line.move_line_id.debit, line.move_line_id.credit, precision_digits=prec) and not float_compare(line.move_line_id.debit, 0.0, precision_digits=prec)):
1172 # convert the amount set on the voucher line into the currency of the voucher's company
1173 # this calls res_curreny.compute() with the right context, so that it will take either the rate on the voucher if it is relevant or will use the default behaviour
1174 amount = self._convert_amount(cr, uid, line.untax_amount or line.amount, voucher.id, context=ctx)
1175 # if the amount encoded in voucher is equal to the amount unreconciled, we need to compute the
1176 # currency rate difference
1177 if line.amount == line.amount_unreconciled:
1178 if not line.move_line_id:
1179 raise osv.except_osv(_('Wrong voucher line'),_("The invoice you are willing to pay is not valid anymore."))
1180 sign = line.type =='dr' and -1 or 1
1181 currency_rate_difference = sign * (line.move_line_id.amount_residual - amount)
1183 currency_rate_difference = 0.0
1185 'journal_id': voucher.journal_id.id,
1186 'period_id': voucher.period_id.id,
1187 'name': line.name or '/',
1188 'account_id': line.account_id.id,
1190 'partner_id': voucher.partner_id.id,
1191 'currency_id': line.move_line_id and (company_currency <> line.move_line_id.currency_id.id and line.move_line_id.currency_id.id) or False,
1192 'analytic_account_id': line.account_analytic_id and line.account_analytic_id.id or False,
1196 'date': voucher.date
1200 if line.type == 'dr':
1205 if (line.type=='dr'):
1207 move_line['debit'] = amount
1210 move_line['credit'] = amount
1212 if voucher.tax_id and voucher.type in ('sale', 'purchase'):
1214 'account_tax_id': voucher.tax_id.id,
1217 if move_line.get('account_tax_id', False):
1218 tax_data = tax_obj.browse(cr, uid, [move_line['account_tax_id']], context=context)[0]
1219 if not (tax_data.base_code_id and tax_data.tax_code_id):
1220 raise osv.except_osv(_('No Account Base Code and Account Tax Code!'),_("You have to configure account base code and account tax code on the '%s' tax!") % (tax_data.name))
1222 # compute the amount in foreign currency
1223 foreign_currency_diff = 0.0
1224 amount_currency = False
1225 if line.move_line_id:
1226 # We want to set it on the account move line as soon as the original line had a foreign currency
1227 if line.move_line_id.currency_id and line.move_line_id.currency_id.id != company_currency:
1228 # we compute the amount in that foreign currency.
1229 if line.move_line_id.currency_id.id == current_currency:
1230 # if the voucher and the voucher line share the same currency, there is no computation to do
1231 sign = (move_line['debit'] - move_line['credit']) < 0 and -1 or 1
1232 amount_currency = sign * (line.amount)
1234 # if the rate is specified on the voucher, it will be used thanks to the special keys in the context
1235 # otherwise we use the rates of the system
1236 amount_currency = currency_obj.compute(cr, uid, company_currency, line.move_line_id.currency_id.id, move_line['debit']-move_line['credit'], context=ctx)
1237 if line.amount == line.amount_unreconciled:
1238 foreign_currency_diff = line.move_line_id.amount_residual_currency - abs(amount_currency)
1240 move_line['amount_currency'] = amount_currency
1241 voucher_line = move_line_obj.create(cr, uid, move_line)
1242 rec_ids = [voucher_line, line.move_line_id.id]
1244 if not currency_obj.is_zero(cr, uid, voucher.company_id.currency_id, currency_rate_difference):
1245 # Change difference entry in company currency
1246 exch_lines = self._get_exchange_lines(cr, uid, line, move_id, currency_rate_difference, company_currency, current_currency, context=context)
1247 new_id = move_line_obj.create(cr, uid, exch_lines[0],context)
1248 move_line_obj.create(cr, uid, exch_lines[1], context)
1249 rec_ids.append(new_id)
1251 if line.move_line_id and line.move_line_id.currency_id and not currency_obj.is_zero(cr, uid, line.move_line_id.currency_id, foreign_currency_diff):
1252 # Change difference entry in voucher currency
1253 move_line_foreign_currency = {
1254 'journal_id': line.voucher_id.journal_id.id,
1255 'period_id': line.voucher_id.period_id.id,
1256 'name': _('change')+': '+(line.name or '/'),
1257 'account_id': line.account_id.id,
1259 'partner_id': line.voucher_id.partner_id.id,
1260 'currency_id': line.move_line_id.currency_id.id,
1261 'amount_currency': -1 * foreign_currency_diff,
1265 'date': line.voucher_id.date,
1267 new_id = move_line_obj.create(cr, uid, move_line_foreign_currency, context=context)
1268 rec_ids.append(new_id)
1269 if line.move_line_id.id:
1270 rec_lst_ids.append(rec_ids)
1271 return (tot_line, rec_lst_ids)
1273 def writeoff_move_line_get(self, cr, uid, voucher_id, line_total, move_id, name, company_currency, current_currency, context=None):
1275 Set a dict to be use to create the writeoff move line.
1277 :param voucher_id: Id of voucher what we are creating account_move.
1278 :param line_total: Amount remaining to be allocated on lines.
1279 :param move_id: Id of account move where this line will be added.
1280 :param name: Description of account move line.
1281 :param company_currency: id of currency of the company to which the voucher belong
1282 :param current_currency: id of currency of the voucher
1283 :return: mapping between fieldname and value of account move line to create
1286 currency_obj = self.pool.get('res.currency')
1289 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1290 current_currency_obj = voucher.currency_id or voucher.journal_id.company_id.currency_id
1292 if not currency_obj.is_zero(cr, uid, current_currency_obj, line_total):
1296 if voucher.payment_option == 'with_writeoff':
1297 account_id = voucher.writeoff_acc_id.id
1298 write_off_name = voucher.comment
1299 elif voucher.partner_id:
1300 if voucher.type in ('sale', 'receipt'):
1301 account_id = voucher.partner_id.property_account_receivable.id
1303 account_id = voucher.partner_id.property_account_payable.id
1305 # fallback on account of voucher
1306 account_id = voucher.account_id.id
1307 sign = voucher.type == 'payment' and -1 or 1
1309 'name': write_off_name or name,
1310 'account_id': account_id,
1312 'partner_id': voucher.partner_id.id,
1313 'date': voucher.date,
1314 'credit': diff > 0 and diff or 0.0,
1315 'debit': diff < 0 and -diff or 0.0,
1316 'amount_currency': company_currency <> current_currency and (sign * -1 * voucher.writeoff_amount) or 0.0,
1317 'currency_id': company_currency <> current_currency and current_currency or False,
1318 'analytic_account_id': voucher.analytic_id and voucher.analytic_id.id or False,
1323 def _get_company_currency(self, cr, uid, voucher_id, context=None):
1325 Get the currency of the actual company.
1327 :param voucher_id: Id of the voucher what i want to obtain company currency.
1328 :return: currency id of the company of the voucher
1331 return self.pool.get('account.voucher').browse(cr,uid,voucher_id,context).journal_id.company_id.currency_id.id
1333 def _get_current_currency(self, cr, uid, voucher_id, context=None):
1335 Get the currency of the voucher.
1337 :param voucher_id: Id of the voucher what i want to obtain current currency.
1338 :return: currency id of the voucher
1341 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1342 return voucher.currency_id.id or self._get_company_currency(cr,uid,voucher.id,context)
1344 def action_move_line_create(self, cr, uid, ids, context=None):
1346 Confirm the vouchers given in ids and create the journal entries for each of them
1350 move_pool = self.pool.get('account.move')
1351 move_line_pool = self.pool.get('account.move.line')
1352 for voucher in self.browse(cr, uid, ids, context=context):
1353 local_context = dict(context, force_company=voucher.journal_id.company_id.id)
1356 company_currency = self._get_company_currency(cr, uid, voucher.id, context)
1357 current_currency = self._get_current_currency(cr, uid, voucher.id, context)
1358 # we select the context to use accordingly if it's a multicurrency case or not
1359 context = self._sel_context(cr, uid, voucher.id, context)
1360 # But for the operations made by _convert_amount, we always need to give the date in the context
1361 ctx = context.copy()
1362 ctx.update({'date': voucher.date})
1363 # Create the account move record.
1364 move_id = move_pool.create(cr, uid, self.account_move_get(cr, uid, voucher.id, context=context), context=context)
1365 # Get the name of the account_move just created
1366 name = move_pool.browse(cr, uid, move_id, context=context).name
1367 # Create the first line of the voucher
1368 move_line_id = move_line_pool.create(cr, uid, self.first_move_line_get(cr,uid,voucher.id, move_id, company_currency, current_currency, local_context), local_context)
1369 move_line_brw = move_line_pool.browse(cr, uid, move_line_id, context=context)
1370 line_total = move_line_brw.debit - move_line_brw.credit
1372 if voucher.type == 'sale':
1373 line_total = line_total - self._convert_amount(cr, uid, voucher.tax_amount, voucher.id, context=ctx)
1374 elif voucher.type == 'purchase':
1375 line_total = line_total + self._convert_amount(cr, uid, voucher.tax_amount, voucher.id, context=ctx)
1376 # Create one move line per voucher line where amount is not 0.0
1377 line_total, rec_list_ids = self.voucher_move_line_create(cr, uid, voucher.id, line_total, move_id, company_currency, current_currency, context)
1379 # Create the writeoff line if needed
1380 ml_writeoff = self.writeoff_move_line_get(cr, uid, voucher.id, line_total, move_id, name, company_currency, current_currency, local_context)
1382 move_line_pool.create(cr, uid, ml_writeoff, local_context)
1383 # We post the voucher.
1384 self.write(cr, uid, [voucher.id], {
1389 if voucher.journal_id.entry_posted:
1390 move_pool.post(cr, uid, [move_id], context={})
1391 # We automatically reconcile the account move lines.
1393 for rec_ids in rec_list_ids:
1394 if len(rec_ids) >= 2:
1395 reconcile = move_line_pool.reconcile_partial(cr, uid, rec_ids, writeoff_acc_id=voucher.writeoff_acc_id.id, writeoff_period_id=voucher.period_id.id, writeoff_journal_id=voucher.journal_id.id)
1398 class account_voucher_line(osv.osv):
1399 _name = 'account.voucher.line'
1400 _description = 'Voucher Lines'
1401 _order = "move_line_id"
1403 # If the payment is in the same currency than the invoice, we keep the same amount
1404 # Otherwise, we compute from invoice currency to payment currency
1405 def _compute_balance(self, cr, uid, ids, name, args, context=None):
1406 currency_pool = self.pool.get('res.currency')
1408 for line in self.browse(cr, uid, ids, context=context):
1409 ctx = context.copy()
1410 ctx.update({'date': line.voucher_id.date})
1411 voucher_rate = self.pool.get('res.currency').read(cr, uid, line.voucher_id.currency_id.id, ['rate'], context=ctx)['rate']
1413 'voucher_special_currency': line.voucher_id.payment_rate_currency_id and line.voucher_id.payment_rate_currency_id.id or False,
1414 'voucher_special_currency_rate': line.voucher_id.payment_rate * voucher_rate})
1416 company_currency = line.voucher_id.journal_id.company_id.currency_id.id
1417 voucher_currency = line.voucher_id.currency_id and line.voucher_id.currency_id.id or company_currency
1418 move_line = line.move_line_id or False
1421 res['amount_original'] = 0.0
1422 res['amount_unreconciled'] = 0.0
1423 elif move_line.currency_id and voucher_currency==move_line.currency_id.id:
1424 res['amount_original'] = abs(move_line.amount_currency)
1425 res['amount_unreconciled'] = abs(move_line.amount_residual_currency)
1427 #always use the amount booked in the company currency as the basis of the conversion into the voucher currency
1428 res['amount_original'] = currency_pool.compute(cr, uid, company_currency, voucher_currency, move_line.credit or move_line.debit or 0.0, context=ctx)
1429 res['amount_unreconciled'] = currency_pool.compute(cr, uid, company_currency, voucher_currency, abs(move_line.amount_residual), context=ctx)
1431 rs_data[line.id] = res
1434 def _currency_id(self, cr, uid, ids, name, args, context=None):
1436 This function returns the currency id of a voucher line. It's either the currency of the
1437 associated move line (if any) or the currency of the voucher or the company currency.
1440 for line in self.browse(cr, uid, ids, context=context):
1441 move_line = line.move_line_id
1443 res[line.id] = move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id
1445 res[line.id] = line.voucher_id.currency_id and line.voucher_id.currency_id.id or line.voucher_id.company_id.currency_id.id
1449 'voucher_id':fields.many2one('account.voucher', 'Voucher', required=1, ondelete='cascade'),
1450 'name':fields.char('Description',),
1451 'account_id':fields.many2one('account.account','Account', required=True),
1452 'partner_id':fields.related('voucher_id', 'partner_id', type='many2one', relation='res.partner', string='Partner'),
1453 'untax_amount':fields.float('Untax Amount'),
1454 'amount':fields.float('Amount', digits_compute=dp.get_precision('Account')),
1455 'reconcile': fields.boolean('Full Reconcile'),
1456 'type':fields.selection([('dr','Debit'),('cr','Credit')], 'Dr/Cr'),
1457 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'),
1458 'move_line_id': fields.many2one('account.move.line', 'Journal Item', copy=False),
1459 'date_original': fields.related('move_line_id','date', type='date', relation='account.move.line', string='Date', readonly=1),
1460 'date_due': fields.related('move_line_id','date_maturity', type='date', relation='account.move.line', string='Due Date', readonly=1),
1461 'amount_original': fields.function(_compute_balance, multi='dc', type='float', string='Original Amount', store=True, digits_compute=dp.get_precision('Account')),
1462 'amount_unreconciled': fields.function(_compute_balance, multi='dc', type='float', string='Open Balance', store=True, digits_compute=dp.get_precision('Account')),
1463 'company_id': fields.related('voucher_id','company_id', relation='res.company', type='many2one', string='Company', store=True, readonly=True),
1464 'currency_id': fields.function(_currency_id, string='Currency', type='many2one', relation='res.currency', readonly=True),
1470 def onchange_reconcile(self, cr, uid, ids, reconcile, amount, amount_unreconciled, context=None):
1471 vals = {'amount': 0.0}
1473 vals = { 'amount': amount_unreconciled}
1474 return {'value': vals}
1476 def onchange_amount(self, cr, uid, ids, amount, amount_unreconciled, context=None):
1479 vals['reconcile'] = (amount == amount_unreconciled)
1480 return {'value': vals}
1482 def onchange_move_line_id(self, cr, user, ids, move_line_id, context=None):
1484 Returns a dict that contains new values and context
1486 @param move_line_id: latest value from user input for field move_line_id
1487 @param args: other arguments
1488 @param context: context arguments, like lang, time zone
1490 @return: Returns a dict which contains new values, and context
1493 move_line_pool = self.pool.get('account.move.line')
1495 move_line = move_line_pool.browse(cr, user, move_line_id, context=context)
1496 if move_line.credit:
1501 'account_id': move_line.account_id.id,
1503 'currency_id': move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id,
1509 def default_get(self, cr, user, fields_list, context=None):
1511 Returns default values for fields
1512 @param fields_list: list of fields, for which default values are required to be read
1513 @param context: context arguments, like lang, time zone
1515 @return: Returns a dict that contains default values for fields
1519 journal_id = context.get('journal_id', False)
1520 partner_id = context.get('partner_id', False)
1521 journal_pool = self.pool.get('account.journal')
1522 partner_pool = self.pool.get('res.partner')
1523 values = super(account_voucher_line, self).default_get(cr, user, fields_list, context=context)
1524 if (not journal_id) or ('account_id' not in fields_list):
1526 journal = journal_pool.browse(cr, user, journal_id, context=context)
1529 if journal.type in ('sale', 'sale_refund'):
1530 account_id = journal.default_credit_account_id and journal.default_credit_account_id.id or False
1532 elif journal.type in ('purchase', 'expense', 'purchase_refund'):
1533 account_id = journal.default_debit_account_id and journal.default_debit_account_id.id or False
1536 partner = partner_pool.browse(cr, user, partner_id, context=context)
1537 if context.get('type') == 'payment':
1539 account_id = partner.property_account_payable.id
1540 elif context.get('type') == 'receipt':
1541 account_id = partner.property_account_receivable.id
1544 'account_id':account_id,
1549 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: