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:
191 for l in line_cr_ids:
192 credit += l['amount']
193 return amount - sign * (credit - debit)
195 def onchange_line_ids(self, cr, uid, ids, line_dr_ids, line_cr_ids, amount, voucher_currency, type, context=None):
196 context = context or {}
197 if not line_dr_ids and not line_cr_ids:
198 return {'value':{'writeoff_amount': 0.0}}
199 line_osv = self.pool.get("account.voucher.line")
200 line_dr_ids = resolve_o2m_operations(cr, uid, line_osv, line_dr_ids, ['amount'], context)
201 line_cr_ids = resolve_o2m_operations(cr, uid, line_osv, line_cr_ids, ['amount'], context)
202 #compute the field is_multi_currency that is used to hide/display options linked to secondary currency on the voucher
203 is_multi_currency = False
204 #loop on the voucher lines to see if one of these has a secondary currency. If yes, we need to see the options
205 for voucher_line in line_dr_ids+line_cr_ids:
206 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')
207 if line_id and self.pool.get('account.move.line').browse(cr, uid, line_id, context=context).currency_id:
208 is_multi_currency = True
210 return {'value': {'writeoff_amount': self._compute_writeoff_amount(cr, uid, line_dr_ids, line_cr_ids, amount, type), 'is_multi_currency': is_multi_currency}}
212 def _get_journal_currency(self, cr, uid, ids, name, args, context=None):
214 for voucher in self.browse(cr, uid, ids, context=context):
215 res[voucher.id] = voucher.journal_id.currency and voucher.journal_id.currency.id or voucher.company_id.currency_id.id
218 def _get_writeoff_amount(self, cr, uid, ids, name, args, context=None):
219 if not ids: return {}
220 currency_obj = self.pool.get('res.currency')
223 for voucher in self.browse(cr, uid, ids, context=context):
224 sign = voucher.type == 'payment' and -1 or 1
225 for l in voucher.line_dr_ids:
227 for l in voucher.line_cr_ids:
229 currency = voucher.currency_id or voucher.company_id.currency_id
230 res[voucher.id] = currency_obj.round(cr, uid, currency, voucher.amount - sign * (credit - debit))
233 def _paid_amount_in_company_currency(self, cr, uid, ids, name, args, context=None):
238 for v in self.browse(cr, uid, ids, context=context):
239 ctx.update({'date': v.date})
240 #make a new call to browse in order to have the right date in the context, to get the right currency rate
241 voucher = self.browse(cr, uid, v.id, context=ctx)
243 'voucher_special_currency': voucher.payment_rate_currency_id and voucher.payment_rate_currency_id.id or False,
244 'voucher_special_currency_rate': voucher.currency_id.rate * voucher.payment_rate,})
245 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)
248 def _get_currency_help_label(self, cr, uid, currency_id, payment_rate, payment_rate_currency_id, context=None):
250 This function builds a string to help the users to understand the behavior of the payment rate fields they can specify on the voucher.
251 This string is only used to improve the usability in the voucher form view and has no other effect.
253 :param currency_id: the voucher currency
254 :type currency_id: integer
255 :param payment_rate: the value of the payment_rate field of the voucher
256 :type payment_rate: float
257 :param payment_rate_currency_id: the value of the payment_rate_currency_id field of the voucher
258 :type payment_rate_currency_id: integer
259 :return: translated string giving a tip on what's the effect of the current payment rate specified
262 rml_parser = report_sxw.rml_parse(cr, uid, 'currency_help_label', context=context)
263 currency_pool = self.pool.get('res.currency')
264 currency_str = payment_rate_str = ''
266 currency_str = rml_parser.formatLang(1, currency_obj=currency_pool.browse(cr, uid, currency_id, context=context))
267 if payment_rate_currency_id:
268 payment_rate_str = rml_parser.formatLang(payment_rate, currency_obj=currency_pool.browse(cr, uid, payment_rate_currency_id, context=context))
269 currency_help_label = _('At the operation date, the exchange rate was\n%s = %s') % (currency_str, payment_rate_str)
270 return currency_help_label
272 def _fnct_currency_help_label(self, cr, uid, ids, name, args, context=None):
274 for voucher in self.browse(cr, uid, ids, context=context):
275 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)
278 _name = 'account.voucher'
279 _description = 'Accounting Voucher'
280 _inherit = ['mail.thread']
281 _order = "date desc, id desc"
282 # _rec_name = 'number'
285 'account_voucher.mt_voucher_state_change': lambda self, cr, uid, obj, ctx=None: True,
290 'type':fields.selection([
292 ('purchase','Purchase'),
293 ('payment','Payment'),
294 ('receipt','Receipt'),
295 ],'Default Type', readonly=True, states={'draft':[('readonly',False)]}),
296 'name':fields.char('Memo', readonly=True, states={'draft':[('readonly',False)]}),
297 'date':fields.date('Date', readonly=True, select=True, states={'draft':[('readonly',False)]},
298 help="Effective date for accounting entries", copy=False),
299 'journal_id':fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
300 'account_id':fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}),
301 'line_ids':fields.one2many('account.voucher.line', 'voucher_id', 'Voucher Lines',
302 readonly=True, copy=True,
303 states={'draft':[('readonly',False)]}),
304 'line_cr_ids':fields.one2many('account.voucher.line','voucher_id','Credits',
305 domain=[('type','=','cr')], context={'default_type':'cr'}, readonly=True, states={'draft':[('readonly',False)]}),
306 'line_dr_ids':fields.one2many('account.voucher.line','voucher_id','Debits',
307 domain=[('type','=','dr')], context={'default_type':'dr'}, readonly=True, states={'draft':[('readonly',False)]}),
308 'period_id': fields.many2one('account.period', 'Period', required=True, readonly=True, states={'draft':[('readonly',False)]}),
309 'narration':fields.text('Notes', readonly=True, states={'draft':[('readonly',False)]}),
310 'currency_id': fields.function(_get_journal_currency, type='many2one', relation='res.currency', string='Currency', readonly=True, required=True),
311 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
312 'state':fields.selection(
314 ('cancel','Cancelled'),
315 ('proforma','Pro-forma'),
317 ], 'Status', readonly=True, track_visibility='onchange', copy=False,
318 help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed Voucher. \
319 \n* The \'Pro-forma\' when voucher is in Pro-forma status,voucher does not have an voucher number. \
320 \n* The \'Posted\' status is used when user create voucher,a voucher number is generated and voucher entries are created in account \
321 \n* The \'Cancelled\' status is used when user cancel voucher.'),
322 'amount': fields.float('Total', digits_compute=dp.get_precision('Account'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
323 'tax_amount':fields.float('Tax Amount', digits_compute=dp.get_precision('Account'), readonly=True),
324 'reference': fields.char('Ref #', readonly=True, states={'draft':[('readonly',False)]},
325 help="Transaction reference number.", copy=False),
326 'number': fields.char('Number', readonly=True, copy=False),
327 'move_id':fields.many2one('account.move', 'Account Entry', copy=False),
328 'move_ids': fields.related('move_id','line_id', type='one2many', relation='account.move.line', string='Journal Items', readonly=True),
329 'partner_id':fields.many2one('res.partner', 'Partner', change_default=1, readonly=True, states={'draft':[('readonly',False)]}),
330 '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'),
331 'paid': fields.function(_check_paid, string='Paid', type='boolean', help="The Voucher has been totally paid."),
332 'pay_now':fields.selection([
333 ('pay_now','Pay Directly'),
334 ('pay_later','Pay Later or Group Funds'),
335 ],'Payment', select=True, readonly=True, states={'draft':[('readonly',False)]}),
336 'tax_id': fields.many2one('account.tax', 'Tax', readonly=True, states={'draft':[('readonly',False)]}, domain=[('price_include','=', False)], help="Only for tax excluded from price"),
337 'pre_line':fields.boolean('Previous Payments ?', required=False),
338 'date_due': fields.date('Due Date', readonly=True, select=True, states={'draft':[('readonly',False)]}),
339 'payment_option':fields.selection([
340 ('without_writeoff', 'Keep Open'),
341 ('with_writeoff', 'Reconcile Payment Balance'),
342 ], '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)"),
343 'writeoff_acc_id': fields.many2one('account.account', 'Counterpart Account', readonly=True, states={'draft': [('readonly', False)]}),
344 'comment': fields.char('Counterpart Comment', required=True, readonly=True, states={'draft': [('readonly', False)]}),
345 'analytic_id': fields.many2one('account.analytic.account','Write-Off Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
346 '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."),
347 'payment_rate_currency_id': fields.many2one('res.currency', 'Payment Rate Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}),
348 'payment_rate': fields.float('Exchange Rate', digits=(12,6), required=True, readonly=True, states={'draft': [('readonly', False)]},
349 help='The specific rate that will be used, in this voucher, between the selected currency (in \'Payment Rate Currency\' field) and the voucher currency.'),
350 'paid_amount_in_company_currency': fields.function(_paid_amount_in_company_currency, string='Paid Amount in Company Currency', type='float', readonly=True),
351 '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'),
352 '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"),
355 'period_id': _get_period,
356 'partner_id': _get_partner,
357 'journal_id':_get_journal,
358 'currency_id': _get_currency,
359 'reference': _get_reference,
360 'narration':_get_narration,
361 'amount': _get_amount,
364 'pay_now': 'pay_now',
366 'date': lambda *a: time.strftime('%Y-%m-%d'),
367 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.voucher',context=c),
369 'payment_option': 'without_writeoff',
370 'comment': _('Write-Off'),
372 'payment_rate_currency_id': _get_payment_rate_currency,
375 def compute_tax(self, cr, uid, ids, context=None):
376 tax_pool = self.pool.get('account.tax')
377 partner_pool = self.pool.get('res.partner')
378 position_pool = self.pool.get('account.fiscal.position')
379 voucher_line_pool = self.pool.get('account.voucher.line')
380 voucher_pool = self.pool.get('account.voucher')
381 if context is None: context = {}
383 for voucher in voucher_pool.browse(cr, uid, ids, context=context):
385 for line in voucher.line_ids:
386 voucher_amount += line.untax_amount or line.amount
387 line.amount = line.untax_amount or line.amount
388 voucher_line_pool.write(cr, uid, [line.id], {'amount':line.amount, 'untax_amount':line.untax_amount})
390 if not voucher.tax_id:
391 self.write(cr, uid, [voucher.id], {'amount':voucher_amount, 'tax_amount':0.0})
394 tax = [tax_pool.browse(cr, uid, voucher.tax_id.id, context=context)]
395 partner = partner_pool.browse(cr, uid, voucher.partner_id.id, context=context) or False
396 taxes = position_pool.map_tax(cr, uid, partner and partner.property_account_position or False, tax)
397 tax = tax_pool.browse(cr, uid, taxes, context=context)
399 total = voucher_amount
402 if not tax[0].price_include:
403 for line in voucher.line_ids:
404 for tax_line in tax_pool.compute_all(cr, uid, tax, line.amount, 1).get('taxes', []):
405 total_tax += tax_line.get('amount', 0.0)
408 for line in voucher.line_ids:
412 for tax_line in tax_pool.compute_all(cr, uid, tax, line.untax_amount or line.amount, 1).get('taxes', []):
413 line_tax += tax_line.get('amount', 0.0)
414 line_total += tax_line.get('price_unit')
415 total_tax += line_tax
416 untax_amount = line.untax_amount or line.amount
417 voucher_line_pool.write(cr, uid, [line.id], {'amount':line_total, 'untax_amount':untax_amount})
419 self.write(cr, uid, [voucher.id], {'amount':total, 'tax_amount':total_tax})
422 def onchange_price(self, cr, uid, ids, line_ids, tax_id, partner_id=False, context=None):
423 context = context or {}
424 tax_pool = self.pool.get('account.tax')
425 partner_pool = self.pool.get('res.partner')
426 position_pool = self.pool.get('account.fiscal.position')
427 line_pool = self.pool.get('account.voucher.line')
436 line_ids = resolve_o2m_operations(cr, uid, line_pool, line_ids, ["amount"], context)
439 for line in line_ids:
441 line_amount = line.get('amount',0.0)
444 tax = [tax_pool.browse(cr, uid, tax_id, context=context)]
446 partner = partner_pool.browse(cr, uid, partner_id, context=context) or False
447 taxes = position_pool.map_tax(cr, uid, partner and partner.property_account_position or False, tax)
448 tax = tax_pool.browse(cr, uid, taxes, context=context)
450 if not tax[0].price_include:
451 for tax_line in tax_pool.compute_all(cr, uid, tax, line_amount, 1).get('taxes', []):
452 total_tax += tax_line.get('amount')
454 voucher_total += line_amount
455 total = voucher_total + total_tax
458 'amount': total or voucher_total,
459 'tax_amount': total_tax
465 def onchange_term_id(self, cr, uid, ids, term_id, amount):
466 term_pool = self.pool.get('account.payment.term')
469 default = {'date_due':False}
470 if term_id and amount:
471 terms = term_pool.compute(cr, uid, term_id, amount)
473 due_date = terms[-1][0]
477 return {'value':default}
479 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):
481 Returns a dict that contains new values and context
483 @param partner_id: latest value from user input for field partner_id
484 @param args: other arguments
485 @param context: context arguments, like lang, time zone
487 @return: Returns a dict which contains new values, and context
493 if not partner_id or not journal_id:
496 partner_pool = self.pool.get('res.partner')
497 journal_pool = self.pool.get('account.journal')
499 journal = journal_pool.browse(cr, uid, journal_id, context=context)
500 partner = partner_pool.browse(cr, uid, partner_id, context=context)
503 if journal.type in ('sale','sale_refund'):
504 account_id = partner.property_account_receivable.id
506 elif journal.type in ('purchase', 'purchase_refund','expense'):
507 account_id = partner.property_account_payable.id
510 if not journal.default_credit_account_id or not journal.default_debit_account_id:
511 raise osv.except_osv(_('Error!'), _('Please define default credit/debit accounts on the journal "%s".') % (journal.name))
512 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
515 default['value']['account_id'] = account_id
516 default['value']['type'] = ttype or tr_type
518 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)
519 default['value'].update(vals.get('value'))
523 def onchange_rate(self, cr, uid, ids, rate, amount, currency_id, payment_rate_currency_id, company_id, context=None):
524 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)}}
525 if rate and amount and currency_id:
526 company_currency = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id
527 #context should contain the date, the payment currency and the payment rate specified on the voucher
528 amount_in_company_currency = self.pool.get('res.currency').compute(cr, uid, currency_id, company_currency.id, amount, context=context)
529 res['value']['paid_amount_in_company_currency'] = amount_in_company_currency
532 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):
536 ctx.update({'date': date})
537 #read the voucher rate with the right date in the context
538 currency_id = currency_id or self.pool.get('res.company').browse(cr, uid, company_id, context=ctx).currency_id.id
539 voucher_rate = self.pool.get('res.currency').read(cr, uid, [currency_id], ['rate'], context=ctx)[0]['rate']
541 'voucher_special_currency': payment_rate_currency_id,
542 'voucher_special_currency_rate': rate * voucher_rate})
543 res = self.recompute_voucher_lines(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=ctx)
544 vals = self.onchange_rate(cr, uid, ids, rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
545 for key in vals.keys():
546 res[key].update(vals[key])
549 def recompute_payment_rate(self, cr, uid, ids, vals, currency_id, date, ttype, journal_id, amount, context=None):
552 #on change of the journal, we need to set also the default value for payment_rate and payment_rate_currency_id
553 currency_obj = self.pool.get('res.currency')
554 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
555 company_id = journal.company_id.id
557 currency_id = currency_id or journal.company_id.currency_id.id
558 payment_rate_currency_id = currency_id
560 ctx.update({'date': date})
562 if ttype == 'receipt':
563 o2m_to_loop = 'line_cr_ids'
564 elif ttype == 'payment':
565 o2m_to_loop = 'line_dr_ids'
566 if o2m_to_loop and 'value' in vals and o2m_to_loop in vals['value']:
567 for voucher_line in vals['value'][o2m_to_loop]:
568 if voucher_line['currency_id'] != currency_id:
569 # we take as default value for the payment_rate_currency_id, the currency of the first invoice that
570 # is not in the voucher currency
571 payment_rate_currency_id = voucher_line['currency_id']
572 tmp = currency_obj.browse(cr, uid, payment_rate_currency_id, context=ctx).rate
573 payment_rate = tmp / currency_obj.browse(cr, uid, currency_id, context=ctx).rate
575 vals['value'].update({
576 'payment_rate': payment_rate,
577 'currency_id': currency_id,
578 'payment_rate_currency_id': payment_rate_currency_id
580 #read the voucher rate with the right date in the context
581 voucher_rate = self.pool.get('res.currency').read(cr, uid, [currency_id], ['rate'], context=ctx)[0]['rate']
583 'voucher_special_currency_rate': payment_rate * voucher_rate,
584 'voucher_special_currency': payment_rate_currency_id})
585 res = self.onchange_rate(cr, uid, ids, payment_rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
586 for key in res.keys():
587 vals[key].update(res[key])
590 def basic_onchange_partner(self, cr, uid, ids, partner_id, journal_id, ttype, context=None):
591 partner_pool = self.pool.get('res.partner')
592 journal_pool = self.pool.get('account.journal')
593 res = {'value': {'account_id': False}}
594 if not partner_id or not journal_id:
597 journal = journal_pool.browse(cr, uid, journal_id, context=context)
598 partner = partner_pool.browse(cr, uid, partner_id, context=context)
600 if journal.type in ('sale','sale_refund'):
601 account_id = partner.property_account_receivable.id
602 elif journal.type in ('purchase', 'purchase_refund','expense'):
603 account_id = partner.property_account_payable.id
605 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
607 res['value']['account_id'] = account_id
610 def onchange_partner_id(self, cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=None):
615 #TODO: comment me and use me directly in the sales/purchases views
616 res = self.basic_onchange_partner(cr, uid, ids, partner_id, journal_id, ttype, context=context)
617 if ttype in ['sale', 'purchase']:
620 # 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
621 ctx.update({'date': date})
622 vals = self.recompute_voucher_lines(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=ctx)
623 vals2 = self.recompute_payment_rate(cr, uid, ids, vals, currency_id, date, ttype, journal_id, amount, context=context)
624 for key in vals.keys():
625 res[key].update(vals[key])
626 for key in vals2.keys():
627 res[key].update(vals2[key])
628 #TODO: can probably be removed now
629 #TODO: onchange_partner_id() should not returns [pre_line, line_dr_ids, payment_rate...] for type sale, and not
630 # [pre_line, line_cr_ids, payment_rate...] for type purchase.
631 # We should definitively split account.voucher object in two and make distinct on_change functions. In the
632 # meanwhile, bellow lines must be there because the fields aren't present in the view, what crashes if the
633 # onchange returns a value for them
635 del(res['value']['line_dr_ids'])
636 del(res['value']['pre_line'])
637 del(res['value']['payment_rate'])
638 elif ttype == 'purchase':
639 del(res['value']['line_cr_ids'])
640 del(res['value']['pre_line'])
641 del(res['value']['payment_rate'])
644 def recompute_voucher_lines(self, cr, uid, ids, partner_id, journal_id, price, currency_id, ttype, date, context=None):
646 Returns a dict that contains new values and context
648 @param partner_id: latest value from user input for field partner_id
649 @param args: other arguments
650 @param context: context arguments, like lang, time zone
652 @return: Returns a dict which contains new values, and context
654 def _remove_noise_in_o2m():
655 """if the line is partially reconciled, then we must pay attention to display it only once and
657 This function returns True if the line is considered as noise and should not be displayed
659 if line.reconcile_partial_id:
660 if currency_id == line.currency_id.id:
661 if line.amount_residual_currency <= 0:
664 if line.amount_residual <= 0:
670 context_multi_currency = context.copy()
672 currency_pool = self.pool.get('res.currency')
673 move_line_pool = self.pool.get('account.move.line')
674 partner_pool = self.pool.get('res.partner')
675 journal_pool = self.pool.get('account.journal')
676 line_pool = self.pool.get('account.voucher.line')
680 'value': {'line_dr_ids': [] ,'line_cr_ids': [] ,'pre_line': False,},
684 line_ids = ids and line_pool.search(cr, uid, [('voucher_id', '=', ids[0])]) or False
686 line_pool.unlink(cr, uid, line_ids)
688 if not partner_id or not journal_id:
691 journal = journal_pool.browse(cr, uid, journal_id, context=context)
692 partner = partner_pool.browse(cr, uid, partner_id, context=context)
693 currency_id = currency_id or journal.company_id.currency_id.id
698 if context.get('account_id'):
699 account_type = self.pool['account.account'].browse(cr, uid, context['account_id'], context=context).type
700 if ttype == 'payment':
702 account_type = 'payable'
703 total_debit = price or 0.0
705 total_credit = price or 0.0
707 account_type = 'receivable'
709 if not context.get('move_line_ids', False):
710 ids = move_line_pool.search(cr, uid, [('state','=','valid'), ('account_id.type', '=', account_type), ('reconcile_id', '=', False), ('partner_id', '=', partner_id)], context=context)
712 ids = context['move_line_ids']
713 invoice_id = context.get('invoice_id', False)
714 company_currency = journal.company_id.currency_id.id
715 move_lines_found = []
717 #order the lines by most old first
719 account_move_lines = move_line_pool.browse(cr, uid, ids, context=context)
721 #compute the total debit/credit and look for a matching open amount or invoice
722 for line in account_move_lines:
723 if _remove_noise_in_o2m():
727 if line.invoice.id == invoice_id:
728 #if the invoice linked to the voucher line is equal to the invoice_id in context
729 #then we assign the amount on that line, whatever the other voucher lines
730 move_lines_found.append(line.id)
731 elif currency_id == company_currency:
732 #otherwise treatments is the same but with other field names
733 if line.amount_residual == price:
734 #if the amount residual is equal the amount voucher, we assign it to that voucher
735 #line, whatever the other voucher lines
736 move_lines_found.append(line.id)
738 #otherwise we will split the voucher amount on each line (by most old first)
739 total_credit += line.credit or 0.0
740 total_debit += line.debit or 0.0
741 elif currency_id == line.currency_id.id:
742 if line.amount_residual_currency == price:
743 move_lines_found.append(line.id)
745 total_credit += line.credit and line.amount_currency or 0.0
746 total_debit += line.debit and line.amount_currency or 0.0
748 remaining_amount = price
749 #voucher line creation
750 for line in account_move_lines:
752 if _remove_noise_in_o2m():
755 if line.currency_id and currency_id == line.currency_id.id:
756 amount_original = abs(line.amount_currency)
757 amount_unreconciled = abs(line.amount_residual_currency)
759 #always use the amount booked in the company currency as the basis of the conversion into the voucher currency
760 amount_original = currency_pool.compute(cr, uid, company_currency, currency_id, line.credit or line.debit or 0.0, context=context_multi_currency)
761 amount_unreconciled = currency_pool.compute(cr, uid, company_currency, currency_id, abs(line.amount_residual), context=context_multi_currency)
762 line_currency_id = line.currency_id and line.currency_id.id or company_currency
764 'name':line.move_id.name,
765 'type': line.credit and 'dr' or 'cr',
766 'move_line_id':line.id,
767 'account_id':line.account_id.id,
768 'amount_original': amount_original,
769 'amount': (line.id in move_lines_found) and min(abs(remaining_amount), amount_unreconciled) or 0.0,
770 'date_original':line.date,
771 'date_due':line.date_maturity,
772 'amount_unreconciled': amount_unreconciled,
773 'currency_id': line_currency_id,
775 remaining_amount -= rs['amount']
776 #in case a corresponding move_line hasn't been found, we now try to assign the voucher amount
777 #on existing invoices: we split voucher amount by most old first, but only for lines in the same currency
778 if not move_lines_found:
779 if currency_id == line_currency_id:
781 amount = min(amount_unreconciled, abs(total_debit))
782 rs['amount'] = amount
783 total_debit -= amount
785 amount = min(amount_unreconciled, abs(total_credit))
786 rs['amount'] = amount
787 total_credit -= amount
789 if rs['amount_unreconciled'] == rs['amount']:
790 rs['reconcile'] = True
792 if rs['type'] == 'cr':
793 default['value']['line_cr_ids'].append(rs)
795 default['value']['line_dr_ids'].append(rs)
797 if len(default['value']['line_cr_ids']) > 0:
798 default['value']['pre_line'] = 1
799 elif len(default['value']['line_dr_ids']) > 0:
800 default['value']['pre_line'] = 1
801 default['value']['writeoff_amount'] = self._compute_writeoff_amount(cr, uid, default['value']['line_dr_ids'], default['value']['line_cr_ids'], price, ttype)
804 def onchange_payment_rate_currency(self, cr, uid, ids, currency_id, payment_rate, payment_rate_currency_id, date, amount, company_id, context=None):
809 #set the default payment rate of the voucher and compute the paid amount in company currency
811 ctx.update({'date': date})
812 #read the voucher rate with the right date in the context
813 voucher_rate = self.pool.get('res.currency').read(cr, uid, [currency_id], ['rate'], context=ctx)[0]['rate']
815 'voucher_special_currency_rate': payment_rate * voucher_rate,
816 'voucher_special_currency': payment_rate_currency_id})
817 vals = self.onchange_rate(cr, uid, ids, payment_rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
818 for key in vals.keys():
819 res[key].update(vals[key])
822 def onchange_date(self, cr, uid, ids, date, currency_id, payment_rate_currency_id, amount, company_id, context=None):
824 @param date: latest value from user input for field date
825 @param args: other arguments
826 @param context: context arguments, like lang, time zone
827 @return: Returns a dict which contains new values, and context
832 #set the period of the voucher
833 period_pool = self.pool.get('account.period')
834 currency_obj = self.pool.get('res.currency')
836 ctx.update({'company_id': company_id, 'account_period_prefer_normal': True})
837 voucher_currency_id = currency_id or self.pool.get('res.company').browse(cr, uid, company_id, context=ctx).currency_id.id
838 pids = period_pool.find(cr, uid, date, context=ctx)
840 res['value'].update({'period_id':pids[0]})
841 if payment_rate_currency_id:
842 ctx.update({'date': date})
844 if payment_rate_currency_id != currency_id:
845 tmp = currency_obj.browse(cr, uid, payment_rate_currency_id, context=ctx).rate
846 payment_rate = tmp / currency_obj.browse(cr, uid, voucher_currency_id, context=ctx).rate
847 vals = self.onchange_payment_rate_currency(cr, uid, ids, voucher_currency_id, payment_rate, payment_rate_currency_id, date, amount, company_id, context=context)
848 vals['value'].update({'payment_rate': payment_rate})
849 for key in vals.keys():
850 res[key].update(vals[key])
853 def onchange_journal(self, cr, uid, ids, journal_id, line_ids, tax_id, partner_id, date, amount, ttype, company_id, context=None):
858 journal_pool = self.pool.get('account.journal')
859 journal = journal_pool.browse(cr, uid, journal_id, context=context)
860 account_id = journal.default_credit_account_id or journal.default_debit_account_id
862 if account_id and account_id.tax_ids:
863 tax_id = account_id.tax_ids[0].id
866 if ttype in ('sale', 'purchase'):
867 vals = self.onchange_price(cr, uid, ids, line_ids, tax_id, partner_id, context)
868 vals['value'].update({'tax_id':tax_id,'amount': amount})
871 currency_id = journal.currency.id
873 currency_id = journal.company_id.currency_id.id
874 vals['value'].update({'currency_id': currency_id})
875 #in case we want to register the payment directly from an invoice, it's confusing to allow to switch the journal
876 #without seeing that the amount is expressed in the journal currency, and not in the invoice currency. So to avoid
877 #this common mistake, we simply reset the amount to 0 if the currency is not the invoice currency.
878 if context.get('payment_expected_currency') and currency_id != context.get('payment_expected_currency'):
879 vals['value']['amount'] = 0
882 res = self.onchange_partner_id(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context)
883 for key in res.keys():
884 vals[key].update(res[key])
887 def button_proforma_voucher(self, cr, uid, ids, context=None):
888 self.signal_workflow(cr, uid, ids, 'proforma_voucher')
889 return {'type': 'ir.actions.act_window_close'}
891 def proforma_voucher(self, cr, uid, ids, context=None):
892 self.action_move_line_create(cr, uid, ids, context=context)
895 def action_cancel_draft(self, cr, uid, ids, context=None):
896 self.create_workflow(cr, uid, ids)
897 self.write(cr, uid, ids, {'state':'draft'})
900 def cancel_voucher(self, cr, uid, ids, context=None):
901 reconcile_pool = self.pool.get('account.move.reconcile')
902 move_pool = self.pool.get('account.move')
903 move_line_pool = self.pool.get('account.move.line')
904 for voucher in self.browse(cr, uid, ids, context=context):
905 # refresh to make sure you don't unlink an already removed move
907 for line in voucher.move_ids:
908 # refresh to make sure you don't unreconcile an already unreconciled entry
910 if line.reconcile_id:
911 move_lines = [move_line.id for move_line in line.reconcile_id.line_id]
912 move_lines.remove(line.id)
913 reconcile_pool.unlink(cr, uid, [line.reconcile_id.id])
914 if len(move_lines) >= 2:
915 move_line_pool.reconcile_partial(cr, uid, move_lines, 'auto',context=context)
917 move_pool.button_cancel(cr, uid, [voucher.move_id.id])
918 move_pool.unlink(cr, uid, [voucher.move_id.id])
923 self.write(cr, uid, ids, res)
926 def unlink(self, cr, uid, ids, context=None):
927 for t in self.read(cr, uid, ids, ['state'], context=context):
928 if t['state'] not in ('draft', 'cancel'):
929 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete voucher(s) which are already opened or paid.'))
930 return super(account_voucher, self).unlink(cr, uid, ids, context=context)
932 def onchange_payment(self, cr, uid, ids, pay_now, journal_id, partner_id, ttype='sale'):
937 partner_pool = self.pool.get('res.partner')
938 journal_pool = self.pool.get('account.journal')
939 if pay_now == 'pay_later':
940 partner = partner_pool.browse(cr, uid, partner_id)
941 journal = journal_pool.browse(cr, uid, journal_id)
942 if journal.type in ('sale','sale_refund'):
943 account_id = partner.property_account_receivable.id
944 elif journal.type in ('purchase', 'purchase_refund','expense'):
945 account_id = partner.property_account_payable.id
947 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
949 res['account_id'] = account_id
952 def _sel_context(self, cr, uid, voucher_id, context=None):
954 Select the context to use accordingly if it needs to be multicurrency or not.
956 :param voucher_id: Id of the actual voucher
957 :return: The returned context will be the same as given in parameter if the voucher currency is the same
958 than the company currency, otherwise it's a copy of the parameter with an extra key 'date' containing
959 the date of the voucher.
962 company_currency = self._get_company_currency(cr, uid, voucher_id, context)
963 current_currency = self._get_current_currency(cr, uid, voucher_id, context)
964 if current_currency <> company_currency:
965 context_multi_currency = context.copy()
966 voucher = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context)
967 context_multi_currency.update({'date': voucher.date})
968 return context_multi_currency
971 def first_move_line_get(self, cr, uid, voucher_id, move_id, company_currency, current_currency, context=None):
973 Return a dict to be use to create the first account move line of given voucher.
975 :param voucher_id: Id of voucher what we are creating account_move.
976 :param move_id: Id of account move where this line will be added.
977 :param company_currency: id of currency of the company to which the voucher belong
978 :param current_currency: id of currency of the voucher
979 :return: mapping between fieldname and value of account move line to create
982 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
984 # TODO: is there any other alternative then the voucher type ??
985 # ANSWER: We can have payment and receipt "In Advance".
986 # TODO: Make this logic available.
987 # -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
988 if voucher.type in ('purchase', 'payment'):
989 credit = voucher.paid_amount_in_company_currency
990 elif voucher.type in ('sale', 'receipt'):
991 debit = voucher.paid_amount_in_company_currency
992 if debit < 0: credit = -debit; debit = 0.0
993 if credit < 0: debit = -credit; credit = 0.0
994 sign = debit - credit < 0 and -1 or 1
995 #set the first line of the voucher
997 'name': voucher.name or '/',
1000 'account_id': voucher.account_id.id,
1002 'journal_id': voucher.journal_id.id,
1003 'period_id': voucher.period_id.id,
1004 'partner_id': voucher.partner_id.id,
1005 'currency_id': company_currency <> current_currency and current_currency or False,
1006 'amount_currency': (sign * abs(voucher.amount) # amount < 0 for refunds
1007 if company_currency != current_currency else 0.0),
1008 'date': voucher.date,
1009 'date_maturity': voucher.date_due
1013 def account_move_get(self, cr, uid, voucher_id, context=None):
1015 This method prepare the creation of the account move related to the given voucher.
1017 :param voucher_id: Id of voucher for which we are creating account_move.
1018 :return: mapping between fieldname and value of account move to create
1021 seq_obj = self.pool.get('ir.sequence')
1022 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1024 name = voucher.number
1025 elif voucher.journal_id.sequence_id:
1026 if not voucher.journal_id.sequence_id.active:
1027 raise osv.except_osv(_('Configuration Error !'),
1028 _('Please activate the sequence of selected journal !'))
1030 c.update({'fiscalyear_id': voucher.period_id.fiscalyear_id.id})
1031 name = seq_obj.next_by_id(cr, uid, voucher.journal_id.sequence_id.id, context=c)
1033 raise osv.except_osv(_('Error!'),
1034 _('Please define a sequence on the journal.'))
1035 if not voucher.reference:
1036 ref = name.replace('/','')
1038 ref = voucher.reference
1042 'journal_id': voucher.journal_id.id,
1043 'narration': voucher.narration,
1044 'date': voucher.date,
1046 'period_id': voucher.period_id.id,
1050 def _get_exchange_lines(self, cr, uid, line, move_id, amount_residual, company_currency, current_currency, context=None):
1052 Prepare the two lines in company currency due to currency rate difference.
1054 :param line: browse record of the voucher.line for which we want to create currency rate difference accounting
1056 :param move_id: Account move wher the move lines will be.
1057 :param amount_residual: Amount to be posted.
1058 :param company_currency: id of currency of the company to which the voucher belong
1059 :param current_currency: id of currency of the voucher
1060 :return: the account move line and its counterpart to create, depicted as mapping between fieldname and value
1061 :rtype: tuple of dict
1063 if amount_residual > 0:
1064 account_id = line.voucher_id.company_id.expense_currency_exchange_account_id
1066 model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'account', 'action_account_form')
1067 msg = _("You should configure the 'Loss Exchange Rate Account' to manage automatically the booking of accounting entries related to differences between exchange rates.")
1068 raise openerp.exceptions.RedirectWarning(msg, action_id, _('Go to the configuration panel'))
1070 account_id = line.voucher_id.company_id.income_currency_exchange_account_id
1072 model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'account', 'action_account_form')
1073 msg = _("You should configure the 'Gain Exchange Rate Account' to manage automatically the booking of accounting entries related to differences between exchange rates.")
1074 raise openerp.exceptions.RedirectWarning(msg, action_id, _('Go to the configuration panel'))
1075 # Even if the amount_currency is never filled, we need to pass the foreign currency because otherwise
1076 # the receivable/payable account may have a secondary currency, which render this field mandatory
1077 if line.account_id.currency_id:
1078 account_currency_id = line.account_id.currency_id.id
1080 account_currency_id = company_currency <> current_currency and current_currency or False
1082 'journal_id': line.voucher_id.journal_id.id,
1083 'period_id': line.voucher_id.period_id.id,
1084 'name': _('change')+': '+(line.name or '/'),
1085 'account_id': line.account_id.id,
1087 'partner_id': line.voucher_id.partner_id.id,
1088 'currency_id': account_currency_id,
1089 'amount_currency': 0.0,
1091 'credit': amount_residual > 0 and amount_residual or 0.0,
1092 'debit': amount_residual < 0 and -amount_residual or 0.0,
1093 'date': line.voucher_id.date,
1095 move_line_counterpart = {
1096 'journal_id': line.voucher_id.journal_id.id,
1097 'period_id': line.voucher_id.period_id.id,
1098 'name': _('change')+': '+(line.name or '/'),
1099 'account_id': account_id.id,
1101 'amount_currency': 0.0,
1102 'partner_id': line.voucher_id.partner_id.id,
1103 'currency_id': account_currency_id,
1105 'debit': amount_residual > 0 and amount_residual or 0.0,
1106 'credit': amount_residual < 0 and -amount_residual or 0.0,
1107 'date': line.voucher_id.date,
1109 return (move_line, move_line_counterpart)
1111 def _convert_amount(self, cr, uid, amount, voucher_id, context=None):
1113 This function convert the amount given in company currency. It takes either the rate in the voucher (if the
1114 payment_rate_currency_id is relevant) either the rate encoded in the system.
1116 :param amount: float. The amount to convert
1117 :param voucher: id of the voucher on which we want the conversion
1118 :param context: to context to use for the conversion. It may contain the key 'date' set to the voucher date
1119 field in order to select the good rate to use.
1120 :return: the amount in the currency of the voucher's company
1125 currency_obj = self.pool.get('res.currency')
1126 voucher = self.browse(cr, uid, voucher_id, context=context)
1127 return currency_obj.compute(cr, uid, voucher.currency_id.id, voucher.company_id.currency_id.id, amount, context=context)
1129 def voucher_move_line_create(self, cr, uid, voucher_id, line_total, move_id, company_currency, current_currency, context=None):
1131 Create one account move line, on the given account move, per voucher line where amount is not 0.0.
1132 It returns Tuple with tot_line what is total of difference between debit and credit and
1133 a list of lists with ids to be reconciled with this format (total_deb_cred,list_of_lists).
1135 :param voucher_id: Voucher id what we are working with
1136 :param line_total: Amount of the first line, which correspond to the amount we should totally split among all voucher lines.
1137 :param move_id: Account move wher those lines will be joined.
1138 :param company_currency: id of currency of the company to which the voucher belong
1139 :param current_currency: id of currency of the voucher
1140 :return: Tuple build as (remaining amount not allocated on voucher lines, list of account_move_line created in this method)
1141 :rtype: tuple(float, list of int)
1145 move_line_obj = self.pool.get('account.move.line')
1146 currency_obj = self.pool.get('res.currency')
1147 tax_obj = self.pool.get('account.tax')
1148 tot_line = line_total
1151 date = self.read(cr, uid, [voucher_id], ['date'], context=context)[0]['date']
1152 ctx = context.copy()
1153 ctx.update({'date': date})
1154 voucher = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context=ctx)
1155 voucher_currency = voucher.journal_id.currency or voucher.company_id.currency_id
1157 'voucher_special_currency_rate': voucher_currency.rate * voucher.payment_rate ,
1158 'voucher_special_currency': voucher.payment_rate_currency_id and voucher.payment_rate_currency_id.id or False,})
1159 prec = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')
1160 for line in voucher.line_ids:
1161 #create one move line per voucher line where amount is not 0.0
1162 # AND (second part of the clause) only if the original move line was not having debit = credit = 0 (which is a legal value)
1163 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)):
1165 # convert the amount set on the voucher line into the currency of the voucher's company
1166 # 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
1167 amount = self._convert_amount(cr, uid, line.untax_amount or line.amount, voucher.id, context=ctx)
1168 # if the amount encoded in voucher is equal to the amount unreconciled, we need to compute the
1169 # currency rate difference
1170 if line.amount == line.amount_unreconciled:
1171 if not line.move_line_id:
1172 raise osv.except_osv(_('Wrong voucher line'),_("The invoice you are willing to pay is not valid anymore."))
1173 sign = line.type =='dr' and -1 or 1
1174 currency_rate_difference = sign * (line.move_line_id.amount_residual - amount)
1176 currency_rate_difference = 0.0
1178 'journal_id': voucher.journal_id.id,
1179 'period_id': voucher.period_id.id,
1180 'name': line.name or '/',
1181 'account_id': line.account_id.id,
1183 'partner_id': voucher.partner_id.id,
1184 '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,
1185 'analytic_account_id': line.account_analytic_id and line.account_analytic_id.id or False,
1189 'date': voucher.date
1193 if line.type == 'dr':
1198 if (line.type=='dr'):
1200 move_line['debit'] = amount
1203 move_line['credit'] = amount
1205 if voucher.tax_id and voucher.type in ('sale', 'purchase'):
1207 'account_tax_id': voucher.tax_id.id,
1210 if move_line.get('account_tax_id', False):
1211 tax_data = tax_obj.browse(cr, uid, [move_line['account_tax_id']], context=context)[0]
1212 if not (tax_data.base_code_id and tax_data.tax_code_id):
1213 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))
1215 # compute the amount in foreign currency
1216 foreign_currency_diff = 0.0
1217 amount_currency = False
1218 if line.move_line_id:
1219 # We want to set it on the account move line as soon as the original line had a foreign currency
1220 if line.move_line_id.currency_id and line.move_line_id.currency_id.id != company_currency:
1221 # we compute the amount in that foreign currency.
1222 if line.move_line_id.currency_id.id == current_currency:
1223 # if the voucher and the voucher line share the same currency, there is no computation to do
1224 sign = (move_line['debit'] - move_line['credit']) < 0 and -1 or 1
1225 amount_currency = sign * (line.amount)
1227 # if the rate is specified on the voucher, it will be used thanks to the special keys in the context
1228 # otherwise we use the rates of the system
1229 amount_currency = currency_obj.compute(cr, uid, company_currency, line.move_line_id.currency_id.id, move_line['debit']-move_line['credit'], context=ctx)
1230 if line.amount == line.amount_unreconciled:
1231 foreign_currency_diff = line.move_line_id.amount_residual_currency - abs(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)
1262 if line.move_line_id.id:
1263 rec_lst_ids.append(rec_ids)
1264 return (tot_line, rec_lst_ids)
1266 def writeoff_move_line_get(self, cr, uid, voucher_id, line_total, move_id, name, company_currency, current_currency, context=None):
1268 Set a dict to be use to create the writeoff move line.
1270 :param voucher_id: Id of voucher what we are creating account_move.
1271 :param line_total: Amount remaining to be allocated on lines.
1272 :param move_id: Id of account move where this line will be added.
1273 :param name: Description of account move line.
1274 :param company_currency: id of currency of the company to which the voucher belong
1275 :param current_currency: id of currency of the voucher
1276 :return: mapping between fieldname and value of account move line to create
1279 currency_obj = self.pool.get('res.currency')
1282 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1283 current_currency_obj = voucher.currency_id or voucher.journal_id.company_id.currency_id
1285 if not currency_obj.is_zero(cr, uid, current_currency_obj, line_total):
1289 if voucher.payment_option == 'with_writeoff':
1290 account_id = voucher.writeoff_acc_id.id
1291 write_off_name = voucher.comment
1292 elif voucher.partner_id:
1293 if voucher.type in ('sale', 'receipt'):
1294 account_id = voucher.partner_id.property_account_receivable.id
1296 account_id = voucher.partner_id.property_account_payable.id
1298 # fallback on account of voucher
1299 account_id = voucher.account_id.id
1300 sign = voucher.type == 'payment' and -1 or 1
1302 'name': write_off_name or name,
1303 'account_id': account_id,
1305 'partner_id': voucher.partner_id.id,
1306 'date': voucher.date,
1307 'credit': diff > 0 and diff or 0.0,
1308 'debit': diff < 0 and -diff or 0.0,
1309 'amount_currency': company_currency <> current_currency and (sign * -1 * voucher.writeoff_amount) or 0.0,
1310 'currency_id': company_currency <> current_currency and current_currency or False,
1311 'analytic_account_id': voucher.analytic_id and voucher.analytic_id.id or False,
1316 def _get_company_currency(self, cr, uid, voucher_id, context=None):
1318 Get the currency of the actual company.
1320 :param voucher_id: Id of the voucher what i want to obtain company currency.
1321 :return: currency id of the company of the voucher
1324 return self.pool.get('account.voucher').browse(cr,uid,voucher_id,context).journal_id.company_id.currency_id.id
1326 def _get_current_currency(self, cr, uid, voucher_id, context=None):
1328 Get the currency of the voucher.
1330 :param voucher_id: Id of the voucher what i want to obtain current currency.
1331 :return: currency id of the voucher
1334 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1335 return voucher.currency_id.id or self._get_company_currency(cr,uid,voucher.id,context)
1337 def action_move_line_create(self, cr, uid, ids, context=None):
1339 Confirm the vouchers given in ids and create the journal entries for each of them
1343 move_pool = self.pool.get('account.move')
1344 move_line_pool = self.pool.get('account.move.line')
1345 for voucher in self.browse(cr, uid, ids, context=context):
1346 local_context = dict(context, force_company=voucher.journal_id.company_id.id)
1349 company_currency = self._get_company_currency(cr, uid, voucher.id, context)
1350 current_currency = self._get_current_currency(cr, uid, voucher.id, context)
1351 # we select the context to use accordingly if it's a multicurrency case or not
1352 context = self._sel_context(cr, uid, voucher.id, context)
1353 # But for the operations made by _convert_amount, we always need to give the date in the context
1354 ctx = context.copy()
1355 ctx.update({'date': voucher.date})
1356 # Create the account move record.
1357 move_id = move_pool.create(cr, uid, self.account_move_get(cr, uid, voucher.id, context=context), context=context)
1358 # Get the name of the account_move just created
1359 name = move_pool.browse(cr, uid, move_id, context=context).name
1360 # Create the first line of the voucher
1361 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)
1362 move_line_brw = move_line_pool.browse(cr, uid, move_line_id, context=context)
1363 line_total = move_line_brw.debit - move_line_brw.credit
1365 if voucher.type == 'sale':
1366 line_total = line_total - self._convert_amount(cr, uid, voucher.tax_amount, voucher.id, context=ctx)
1367 elif voucher.type == 'purchase':
1368 line_total = line_total + self._convert_amount(cr, uid, voucher.tax_amount, voucher.id, context=ctx)
1369 # Create one move line per voucher line where amount is not 0.0
1370 line_total, rec_list_ids = self.voucher_move_line_create(cr, uid, voucher.id, line_total, move_id, company_currency, current_currency, context)
1372 # Create the writeoff line if needed
1373 ml_writeoff = self.writeoff_move_line_get(cr, uid, voucher.id, line_total, move_id, name, company_currency, current_currency, local_context)
1375 move_line_pool.create(cr, uid, ml_writeoff, local_context)
1376 # We post the voucher.
1377 self.write(cr, uid, [voucher.id], {
1382 if voucher.journal_id.entry_posted:
1383 move_pool.post(cr, uid, [move_id], context={})
1384 # We automatically reconcile the account move lines.
1386 for rec_ids in rec_list_ids:
1387 if len(rec_ids) >= 2:
1388 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)
1391 class account_voucher_line(osv.osv):
1392 _name = 'account.voucher.line'
1393 _description = 'Voucher Lines'
1394 _order = "move_line_id"
1396 # If the payment is in the same currency than the invoice, we keep the same amount
1397 # Otherwise, we compute from invoice currency to payment currency
1398 def _compute_balance(self, cr, uid, ids, name, args, context=None):
1399 currency_pool = self.pool.get('res.currency')
1401 for line in self.browse(cr, uid, ids, context=context):
1402 ctx = context.copy()
1403 ctx.update({'date': line.voucher_id.date})
1404 voucher_rate = self.pool.get('res.currency').read(cr, uid, line.voucher_id.currency_id.id, ['rate'], context=ctx)['rate']
1406 'voucher_special_currency': line.voucher_id.payment_rate_currency_id and line.voucher_id.payment_rate_currency_id.id or False,
1407 'voucher_special_currency_rate': line.voucher_id.payment_rate * voucher_rate})
1409 company_currency = line.voucher_id.journal_id.company_id.currency_id.id
1410 voucher_currency = line.voucher_id.currency_id and line.voucher_id.currency_id.id or company_currency
1411 move_line = line.move_line_id or False
1414 res['amount_original'] = 0.0
1415 res['amount_unreconciled'] = 0.0
1416 elif move_line.currency_id and voucher_currency==move_line.currency_id.id:
1417 res['amount_original'] = abs(move_line.amount_currency)
1418 res['amount_unreconciled'] = abs(move_line.amount_residual_currency)
1420 #always use the amount booked in the company currency as the basis of the conversion into the voucher currency
1421 res['amount_original'] = currency_pool.compute(cr, uid, company_currency, voucher_currency, move_line.credit or move_line.debit or 0.0, context=ctx)
1422 res['amount_unreconciled'] = currency_pool.compute(cr, uid, company_currency, voucher_currency, abs(move_line.amount_residual), context=ctx)
1424 rs_data[line.id] = res
1427 def _currency_id(self, cr, uid, ids, name, args, context=None):
1429 This function returns the currency id of a voucher line. It's either the currency of the
1430 associated move line (if any) or the currency of the voucher or the company currency.
1433 for line in self.browse(cr, uid, ids, context=context):
1434 move_line = line.move_line_id
1436 res[line.id] = move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id
1438 res[line.id] = line.voucher_id.currency_id and line.voucher_id.currency_id.id or line.voucher_id.company_id.currency_id.id
1442 'voucher_id':fields.many2one('account.voucher', 'Voucher', required=1, ondelete='cascade'),
1443 'name':fields.char('Description',),
1444 'account_id':fields.many2one('account.account','Account', required=True),
1445 'partner_id':fields.related('voucher_id', 'partner_id', type='many2one', relation='res.partner', string='Partner'),
1446 'untax_amount':fields.float('Untax Amount'),
1447 'amount':fields.float('Amount', digits_compute=dp.get_precision('Account')),
1448 'reconcile': fields.boolean('Full Reconcile'),
1449 'type':fields.selection([('dr','Debit'),('cr','Credit')], 'Dr/Cr'),
1450 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'),
1451 'move_line_id': fields.many2one('account.move.line', 'Journal Item', copy=False),
1452 'date_original': fields.related('move_line_id','date', type='date', relation='account.move.line', string='Date', readonly=1),
1453 'date_due': fields.related('move_line_id','date_maturity', type='date', relation='account.move.line', string='Due Date', readonly=1),
1454 'amount_original': fields.function(_compute_balance, multi='dc', type='float', string='Original Amount', store=True, digits_compute=dp.get_precision('Account')),
1455 'amount_unreconciled': fields.function(_compute_balance, multi='dc', type='float', string='Open Balance', store=True, digits_compute=dp.get_precision('Account')),
1456 'company_id': fields.related('voucher_id','company_id', relation='res.company', type='many2one', string='Company', store=True, readonly=True),
1457 'currency_id': fields.function(_currency_id, string='Currency', type='many2one', relation='res.currency', readonly=True),
1463 def onchange_reconcile(self, cr, uid, ids, reconcile, amount, amount_unreconciled, context=None):
1464 vals = {'amount': 0.0}
1466 vals = { 'amount': amount_unreconciled}
1467 return {'value': vals}
1469 def onchange_amount(self, cr, uid, ids, amount, amount_unreconciled, context=None):
1472 vals['reconcile'] = (amount == amount_unreconciled)
1473 return {'value': vals}
1475 def onchange_move_line_id(self, cr, user, ids, move_line_id, context=None):
1477 Returns a dict that contains new values and context
1479 @param move_line_id: latest value from user input for field move_line_id
1480 @param args: other arguments
1481 @param context: context arguments, like lang, time zone
1483 @return: Returns a dict which contains new values, and context
1486 move_line_pool = self.pool.get('account.move.line')
1488 move_line = move_line_pool.browse(cr, user, move_line_id, context=context)
1489 if move_line.credit:
1494 'account_id': move_line.account_id.id,
1496 'currency_id': move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id,
1502 def default_get(self, cr, user, fields_list, context=None):
1504 Returns default values for fields
1505 @param fields_list: list of fields, for which default values are required to be read
1506 @param context: context arguments, like lang, time zone
1508 @return: Returns a dict that contains default values for fields
1512 journal_id = context.get('journal_id', False)
1513 partner_id = context.get('partner_id', False)
1514 journal_pool = self.pool.get('account.journal')
1515 partner_pool = self.pool.get('res.partner')
1516 values = super(account_voucher_line, self).default_get(cr, user, fields_list, context=context)
1517 if (not journal_id) or ('account_id' not in fields_list):
1519 journal = journal_pool.browse(cr, user, journal_id, context=context)
1522 if journal.type in ('sale', 'sale_refund'):
1523 account_id = journal.default_credit_account_id and journal.default_credit_account_id.id or False
1525 elif journal.type in ('purchase', 'expense', 'purchase_refund'):
1526 account_id = journal.default_debit_account_id and journal.default_debit_account_id.id or False
1529 partner = partner_pool.browse(cr, user, partner_id, context=context)
1530 if context.get('type') == 'payment':
1532 account_id = partner.property_account_payable.id
1533 elif context.get('type') == 'receipt':
1534 account_id = partner.property_account_receivable.id
1537 'account_id':account_id,
1542 def resolve_o2m_operations(cr, uid, target_osv, operations, fields, context):
1544 for operation in operations:
1546 if not isinstance(operation, (list, tuple)):
1547 result = target_osv.read(cr, uid, operation, fields, context=context)
1548 elif operation[0] == 0:
1549 # may be necessary to check if all the fields are here and get the default values?
1550 result = operation[2]
1551 elif operation[0] == 1:
1552 result = target_osv.read(cr, uid, operation[1], fields, context=context)
1553 if not result: result = {}
1554 result.update(operation[2])
1555 elif operation[0] == 4:
1556 result = target_osv.read(cr, uid, operation[1], fields, context=context)
1558 results.append(result)
1562 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: