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 netsvc
26 from openerp.osv import fields, osv
27 import openerp.addons.decimal_precision as dp
28 from openerp.tools.translate import _
29 from openerp.tools import float_compare
31 class res_currency(osv.osv):
32 _inherit = "res.currency"
34 def _current_rate(self, cr, uid, ids, name, arg, context=None):
37 res = super(res_currency, self)._current_rate(cr, uid, ids, name, arg, context=context)
38 if context.get('voucher_special_currency') in ids and context.get('voucher_special_currency_rate'):
39 res[context.get('voucher_special_currency')] = context.get('voucher_special_currency_rate')
43 # same definition of rate that in base in order to just overwrite the function
44 'rate': fields.function(_current_rate, string='Current Rate', digits=(12,6),
45 help='The rate of the currency to the currency of rate 1.'),
49 class res_company(osv.osv):
50 _inherit = "res.company"
52 'income_currency_exchange_account_id': fields.many2one(
54 string="Gain Exchange Rate Account",
55 domain="[('type', '=', 'other')]",),
56 'expense_currency_exchange_account_id': fields.many2one(
58 string="Loss Exchange Rate Account",
59 domain="[('type', '=', 'other')]",),
64 class account_config_settings(osv.osv_memory):
65 _inherit = 'account.config.settings'
67 'income_currency_exchange_account_id': fields.related(
68 'company_id', 'income_currency_exchange_account_id',
70 relation='account.account',
71 string="Gain Exchange Rate Account",
72 domain="[('type', '=', 'other')]"),
73 'expense_currency_exchange_account_id': fields.related(
74 'company_id', 'expense_currency_exchange_account_id',
76 relation='account.account',
77 string="Loss Exchange Rate Account",
78 domain="[('type', '=', 'other')]"),
80 def onchange_company_id(self, cr, uid, ids, company_id, context=None):
81 res = super(account_config_settings, self).onchange_company_id(cr, uid, ids, company_id, context=context)
83 company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
84 res['value'].update({'income_currency_exchange_account_id': company.income_currency_exchange_account_id and company.income_currency_exchange_account_id.id or False,
85 'expense_currency_exchange_account_id': company.expense_currency_exchange_account_id and company.expense_currency_exchange_account_id.id or False})
87 res['value'].update({'income_currency_exchange_account_id': False,
88 'expense_currency_exchange_account_id': False})
91 class account_voucher(osv.osv):
92 def _check_paid(self, cr, uid, ids, name, args, context=None):
94 for voucher in self.browse(cr, uid, ids, context=context):
95 res[voucher.id] = any([((line.account_id.type, 'in', ('receivable', 'payable')) and line.reconcile_id) for line in voucher.move_ids])
98 def _get_type(self, cr, uid, context=None):
101 return context.get('type', False)
103 def _get_period(self, cr, uid, context=None):
104 if context is None: context = {}
105 if context.get('period_id', False):
106 return context.get('period_id')
107 ctx = dict(context, account_period_prefer_normal=True)
108 periods = self.pool.get('account.period').find(cr, uid, context=ctx)
109 return periods and periods[0] or False
111 def _make_journal_search(self, cr, uid, ttype, context=None):
112 journal_pool = self.pool.get('account.journal')
113 return journal_pool.search(cr, uid, [('type', '=', ttype)], limit=1)
115 def _get_journal(self, cr, uid, context=None):
116 if context is None: context = {}
117 invoice_pool = self.pool.get('account.invoice')
118 journal_pool = self.pool.get('account.journal')
119 if context.get('invoice_id', False):
120 currency_id = invoice_pool.browse(cr, uid, context['invoice_id'], context=context).currency_id.id
121 journal_id = journal_pool.search(cr, uid, [('currency', '=', currency_id)], limit=1)
122 return journal_id and journal_id[0] or False
123 if context.get('journal_id', False):
124 return context.get('journal_id')
125 if not context.get('journal_id', False) and context.get('search_default_journal_id', False):
126 return context.get('search_default_journal_id')
128 ttype = context.get('type', 'bank')
129 if ttype in ('payment', 'receipt'):
131 res = self._make_journal_search(cr, uid, ttype, context=context)
132 return res and res[0] or False
134 def _get_tax(self, cr, uid, context=None):
135 if context is None: context = {}
136 journal_pool = self.pool.get('account.journal')
137 journal_id = context.get('journal_id', False)
139 ttype = context.get('type', 'bank')
140 res = journal_pool.search(cr, uid, [('type', '=', ttype)], limit=1)
147 journal = journal_pool.browse(cr, uid, journal_id, context=context)
148 account_id = journal.default_credit_account_id or journal.default_debit_account_id
149 if account_id and account_id.tax_ids:
150 tax_id = account_id.tax_ids[0].id
154 def _get_payment_rate_currency(self, cr, uid, context=None):
156 Return the default value for field payment_rate_currency_id: the currency of the journal
157 if there is one, otherwise the currency of the user's company
159 if context is None: context = {}
160 journal_pool = self.pool.get('account.journal')
161 journal_id = context.get('journal_id', False)
163 journal = journal_pool.browse(cr, uid, journal_id, context=context)
165 return journal.currency.id
166 #no journal given in the context, use company currency as default
167 return self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
169 def _get_currency(self, cr, uid, context=None):
170 if context is None: context = {}
171 journal_pool = self.pool.get('account.journal')
172 journal_id = context.get('journal_id', False)
174 journal = journal_pool.browse(cr, uid, journal_id, context=context)
176 return journal.currency.id
177 return self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
179 def _get_partner(self, cr, uid, context=None):
180 if context is None: context = {}
181 return context.get('partner_id', False)
183 def _get_reference(self, cr, uid, context=None):
184 if context is None: context = {}
185 return context.get('reference', False)
187 def _get_narration(self, cr, uid, context=None):
188 if context is None: context = {}
189 return context.get('narration', False)
191 def _get_amount(self, cr, uid, context=None):
194 return context.get('amount', 0.0)
196 def name_get(self, cr, uid, ids, context=None):
199 if context is None: context = {}
200 return [(r['id'], (str("%.2f" % r['amount']) or '')) for r in self.read(cr, uid, ids, ['amount'], context, load='_classic_write')]
202 def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
203 mod_obj = self.pool.get('ir.model.data')
204 if context is None: context = {}
206 if view_type == 'form':
207 if not view_id and context.get('invoice_type'):
208 if context.get('invoice_type') in ('out_invoice', 'out_refund'):
209 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_receipt_form')
211 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_payment_form')
212 result = result and result[1] or False
214 if not view_id and context.get('line_type'):
215 if context.get('line_type') == 'customer':
216 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_receipt_form')
218 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_payment_form')
219 result = result and result[1] or False
222 res = super(account_voucher, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
223 doc = etree.XML(res['arch'])
225 if context.get('type', 'sale') in ('purchase', 'payment'):
226 nodes = doc.xpath("//field[@name='partner_id']")
228 node.set('context', "{'search_default_supplier': 1}")
229 if context.get('invoice_type','') in ('in_invoice', 'in_refund'):
230 node.set('string', _("Supplier"))
231 res['arch'] = etree.tostring(doc)
234 def _compute_writeoff_amount(self, cr, uid, line_dr_ids, line_cr_ids, amount, type):
236 sign = type == 'payment' and -1 or 1
237 for l in line_dr_ids:
239 for l in line_cr_ids:
240 credit += l['amount']
241 return amount - sign * (credit - debit)
243 def onchange_line_ids(self, cr, uid, ids, line_dr_ids, line_cr_ids, amount, voucher_currency, type, context=None):
244 context = context or {}
245 if not line_dr_ids and not line_cr_ids:
246 return {'value':{'writeoff_amount': 0.0}}
247 line_osv = self.pool.get("account.voucher.line")
248 line_dr_ids = resolve_o2m_operations(cr, uid, line_osv, line_dr_ids, ['amount'], context)
249 line_cr_ids = resolve_o2m_operations(cr, uid, line_osv, line_cr_ids, ['amount'], context)
251 #compute the field is_multi_currency that is used to hide/display options linked to secondary currency on the voucher
252 is_multi_currency = False
253 #loop on the voucher lines to see if one of these has a secondary currency. If yes, we need to see the options
254 for voucher_line in line_dr_ids+line_cr_ids:
255 line_currency = voucher_line.get('move_line_id', False) and self.pool.get('account.move.line').browse(cr, uid, voucher_line.get('move_line_id'), context=context).currency_id
257 is_multi_currency = True
259 return {'value': {'writeoff_amount': self._compute_writeoff_amount(cr, uid, line_dr_ids, line_cr_ids, amount, type), 'is_multi_currency': is_multi_currency}}
261 def _get_journal_currency(self, cr, uid, ids, name, args, context=None):
263 for voucher in self.browse(cr, uid, ids, context=context):
264 res[voucher.id] = voucher.journal_id.currency and voucher.journal_id.currency.id or voucher.company_id.currency_id.id
267 def _get_writeoff_amount(self, cr, uid, ids, name, args, context=None):
268 if not ids: return {}
269 currency_obj = self.pool.get('res.currency')
272 for voucher in self.browse(cr, uid, ids, context=context):
273 sign = voucher.type == 'payment' and -1 or 1
274 for l in voucher.line_dr_ids:
276 for l in voucher.line_cr_ids:
278 currency = voucher.currency_id or voucher.company_id.currency_id
279 res[voucher.id] = currency_obj.round(cr, uid, currency, voucher.amount - sign * (credit - debit))
282 def _paid_amount_in_company_currency(self, cr, uid, ids, name, args, context=None):
287 for v in self.browse(cr, uid, ids, context=context):
288 ctx.update({'date': v.date})
289 #make a new call to browse in order to have the right date in the context, to get the right currency rate
290 voucher = self.browse(cr, uid, v.id, context=ctx)
292 'voucher_special_currency': voucher.payment_rate_currency_id and voucher.payment_rate_currency_id.id or False,
293 'voucher_special_currency_rate': voucher.currency_id.rate * voucher.payment_rate,})
294 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)
297 _name = 'account.voucher'
298 _description = 'Accounting Voucher'
299 _inherit = ['mail.thread']
300 _order = "date desc, id desc"
301 # _rec_name = 'number'
304 'account_voucher.mt_voucher_state_change': lambda self, cr, uid, obj, ctx=None: True,
309 'active': fields.boolean('Active', help="By default, reconciliation vouchers made on draft bank statements are set as inactive, which allow to hide the customer/supplier payment while the bank statement isn't confirmed."),
310 'type':fields.selection([
312 ('purchase','Purchase'),
313 ('payment','Payment'),
314 ('receipt','Receipt'),
315 ],'Default Type', readonly=True, states={'draft':[('readonly',False)]}),
316 'name':fields.char('Memo', size=256, readonly=True, states={'draft':[('readonly',False)]}),
317 'date':fields.date('Date', readonly=True, select=True, states={'draft':[('readonly',False)]}, help="Effective date for accounting entries"),
318 'journal_id':fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
319 'account_id':fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}),
320 'line_ids':fields.one2many('account.voucher.line','voucher_id','Voucher Lines', readonly=True, states={'draft':[('readonly',False)]}),
321 'line_cr_ids':fields.one2many('account.voucher.line','voucher_id','Credits',
322 domain=[('type','=','cr')], context={'default_type':'cr'}, readonly=True, states={'draft':[('readonly',False)]}),
323 'line_dr_ids':fields.one2many('account.voucher.line','voucher_id','Debits',
324 domain=[('type','=','dr')], context={'default_type':'dr'}, readonly=True, states={'draft':[('readonly',False)]}),
325 'period_id': fields.many2one('account.period', 'Period', required=True, readonly=True, states={'draft':[('readonly',False)]}),
326 'narration':fields.text('Notes', readonly=True, states={'draft':[('readonly',False)]}),
327 'currency_id': fields.function(_get_journal_currency, type='many2one', relation='res.currency', string='Currency', readonly=True, required=True),
328 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
329 'state':fields.selection(
331 ('cancel','Cancelled'),
332 ('proforma','Pro-forma'),
334 ], 'Status', readonly=True, size=32, track_visibility='onchange',
335 help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed Voucher. \
336 \n* The \'Pro-forma\' when voucher is in Pro-forma status,voucher does not have an voucher number. \
337 \n* The \'Posted\' status is used when user create voucher,a voucher number is generated and voucher entries are created in account \
338 \n* The \'Cancelled\' status is used when user cancel voucher.'),
339 'amount': fields.float('Total', digits_compute=dp.get_precision('Account'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
340 'tax_amount':fields.float('Tax Amount', digits_compute=dp.get_precision('Account'), readonly=True, states={'draft':[('readonly',False)]}),
341 'reference': fields.char('Ref #', size=64, readonly=True, states={'draft':[('readonly',False)]}, help="Transaction reference number."),
342 'number': fields.char('Number', size=32, readonly=True,),
343 'move_id':fields.many2one('account.move', 'Account Entry'),
344 'move_ids': fields.related('move_id','line_id', type='one2many', relation='account.move.line', string='Journal Items', readonly=True),
345 'partner_id':fields.many2one('res.partner', 'Partner', change_default=1, readonly=True, states={'draft':[('readonly',False)]}),
346 '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'),
347 'paid': fields.function(_check_paid, string='Paid', type='boolean', help="The Voucher has been totally paid."),
348 'pay_now':fields.selection([
349 ('pay_now','Pay Directly'),
350 ('pay_later','Pay Later or Group Funds'),
351 ],'Payment', select=True, readonly=True, states={'draft':[('readonly',False)]}),
352 'tax_id': fields.many2one('account.tax', 'Tax', readonly=True, states={'draft':[('readonly',False)]}, domain=[('price_include','=', False)], help="Only for tax excluded from price"),
353 'pre_line':fields.boolean('Previous Payments ?', required=False),
354 'date_due': fields.date('Due Date', readonly=True, select=True, states={'draft':[('readonly',False)]}),
355 'payment_option':fields.selection([
356 ('without_writeoff', 'Keep Open'),
357 ('with_writeoff', 'Reconcile Payment Balance'),
358 ], '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)"),
359 'writeoff_acc_id': fields.many2one('account.account', 'Counterpart Account', readonly=True, states={'draft': [('readonly', False)]}),
360 'comment': fields.char('Counterpart Comment', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
361 'analytic_id': fields.many2one('account.analytic.account','Write-Off Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
362 '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."),
363 'payment_rate_currency_id': fields.many2one('res.currency', 'Payment Rate Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}),
364 'payment_rate': fields.float('Exchange Rate', digits=(12,6), required=True, readonly=True, states={'draft': [('readonly', False)]},
365 help='The specific rate that will be used, in this voucher, between the selected currency (in \'Payment Rate Currency\' field) and the voucher currency.'),
366 'paid_amount_in_company_currency': fields.function(_paid_amount_in_company_currency, string='Paid Amount in Company Currency', type='float', readonly=True),
367 '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'),
371 'period_id': _get_period,
372 'partner_id': _get_partner,
373 'journal_id':_get_journal,
374 'currency_id': _get_currency,
375 'reference': _get_reference,
376 'narration':_get_narration,
377 'amount': _get_amount,
380 'pay_now': 'pay_now',
382 'date': lambda *a: time.strftime('%Y-%m-%d'),
383 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.voucher',context=c),
385 'payment_option': 'without_writeoff',
386 'comment': _('Write-Off'),
388 'payment_rate_currency_id': _get_payment_rate_currency,
391 def compute_tax(self, cr, uid, ids, context=None):
392 tax_pool = self.pool.get('account.tax')
393 partner_pool = self.pool.get('res.partner')
394 position_pool = self.pool.get('account.fiscal.position')
395 voucher_line_pool = self.pool.get('account.voucher.line')
396 voucher_pool = self.pool.get('account.voucher')
397 if context is None: context = {}
399 for voucher in voucher_pool.browse(cr, uid, ids, context=context):
401 for line in voucher.line_ids:
402 voucher_amount += line.untax_amount or line.amount
403 line.amount = line.untax_amount or line.amount
404 voucher_line_pool.write(cr, uid, [line.id], {'amount':line.amount, 'untax_amount':line.untax_amount})
406 if not voucher.tax_id:
407 self.write(cr, uid, [voucher.id], {'amount':voucher_amount, 'tax_amount':0.0})
410 tax = [tax_pool.browse(cr, uid, voucher.tax_id.id, context=context)]
411 partner = partner_pool.browse(cr, uid, voucher.partner_id.id, context=context) or False
412 taxes = position_pool.map_tax(cr, uid, partner and partner.property_account_position or False, tax)
413 tax = tax_pool.browse(cr, uid, taxes, context=context)
415 total = voucher_amount
418 if not tax[0].price_include:
419 for line in voucher.line_ids:
420 for tax_line in tax_pool.compute_all(cr, uid, tax, line.amount, 1).get('taxes', []):
421 total_tax += tax_line.get('amount', 0.0)
424 for line in voucher.line_ids:
428 for tax_line in tax_pool.compute_all(cr, uid, tax, line.untax_amount or line.amount, 1).get('taxes', []):
429 line_tax += tax_line.get('amount', 0.0)
430 line_total += tax_line.get('price_unit')
431 total_tax += line_tax
432 untax_amount = line.untax_amount or line.amount
433 voucher_line_pool.write(cr, uid, [line.id], {'amount':line_total, 'untax_amount':untax_amount})
435 self.write(cr, uid, [voucher.id], {'amount':total, 'tax_amount':total_tax})
438 def onchange_price(self, cr, uid, ids, line_ids, tax_id, partner_id=False, context=None):
439 context = context or {}
440 tax_pool = self.pool.get('account.tax')
441 partner_pool = self.pool.get('res.partner')
442 position_pool = self.pool.get('account.fiscal.position')
443 line_pool = self.pool.get('account.voucher.line')
452 line_ids = resolve_o2m_operations(cr, uid, line_pool, line_ids, ["amount"], context)
455 for line in line_ids:
457 line_amount = line.get('amount',0.0)
460 tax = [tax_pool.browse(cr, uid, tax_id, context=context)]
462 partner = partner_pool.browse(cr, uid, partner_id, context=context) or False
463 taxes = position_pool.map_tax(cr, uid, partner and partner.property_account_position or False, tax)
464 tax = tax_pool.browse(cr, uid, taxes, context=context)
466 if not tax[0].price_include:
467 for tax_line in tax_pool.compute_all(cr, uid, tax, line_amount, 1).get('taxes', []):
468 total_tax += tax_line.get('amount')
470 voucher_total += line_amount
471 total = voucher_total + total_tax
474 'amount': total or voucher_total,
475 'tax_amount': total_tax
481 def onchange_term_id(self, cr, uid, ids, term_id, amount):
482 term_pool = self.pool.get('account.payment.term')
485 default = {'date_due':False}
486 if term_id and amount:
487 terms = term_pool.compute(cr, uid, term_id, amount)
489 due_date = terms[-1][0]
493 return {'value':default}
495 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):
497 Returns a dict that contains new values and context
499 @param partner_id: latest value from user input for field partner_id
500 @param args: other arguments
501 @param context: context arguments, like lang, time zone
503 @return: Returns a dict which contains new values, and context
509 if not partner_id or not journal_id:
512 partner_pool = self.pool.get('res.partner')
513 journal_pool = self.pool.get('account.journal')
515 journal = journal_pool.browse(cr, uid, journal_id, context=context)
516 partner = partner_pool.browse(cr, uid, partner_id, context=context)
519 if journal.type in ('sale','sale_refund'):
520 account_id = partner.property_account_receivable.id
522 elif journal.type in ('purchase', 'purchase_refund','expense'):
523 account_id = partner.property_account_payable.id
526 if not journal.default_credit_account_id or not journal.default_debit_account_id:
527 raise osv.except_osv(_('Error!'), _('Please define default credit/debit accounts on the journal "%s".') % (journal.name))
528 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
531 default['value']['account_id'] = account_id
532 default['value']['type'] = ttype or tr_type
534 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)
535 default['value'].update(vals.get('value'))
539 def onchange_rate(self, cr, uid, ids, rate, amount, currency_id, payment_rate_currency_id, company_id, context=None):
540 res = {'value': {'paid_amount_in_company_currency': amount}}
541 if rate and amount and currency_id:
542 company_currency = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id
543 #context should contain the date, the payment currency and the payment rate specified on the voucher
544 amount_in_company_currency = self.pool.get('res.currency').compute(cr, uid, currency_id, company_currency.id, amount, context=context)
545 res['value']['paid_amount_in_company_currency'] = amount_in_company_currency
548 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):
552 ctx.update({'date': date})
553 #read the voucher rate with the right date in the context
554 currency_id = currency_id or self.pool.get('res.company').browse(cr, uid, company_id, context=ctx).currency_id.id
555 voucher_rate = self.pool.get('res.currency').read(cr, uid, currency_id, ['rate'], context=ctx)['rate']
557 'voucher_special_currency': payment_rate_currency_id,
558 'voucher_special_currency_rate': rate * voucher_rate})
559 res = self.recompute_voucher_lines(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=ctx)
560 vals = self.onchange_rate(cr, uid, ids, rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
561 for key in vals.keys():
562 res[key].update(vals[key])
565 def recompute_payment_rate(self, cr, uid, ids, vals, currency_id, date, ttype, journal_id, amount, context=None):
568 #on change of the journal, we need to set also the default value for payment_rate and payment_rate_currency_id
569 currency_obj = self.pool.get('res.currency')
570 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
571 company_id = journal.company_id.id
573 currency_id = currency_id or journal.company_id.currency_id.id
574 payment_rate_currency_id = currency_id
576 ctx.update({'date': date})
578 if ttype == 'receipt':
579 o2m_to_loop = 'line_cr_ids'
580 elif ttype == 'payment':
581 o2m_to_loop = 'line_dr_ids'
582 if o2m_to_loop and 'value' in vals and o2m_to_loop in vals['value']:
583 for voucher_line in vals['value'][o2m_to_loop]:
584 if voucher_line['currency_id'] != currency_id:
585 # we take as default value for the payment_rate_currency_id, the currency of the first invoice that
586 # is not in the voucher currency
587 payment_rate_currency_id = voucher_line['currency_id']
588 tmp = currency_obj.browse(cr, uid, payment_rate_currency_id, context=ctx).rate
589 payment_rate = tmp / currency_obj.browse(cr, uid, currency_id, context=ctx).rate
591 vals['value'].update({
592 'payment_rate': payment_rate,
593 'currency_id': currency_id,
594 'payment_rate_currency_id': payment_rate_currency_id
596 #read the voucher rate with the right date in the context
597 voucher_rate = self.pool.get('res.currency').read(cr, uid, currency_id, ['rate'], context=ctx)['rate']
599 'voucher_special_currency_rate': payment_rate * voucher_rate,
600 'voucher_special_currency': payment_rate_currency_id})
601 res = self.onchange_rate(cr, uid, ids, payment_rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
602 for key in res.keys():
603 vals[key].update(res[key])
606 def basic_onchange_partner(self, cr, uid, ids, partner_id, journal_id, ttype, context=None):
607 partner_pool = self.pool.get('res.partner')
608 journal_pool = self.pool.get('account.journal')
609 res = {'value': {'account_id': False}}
610 if not partner_id or not journal_id:
613 journal = journal_pool.browse(cr, uid, journal_id, context=context)
614 partner = partner_pool.browse(cr, uid, partner_id, context=context)
616 if journal.type in ('sale','sale_refund'):
617 account_id = partner.property_account_receivable.id
618 elif journal.type in ('purchase', 'purchase_refund','expense'):
619 account_id = partner.property_account_payable.id
621 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
623 res['value']['account_id'] = account_id
626 def onchange_partner_id(self, cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=None):
631 #TODO: comment me and use me directly in the sales/purchases views
632 res = self.basic_onchange_partner(cr, uid, ids, partner_id, journal_id, ttype, context=context)
633 if ttype in ['sale', 'purchase']:
636 # 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
637 ctx.update({'date': date})
638 vals = self.recompute_voucher_lines(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=ctx)
639 vals2 = self.recompute_payment_rate(cr, uid, ids, vals, currency_id, date, ttype, journal_id, amount, context=context)
640 for key in vals.keys():
641 res[key].update(vals[key])
642 for key in vals2.keys():
643 res[key].update(vals2[key])
644 #TODO: can probably be removed now
645 #TODO: onchange_partner_id() should not returns [pre_line, line_dr_ids, payment_rate...] for type sale, and not
646 # [pre_line, line_cr_ids, payment_rate...] for type purchase.
647 # We should definitively split account.voucher object in two and make distinct on_change functions. In the
648 # meanwhile, bellow lines must be there because the fields aren't present in the view, what crashes if the
649 # onchange returns a value for them
651 del(res['value']['line_dr_ids'])
652 del(res['value']['pre_line'])
653 del(res['value']['payment_rate'])
654 elif ttype == 'purchase':
655 del(res['value']['line_cr_ids'])
656 del(res['value']['pre_line'])
657 del(res['value']['payment_rate'])
660 def recompute_voucher_lines(self, cr, uid, ids, partner_id, journal_id, price, currency_id, ttype, date, context=None):
662 Returns a dict that contains new values and context
664 @param partner_id: latest value from user input for field partner_id
665 @param args: other arguments
666 @param context: context arguments, like lang, time zone
668 @return: Returns a dict which contains new values, and context
670 def _remove_noise_in_o2m():
671 """if the line is partially reconciled, then we must pay attention to display it only once and
673 This function returns True if the line is considered as noise and should not be displayed
675 if line.reconcile_partial_id:
676 if currency_id == line.currency_id.id:
677 if line.amount_residual_currency <= 0:
680 if line.amount_residual <= 0:
686 context_multi_currency = context.copy()
688 currency_pool = self.pool.get('res.currency')
689 move_line_pool = self.pool.get('account.move.line')
690 partner_pool = self.pool.get('res.partner')
691 journal_pool = self.pool.get('account.journal')
692 line_pool = self.pool.get('account.voucher.line')
696 'value': {'line_dr_ids': [] ,'line_cr_ids': [] ,'pre_line': False,},
700 line_ids = ids and line_pool.search(cr, uid, [('voucher_id', '=', ids[0])]) or False
702 line_pool.unlink(cr, uid, line_ids)
704 if not partner_id or not journal_id:
707 journal = journal_pool.browse(cr, uid, journal_id, context=context)
708 partner = partner_pool.browse(cr, uid, partner_id, context=context)
709 currency_id = currency_id or journal.company_id.currency_id.id
713 account_type = 'receivable'
714 if ttype == 'payment':
715 account_type = 'payable'
716 total_debit = price or 0.0
718 total_credit = price or 0.0
719 account_type = 'receivable'
721 if not context.get('move_line_ids', False):
722 ids = move_line_pool.search(cr, uid, [('state','=','valid'), ('account_id.type', '=', account_type), ('reconcile_id', '=', False), ('partner_id', '=', partner_id)], context=context)
724 ids = context['move_line_ids']
725 invoice_id = context.get('invoice_id', False)
726 company_currency = journal.company_id.currency_id.id
727 move_line_found = False
729 #order the lines by most old first
731 account_move_lines = move_line_pool.browse(cr, uid, ids, context=context)
733 #compute the total debit/credit and look for a matching open amount or invoice
734 for line in account_move_lines:
735 if _remove_noise_in_o2m():
739 if line.invoice.id == invoice_id:
740 #if the invoice linked to the voucher line is equal to the invoice_id in context
741 #then we assign the amount on that line, whatever the other voucher lines
742 move_line_found = line.id
744 elif currency_id == company_currency:
745 #otherwise treatments is the same but with other field names
746 if line.amount_residual == price:
747 #if the amount residual is equal the amount voucher, we assign it to that voucher
748 #line, whatever the other voucher lines
749 move_line_found = line.id
751 #otherwise we will split the voucher amount on each line (by most old first)
752 total_credit += line.credit or 0.0
753 total_debit += line.debit or 0.0
754 elif currency_id == line.currency_id.id:
755 if line.amount_residual_currency == price:
756 move_line_found = line.id
758 total_credit += line.credit and line.amount_currency or 0.0
759 total_debit += line.debit and line.amount_currency or 0.0
761 #voucher line creation
762 for line in account_move_lines:
764 if _remove_noise_in_o2m():
767 if line.currency_id and currency_id == line.currency_id.id:
768 amount_original = abs(line.amount_currency)
769 amount_unreconciled = abs(line.amount_residual_currency)
771 #always use the amount booked in the company currency as the basis of the conversion into the voucher currency
772 amount_original = currency_pool.compute(cr, uid, company_currency, currency_id, line.credit or line.debit or 0.0, context=context_multi_currency)
773 amount_unreconciled = currency_pool.compute(cr, uid, company_currency, currency_id, abs(line.amount_residual), context=context_multi_currency)
774 line_currency_id = line.currency_id and line.currency_id.id or company_currency
776 'name':line.move_id.name,
777 'type': line.credit and 'dr' or 'cr',
778 'move_line_id':line.id,
779 'account_id':line.account_id.id,
780 'amount_original': amount_original,
781 'amount': (move_line_found == line.id) and min(abs(price), amount_unreconciled) or 0.0,
782 'date_original':line.date,
783 'date_due':line.date_maturity,
784 'amount_unreconciled': amount_unreconciled,
785 'currency_id': line_currency_id,
787 #in case a corresponding move_line hasn't been found, we now try to assign the voucher amount
788 #on existing invoices: we split voucher amount by most old first, but only for lines in the same currency
789 if not move_line_found:
790 if currency_id == line_currency_id:
792 amount = min(amount_unreconciled, abs(total_debit))
793 rs['amount'] = amount
794 total_debit -= amount
796 amount = min(amount_unreconciled, abs(total_credit))
797 rs['amount'] = amount
798 total_credit -= amount
800 if rs['amount_unreconciled'] == rs['amount']:
801 rs['reconcile'] = True
803 if rs['type'] == 'cr':
804 default['value']['line_cr_ids'].append(rs)
806 default['value']['line_dr_ids'].append(rs)
808 if ttype == 'payment' and len(default['value']['line_cr_ids']) > 0:
809 default['value']['pre_line'] = 1
810 elif ttype == 'receipt' and len(default['value']['line_dr_ids']) > 0:
811 default['value']['pre_line'] = 1
812 default['value']['writeoff_amount'] = self._compute_writeoff_amount(cr, uid, default['value']['line_dr_ids'], default['value']['line_cr_ids'], price, ttype)
815 def onchange_payment_rate_currency(self, cr, uid, ids, currency_id, payment_rate, payment_rate_currency_id, date, amount, company_id, context=None):
819 if currency_id and currency_id == payment_rate_currency_id:
820 #set the default payment rate of the voucher and compute the paid amount in company currency
822 ctx.update({'date': date})
823 #read the voucher rate with the right date in the context
824 voucher_rate = self.pool.get('res.currency').read(cr, uid, currency_id, ['rate'], context=ctx)['rate']
826 'voucher_special_currency_rate': payment_rate * voucher_rate,
827 'voucher_special_currency': payment_rate_currency_id})
828 vals = self.onchange_rate(cr, uid, ids, payment_rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
829 for key in vals.keys():
830 res[key].update(vals[key])
833 def onchange_date(self, cr, uid, ids, date, currency_id, payment_rate_currency_id, amount, company_id, context=None):
835 @param date: latest value from user input for field date
836 @param args: other arguments
837 @param context: context arguments, like lang, time zone
838 @return: Returns a dict which contains new values, and context
843 #set the period of the voucher
844 period_pool = self.pool.get('account.period')
845 currency_obj = self.pool.get('res.currency')
847 ctx.update({'company_id': company_id, 'account_period_prefer_normal': True})
848 voucher_currency_id = currency_id or self.pool.get('res.company').browse(cr, uid, company_id, context=ctx).currency_id.id
849 pids = period_pool.find(cr, uid, date, context=ctx)
851 res['value'].update({'period_id':pids[0]})
852 if payment_rate_currency_id:
853 ctx.update({'date': date})
855 if payment_rate_currency_id != currency_id:
856 tmp = currency_obj.browse(cr, uid, payment_rate_currency_id, context=ctx).rate
857 payment_rate = tmp / currency_obj.browse(cr, uid, voucher_currency_id, context=ctx).rate
858 vals = self.onchange_payment_rate_currency(cr, uid, ids, voucher_currency_id, payment_rate, payment_rate_currency_id, date, amount, company_id, context=context)
859 vals['value'].update({'payment_rate': payment_rate})
860 for key in vals.keys():
861 res[key].update(vals[key])
864 def onchange_journal(self, cr, uid, ids, journal_id, line_ids, tax_id, partner_id, date, amount, ttype, company_id, context=None):
867 journal_pool = self.pool.get('account.journal')
868 journal = journal_pool.browse(cr, uid, journal_id, context=context)
869 account_id = journal.default_credit_account_id or journal.default_debit_account_id
871 if account_id and account_id.tax_ids:
872 tax_id = account_id.tax_ids[0].id
875 if ttype in ('sale', 'purchase'):
876 vals = self.onchange_price(cr, uid, ids, line_ids, tax_id, partner_id, context)
877 vals['value'].update({'tax_id':tax_id,'amount': amount})
880 currency_id = journal.currency.id
882 currency_id = journal.company_id.currency_id.id
883 vals['value'].update({'currency_id': currency_id})
884 res = self.onchange_partner_id(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context)
885 for key in res.keys():
886 vals[key].update(res[key])
889 def button_proforma_voucher(self, cr, uid, ids, context=None):
890 context = context or {}
891 wf_service = netsvc.LocalService("workflow")
893 wf_service.trg_validate(uid, 'account.voucher', vid, 'proforma_voucher', cr)
894 return {'type': 'ir.actions.act_window_close'}
896 def proforma_voucher(self, cr, uid, ids, context=None):
897 self.action_move_line_create(cr, uid, ids, context=context)
900 def action_cancel_draft(self, cr, uid, ids, context=None):
901 wf_service = netsvc.LocalService("workflow")
902 for voucher_id in ids:
903 wf_service.trg_create(uid, 'account.voucher', voucher_id, cr)
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')
911 for voucher in self.browse(cr, uid, ids, context=context):
913 for line in voucher.move_ids:
914 if line.reconcile_id:
915 recs += [line.reconcile_id.id]
916 if line.reconcile_partial_id:
917 recs += [line.reconcile_partial_id.id]
919 reconcile_pool.unlink(cr, uid, recs)
922 move_pool.button_cancel(cr, uid, [voucher.move_id.id])
923 move_pool.unlink(cr, uid, [voucher.move_id.id])
928 self.write(cr, uid, ids, res)
931 def unlink(self, cr, uid, ids, context=None):
932 for t in self.read(cr, uid, ids, ['state'], context=context):
933 if t['state'] not in ('draft', 'cancel'):
934 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete voucher(s) which are already opened or paid.'))
935 return super(account_voucher, self).unlink(cr, uid, ids, context=context)
937 def onchange_payment(self, cr, uid, ids, pay_now, journal_id, partner_id, ttype='sale'):
941 res = {'account_id':False}
942 partner_pool = self.pool.get('res.partner')
943 journal_pool = self.pool.get('account.journal')
944 if pay_now == 'pay_later':
945 partner = partner_pool.browse(cr, uid, partner_id)
946 journal = journal_pool.browse(cr, uid, journal_id)
947 if journal.type in ('sale','sale_refund'):
948 account_id = partner.property_account_receivable.id
949 elif journal.type in ('purchase', 'purchase_refund','expense'):
950 account_id = partner.property_account_payable.id
952 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
953 res['account_id'] = account_id
956 def _sel_context(self, cr, uid, voucher_id, context=None):
958 Select the context to use accordingly if it needs to be multicurrency or not.
960 :param voucher_id: Id of the actual voucher
961 :return: The returned context will be the same as given in parameter if the voucher currency is the same
962 than the company currency, otherwise it's a copy of the parameter with an extra key 'date' containing
963 the date of the voucher.
966 company_currency = self._get_company_currency(cr, uid, voucher_id, context)
967 current_currency = self._get_current_currency(cr, uid, voucher_id, context)
968 if current_currency <> company_currency:
969 context_multi_currency = context.copy()
970 voucher = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context)
971 context_multi_currency.update({'date': voucher.date})
972 return context_multi_currency
975 def first_move_line_get(self, cr, uid, voucher_id, move_id, company_currency, current_currency, context=None):
977 Return a dict to be use to create the first account move line of given voucher.
979 :param voucher_id: Id of voucher what we are creating account_move.
980 :param move_id: Id of account move where this line will be added.
981 :param company_currency: id of currency of the company to which the voucher belong
982 :param current_currency: id of currency of the voucher
983 :return: mapping between fieldname and value of account move line to create
986 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
988 # TODO: is there any other alternative then the voucher type ??
989 # ANSWER: We can have payment and receipt "In Advance".
990 # TODO: Make this logic available.
991 # -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
992 if voucher.type in ('purchase', 'payment'):
993 credit = voucher.paid_amount_in_company_currency
994 elif voucher.type in ('sale', 'receipt'):
995 debit = voucher.paid_amount_in_company_currency
996 if debit < 0: credit = -debit; debit = 0.0
997 if credit < 0: debit = -credit; credit = 0.0
998 sign = debit - credit < 0 and -1 or 1
999 #set the first line of the voucher
1001 'name': voucher.name or '/',
1004 'account_id': voucher.account_id.id,
1006 'journal_id': voucher.journal_id.id,
1007 'period_id': voucher.period_id.id,
1008 'partner_id': voucher.partner_id.id,
1009 'currency_id': company_currency <> current_currency and current_currency or False,
1010 'amount_currency': company_currency <> current_currency and sign * voucher.amount or 0.0,
1011 'date': voucher.date,
1012 'date_maturity': voucher.date_due
1016 def account_move_get(self, cr, uid, voucher_id, context=None):
1018 This method prepare the creation of the account move related to the given voucher.
1020 :param voucher_id: Id of voucher for which we are creating account_move.
1021 :return: mapping between fieldname and value of account move to create
1024 seq_obj = self.pool.get('ir.sequence')
1025 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1027 name = voucher.number
1028 elif voucher.journal_id.sequence_id:
1029 if not voucher.journal_id.sequence_id.active:
1030 raise osv.except_osv(_('Configuration Error !'),
1031 _('Please activate the sequence of selected journal !'))
1033 c.update({'fiscalyear_id': voucher.period_id.fiscalyear_id.id})
1034 name = seq_obj.next_by_id(cr, uid, voucher.journal_id.sequence_id.id, context=c)
1036 raise osv.except_osv(_('Error!'),
1037 _('Please define a sequence on the journal.'))
1038 if not voucher.reference:
1039 ref = name.replace('/','')
1041 ref = voucher.reference
1045 'journal_id': voucher.journal_id.id,
1046 'narration': voucher.narration,
1047 'date': voucher.date,
1049 'period_id': voucher.period_id.id,
1053 def _get_exchange_lines(self, cr, uid, line, move_id, amount_residual, company_currency, current_currency, context=None):
1055 Prepare the two lines in company currency due to currency rate difference.
1057 :param line: browse record of the voucher.line for which we want to create currency rate difference accounting
1059 :param move_id: Account move wher the move lines will be.
1060 :param amount_residual: Amount to be posted.
1061 :param company_currency: id of currency of the company to which the voucher belong
1062 :param current_currency: id of currency of the voucher
1063 :return: the account move line and its counterpart to create, depicted as mapping between fieldname and value
1064 :rtype: tuple of dict
1066 if amount_residual > 0:
1067 account_id = line.voucher_id.company_id.expense_currency_exchange_account_id
1069 raise osv.except_osv(_('Insufficient Configuration!'),_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
1071 account_id = line.voucher_id.company_id.income_currency_exchange_account_id
1073 raise osv.except_osv(_('Insufficient Configuration!'),_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
1074 # Even if the amount_currency is never filled, we need to pass the foreign currency because otherwise
1075 # the receivable/payable account may have a secondary currency, which render this field mandatory
1076 if line.account_id.currency_id:
1077 account_currency_id = line.account_id.currency_id.id
1079 account_currency_id = company_currency <> current_currency and current_currency or False
1081 'journal_id': line.voucher_id.journal_id.id,
1082 'period_id': line.voucher_id.period_id.id,
1083 'name': _('change')+': '+(line.name or '/'),
1084 'account_id': line.account_id.id,
1086 'partner_id': line.voucher_id.partner_id.id,
1087 'currency_id': account_currency_id,
1088 'amount_currency': 0.0,
1090 'credit': amount_residual > 0 and amount_residual or 0.0,
1091 'debit': amount_residual < 0 and -amount_residual or 0.0,
1092 'date': line.voucher_id.date,
1094 move_line_counterpart = {
1095 'journal_id': line.voucher_id.journal_id.id,
1096 'period_id': line.voucher_id.period_id.id,
1097 'name': _('change')+': '+(line.name or '/'),
1098 'account_id': account_id.id,
1100 'amount_currency': 0.0,
1101 'partner_id': line.voucher_id.partner_id.id,
1102 'currency_id': account_currency_id,
1104 'debit': amount_residual > 0 and amount_residual or 0.0,
1105 'credit': amount_residual < 0 and -amount_residual or 0.0,
1106 'date': line.voucher_id.date,
1108 return (move_line, move_line_counterpart)
1110 def _convert_amount(self, cr, uid, amount, voucher_id, context=None):
1112 This function convert the amount given in company currency. It takes either the rate in the voucher (if the
1113 payment_rate_currency_id is relevant) either the rate encoded in the system.
1115 :param amount: float. The amount to convert
1116 :param voucher: id of the voucher on which we want the conversion
1117 :param context: to context to use for the conversion. It may contain the key 'date' set to the voucher date
1118 field in order to select the good rate to use.
1119 :return: the amount in the currency of the voucher's company
1124 currency_obj = self.pool.get('res.currency')
1125 voucher = self.browse(cr, uid, voucher_id, context=context)
1126 return currency_obj.compute(cr, uid, voucher.currency_id.id, voucher.company_id.currency_id.id, amount, context=context)
1128 def voucher_move_line_create(self, cr, uid, voucher_id, line_total, move_id, company_currency, current_currency, context=None):
1130 Create one account move line, on the given account move, per voucher line where amount is not 0.0.
1131 It returns Tuple with tot_line what is total of difference between debit and credit and
1132 a list of lists with ids to be reconciled with this format (total_deb_cred,list_of_lists).
1134 :param voucher_id: Voucher id what we are working with
1135 :param line_total: Amount of the first line, which correspond to the amount we should totally split among all voucher lines.
1136 :param move_id: Account move wher those lines will be joined.
1137 :param company_currency: id of currency of the company to which the voucher belong
1138 :param current_currency: id of currency of the voucher
1139 :return: Tuple build as (remaining amount not allocated on voucher lines, list of account_move_line created in this method)
1140 :rtype: tuple(float, list of int)
1144 move_line_obj = self.pool.get('account.move.line')
1145 currency_obj = self.pool.get('res.currency')
1146 tax_obj = self.pool.get('account.tax')
1147 tot_line = line_total
1150 date = self.read(cr, uid, voucher_id, ['date'], context=context)['date']
1151 ctx = context.copy()
1152 ctx.update({'date': date})
1153 voucher = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context=ctx)
1154 voucher_currency = voucher.journal_id.currency or voucher.company_id.currency_id
1156 'voucher_special_currency_rate': voucher_currency.rate * voucher.payment_rate ,
1157 'voucher_special_currency': voucher.payment_rate_currency_id and voucher.payment_rate_currency_id.id or False,})
1158 prec = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')
1159 for line in voucher.line_ids:
1160 #create one move line per voucher line where amount is not 0.0
1161 # AND (second part of the clause) only if the original move line was not having debit = credit = 0 (which is a legal value)
1162 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_rounding=prec) and not float_compare(line.move_line_id.debit, 0.0, precision_rounding=prec)):
1164 # convert the amount set on the voucher line into the currency of the voucher's company
1165 # 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
1166 amount = self._convert_amount(cr, uid, line.untax_amount or line.amount, voucher.id, context=ctx)
1167 # if the amount encoded in voucher is equal to the amount unreconciled, we need to compute the
1168 # currency rate difference
1169 if line.amount == line.amount_unreconciled:
1170 if not line.move_line_id:
1171 raise osv.except_osv(_('Wrong voucher line'),_("The invoice you are willing to pay is not valid anymore."))
1172 sign = voucher.type in ('payment', 'purchase') and -1 or 1
1173 currency_rate_difference = sign * (line.move_line_id.amount_residual - amount)
1175 currency_rate_difference = 0.0
1177 'journal_id': voucher.journal_id.id,
1178 'period_id': voucher.period_id.id,
1179 'name': line.name or '/',
1180 'account_id': line.account_id.id,
1182 'partner_id': voucher.partner_id.id,
1183 '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,
1184 'analytic_account_id': line.account_analytic_id and line.account_analytic_id.id or False,
1188 'date': voucher.date
1192 if line.type == 'dr':
1197 if (line.type=='dr'):
1199 move_line['debit'] = amount
1202 move_line['credit'] = amount
1204 if voucher.tax_id and voucher.type in ('sale', 'purchase'):
1206 'account_tax_id': voucher.tax_id.id,
1209 if move_line.get('account_tax_id', False):
1210 tax_data = tax_obj.browse(cr, uid, [move_line['account_tax_id']], context=context)[0]
1211 if not (tax_data.base_code_id and tax_data.tax_code_id):
1212 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))
1214 # compute the amount in foreign currency
1215 foreign_currency_diff = 0.0
1216 amount_currency = False
1217 if line.move_line_id:
1218 # We want to set it on the account move line as soon as the original line had a foreign currency
1219 if line.move_line_id.currency_id and line.move_line_id.currency_id.id != company_currency:
1220 # we compute the amount in that foreign currency.
1221 if line.move_line_id.currency_id.id == current_currency:
1222 # if the voucher and the voucher line share the same currency, there is no computation to do
1223 sign = (move_line['debit'] - move_line['credit']) < 0 and -1 or 1
1224 amount_currency = sign * (line.amount)
1226 # if the rate is specified on the voucher, it will be used thanks to the special keys in the context
1227 # otherwise we use the rates of the system
1228 amount_currency = currency_obj.compute(cr, uid, company_currency, line.move_line_id.currency_id.id, move_line['debit']-move_line['credit'], context=ctx)
1229 if line.amount == line.amount_unreconciled and line.move_line_id.currency_id.id == voucher_currency:
1230 sign = voucher.type in ('payment', 'purchase') and -1 or 1
1231 foreign_currency_diff = sign * line.move_line_id.amount_residual_currency + amount_currency
1233 move_line['amount_currency'] = amount_currency
1234 voucher_line = move_line_obj.create(cr, uid, move_line)
1235 rec_ids = [voucher_line, line.move_line_id.id]
1237 if not currency_obj.is_zero(cr, uid, voucher.company_id.currency_id, currency_rate_difference):
1238 # Change difference entry in company currency
1239 exch_lines = self._get_exchange_lines(cr, uid, line, move_id, currency_rate_difference, company_currency, current_currency, context=context)
1240 new_id = move_line_obj.create(cr, uid, exch_lines[0],context)
1241 move_line_obj.create(cr, uid, exch_lines[1], context)
1242 rec_ids.append(new_id)
1244 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):
1245 # Change difference entry in voucher currency
1246 move_line_foreign_currency = {
1247 'journal_id': line.voucher_id.journal_id.id,
1248 'period_id': line.voucher_id.period_id.id,
1249 'name': _('change')+': '+(line.name or '/'),
1250 'account_id': line.account_id.id,
1252 'partner_id': line.voucher_id.partner_id.id,
1253 'currency_id': line.move_line_id.currency_id.id,
1254 'amount_currency': -1 * foreign_currency_diff,
1258 'date': line.voucher_id.date,
1260 new_id = move_line_obj.create(cr, uid, move_line_foreign_currency, context=context)
1261 rec_ids.append(new_id)
1263 if line.move_line_id.id:
1264 rec_lst_ids.append(rec_ids)
1266 return (tot_line, rec_lst_ids)
1268 def writeoff_move_line_get(self, cr, uid, voucher_id, line_total, move_id, name, company_currency, current_currency, context=None):
1270 Set a dict to be use to create the writeoff move line.
1272 :param voucher_id: Id of voucher what we are creating account_move.
1273 :param line_total: Amount remaining to be allocated on lines.
1274 :param move_id: Id of account move where this line will be added.
1275 :param name: Description of account move line.
1276 :param company_currency: id of currency of the company to which the voucher belong
1277 :param current_currency: id of currency of the voucher
1278 :return: mapping between fieldname and value of account move line to create
1281 currency_obj = self.pool.get('res.currency')
1284 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1285 current_currency_obj = voucher.currency_id or voucher.journal_id.company_id.currency_id
1287 if not currency_obj.is_zero(cr, uid, current_currency_obj, line_total):
1291 if voucher.payment_option == 'with_writeoff':
1292 account_id = voucher.writeoff_acc_id.id
1293 write_off_name = voucher.comment
1294 elif voucher.type in ('sale', 'receipt'):
1295 account_id = voucher.partner_id.property_account_receivable.id
1297 account_id = voucher.partner_id.property_account_payable.id
1298 sign = voucher.type == 'payment' and -1 or 1
1300 'name': write_off_name or name,
1301 'account_id': account_id,
1303 'partner_id': voucher.partner_id.id,
1304 'date': voucher.date,
1305 'credit': diff > 0 and diff or 0.0,
1306 'debit': diff < 0 and -diff or 0.0,
1307 'amount_currency': company_currency <> current_currency and (sign * -1 * voucher.writeoff_amount) or False,
1308 'currency_id': company_currency <> current_currency and current_currency or False,
1309 'analytic_account_id': voucher.analytic_id and voucher.analytic_id.id or False,
1314 def _get_company_currency(self, cr, uid, voucher_id, context=None):
1316 Get the currency of the actual company.
1318 :param voucher_id: Id of the voucher what i want to obtain company currency.
1319 :return: currency id of the company of the voucher
1322 return self.pool.get('account.voucher').browse(cr,uid,voucher_id,context).journal_id.company_id.currency_id.id
1324 def _get_current_currency(self, cr, uid, voucher_id, context=None):
1326 Get the currency of the voucher.
1328 :param voucher_id: Id of the voucher what i want to obtain current currency.
1329 :return: currency id of the voucher
1332 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1333 return voucher.currency_id.id or self._get_company_currency(cr,uid,voucher.id,context)
1335 def action_move_line_create(self, cr, uid, ids, context=None):
1337 Confirm the vouchers given in ids and create the journal entries for each of them
1341 move_pool = self.pool.get('account.move')
1342 move_line_pool = self.pool.get('account.move.line')
1343 for voucher in self.browse(cr, uid, ids, context=context):
1346 company_currency = self._get_company_currency(cr, uid, voucher.id, context)
1347 current_currency = self._get_current_currency(cr, uid, voucher.id, context)
1348 # we select the context to use accordingly if it's a multicurrency case or not
1349 context = self._sel_context(cr, uid, voucher.id, context)
1350 # But for the operations made by _convert_amount, we always need to give the date in the context
1351 ctx = context.copy()
1352 ctx.update({'date': voucher.date})
1353 # Create the account move record.
1354 move_id = move_pool.create(cr, uid, self.account_move_get(cr, uid, voucher.id, context=context), context=context)
1355 # Get the name of the account_move just created
1356 name = move_pool.browse(cr, uid, move_id, context=context).name
1357 # Create the first line of the voucher
1358 move_line_id = move_line_pool.create(cr, uid, self.first_move_line_get(cr,uid,voucher.id, move_id, company_currency, current_currency, context), context)
1359 move_line_brw = move_line_pool.browse(cr, uid, move_line_id, context=context)
1360 line_total = move_line_brw.debit - move_line_brw.credit
1362 if voucher.type == 'sale':
1363 line_total = line_total - self._convert_amount(cr, uid, voucher.tax_amount, voucher.id, context=ctx)
1364 elif voucher.type == 'purchase':
1365 line_total = line_total + self._convert_amount(cr, uid, voucher.tax_amount, voucher.id, context=ctx)
1366 # Create one move line per voucher line where amount is not 0.0
1367 line_total, rec_list_ids = self.voucher_move_line_create(cr, uid, voucher.id, line_total, move_id, company_currency, current_currency, context)
1369 # Create the writeoff line if needed
1370 ml_writeoff = self.writeoff_move_line_get(cr, uid, voucher.id, line_total, move_id, name, company_currency, current_currency, context)
1372 move_line_pool.create(cr, uid, ml_writeoff, context)
1373 # We post the voucher.
1374 self.write(cr, uid, [voucher.id], {
1379 if voucher.journal_id.entry_posted:
1380 move_pool.post(cr, uid, [move_id], context={})
1381 # We automatically reconcile the account move lines.
1383 for rec_ids in rec_list_ids:
1384 if len(rec_ids) >= 2:
1385 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)
1388 def copy(self, cr, uid, id, default=None, context=None):
1395 'line_cr_ids': False,
1396 'line_dr_ids': False,
1399 if 'date' not in default:
1400 default['date'] = time.strftime('%Y-%m-%d')
1401 return super(account_voucher, self).copy(cr, uid, id, default, context)
1404 class account_voucher_line(osv.osv):
1405 _name = 'account.voucher.line'
1406 _description = 'Voucher Lines'
1407 _order = "move_line_id"
1409 # If the payment is in the same currency than the invoice, we keep the same amount
1410 # Otherwise, we compute from invoice currency to payment currency
1411 def _compute_balance(self, cr, uid, ids, name, args, context=None):
1412 currency_pool = self.pool.get('res.currency')
1414 for line in self.browse(cr, uid, ids, context=context):
1415 ctx = context.copy()
1416 ctx.update({'date': line.voucher_id.date})
1417 voucher_rate = self.pool.get('res.currency').read(cr, uid, line.voucher_id.currency_id.id, ['rate'], context=ctx)['rate']
1419 'voucher_special_currency': line.voucher_id.payment_rate_currency_id and line.voucher_id.payment_rate_currency_id.id or False,
1420 'voucher_special_currency_rate': line.voucher_id.payment_rate * voucher_rate})
1422 company_currency = line.voucher_id.journal_id.company_id.currency_id.id
1423 voucher_currency = line.voucher_id.currency_id and line.voucher_id.currency_id.id or company_currency
1424 move_line = line.move_line_id or False
1427 res['amount_original'] = 0.0
1428 res['amount_unreconciled'] = 0.0
1429 elif move_line.currency_id and voucher_currency==move_line.currency_id.id:
1430 res['amount_original'] = abs(move_line.amount_currency)
1431 res['amount_unreconciled'] = abs(move_line.amount_residual_currency)
1433 #always use the amount booked in the company currency as the basis of the conversion into the voucher currency
1434 res['amount_original'] = currency_pool.compute(cr, uid, company_currency, voucher_currency, move_line.credit or move_line.debit or 0.0, context=ctx)
1435 res['amount_unreconciled'] = currency_pool.compute(cr, uid, company_currency, voucher_currency, abs(move_line.amount_residual), context=ctx)
1437 rs_data[line.id] = res
1440 def _currency_id(self, cr, uid, ids, name, args, context=None):
1442 This function returns the currency id of a voucher line. It's either the currency of the
1443 associated move line (if any) or the currency of the voucher or the company currency.
1446 for line in self.browse(cr, uid, ids, context=context):
1447 move_line = line.move_line_id
1449 res[line.id] = move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id
1451 res[line.id] = line.voucher_id.currency_id and line.voucher_id.currency_id.id or line.voucher_id.company_id.currency_id.id
1455 'voucher_id':fields.many2one('account.voucher', 'Voucher', required=1, ondelete='cascade'),
1456 'name':fields.char('Description', size=256),
1457 'account_id':fields.many2one('account.account','Account', required=True),
1458 'partner_id':fields.related('voucher_id', 'partner_id', type='many2one', relation='res.partner', string='Partner'),
1459 'untax_amount':fields.float('Untax Amount'),
1460 'amount':fields.float('Amount', digits_compute=dp.get_precision('Account')),
1461 'reconcile': fields.boolean('Full Reconcile'),
1462 'type':fields.selection([('dr','Debit'),('cr','Credit')], 'Dr/Cr'),
1463 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'),
1464 'move_line_id': fields.many2one('account.move.line', 'Journal Item'),
1465 'date_original': fields.related('move_line_id','date', type='date', relation='account.move.line', string='Date', readonly=1),
1466 'date_due': fields.related('move_line_id','date_maturity', type='date', relation='account.move.line', string='Due Date', readonly=1),
1467 'amount_original': fields.function(_compute_balance, multi='dc', type='float', string='Original Amount', store=True, digits_compute=dp.get_precision('Account')),
1468 'amount_unreconciled': fields.function(_compute_balance, multi='dc', type='float', string='Open Balance', store=True, digits_compute=dp.get_precision('Account')),
1469 'company_id': fields.related('voucher_id','company_id', relation='res.company', type='many2one', string='Company', store=True, readonly=True),
1470 'currency_id': fields.function(_currency_id, string='Currency', type='many2one', relation='res.currency', readonly=True),
1476 def onchange_reconcile(self, cr, uid, ids, reconcile, amount, amount_unreconciled, context=None):
1477 vals = {'amount': 0.0}
1479 vals = { 'amount': amount_unreconciled}
1480 return {'value': vals}
1482 def onchange_amount(self, cr, uid, ids, amount, amount_unreconciled, context=None):
1485 vals['reconcile'] = (amount == amount_unreconciled)
1486 return {'value': vals}
1488 def onchange_move_line_id(self, cr, user, ids, move_line_id, context=None):
1490 Returns a dict that contains new values and context
1492 @param move_line_id: latest value from user input for field move_line_id
1493 @param args: other arguments
1494 @param context: context arguments, like lang, time zone
1496 @return: Returns a dict which contains new values, and context
1499 move_line_pool = self.pool.get('account.move.line')
1501 move_line = move_line_pool.browse(cr, user, move_line_id, context=context)
1502 if move_line.credit:
1507 'account_id': move_line.account_id.id,
1509 'currency_id': move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id,
1515 def default_get(self, cr, user, fields_list, context=None):
1517 Returns default values for fields
1518 @param fields_list: list of fields, for which default values are required to be read
1519 @param context: context arguments, like lang, time zone
1521 @return: Returns a dict that contains default values for fields
1525 journal_id = context.get('journal_id', False)
1526 partner_id = context.get('partner_id', False)
1527 journal_pool = self.pool.get('account.journal')
1528 partner_pool = self.pool.get('res.partner')
1529 values = super(account_voucher_line, self).default_get(cr, user, fields_list, context=context)
1530 if (not journal_id) or ('account_id' not in fields_list):
1532 journal = journal_pool.browse(cr, user, journal_id, context=context)
1535 if journal.type in ('sale', 'sale_refund'):
1536 account_id = journal.default_credit_account_id and journal.default_credit_account_id.id or False
1538 elif journal.type in ('purchase', 'expense', 'purchase_refund'):
1539 account_id = journal.default_debit_account_id and journal.default_debit_account_id.id or False
1542 partner = partner_pool.browse(cr, user, partner_id, context=context)
1543 if context.get('type') == 'payment':
1545 account_id = partner.property_account_payable.id
1546 elif context.get('type') == 'receipt':
1547 account_id = partner.property_account_receivable.id
1550 'account_id':account_id,
1554 account_voucher_line()
1556 class account_bank_statement(osv.osv):
1557 _inherit = 'account.bank.statement'
1559 def button_confirm_bank(self, cr, uid, ids, context=None):
1560 voucher_obj = self.pool.get('account.voucher')
1562 for statement in self.browse(cr, uid, ids, context=context):
1563 voucher_ids += [line.voucher_id.id for line in statement.line_ids if line.voucher_id]
1565 voucher_obj.write(cr, uid, voucher_ids, {'active': True}, context=context)
1566 return super(account_bank_statement, self).button_confirm_bank(cr, uid, ids, context=context)
1568 def button_cancel(self, cr, uid, ids, context=None):
1569 voucher_obj = self.pool.get('account.voucher')
1570 for st in self.browse(cr, uid, ids, context=context):
1572 for line in st.line_ids:
1574 voucher_ids.append(line.voucher_id.id)
1575 voucher_obj.cancel_voucher(cr, uid, voucher_ids, context)
1576 return super(account_bank_statement, self).button_cancel(cr, uid, ids, context=context)
1578 def create_move_from_st_line(self, cr, uid, st_line_id, company_currency_id, next_number, context=None):
1579 voucher_obj = self.pool.get('account.voucher')
1580 wf_service = netsvc.LocalService("workflow")
1581 move_line_obj = self.pool.get('account.move.line')
1582 bank_st_line_obj = self.pool.get('account.bank.statement.line')
1583 st_line = bank_st_line_obj.browse(cr, uid, st_line_id, context=context)
1584 if st_line.voucher_id:
1585 voucher_obj.write(cr, uid, [st_line.voucher_id.id], {'number': next_number}, context=context)
1586 if st_line.voucher_id.state == 'cancel':
1587 voucher_obj.action_cancel_draft(cr, uid, [st_line.voucher_id.id], context=context)
1588 wf_service.trg_validate(uid, 'account.voucher', st_line.voucher_id.id, 'proforma_voucher', cr)
1590 v = voucher_obj.browse(cr, uid, st_line.voucher_id.id, context=context)
1591 bank_st_line_obj.write(cr, uid, [st_line_id], {
1592 'move_ids': [(4, v.move_id.id, False)]
1595 return move_line_obj.write(cr, uid, [x.id for x in v.move_ids], {'statement_id': st_line.statement_id.id}, context=context)
1596 return super(account_bank_statement, self).create_move_from_st_line(cr, uid, st_line.id, company_currency_id, next_number, context=context)
1598 def write(self, cr, uid, ids, vals, context=None):
1599 # Restrict to modify the journal if we already have some voucher of reconciliation created/generated.
1600 # Because the voucher keeps in memory the journal it was created with.
1601 for bk_st in self.browse(cr, uid, ids, context=context):
1602 if vals.get('journal_id') and bk_st.line_ids:
1603 if any([x.voucher_id and True or False for x in bk_st.line_ids]):
1604 raise osv.except_osv(_('Unable to change journal !'), _('You can not change the journal as you already reconciled some statement lines!'))
1605 return super(account_bank_statement, self).write(cr, uid, ids, vals, context=context)
1607 account_bank_statement()
1609 class account_bank_statement_line(osv.osv):
1610 _inherit = 'account.bank.statement.line'
1612 def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
1613 res = super(account_bank_statement_line, self).onchange_partner_id(cr, uid, ids, partner_id, context=context)
1614 if 'value' not in res:
1616 res['value'].update({'voucher_id' : False})
1619 def onchange_amount(self, cr, uid, ids, amount, context=None):
1620 return {'value' : {'voucher_id' : False}}
1622 def _amount_reconciled(self, cursor, user, ids, name, args, context=None):
1626 for line in self.browse(cursor, user, ids, context=context):
1628 res[line.id] = line.voucher_id.amount#
1633 def _check_amount(self, cr, uid, ids, context=None):
1634 for obj in self.browse(cr, uid, ids, context=context):
1636 diff = abs(obj.amount) - obj.voucher_id.amount
1637 if not self.pool.get('res.currency').is_zero(cr, uid, obj.statement_id.currency, diff):
1642 (_check_amount, 'The amount of the voucher must be the same amount as the one on the statement line.', ['amount']),
1646 'amount_reconciled': fields.function(_amount_reconciled,
1647 string='Amount reconciled', type='float'),
1648 'voucher_id': fields.many2one('account.voucher', 'Reconciliation'),
1651 def unlink(self, cr, uid, ids, context=None):
1652 voucher_obj = self.pool.get('account.voucher')
1653 statement_line = self.browse(cr, uid, ids, context=context)
1655 for st_line in statement_line:
1656 if st_line.voucher_id:
1657 unlink_ids.append(st_line.voucher_id.id)
1658 voucher_obj.unlink(cr, uid, unlink_ids, context=context)
1659 return super(account_bank_statement_line, self).unlink(cr, uid, ids, context=context)
1661 account_bank_statement_line()
1663 def resolve_o2m_operations(cr, uid, target_osv, operations, fields, context):
1665 for operation in operations:
1667 if not isinstance(operation, (list, tuple)):
1668 result = target_osv.read(cr, uid, operation, fields, context=context)
1669 elif operation[0] == 0:
1670 # may be necessary to check if all the fields are here and get the default values?
1671 result = operation[2]
1672 elif operation[0] == 1:
1673 result = target_osv.read(cr, uid, operation[1], fields, context=context)
1674 if not result: result = {}
1675 result.update(operation[2])
1676 elif operation[0] == 4:
1677 result = target_osv.read(cr, uid, operation[1], fields, context=context)
1679 results.append(result)
1683 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: