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 invoice = invoice_pool.browse(cr, uid, context['invoice_id'], context=context)
73 journal_id = journal_pool.search(cr, uid, [
74 ('currency', '=', invoice.currency_id.id), ('company_id', '=', invoice.company_id.id)
75 ], limit=1, context=context)
76 return journal_id and journal_id[0] or False
77 if context.get('journal_id', False):
78 return context.get('journal_id')
79 if not context.get('journal_id', False) and context.get('search_default_journal_id', False):
80 return context.get('search_default_journal_id')
82 ttype = context.get('type', 'bank')
83 if ttype in ('payment', 'receipt'):
85 res = self._make_journal_search(cr, uid, ttype, context=context)
86 return res and res[0] or False
88 def _get_tax(self, cr, uid, context=None):
89 if context is None: context = {}
90 journal_pool = self.pool.get('account.journal')
91 journal_id = context.get('journal_id', False)
93 ttype = context.get('type', 'bank')
94 res = journal_pool.search(cr, uid, [('type', '=', ttype)], limit=1)
101 journal = journal_pool.browse(cr, uid, journal_id, context=context)
102 account_id = journal.default_credit_account_id or journal.default_debit_account_id
103 if account_id and account_id.tax_ids:
104 tax_id = account_id.tax_ids[0].id
108 def _get_payment_rate_currency(self, cr, uid, context=None):
110 Return the default value for field payment_rate_currency_id: the currency of the journal
111 if there is one, otherwise the currency of the user's company
113 if context is None: context = {}
114 journal_pool = self.pool.get('account.journal')
115 journal_id = context.get('journal_id', False)
117 journal = journal_pool.browse(cr, uid, journal_id, context=context)
119 return journal.currency.id
120 #no journal given in the context, use company currency as default
121 return self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
123 def _get_currency(self, cr, uid, context=None):
124 if context is None: context = {}
125 journal_pool = self.pool.get('account.journal')
126 journal_id = context.get('journal_id', False)
128 journal = journal_pool.browse(cr, uid, journal_id, context=context)
130 return journal.currency.id
131 return self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
133 def _get_partner(self, cr, uid, context=None):
134 if context is None: context = {}
135 return context.get('partner_id', False)
137 def _get_reference(self, cr, uid, context=None):
138 if context is None: context = {}
139 return context.get('reference', False)
141 def _get_narration(self, cr, uid, context=None):
142 if context is None: context = {}
143 return context.get('narration', False)
145 def _get_amount(self, cr, uid, context=None):
148 return context.get('amount', 0.0)
150 def name_get(self, cr, uid, ids, context=None):
153 if context is None: context = {}
154 return [(r['id'], (r['number'] or _('Voucher'))) for r in self.read(cr, uid, ids, ['number'], context, load='_classic_write')]
156 def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
157 mod_obj = self.pool.get('ir.model.data')
158 if context is None: context = {}
160 if view_type == 'form':
161 if not view_id and context.get('invoice_type'):
162 if context.get('invoice_type') in ('out_invoice', 'out_refund'):
163 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_receipt_form')
165 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_payment_form')
166 result = result and result[1] or False
168 if not view_id and context.get('line_type'):
169 if context.get('line_type') == 'customer':
170 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_receipt_form')
172 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_payment_form')
173 result = result and result[1] or False
176 res = super(account_voucher, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
177 doc = etree.XML(res['arch'])
179 if context.get('type', 'sale') in ('purchase', 'payment'):
180 nodes = doc.xpath("//field[@name='partner_id']")
182 node.set('context', "{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1}")
183 if context.get('invoice_type','') in ('in_invoice', 'in_refund'):
184 node.set('string', _("Supplier"))
185 res['arch'] = etree.tostring(doc)
188 def _compute_writeoff_amount(self, cr, uid, line_dr_ids, line_cr_ids, amount, type):
190 sign = type == 'payment' and -1 or 1
191 for l in line_dr_ids:
192 if isinstance(l, dict):
194 for l in line_cr_ids:
195 if isinstance(l, dict):
196 credit += l['amount']
197 return amount - sign * (credit - debit)
199 def onchange_line_ids(self, cr, uid, ids, line_dr_ids, line_cr_ids, amount, voucher_currency, type, context=None):
200 context = context or {}
201 if not line_dr_ids and not line_cr_ids:
202 return {'value':{'writeoff_amount': 0.0}}
203 # resolve lists of commands into lists of dicts
204 line_dr_ids = self.resolve_2many_commands(cr, uid, 'line_dr_ids', line_dr_ids, ['amount'], context)
205 line_cr_ids = self.resolve_2many_commands(cr, uid, 'line_cr_ids', line_cr_ids, ['amount'], context)
206 #compute the field is_multi_currency that is used to hide/display options linked to secondary currency on the voucher
207 is_multi_currency = False
208 #loop on the voucher lines to see if one of these has a secondary currency. If yes, we need to see the options
209 for voucher_line in line_dr_ids+line_cr_ids:
210 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')
211 if line_id and self.pool.get('account.move.line').browse(cr, uid, line_id, context=context).currency_id:
212 is_multi_currency = True
214 return {'value': {'writeoff_amount': self._compute_writeoff_amount(cr, uid, line_dr_ids, line_cr_ids, amount, type), 'is_multi_currency': is_multi_currency}}
216 def _get_journal_currency(self, cr, uid, ids, name, args, context=None):
218 for voucher in self.browse(cr, uid, ids, context=context):
219 res[voucher.id] = voucher.journal_id.currency and voucher.journal_id.currency.id or voucher.company_id.currency_id.id
222 def _get_writeoff_amount(self, cr, uid, ids, name, args, context=None):
223 if not ids: return {}
224 currency_obj = self.pool.get('res.currency')
227 for voucher in self.browse(cr, uid, ids, context=context):
228 sign = voucher.type == 'payment' and -1 or 1
229 for l in voucher.line_dr_ids:
231 for l in voucher.line_cr_ids:
233 currency = voucher.currency_id or voucher.company_id.currency_id
234 res[voucher.id] = currency_obj.round(cr, uid, currency, voucher.amount - sign * (credit - debit))
237 def _paid_amount_in_company_currency(self, cr, uid, ids, name, args, context=None):
242 for v in self.browse(cr, uid, ids, context=context):
243 ctx.update({'date': v.date})
244 #make a new call to browse in order to have the right date in the context, to get the right currency rate
245 voucher = self.browse(cr, uid, v.id, context=ctx)
247 'voucher_special_currency': voucher.payment_rate_currency_id and voucher.payment_rate_currency_id.id or False,
248 'voucher_special_currency_rate': voucher.currency_id.rate * voucher.payment_rate,})
249 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)
252 def _get_currency_help_label(self, cr, uid, currency_id, payment_rate, payment_rate_currency_id, context=None):
254 This function builds a string to help the users to understand the behavior of the payment rate fields they can specify on the voucher.
255 This string is only used to improve the usability in the voucher form view and has no other effect.
257 :param currency_id: the voucher currency
258 :type currency_id: integer
259 :param payment_rate: the value of the payment_rate field of the voucher
260 :type payment_rate: float
261 :param payment_rate_currency_id: the value of the payment_rate_currency_id field of the voucher
262 :type payment_rate_currency_id: integer
263 :return: translated string giving a tip on what's the effect of the current payment rate specified
266 rml_parser = report_sxw.rml_parse(cr, uid, 'currency_help_label', context=context)
267 currency_pool = self.pool.get('res.currency')
268 currency_str = payment_rate_str = ''
270 currency_str = rml_parser.formatLang(1, currency_obj=currency_pool.browse(cr, uid, currency_id, context=context))
271 if payment_rate_currency_id:
272 payment_rate_str = rml_parser.formatLang(payment_rate, currency_obj=currency_pool.browse(cr, uid, payment_rate_currency_id, context=context))
273 currency_help_label = _('At the operation date, the exchange rate was\n%s = %s') % (currency_str, payment_rate_str)
274 return currency_help_label
276 def _fnct_currency_help_label(self, cr, uid, ids, name, args, context=None):
278 for voucher in self.browse(cr, uid, ids, context=context):
279 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)
282 _name = 'account.voucher'
283 _description = 'Accounting Voucher'
284 _inherit = ['mail.thread']
285 _order = "date desc, id desc"
286 # _rec_name = 'number'
289 'account_voucher.mt_voucher_state_change': lambda self, cr, uid, obj, ctx=None: True,
294 'type':fields.selection([
296 ('purchase','Purchase'),
297 ('payment','Payment'),
298 ('receipt','Receipt'),
299 ],'Default Type', readonly=True, states={'draft':[('readonly',False)]}),
300 'name':fields.char('Memo', readonly=True, states={'draft':[('readonly',False)]}),
301 'date':fields.date('Date', readonly=True, select=True, states={'draft':[('readonly',False)]},
302 help="Effective date for accounting entries", copy=False),
303 'journal_id':fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
304 'account_id':fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}),
305 'line_ids':fields.one2many('account.voucher.line', 'voucher_id', 'Voucher Lines',
306 readonly=True, copy=True,
307 states={'draft':[('readonly',False)]}),
308 'line_cr_ids':fields.one2many('account.voucher.line','voucher_id','Credits',
309 domain=[('type','=','cr')], context={'default_type':'cr'}, readonly=True, states={'draft':[('readonly',False)]}),
310 'line_dr_ids':fields.one2many('account.voucher.line','voucher_id','Debits',
311 domain=[('type','=','dr')], context={'default_type':'dr'}, readonly=True, states={'draft':[('readonly',False)]}),
312 'period_id': fields.many2one('account.period', 'Period', required=True, readonly=True, states={'draft':[('readonly',False)]}),
313 'narration':fields.text('Notes', readonly=True, states={'draft':[('readonly',False)]}),
314 'currency_id': fields.function(_get_journal_currency, type='many2one', relation='res.currency', string='Currency', readonly=True, required=True),
315 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
316 'state':fields.selection(
318 ('cancel','Cancelled'),
319 ('proforma','Pro-forma'),
321 ], 'Status', readonly=True, track_visibility='onchange', copy=False,
322 help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed Voucher. \
323 \n* The \'Pro-forma\' when voucher is in Pro-forma status,voucher does not have an voucher number. \
324 \n* The \'Posted\' status is used when user create voucher,a voucher number is generated and voucher entries are created in account \
325 \n* The \'Cancelled\' status is used when user cancel voucher.'),
326 'amount': fields.float('Total', digits_compute=dp.get_precision('Account'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
327 'tax_amount':fields.float('Tax Amount', digits_compute=dp.get_precision('Account'), readonly=True),
328 'reference': fields.char('Ref #', readonly=True, states={'draft':[('readonly',False)]},
329 help="Transaction reference number.", copy=False),
330 'number': fields.char('Number', readonly=True, copy=False),
331 'move_id':fields.many2one('account.move', 'Account Entry', copy=False),
332 'move_ids': fields.related('move_id','line_id', type='one2many', relation='account.move.line', string='Journal Items', readonly=True),
333 'partner_id':fields.many2one('res.partner', 'Partner', change_default=1, readonly=True, states={'draft':[('readonly',False)]}),
334 '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'),
335 'paid': fields.function(_check_paid, string='Paid', type='boolean', help="The Voucher has been totally paid."),
336 'pay_now':fields.selection([
337 ('pay_now','Pay Directly'),
338 ('pay_later','Pay Later or Group Funds'),
339 ],'Payment', select=True, readonly=True, states={'draft':[('readonly',False)]}),
340 'tax_id': fields.many2one('account.tax', 'Tax', readonly=True, states={'draft':[('readonly',False)]}, domain=[('price_include','=', False)], help="Only for tax excluded from price"),
341 'pre_line':fields.boolean('Previous Payments ?', required=False),
342 'date_due': fields.date('Due Date', readonly=True, select=True, states={'draft':[('readonly',False)]}),
343 'payment_option':fields.selection([
344 ('without_writeoff', 'Keep Open'),
345 ('with_writeoff', 'Reconcile Payment Balance'),
346 ], '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)"),
347 'writeoff_acc_id': fields.many2one('account.account', 'Counterpart Account', readonly=True, states={'draft': [('readonly', False)]}),
348 'comment': fields.char('Counterpart Comment', required=True, readonly=True, states={'draft': [('readonly', False)]}),
349 'analytic_id': fields.many2one('account.analytic.account','Write-Off Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
350 '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."),
351 'payment_rate_currency_id': fields.many2one('res.currency', 'Payment Rate Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}),
352 'payment_rate': fields.float('Exchange Rate', digits=(12,6), required=True, readonly=True, states={'draft': [('readonly', False)]},
353 help='The specific rate that will be used, in this voucher, between the selected currency (in \'Payment Rate Currency\' field) and the voucher currency.'),
354 'paid_amount_in_company_currency': fields.function(_paid_amount_in_company_currency, string='Paid Amount in Company Currency', type='float', readonly=True),
355 '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'),
356 '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"),
359 'period_id': _get_period,
360 'partner_id': _get_partner,
361 'journal_id':_get_journal,
362 'currency_id': _get_currency,
363 'reference': _get_reference,
364 'narration':_get_narration,
365 'amount': _get_amount,
368 'pay_now': 'pay_now',
370 'date': lambda *a: time.strftime('%Y-%m-%d'),
371 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.voucher',context=c),
373 'payment_option': 'without_writeoff',
374 'comment': _('Write-Off'),
376 'payment_rate_currency_id': _get_payment_rate_currency,
379 def compute_tax(self, cr, uid, ids, context=None):
380 tax_pool = self.pool.get('account.tax')
381 partner_pool = self.pool.get('res.partner')
382 position_pool = self.pool.get('account.fiscal.position')
383 voucher_line_pool = self.pool.get('account.voucher.line')
384 voucher_pool = self.pool.get('account.voucher')
385 if context is None: context = {}
387 for voucher in voucher_pool.browse(cr, uid, ids, context=context):
389 for line in voucher.line_ids:
390 voucher_amount += line.untax_amount or line.amount
391 line.amount = line.untax_amount or line.amount
392 voucher_line_pool.write(cr, uid, [line.id], {'amount':line.amount, 'untax_amount':line.untax_amount})
394 if not voucher.tax_id:
395 self.write(cr, uid, [voucher.id], {'amount':voucher_amount, 'tax_amount':0.0})
398 tax = [tax_pool.browse(cr, uid, voucher.tax_id.id, context=context)]
399 partner = partner_pool.browse(cr, uid, voucher.partner_id.id, context=context) or False
400 taxes = position_pool.map_tax(cr, uid, partner and partner.property_account_position or False, tax)
401 tax = tax_pool.browse(cr, uid, taxes, context=context)
403 total = voucher_amount
406 if not tax[0].price_include:
407 for line in voucher.line_ids:
408 for tax_line in tax_pool.compute_all(cr, uid, tax, line.amount, 1).get('taxes', []):
409 total_tax += tax_line.get('amount', 0.0)
412 for line in voucher.line_ids:
416 for tax_line in tax_pool.compute_all(cr, uid, tax, line.untax_amount or line.amount, 1).get('taxes', []):
417 line_tax += tax_line.get('amount', 0.0)
418 line_total += tax_line.get('price_unit')
419 total_tax += line_tax
420 untax_amount = line.untax_amount or line.amount
421 voucher_line_pool.write(cr, uid, [line.id], {'amount':line_total, 'untax_amount':untax_amount})
423 self.write(cr, uid, [voucher.id], {'amount':total, 'tax_amount':total_tax})
426 def onchange_price(self, cr, uid, ids, line_ids, tax_id, partner_id=False, context=None):
427 context = context or {}
428 tax_pool = self.pool.get('account.tax')
429 partner_pool = self.pool.get('res.partner')
430 position_pool = self.pool.get('account.fiscal.position')
439 # resolve the list of commands into a list of dicts
440 line_ids = self.resolve_2many_commands(cr, uid, 'line_ids', line_ids, ['amount'], context)
443 for line in line_ids:
445 line_amount = line.get('amount',0.0)
448 tax = [tax_pool.browse(cr, uid, tax_id, context=context)]
450 partner = partner_pool.browse(cr, uid, partner_id, context=context) or False
451 taxes = position_pool.map_tax(cr, uid, partner and partner.property_account_position or False, tax)
452 tax = tax_pool.browse(cr, uid, taxes, context=context)
454 if not tax[0].price_include:
455 for tax_line in tax_pool.compute_all(cr, uid, tax, line_amount, 1).get('taxes', []):
456 total_tax += tax_line.get('amount')
458 voucher_total += line_amount
459 total = voucher_total + total_tax
462 'amount': total or voucher_total,
463 'tax_amount': total_tax
469 def onchange_term_id(self, cr, uid, ids, term_id, amount):
470 term_pool = self.pool.get('account.payment.term')
473 default = {'date_due':False}
474 if term_id and amount:
475 terms = term_pool.compute(cr, uid, term_id, amount)
477 due_date = terms[-1][0]
481 return {'value':default}
483 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):
485 Returns a dict that contains new values and context
487 @param partner_id: latest value from user input for field partner_id
488 @param args: other arguments
489 @param context: context arguments, like lang, time zone
491 @return: Returns a dict which contains new values, and context
497 if not partner_id or not journal_id:
500 partner_pool = self.pool.get('res.partner')
501 journal_pool = self.pool.get('account.journal')
503 journal = journal_pool.browse(cr, uid, journal_id, context=context)
504 partner = partner_pool.browse(cr, uid, partner_id, context=context)
507 if journal.type in ('sale','sale_refund'):
508 account_id = partner.property_account_receivable.id
510 elif journal.type in ('purchase', 'purchase_refund','expense'):
511 account_id = partner.property_account_payable.id
514 if not journal.default_credit_account_id or not journal.default_debit_account_id:
515 raise osv.except_osv(_('Error!'), _('Please define default credit/debit accounts on the journal "%s".') % (journal.name))
516 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
519 default['value']['account_id'] = account_id
520 default['value']['type'] = ttype or tr_type
522 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)
523 default['value'].update(vals.get('value'))
527 def onchange_rate(self, cr, uid, ids, rate, amount, currency_id, payment_rate_currency_id, company_id, context=None):
528 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)}}
529 if rate and amount and currency_id:
530 company_currency = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id
531 #context should contain the date, the payment currency and the payment rate specified on the voucher
532 amount_in_company_currency = self.pool.get('res.currency').compute(cr, uid, currency_id, company_currency.id, amount, context=context)
533 res['value']['paid_amount_in_company_currency'] = amount_in_company_currency
536 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):
540 ctx.update({'date': date})
541 #read the voucher rate with the right date in the context
542 currency_id = currency_id or self.pool.get('res.company').browse(cr, uid, company_id, context=ctx).currency_id.id
543 voucher_rate = self.pool.get('res.currency').read(cr, uid, [currency_id], ['rate'], context=ctx)[0]['rate']
545 'voucher_special_currency': payment_rate_currency_id,
546 'voucher_special_currency_rate': rate * voucher_rate})
547 res = self.recompute_voucher_lines(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=ctx)
548 vals = self.onchange_rate(cr, uid, ids, rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
549 for key in vals.keys():
550 res[key].update(vals[key])
553 def recompute_payment_rate(self, cr, uid, ids, vals, currency_id, date, ttype, journal_id, amount, context=None):
556 #on change of the journal, we need to set also the default value for payment_rate and payment_rate_currency_id
557 currency_obj = self.pool.get('res.currency')
558 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
559 company_id = journal.company_id.id
561 currency_id = currency_id or journal.company_id.currency_id.id
562 payment_rate_currency_id = currency_id
564 ctx.update({'date': date})
566 if ttype == 'receipt':
567 o2m_to_loop = 'line_cr_ids'
568 elif ttype == 'payment':
569 o2m_to_loop = 'line_dr_ids'
570 if o2m_to_loop and 'value' in vals and o2m_to_loop in vals['value']:
571 for voucher_line in vals['value'][o2m_to_loop]:
572 if not isinstance(voucher_line, dict):
574 if voucher_line['currency_id'] != currency_id:
575 # we take as default value for the payment_rate_currency_id, the currency of the first invoice that
576 # is not in the voucher currency
577 payment_rate_currency_id = voucher_line['currency_id']
578 tmp = currency_obj.browse(cr, uid, payment_rate_currency_id, context=ctx).rate
579 payment_rate = tmp / currency_obj.browse(cr, uid, currency_id, context=ctx).rate
581 vals['value'].update({
582 'payment_rate': payment_rate,
583 'currency_id': currency_id,
584 'payment_rate_currency_id': payment_rate_currency_id
586 #read the voucher rate with the right date in the context
587 voucher_rate = self.pool.get('res.currency').read(cr, uid, [currency_id], ['rate'], context=ctx)[0]['rate']
589 'voucher_special_currency_rate': payment_rate * voucher_rate,
590 'voucher_special_currency': payment_rate_currency_id})
591 res = self.onchange_rate(cr, uid, ids, payment_rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
592 for key in res.keys():
593 vals[key].update(res[key])
596 def basic_onchange_partner(self, cr, uid, ids, partner_id, journal_id, ttype, context=None):
597 partner_pool = self.pool.get('res.partner')
598 journal_pool = self.pool.get('account.journal')
599 res = {'value': {'account_id': False}}
600 if not partner_id or not journal_id:
603 journal = journal_pool.browse(cr, uid, journal_id, context=context)
604 partner = partner_pool.browse(cr, uid, partner_id, context=context)
606 if journal.type in ('sale','sale_refund'):
607 account_id = partner.property_account_receivable.id
608 elif journal.type in ('purchase', 'purchase_refund','expense'):
609 account_id = partner.property_account_payable.id
611 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
613 res['value']['account_id'] = account_id
616 def onchange_partner_id(self, cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=None):
621 #TODO: comment me and use me directly in the sales/purchases views
622 res = self.basic_onchange_partner(cr, uid, ids, partner_id, journal_id, ttype, context=context)
623 if ttype in ['sale', 'purchase']:
626 # 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
627 ctx.update({'date': date})
628 vals = self.recompute_voucher_lines(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=ctx)
629 vals2 = self.recompute_payment_rate(cr, uid, ids, vals, currency_id, date, ttype, journal_id, amount, context=context)
630 for key in vals.keys():
631 res[key].update(vals[key])
632 for key in vals2.keys():
633 res[key].update(vals2[key])
634 #TODO: can probably be removed now
635 #TODO: onchange_partner_id() should not returns [pre_line, line_dr_ids, payment_rate...] for type sale, and not
636 # [pre_line, line_cr_ids, payment_rate...] for type purchase.
637 # We should definitively split account.voucher object in two and make distinct on_change functions. In the
638 # meanwhile, bellow lines must be there because the fields aren't present in the view, what crashes if the
639 # onchange returns a value for them
641 del(res['value']['line_dr_ids'])
642 del(res['value']['pre_line'])
643 del(res['value']['payment_rate'])
644 elif ttype == 'purchase':
645 del(res['value']['line_cr_ids'])
646 del(res['value']['pre_line'])
647 del(res['value']['payment_rate'])
650 def recompute_voucher_lines(self, cr, uid, ids, partner_id, journal_id, price, currency_id, ttype, date, context=None):
652 Returns a dict that contains new values and context
654 @param partner_id: latest value from user input for field partner_id
655 @param args: other arguments
656 @param context: context arguments, like lang, time zone
658 @return: Returns a dict which contains new values, and context
660 def _remove_noise_in_o2m():
661 """if the line is partially reconciled, then we must pay attention to display it only once and
663 This function returns True if the line is considered as noise and should not be displayed
665 if line.reconcile_partial_id:
666 if currency_id == line.currency_id.id:
667 if line.amount_residual_currency <= 0:
670 if line.amount_residual <= 0:
676 context_multi_currency = context.copy()
678 currency_pool = self.pool.get('res.currency')
679 move_line_pool = self.pool.get('account.move.line')
680 partner_pool = self.pool.get('res.partner')
681 journal_pool = self.pool.get('account.journal')
682 line_pool = self.pool.get('account.voucher.line')
686 'value': {'line_dr_ids': [], 'line_cr_ids': [], 'pre_line': False},
689 # drop existing lines
690 line_ids = ids and line_pool.search(cr, uid, [('voucher_id', '=', ids[0])])
691 for line in line_pool.browse(cr, uid, line_ids, context=context):
692 if line.type == 'cr':
693 default['value']['line_cr_ids'].append((2, line.id))
695 default['value']['line_dr_ids'].append((2, line.id))
697 if not partner_id or not journal_id:
700 journal = journal_pool.browse(cr, uid, journal_id, context=context)
701 partner = partner_pool.browse(cr, uid, partner_id, context=context)
702 currency_id = currency_id or journal.company_id.currency_id.id
707 if context.get('account_id'):
708 account_type = self.pool['account.account'].browse(cr, uid, context['account_id'], context=context).type
709 if ttype == 'payment':
711 account_type = 'payable'
712 total_debit = price or 0.0
714 total_credit = price or 0.0
716 account_type = 'receivable'
718 if not context.get('move_line_ids', False):
719 ids = move_line_pool.search(cr, uid, [('state','=','valid'), ('account_id.type', '=', account_type), ('reconcile_id', '=', False), ('partner_id', '=', partner_id)], context=context)
721 ids = context['move_line_ids']
722 invoice_id = context.get('invoice_id', False)
723 company_currency = journal.company_id.currency_id.id
724 move_lines_found = []
726 #order the lines by most old first
728 account_move_lines = move_line_pool.browse(cr, uid, ids, context=context)
730 #compute the total debit/credit and look for a matching open amount or invoice
731 for line in account_move_lines:
732 if _remove_noise_in_o2m():
736 if line.invoice.id == invoice_id:
737 #if the invoice linked to the voucher line is equal to the invoice_id in context
738 #then we assign the amount on that line, whatever the other voucher lines
739 move_lines_found.append(line.id)
740 elif currency_id == company_currency:
741 #otherwise treatments is the same but with other field names
742 if line.amount_residual == price:
743 #if the amount residual is equal the amount voucher, we assign it to that voucher
744 #line, whatever the other voucher lines
745 move_lines_found.append(line.id)
747 #otherwise we will split the voucher amount on each line (by most old first)
748 total_credit += line.credit or 0.0
749 total_debit += line.debit or 0.0
750 elif currency_id == line.currency_id.id:
751 if line.amount_residual_currency == price:
752 move_lines_found.append(line.id)
754 total_credit += line.credit and line.amount_currency or 0.0
755 total_debit += line.debit and line.amount_currency or 0.0
757 remaining_amount = price
758 #voucher line creation
759 for line in account_move_lines:
761 if _remove_noise_in_o2m():
764 if line.currency_id and currency_id == line.currency_id.id:
765 amount_original = abs(line.amount_currency)
766 amount_unreconciled = abs(line.amount_residual_currency)
768 #always use the amount booked in the company currency as the basis of the conversion into the voucher currency
769 amount_original = currency_pool.compute(cr, uid, company_currency, currency_id, line.credit or line.debit or 0.0, context=context_multi_currency)
770 amount_unreconciled = currency_pool.compute(cr, uid, company_currency, currency_id, abs(line.amount_residual), context=context_multi_currency)
771 line_currency_id = line.currency_id and line.currency_id.id or company_currency
773 'name':line.move_id.name,
774 'type': line.credit and 'dr' or 'cr',
775 'move_line_id':line.id,
776 'account_id':line.account_id.id,
777 'amount_original': amount_original,
778 'amount': (line.id in move_lines_found) and min(abs(remaining_amount), amount_unreconciled) or 0.0,
779 'date_original':line.date,
780 'date_due':line.date_maturity,
781 'amount_unreconciled': amount_unreconciled,
782 'currency_id': line_currency_id,
784 remaining_amount -= rs['amount']
785 #in case a corresponding move_line hasn't been found, we now try to assign the voucher amount
786 #on existing invoices: we split voucher amount by most old first, but only for lines in the same currency
787 if not move_lines_found:
788 if currency_id == line_currency_id:
790 amount = min(amount_unreconciled, abs(total_debit))
791 rs['amount'] = amount
792 total_debit -= amount
794 amount = min(amount_unreconciled, abs(total_credit))
795 rs['amount'] = amount
796 total_credit -= amount
798 if rs['amount_unreconciled'] == rs['amount']:
799 rs['reconcile'] = True
801 if rs['type'] == 'cr':
802 default['value']['line_cr_ids'].append(rs)
804 default['value']['line_dr_ids'].append(rs)
806 if len(default['value']['line_cr_ids']) > 0:
807 default['value']['pre_line'] = 1
808 elif len(default['value']['line_dr_ids']) > 0:
809 default['value']['pre_line'] = 1
810 default['value']['writeoff_amount'] = self._compute_writeoff_amount(cr, uid, default['value']['line_dr_ids'], default['value']['line_cr_ids'], price, ttype)
813 def onchange_payment_rate_currency(self, cr, uid, ids, currency_id, payment_rate, payment_rate_currency_id, date, amount, company_id, context=None):
818 #set the default payment rate of the voucher and compute the paid amount in company currency
820 ctx.update({'date': date})
821 #read the voucher rate with the right date in the context
822 voucher_rate = self.pool.get('res.currency').read(cr, uid, [currency_id], ['rate'], context=ctx)[0]['rate']
824 'voucher_special_currency_rate': payment_rate * voucher_rate,
825 'voucher_special_currency': payment_rate_currency_id})
826 vals = self.onchange_rate(cr, uid, ids, payment_rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
827 for key in vals.keys():
828 res[key].update(vals[key])
831 def onchange_date(self, cr, uid, ids, date, currency_id, payment_rate_currency_id, amount, company_id, context=None):
833 @param date: latest value from user input for field date
834 @param args: other arguments
835 @param context: context arguments, like lang, time zone
836 @return: Returns a dict which contains new values, and context
841 #set the period of the voucher
842 period_pool = self.pool.get('account.period')
843 currency_obj = self.pool.get('res.currency')
845 ctx.update({'company_id': company_id, 'account_period_prefer_normal': True})
846 voucher_currency_id = currency_id or self.pool.get('res.company').browse(cr, uid, company_id, context=ctx).currency_id.id
847 pids = period_pool.find(cr, uid, date, context=ctx)
849 res['value'].update({'period_id':pids[0]})
850 if payment_rate_currency_id:
851 ctx.update({'date': date})
853 if payment_rate_currency_id != currency_id:
854 tmp = currency_obj.browse(cr, uid, payment_rate_currency_id, context=ctx).rate
855 payment_rate = tmp / currency_obj.browse(cr, uid, voucher_currency_id, context=ctx).rate
856 vals = self.onchange_payment_rate_currency(cr, uid, ids, voucher_currency_id, payment_rate, payment_rate_currency_id, date, amount, company_id, context=context)
857 vals['value'].update({'payment_rate': payment_rate})
858 for key in vals.keys():
859 res[key].update(vals[key])
862 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
884 period_id = self.pool['account.period'].find(cr, uid, context=dict(context, company_id=company_id))
885 vals['value'].update({
886 'currency_id': currency_id,
887 'payment_rate_currency_id': currency_id,
888 'period_id' : period_id
890 #in case we want to register the payment directly from an invoice, it's confusing to allow to switch the journal
891 #without seeing that the amount is expressed in the journal currency, and not in the invoice currency. So to avoid
892 #this common mistake, we simply reset the amount to 0 if the currency is not the invoice currency.
893 if context.get('payment_expected_currency') and currency_id != context.get('payment_expected_currency'):
894 vals['value']['amount'] = 0
897 res = self.onchange_partner_id(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context)
898 for key in res.keys():
899 vals[key].update(res[key])
902 def onchange_company(self, cr, uid, ids, partner_id, journal_id, currency_id, company_id, context=None):
904 If the company changes, check that the journal is in the right company.
905 If not, fetch a new journal.
907 journal_pool = self.pool['account.journal']
908 journal = journal_pool.browse(cr, uid, journal_id, context=context)
909 if journal.company_id.id != company_id:
910 # can not guess type of journal, better remove it
911 return {'value': {'journal_id': False}}
914 def button_proforma_voucher(self, cr, uid, ids, context=None):
915 self.signal_workflow(cr, uid, ids, 'proforma_voucher')
916 return {'type': 'ir.actions.act_window_close'}
918 def proforma_voucher(self, cr, uid, ids, context=None):
919 self.action_move_line_create(cr, uid, ids, context=context)
922 def action_cancel_draft(self, cr, uid, ids, context=None):
923 self.create_workflow(cr, uid, ids)
924 self.write(cr, uid, ids, {'state':'draft'})
927 def cancel_voucher(self, cr, uid, ids, context=None):
928 reconcile_pool = self.pool.get('account.move.reconcile')
929 move_pool = self.pool.get('account.move')
930 move_line_pool = self.pool.get('account.move.line')
931 for voucher in self.browse(cr, uid, ids, context=context):
932 # refresh to make sure you don't unlink an already removed move
934 for line in voucher.move_ids:
935 # refresh to make sure you don't unreconcile an already unreconciled entry
937 if line.reconcile_id:
938 move_lines = [move_line.id for move_line in line.reconcile_id.line_id]
939 move_lines.remove(line.id)
940 reconcile_pool.unlink(cr, uid, [line.reconcile_id.id])
941 if len(move_lines) >= 2:
942 move_line_pool.reconcile_partial(cr, uid, move_lines, 'auto',context=context)
944 move_pool.button_cancel(cr, uid, [voucher.move_id.id])
945 move_pool.unlink(cr, uid, [voucher.move_id.id])
950 self.write(cr, uid, ids, res)
953 def unlink(self, cr, uid, ids, context=None):
954 for t in self.read(cr, uid, ids, ['state'], context=context):
955 if t['state'] not in ('draft', 'cancel'):
956 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete voucher(s) which are already opened or paid.'))
957 return super(account_voucher, self).unlink(cr, uid, ids, context=context)
959 def onchange_payment(self, cr, uid, ids, pay_now, journal_id, partner_id, ttype='sale'):
964 partner_pool = self.pool.get('res.partner')
965 journal_pool = self.pool.get('account.journal')
966 if pay_now == 'pay_later':
967 partner = partner_pool.browse(cr, uid, partner_id)
968 journal = journal_pool.browse(cr, uid, journal_id)
969 if journal.type in ('sale','sale_refund'):
970 account_id = partner.property_account_receivable.id
971 elif journal.type in ('purchase', 'purchase_refund','expense'):
972 account_id = partner.property_account_payable.id
974 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
976 res['account_id'] = account_id
979 def _sel_context(self, cr, uid, voucher_id, context=None):
981 Select the context to use accordingly if it needs to be multicurrency or not.
983 :param voucher_id: Id of the actual voucher
984 :return: The returned context will be the same as given in parameter if the voucher currency is the same
985 than the company currency, otherwise it's a copy of the parameter with an extra key 'date' containing
986 the date of the voucher.
989 company_currency = self._get_company_currency(cr, uid, voucher_id, context)
990 current_currency = self._get_current_currency(cr, uid, voucher_id, context)
991 if current_currency <> company_currency:
992 context_multi_currency = context.copy()
993 voucher = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context)
994 context_multi_currency.update({'date': voucher.date})
995 return context_multi_currency
998 def first_move_line_get(self, cr, uid, voucher_id, move_id, company_currency, current_currency, context=None):
1000 Return a dict to be use to create the first account move line of given voucher.
1002 :param voucher_id: Id of voucher what we are creating account_move.
1003 :param move_id: Id of account move where this line will be added.
1004 :param company_currency: id of currency of the company to which the voucher belong
1005 :param current_currency: id of currency of the voucher
1006 :return: mapping between fieldname and value of account move line to create
1009 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1010 debit = credit = 0.0
1011 # TODO: is there any other alternative then the voucher type ??
1012 # ANSWER: We can have payment and receipt "In Advance".
1013 # TODO: Make this logic available.
1014 # -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
1015 if voucher.type in ('purchase', 'payment'):
1016 credit = voucher.paid_amount_in_company_currency
1017 elif voucher.type in ('sale', 'receipt'):
1018 debit = voucher.paid_amount_in_company_currency
1019 if debit < 0: credit = -debit; debit = 0.0
1020 if credit < 0: debit = -credit; credit = 0.0
1021 sign = debit - credit < 0 and -1 or 1
1022 #set the first line of the voucher
1024 'name': voucher.name or '/',
1027 'account_id': voucher.account_id.id,
1029 'journal_id': voucher.journal_id.id,
1030 'period_id': voucher.period_id.id,
1031 'partner_id': voucher.partner_id.id,
1032 'currency_id': company_currency <> current_currency and current_currency or False,
1033 'amount_currency': (sign * abs(voucher.amount) # amount < 0 for refunds
1034 if company_currency != current_currency else 0.0),
1035 'date': voucher.date,
1036 'date_maturity': voucher.date_due
1040 def account_move_get(self, cr, uid, voucher_id, context=None):
1042 This method prepare the creation of the account move related to the given voucher.
1044 :param voucher_id: Id of voucher for which we are creating account_move.
1045 :return: mapping between fieldname and value of account move to create
1048 seq_obj = self.pool.get('ir.sequence')
1049 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1051 name = voucher.number
1052 elif voucher.journal_id.sequence_id:
1053 if not voucher.journal_id.sequence_id.active:
1054 raise osv.except_osv(_('Configuration Error !'),
1055 _('Please activate the sequence of selected journal !'))
1057 c.update({'fiscalyear_id': voucher.period_id.fiscalyear_id.id})
1058 name = seq_obj.next_by_id(cr, uid, voucher.journal_id.sequence_id.id, context=c)
1060 raise osv.except_osv(_('Error!'),
1061 _('Please define a sequence on the journal.'))
1062 if not voucher.reference:
1063 ref = name.replace('/','')
1065 ref = voucher.reference
1069 'journal_id': voucher.journal_id.id,
1070 'narration': voucher.narration,
1071 'date': voucher.date,
1073 'period_id': voucher.period_id.id,
1077 def _get_exchange_lines(self, cr, uid, line, move_id, amount_residual, company_currency, current_currency, context=None):
1079 Prepare the two lines in company currency due to currency rate difference.
1081 :param line: browse record of the voucher.line for which we want to create currency rate difference accounting
1083 :param move_id: Account move wher the move lines will be.
1084 :param amount_residual: Amount to be posted.
1085 :param company_currency: id of currency of the company to which the voucher belong
1086 :param current_currency: id of currency of the voucher
1087 :return: the account move line and its counterpart to create, depicted as mapping between fieldname and value
1088 :rtype: tuple of dict
1090 if amount_residual > 0:
1091 account_id = line.voucher_id.company_id.expense_currency_exchange_account_id
1093 model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'account', 'action_account_form')
1094 msg = _("You should configure the 'Loss Exchange Rate Account' to manage automatically the booking of accounting entries related to differences between exchange rates.")
1095 raise openerp.exceptions.RedirectWarning(msg, action_id, _('Go to the configuration panel'))
1097 account_id = line.voucher_id.company_id.income_currency_exchange_account_id
1099 model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'account', 'action_account_form')
1100 msg = _("You should configure the 'Gain Exchange Rate Account' to manage automatically the booking of accounting entries related to differences between exchange rates.")
1101 raise openerp.exceptions.RedirectWarning(msg, action_id, _('Go to the configuration panel'))
1102 # Even if the amount_currency is never filled, we need to pass the foreign currency because otherwise
1103 # the receivable/payable account may have a secondary currency, which render this field mandatory
1104 if line.account_id.currency_id:
1105 account_currency_id = line.account_id.currency_id.id
1107 account_currency_id = company_currency <> current_currency and current_currency or False
1109 'journal_id': line.voucher_id.journal_id.id,
1110 'period_id': line.voucher_id.period_id.id,
1111 'name': _('change')+': '+(line.name or '/'),
1112 'account_id': line.account_id.id,
1114 'partner_id': line.voucher_id.partner_id.id,
1115 'currency_id': account_currency_id,
1116 'amount_currency': 0.0,
1118 'credit': amount_residual > 0 and amount_residual or 0.0,
1119 'debit': amount_residual < 0 and -amount_residual or 0.0,
1120 'date': line.voucher_id.date,
1122 move_line_counterpart = {
1123 'journal_id': line.voucher_id.journal_id.id,
1124 'period_id': line.voucher_id.period_id.id,
1125 'name': _('change')+': '+(line.name or '/'),
1126 'account_id': account_id.id,
1128 'amount_currency': 0.0,
1129 'partner_id': line.voucher_id.partner_id.id,
1130 'currency_id': account_currency_id,
1132 'debit': amount_residual > 0 and amount_residual or 0.0,
1133 'credit': amount_residual < 0 and -amount_residual or 0.0,
1134 'date': line.voucher_id.date,
1136 return (move_line, move_line_counterpart)
1138 def _convert_amount(self, cr, uid, amount, voucher_id, context=None):
1140 This function convert the amount given in company currency. It takes either the rate in the voucher (if the
1141 payment_rate_currency_id is relevant) either the rate encoded in the system.
1143 :param amount: float. The amount to convert
1144 :param voucher: id of the voucher on which we want the conversion
1145 :param context: to context to use for the conversion. It may contain the key 'date' set to the voucher date
1146 field in order to select the good rate to use.
1147 :return: the amount in the currency of the voucher's company
1152 currency_obj = self.pool.get('res.currency')
1153 voucher = self.browse(cr, uid, voucher_id, context=context)
1154 return currency_obj.compute(cr, uid, voucher.currency_id.id, voucher.company_id.currency_id.id, amount, context=context)
1156 def voucher_move_line_create(self, cr, uid, voucher_id, line_total, move_id, company_currency, current_currency, context=None):
1158 Create one account move line, on the given account move, per voucher line where amount is not 0.0.
1159 It returns Tuple with tot_line what is total of difference between debit and credit and
1160 a list of lists with ids to be reconciled with this format (total_deb_cred,list_of_lists).
1162 :param voucher_id: Voucher id what we are working with
1163 :param line_total: Amount of the first line, which correspond to the amount we should totally split among all voucher lines.
1164 :param move_id: Account move wher those lines will be joined.
1165 :param company_currency: id of currency of the company to which the voucher belong
1166 :param current_currency: id of currency of the voucher
1167 :return: Tuple build as (remaining amount not allocated on voucher lines, list of account_move_line created in this method)
1168 :rtype: tuple(float, list of int)
1172 move_line_obj = self.pool.get('account.move.line')
1173 currency_obj = self.pool.get('res.currency')
1174 tax_obj = self.pool.get('account.tax')
1175 tot_line = line_total
1178 date = self.read(cr, uid, [voucher_id], ['date'], context=context)[0]['date']
1179 ctx = context.copy()
1180 ctx.update({'date': date})
1181 voucher = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context=ctx)
1182 voucher_currency = voucher.journal_id.currency or voucher.company_id.currency_id
1184 'voucher_special_currency_rate': voucher_currency.rate * voucher.payment_rate ,
1185 'voucher_special_currency': voucher.payment_rate_currency_id and voucher.payment_rate_currency_id.id or False,})
1186 prec = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')
1187 for line in voucher.line_ids:
1188 #create one move line per voucher line where amount is not 0.0
1189 # AND (second part of the clause) only if the original move line was not having debit = credit = 0 (which is a legal value)
1190 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)):
1192 # convert the amount set on the voucher line into the currency of the voucher's company
1193 # 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
1194 amount = self._convert_amount(cr, uid, line.untax_amount or line.amount, voucher.id, context=ctx)
1195 # if the amount encoded in voucher is equal to the amount unreconciled, we need to compute the
1196 # currency rate difference
1197 if line.amount == line.amount_unreconciled:
1198 if not line.move_line_id:
1199 raise osv.except_osv(_('Wrong voucher line'),_("The invoice you are willing to pay is not valid anymore."))
1200 sign = line.type =='dr' and -1 or 1
1201 currency_rate_difference = sign * (line.move_line_id.amount_residual - amount)
1203 currency_rate_difference = 0.0
1205 'journal_id': voucher.journal_id.id,
1206 'period_id': voucher.period_id.id,
1207 'name': line.name or '/',
1208 'account_id': line.account_id.id,
1210 'partner_id': voucher.partner_id.id,
1211 '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,
1212 'analytic_account_id': line.account_analytic_id and line.account_analytic_id.id or False,
1216 'date': voucher.date
1220 if line.type == 'dr':
1225 if (line.type=='dr'):
1227 move_line['debit'] = amount
1230 move_line['credit'] = amount
1232 if voucher.tax_id and voucher.type in ('sale', 'purchase'):
1234 'account_tax_id': voucher.tax_id.id,
1237 if move_line.get('account_tax_id', False):
1238 tax_data = tax_obj.browse(cr, uid, [move_line['account_tax_id']], context=context)[0]
1239 if not (tax_data.base_code_id and tax_data.tax_code_id):
1240 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))
1242 # compute the amount in foreign currency
1243 foreign_currency_diff = 0.0
1244 amount_currency = False
1245 if line.move_line_id:
1246 # We want to set it on the account move line as soon as the original line had a foreign currency
1247 if line.move_line_id.currency_id and line.move_line_id.currency_id.id != company_currency:
1248 # we compute the amount in that foreign currency.
1249 if line.move_line_id.currency_id.id == current_currency:
1250 # if the voucher and the voucher line share the same currency, there is no computation to do
1251 sign = (move_line['debit'] - move_line['credit']) < 0 and -1 or 1
1252 amount_currency = sign * (line.amount)
1254 # if the rate is specified on the voucher, it will be used thanks to the special keys in the context
1255 # otherwise we use the rates of the system
1256 amount_currency = currency_obj.compute(cr, uid, company_currency, line.move_line_id.currency_id.id, move_line['debit']-move_line['credit'], context=ctx)
1257 if line.amount == line.amount_unreconciled:
1258 foreign_currency_diff = line.move_line_id.amount_residual_currency - abs(amount_currency)
1260 move_line['amount_currency'] = amount_currency
1261 voucher_line = move_line_obj.create(cr, uid, move_line)
1262 rec_ids = [voucher_line, line.move_line_id.id]
1264 if not currency_obj.is_zero(cr, uid, voucher.company_id.currency_id, currency_rate_difference):
1265 # Change difference entry in company currency
1266 exch_lines = self._get_exchange_lines(cr, uid, line, move_id, currency_rate_difference, company_currency, current_currency, context=context)
1267 new_id = move_line_obj.create(cr, uid, exch_lines[0],context)
1268 move_line_obj.create(cr, uid, exch_lines[1], context)
1269 rec_ids.append(new_id)
1271 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):
1272 # Change difference entry in voucher currency
1273 move_line_foreign_currency = {
1274 'journal_id': line.voucher_id.journal_id.id,
1275 'period_id': line.voucher_id.period_id.id,
1276 'name': _('change')+': '+(line.name or '/'),
1277 'account_id': line.account_id.id,
1279 'partner_id': line.voucher_id.partner_id.id,
1280 'currency_id': line.move_line_id.currency_id.id,
1281 'amount_currency': -1 * foreign_currency_diff,
1285 'date': line.voucher_id.date,
1287 new_id = move_line_obj.create(cr, uid, move_line_foreign_currency, context=context)
1288 rec_ids.append(new_id)
1289 if line.move_line_id.id:
1290 rec_lst_ids.append(rec_ids)
1291 return (tot_line, rec_lst_ids)
1293 def writeoff_move_line_get(self, cr, uid, voucher_id, line_total, move_id, name, company_currency, current_currency, context=None):
1295 Set a dict to be use to create the writeoff move line.
1297 :param voucher_id: Id of voucher what we are creating account_move.
1298 :param line_total: Amount remaining to be allocated on lines.
1299 :param move_id: Id of account move where this line will be added.
1300 :param name: Description of account move line.
1301 :param company_currency: id of currency of the company to which the voucher belong
1302 :param current_currency: id of currency of the voucher
1303 :return: mapping between fieldname and value of account move line to create
1306 currency_obj = self.pool.get('res.currency')
1309 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1310 current_currency_obj = voucher.currency_id or voucher.journal_id.company_id.currency_id
1312 if not currency_obj.is_zero(cr, uid, current_currency_obj, line_total):
1316 if voucher.payment_option == 'with_writeoff':
1317 account_id = voucher.writeoff_acc_id.id
1318 write_off_name = voucher.comment
1319 elif voucher.partner_id:
1320 if voucher.type in ('sale', 'receipt'):
1321 account_id = voucher.partner_id.property_account_receivable.id
1323 account_id = voucher.partner_id.property_account_payable.id
1325 # fallback on account of voucher
1326 account_id = voucher.account_id.id
1327 sign = voucher.type == 'payment' and -1 or 1
1329 'name': write_off_name or name,
1330 'account_id': account_id,
1332 'partner_id': voucher.partner_id.id,
1333 'date': voucher.date,
1334 'credit': diff > 0 and diff or 0.0,
1335 'debit': diff < 0 and -diff or 0.0,
1336 'amount_currency': company_currency <> current_currency and (sign * -1 * voucher.writeoff_amount) or 0.0,
1337 'currency_id': company_currency <> current_currency and current_currency or False,
1338 'analytic_account_id': voucher.analytic_id and voucher.analytic_id.id or False,
1343 def _get_company_currency(self, cr, uid, voucher_id, context=None):
1345 Get the currency of the actual company.
1347 :param voucher_id: Id of the voucher what i want to obtain company currency.
1348 :return: currency id of the company of the voucher
1351 return self.pool.get('account.voucher').browse(cr,uid,voucher_id,context).journal_id.company_id.currency_id.id
1353 def _get_current_currency(self, cr, uid, voucher_id, context=None):
1355 Get the currency of the voucher.
1357 :param voucher_id: Id of the voucher what i want to obtain current currency.
1358 :return: currency id of the voucher
1361 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1362 return voucher.currency_id.id or self._get_company_currency(cr,uid,voucher.id,context)
1364 def action_move_line_create(self, cr, uid, ids, context=None):
1366 Confirm the vouchers given in ids and create the journal entries for each of them
1370 move_pool = self.pool.get('account.move')
1371 move_line_pool = self.pool.get('account.move.line')
1372 for voucher in self.browse(cr, uid, ids, context=context):
1373 local_context = dict(context, force_company=voucher.journal_id.company_id.id)
1376 company_currency = self._get_company_currency(cr, uid, voucher.id, context)
1377 current_currency = self._get_current_currency(cr, uid, voucher.id, context)
1378 # we select the context to use accordingly if it's a multicurrency case or not
1379 context = self._sel_context(cr, uid, voucher.id, context)
1380 # But for the operations made by _convert_amount, we always need to give the date in the context
1381 ctx = context.copy()
1382 ctx.update({'date': voucher.date})
1383 # Create the account move record.
1384 move_id = move_pool.create(cr, uid, self.account_move_get(cr, uid, voucher.id, context=context), context=context)
1385 # Get the name of the account_move just created
1386 name = move_pool.browse(cr, uid, move_id, context=context).name
1387 # Create the first line of the voucher
1388 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)
1389 move_line_brw = move_line_pool.browse(cr, uid, move_line_id, context=context)
1390 line_total = move_line_brw.debit - move_line_brw.credit
1392 if voucher.type == 'sale':
1393 line_total = line_total - self._convert_amount(cr, uid, voucher.tax_amount, voucher.id, context=ctx)
1394 elif voucher.type == 'purchase':
1395 line_total = line_total + self._convert_amount(cr, uid, voucher.tax_amount, voucher.id, context=ctx)
1396 # Create one move line per voucher line where amount is not 0.0
1397 line_total, rec_list_ids = self.voucher_move_line_create(cr, uid, voucher.id, line_total, move_id, company_currency, current_currency, context)
1399 # Create the writeoff line if needed
1400 ml_writeoff = self.writeoff_move_line_get(cr, uid, voucher.id, line_total, move_id, name, company_currency, current_currency, local_context)
1402 move_line_pool.create(cr, uid, ml_writeoff, local_context)
1403 # We post the voucher.
1404 self.write(cr, uid, [voucher.id], {
1409 if voucher.journal_id.entry_posted:
1410 move_pool.post(cr, uid, [move_id], context={})
1411 # We automatically reconcile the account move lines.
1413 for rec_ids in rec_list_ids:
1414 if len(rec_ids) >= 2:
1415 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)
1418 class account_voucher_line(osv.osv):
1419 _name = 'account.voucher.line'
1420 _description = 'Voucher Lines'
1421 _order = "move_line_id"
1423 # If the payment is in the same currency than the invoice, we keep the same amount
1424 # Otherwise, we compute from invoice currency to payment currency
1425 def _compute_balance(self, cr, uid, ids, name, args, context=None):
1426 currency_pool = self.pool.get('res.currency')
1428 for line in self.browse(cr, uid, ids, context=context):
1429 ctx = context.copy()
1430 ctx.update({'date': line.voucher_id.date})
1431 voucher_rate = self.pool.get('res.currency').read(cr, uid, line.voucher_id.currency_id.id, ['rate'], context=ctx)['rate']
1433 'voucher_special_currency': line.voucher_id.payment_rate_currency_id and line.voucher_id.payment_rate_currency_id.id or False,
1434 'voucher_special_currency_rate': line.voucher_id.payment_rate * voucher_rate})
1436 company_currency = line.voucher_id.journal_id.company_id.currency_id.id
1437 voucher_currency = line.voucher_id.currency_id and line.voucher_id.currency_id.id or company_currency
1438 move_line = line.move_line_id or False
1441 res['amount_original'] = 0.0
1442 res['amount_unreconciled'] = 0.0
1443 elif move_line.currency_id and voucher_currency==move_line.currency_id.id:
1444 res['amount_original'] = abs(move_line.amount_currency)
1445 res['amount_unreconciled'] = abs(move_line.amount_residual_currency)
1447 #always use the amount booked in the company currency as the basis of the conversion into the voucher currency
1448 res['amount_original'] = currency_pool.compute(cr, uid, company_currency, voucher_currency, move_line.credit or move_line.debit or 0.0, context=ctx)
1449 res['amount_unreconciled'] = currency_pool.compute(cr, uid, company_currency, voucher_currency, abs(move_line.amount_residual), context=ctx)
1451 rs_data[line.id] = res
1454 def _currency_id(self, cr, uid, ids, name, args, context=None):
1456 This function returns the currency id of a voucher line. It's either the currency of the
1457 associated move line (if any) or the currency of the voucher or the company currency.
1460 for line in self.browse(cr, uid, ids, context=context):
1461 move_line = line.move_line_id
1463 res[line.id] = move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id
1465 res[line.id] = line.voucher_id.currency_id and line.voucher_id.currency_id.id or line.voucher_id.company_id.currency_id.id
1469 'voucher_id':fields.many2one('account.voucher', 'Voucher', required=1, ondelete='cascade'),
1470 'name':fields.char('Description',),
1471 'account_id':fields.many2one('account.account','Account', required=True),
1472 'partner_id':fields.related('voucher_id', 'partner_id', type='many2one', relation='res.partner', string='Partner'),
1473 'untax_amount':fields.float('Untax Amount'),
1474 'amount':fields.float('Amount', digits_compute=dp.get_precision('Account')),
1475 'reconcile': fields.boolean('Full Reconcile'),
1476 'type':fields.selection([('dr','Debit'),('cr','Credit')], 'Dr/Cr'),
1477 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'),
1478 'move_line_id': fields.many2one('account.move.line', 'Journal Item', copy=False),
1479 'date_original': fields.related('move_line_id','date', type='date', relation='account.move.line', string='Date', readonly=1),
1480 'date_due': fields.related('move_line_id','date_maturity', type='date', relation='account.move.line', string='Due Date', readonly=1),
1481 'amount_original': fields.function(_compute_balance, multi='dc', type='float', string='Original Amount', store=True, digits_compute=dp.get_precision('Account')),
1482 'amount_unreconciled': fields.function(_compute_balance, multi='dc', type='float', string='Open Balance', store=True, digits_compute=dp.get_precision('Account')),
1483 'company_id': fields.related('voucher_id','company_id', relation='res.company', type='many2one', string='Company', store=True, readonly=True),
1484 'currency_id': fields.function(_currency_id, string='Currency', type='many2one', relation='res.currency', readonly=True),
1490 def onchange_reconcile(self, cr, uid, ids, reconcile, amount, amount_unreconciled, context=None):
1491 vals = {'amount': 0.0}
1493 vals = { 'amount': amount_unreconciled}
1494 return {'value': vals}
1496 def onchange_amount(self, cr, uid, ids, amount, amount_unreconciled, context=None):
1499 vals['reconcile'] = (amount == amount_unreconciled)
1500 return {'value': vals}
1502 def onchange_move_line_id(self, cr, user, ids, move_line_id, context=None):
1504 Returns a dict that contains new values and context
1506 @param move_line_id: latest value from user input for field move_line_id
1507 @param args: other arguments
1508 @param context: context arguments, like lang, time zone
1510 @return: Returns a dict which contains new values, and context
1513 move_line_pool = self.pool.get('account.move.line')
1515 move_line = move_line_pool.browse(cr, user, move_line_id, context=context)
1516 if move_line.credit:
1521 'account_id': move_line.account_id.id,
1523 'currency_id': move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id,
1529 def default_get(self, cr, user, fields_list, context=None):
1531 Returns default values for fields
1532 @param fields_list: list of fields, for which default values are required to be read
1533 @param context: context arguments, like lang, time zone
1535 @return: Returns a dict that contains default values for fields
1539 journal_id = context.get('journal_id', False)
1540 partner_id = context.get('partner_id', False)
1541 journal_pool = self.pool.get('account.journal')
1542 partner_pool = self.pool.get('res.partner')
1543 values = super(account_voucher_line, self).default_get(cr, user, fields_list, context=context)
1544 if (not journal_id) or ('account_id' not in fields_list):
1546 journal = journal_pool.browse(cr, user, journal_id, context=context)
1549 if journal.type in ('sale', 'sale_refund'):
1550 account_id = journal.default_credit_account_id and journal.default_credit_account_id.id or False
1552 elif journal.type in ('purchase', 'expense', 'purchase_refund'):
1553 account_id = journal.default_debit_account_id and journal.default_debit_account_id.id or False
1556 partner = partner_pool.browse(cr, user, partner_id, context=context)
1557 if context.get('type') == 'payment':
1559 account_id = partner.property_account_payable.id
1560 elif context.get('type') == 'receipt':
1561 account_id = partner.property_account_receivable.id
1564 'account_id':account_id,
1569 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: