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
31 class res_currency(osv.osv):
32 _inherit = "res.currency"
34 def _get_current_rate(self, cr, uid, ids, raise_on_no_rate=True, context=None):
37 res = super(res_currency, self)._get_current_rate(cr, uid, ids, raise_on_no_rate, context=context)
38 if context.get('voucher_special_currency') in ids and context.get('voucher_special_currency_rate'):
39 res[context.get('voucher_special_currency')] = context.get('voucher_special_currency_rate')
43 class res_company(osv.osv):
44 _inherit = "res.company"
46 'income_currency_exchange_account_id': fields.many2one(
48 string="Gain Exchange Rate Account",
49 domain="[('type', '=', 'other')]",),
50 'expense_currency_exchange_account_id': fields.many2one(
52 string="Loss Exchange Rate Account",
53 domain="[('type', '=', 'other')]",),
57 class account_config_settings(osv.osv_memory):
58 _inherit = 'account.config.settings'
60 'income_currency_exchange_account_id': fields.related(
61 'company_id', 'income_currency_exchange_account_id',
63 relation='account.account',
64 string="Gain Exchange Rate Account",
65 domain="[('type', '=', 'other')]"),
66 'expense_currency_exchange_account_id': fields.related(
67 'company_id', 'expense_currency_exchange_account_id',
69 relation='account.account',
70 string="Loss Exchange Rate Account",
71 domain="[('type', '=', 'other')]"),
73 def onchange_company_id(self, cr, uid, ids, company_id, context=None):
74 res = super(account_config_settings, self).onchange_company_id(cr, uid, ids, company_id, context=context)
76 company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
77 res['value'].update({'income_currency_exchange_account_id': company.income_currency_exchange_account_id and company.income_currency_exchange_account_id.id or False,
78 'expense_currency_exchange_account_id': company.expense_currency_exchange_account_id and company.expense_currency_exchange_account_id.id or False})
80 res['value'].update({'income_currency_exchange_account_id': False,
81 'expense_currency_exchange_account_id': False})
84 class account_voucher(osv.osv):
85 def _check_paid(self, cr, uid, ids, name, args, context=None):
87 for voucher in self.browse(cr, uid, ids, context=context):
88 res[voucher.id] = any([((line.account_id.type, 'in', ('receivable', 'payable')) and line.reconcile_id) for line in voucher.move_ids])
91 def _get_type(self, cr, uid, context=None):
94 return context.get('type', False)
96 def _get_period(self, cr, uid, context=None):
97 if context is None: context = {}
98 if context.get('period_id', False):
99 return context.get('period_id')
100 periods = self.pool.get('account.period').find(cr, uid, context=context)
101 return periods and periods[0] or False
103 def _make_journal_search(self, cr, uid, ttype, context=None):
104 journal_pool = self.pool.get('account.journal')
105 return journal_pool.search(cr, uid, [('type', '=', ttype)], limit=1)
107 def _get_journal(self, cr, uid, context=None):
108 if context is None: context = {}
109 invoice_pool = self.pool.get('account.invoice')
110 journal_pool = self.pool.get('account.journal')
111 if context.get('invoice_id', False):
112 currency_id = invoice_pool.browse(cr, uid, context['invoice_id'], context=context).currency_id.id
113 journal_id = journal_pool.search(cr, uid, [('currency', '=', currency_id)], limit=1)
114 return journal_id and journal_id[0] or False
115 if context.get('journal_id', False):
116 return context.get('journal_id')
117 if not context.get('journal_id', False) and context.get('search_default_journal_id', False):
118 return context.get('search_default_journal_id')
120 ttype = context.get('type', 'bank')
121 if ttype in ('payment', 'receipt'):
123 res = self._make_journal_search(cr, uid, ttype, context=context)
124 return res and res[0] or False
126 def _get_tax(self, cr, uid, context=None):
127 if context is None: context = {}
128 journal_pool = self.pool.get('account.journal')
129 journal_id = context.get('journal_id', False)
131 ttype = context.get('type', 'bank')
132 res = journal_pool.search(cr, uid, [('type', '=', ttype)], limit=1)
139 journal = journal_pool.browse(cr, uid, journal_id, context=context)
140 account_id = journal.default_credit_account_id or journal.default_debit_account_id
141 if account_id and account_id.tax_ids:
142 tax_id = account_id.tax_ids[0].id
146 def _get_payment_rate_currency(self, cr, uid, context=None):
148 Return the default value for field payment_rate_currency_id: the currency of the journal
149 if there is one, otherwise the currency of the user's company
151 if context is None: context = {}
152 journal_pool = self.pool.get('account.journal')
153 journal_id = context.get('journal_id', False)
155 journal = journal_pool.browse(cr, uid, journal_id, context=context)
157 return journal.currency.id
158 #no journal given in the context, use company currency as default
159 return self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
161 def _get_currency(self, cr, uid, context=None):
162 if context is None: context = {}
163 journal_pool = self.pool.get('account.journal')
164 journal_id = context.get('journal_id', False)
166 journal = journal_pool.browse(cr, uid, journal_id, context=context)
168 return journal.currency.id
169 return self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
171 def _get_partner(self, cr, uid, context=None):
172 if context is None: context = {}
173 return context.get('partner_id', False)
175 def _get_reference(self, cr, uid, context=None):
176 if context is None: context = {}
177 return context.get('reference', False)
179 def _get_narration(self, cr, uid, context=None):
180 if context is None: context = {}
181 return context.get('narration', False)
183 def _get_amount(self, cr, uid, context=None):
186 return context.get('amount', 0.0)
188 def name_get(self, cr, uid, ids, context=None):
191 if context is None: context = {}
192 return [(r['id'], (r['number'] or _('Voucher'))) for r in self.read(cr, uid, ids, ['number'], context, load='_classic_write')]
194 def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
195 mod_obj = self.pool.get('ir.model.data')
196 if context is None: context = {}
198 if view_type == 'form':
199 if not view_id and context.get('invoice_type'):
200 if context.get('invoice_type') in ('out_invoice', 'out_refund'):
201 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_receipt_form')
203 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_payment_form')
204 result = result and result[1] or False
206 if not view_id and context.get('line_type'):
207 if context.get('line_type') == 'customer':
208 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_receipt_form')
210 result = mod_obj.get_object_reference(cr, uid, 'account_voucher', 'view_vendor_payment_form')
211 result = result and result[1] or False
214 res = super(account_voucher, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
215 doc = etree.XML(res['arch'])
217 if context.get('type', 'sale') in ('purchase', 'payment'):
218 nodes = doc.xpath("//field[@name='partner_id']")
220 node.set('context', "{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1}")
221 if context.get('invoice_type','') in ('in_invoice', 'in_refund'):
222 node.set('string', _("Supplier"))
223 res['arch'] = etree.tostring(doc)
226 def _compute_writeoff_amount(self, cr, uid, line_dr_ids, line_cr_ids, amount, type):
228 sign = type == 'payment' and -1 or 1
229 for l in line_dr_ids:
231 for l in line_cr_ids:
232 credit += l['amount']
233 return amount - sign * (credit - debit)
235 def onchange_line_ids(self, cr, uid, ids, line_dr_ids, line_cr_ids, amount, voucher_currency, type, context=None):
236 context = context or {}
237 if not line_dr_ids and not line_cr_ids:
238 return {'value':{'writeoff_amount': 0.0}}
239 line_osv = self.pool.get("account.voucher.line")
240 line_dr_ids = resolve_o2m_operations(cr, uid, line_osv, line_dr_ids, ['amount'], context)
241 line_cr_ids = resolve_o2m_operations(cr, uid, line_osv, line_cr_ids, ['amount'], context)
242 #compute the field is_multi_currency that is used to hide/display options linked to secondary currency on the voucher
243 is_multi_currency = False
244 #loop on the voucher lines to see if one of these has a secondary currency. If yes, we need to see the options
245 for voucher_line in line_dr_ids+line_cr_ids:
246 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')
247 if line_id and self.pool.get('account.move.line').browse(cr, uid, line_id, context=context).currency_id:
248 is_multi_currency = True
250 return {'value': {'writeoff_amount': self._compute_writeoff_amount(cr, uid, line_dr_ids, line_cr_ids, amount, type), 'is_multi_currency': is_multi_currency}}
252 def _get_journal_currency(self, cr, uid, ids, name, args, context=None):
254 for voucher in self.browse(cr, uid, ids, context=context):
255 res[voucher.id] = voucher.journal_id.currency and voucher.journal_id.currency.id or voucher.company_id.currency_id.id
258 def _get_writeoff_amount(self, cr, uid, ids, name, args, context=None):
259 if not ids: return {}
260 currency_obj = self.pool.get('res.currency')
263 for voucher in self.browse(cr, uid, ids, context=context):
264 sign = voucher.type == 'payment' and -1 or 1
265 for l in voucher.line_dr_ids:
267 for l in voucher.line_cr_ids:
269 currency = voucher.currency_id or voucher.company_id.currency_id
270 res[voucher.id] = currency_obj.round(cr, uid, currency, voucher.amount - sign * (credit - debit))
273 def _paid_amount_in_company_currency(self, cr, uid, ids, name, args, context=None):
278 for v in self.browse(cr, uid, ids, context=context):
279 ctx.update({'date': v.date})
280 #make a new call to browse in order to have the right date in the context, to get the right currency rate
281 voucher = self.browse(cr, uid, v.id, context=ctx)
283 'voucher_special_currency': voucher.payment_rate_currency_id and voucher.payment_rate_currency_id.id or False,
284 'voucher_special_currency_rate': voucher.currency_id.rate * voucher.payment_rate,})
285 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)
288 def _get_currency_help_label(self, cr, uid, currency_id, payment_rate, payment_rate_currency_id, context=None):
290 This function builds a string to help the users to understand the behavior of the payment rate fields they can specify on the voucher.
291 This string is only used to improve the usability in the voucher form view and has no other effect.
293 :param currency_id: the voucher currency
294 :type currency_id: integer
295 :param payment_rate: the value of the payment_rate field of the voucher
296 :type payment_rate: float
297 :param payment_rate_currency_id: the value of the payment_rate_currency_id field of the voucher
298 :type payment_rate_currency_id: integer
299 :return: translated string giving a tip on what's the effect of the current payment rate specified
302 rml_parser = report_sxw.rml_parse(cr, uid, 'currency_help_label', context=context)
303 currency_pool = self.pool.get('res.currency')
304 currency_str = payment_rate_str = ''
306 currency_str = rml_parser.formatLang(1, currency_obj=currency_pool.browse(cr, uid, currency_id, context=context))
307 if payment_rate_currency_id:
308 payment_rate_str = rml_parser.formatLang(payment_rate, currency_obj=currency_pool.browse(cr, uid, payment_rate_currency_id, context=context))
309 currency_help_label = _('At the operation date, the exchange rate was\n%s = %s') % (currency_str, payment_rate_str)
310 return currency_help_label
312 def _fnct_currency_help_label(self, cr, uid, ids, name, args, context=None):
314 for voucher in self.browse(cr, uid, ids, context=context):
315 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)
318 _name = 'account.voucher'
319 _description = 'Accounting Voucher'
320 _inherit = ['mail.thread']
321 _order = "date desc, id desc"
322 # _rec_name = 'number'
325 'account_voucher.mt_voucher_state_change': lambda self, cr, uid, obj, ctx=None: True,
330 'type':fields.selection([
332 ('purchase','Purchase'),
333 ('payment','Payment'),
334 ('receipt','Receipt'),
335 ],'Default Type', readonly=True, states={'draft':[('readonly',False)]}),
336 'name':fields.char('Memo', size=256, readonly=True, states={'draft':[('readonly',False)]}),
337 'date':fields.date('Date', readonly=True, select=True, states={'draft':[('readonly',False)]}, help="Effective date for accounting entries"),
338 'journal_id':fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
339 'account_id':fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}),
340 'line_ids':fields.one2many('account.voucher.line','voucher_id','Voucher Lines', readonly=True, states={'draft':[('readonly',False)]}),
341 'line_cr_ids':fields.one2many('account.voucher.line','voucher_id','Credits',
342 domain=[('type','=','cr')], context={'default_type':'cr'}, readonly=True, states={'draft':[('readonly',False)]}),
343 'line_dr_ids':fields.one2many('account.voucher.line','voucher_id','Debits',
344 domain=[('type','=','dr')], context={'default_type':'dr'}, readonly=True, states={'draft':[('readonly',False)]}),
345 'period_id': fields.many2one('account.period', 'Period', required=True, readonly=True, states={'draft':[('readonly',False)]}),
346 'narration':fields.text('Notes', readonly=True, states={'draft':[('readonly',False)]}),
347 'currency_id': fields.function(_get_journal_currency, type='many2one', relation='res.currency', string='Currency', readonly=True, required=True),
348 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
349 'state':fields.selection(
351 ('cancel','Cancelled'),
352 ('proforma','Pro-forma'),
354 ], 'Status', readonly=True, size=32, track_visibility='onchange',
355 help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed Voucher. \
356 \n* The \'Pro-forma\' when voucher is in Pro-forma status,voucher does not have an voucher number. \
357 \n* The \'Posted\' status is used when user create voucher,a voucher number is generated and voucher entries are created in account \
358 \n* The \'Cancelled\' status is used when user cancel voucher.'),
359 'amount': fields.float('Total', digits_compute=dp.get_precision('Account'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
360 'tax_amount':fields.float('Tax Amount', digits_compute=dp.get_precision('Account'), readonly=True),
361 'reference': fields.char('Ref #', size=64, readonly=True, states={'draft':[('readonly',False)]}, help="Transaction reference number."),
362 'number': fields.char('Number', size=32, readonly=True,),
363 'move_id':fields.many2one('account.move', 'Account Entry'),
364 'move_ids': fields.related('move_id','line_id', type='one2many', relation='account.move.line', string='Journal Items', readonly=True),
365 'partner_id':fields.many2one('res.partner', 'Partner', change_default=1, readonly=True, states={'draft':[('readonly',False)]}),
366 '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'),
367 'paid': fields.function(_check_paid, string='Paid', type='boolean', help="The Voucher has been totally paid."),
368 'pay_now':fields.selection([
369 ('pay_now','Pay Directly'),
370 ('pay_later','Pay Later or Group Funds'),
371 ],'Payment', select=True, readonly=True, states={'draft':[('readonly',False)]}),
372 'tax_id': fields.many2one('account.tax', 'Tax', readonly=True, states={'draft':[('readonly',False)]}, domain=[('price_include','=', False)], help="Only for tax excluded from price"),
373 'pre_line':fields.boolean('Previous Payments ?', required=False),
374 'date_due': fields.date('Due Date', readonly=True, select=True, states={'draft':[('readonly',False)]}),
375 'payment_option':fields.selection([
376 ('without_writeoff', 'Keep Open'),
377 ('with_writeoff', 'Reconcile Payment Balance'),
378 ], '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)"),
379 'writeoff_acc_id': fields.many2one('account.account', 'Counterpart Account', readonly=True, states={'draft': [('readonly', False)]}),
380 'comment': fields.char('Counterpart Comment', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
381 'analytic_id': fields.many2one('account.analytic.account','Write-Off Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
382 '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."),
383 'payment_rate_currency_id': fields.many2one('res.currency', 'Payment Rate Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}),
384 'payment_rate': fields.float('Exchange Rate', digits=(12,6), required=True, readonly=True, states={'draft': [('readonly', False)]},
385 help='The specific rate that will be used, in this voucher, between the selected currency (in \'Payment Rate Currency\' field) and the voucher currency.'),
386 'paid_amount_in_company_currency': fields.function(_paid_amount_in_company_currency, string='Paid Amount in Company Currency', type='float', readonly=True),
387 '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'),
388 '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"),
391 'period_id': _get_period,
392 'partner_id': _get_partner,
393 'journal_id':_get_journal,
394 'currency_id': _get_currency,
395 'reference': _get_reference,
396 'narration':_get_narration,
397 'amount': _get_amount,
400 'pay_now': 'pay_now',
402 'date': lambda *a: time.strftime('%Y-%m-%d'),
403 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.voucher',context=c),
405 'payment_option': 'without_writeoff',
406 'comment': _('Write-Off'),
408 'payment_rate_currency_id': _get_payment_rate_currency,
411 def compute_tax(self, cr, uid, ids, context=None):
412 tax_pool = self.pool.get('account.tax')
413 partner_pool = self.pool.get('res.partner')
414 position_pool = self.pool.get('account.fiscal.position')
415 voucher_line_pool = self.pool.get('account.voucher.line')
416 voucher_pool = self.pool.get('account.voucher')
417 if context is None: context = {}
419 for voucher in voucher_pool.browse(cr, uid, ids, context=context):
421 for line in voucher.line_ids:
422 voucher_amount += line.untax_amount or line.amount
423 line.amount = line.untax_amount or line.amount
424 voucher_line_pool.write(cr, uid, [line.id], {'amount':line.amount, 'untax_amount':line.untax_amount})
426 if not voucher.tax_id:
427 self.write(cr, uid, [voucher.id], {'amount':voucher_amount, 'tax_amount':0.0})
430 tax = [tax_pool.browse(cr, uid, voucher.tax_id.id, context=context)]
431 partner = partner_pool.browse(cr, uid, voucher.partner_id.id, context=context) or False
432 taxes = position_pool.map_tax(cr, uid, partner and partner.property_account_position or False, tax)
433 tax = tax_pool.browse(cr, uid, taxes, context=context)
435 total = voucher_amount
438 if not tax[0].price_include:
439 for line in voucher.line_ids:
440 for tax_line in tax_pool.compute_all(cr, uid, tax, line.amount, 1).get('taxes', []):
441 total_tax += tax_line.get('amount', 0.0)
444 for line in voucher.line_ids:
448 for tax_line in tax_pool.compute_all(cr, uid, tax, line.untax_amount or line.amount, 1).get('taxes', []):
449 line_tax += tax_line.get('amount', 0.0)
450 line_total += tax_line.get('price_unit')
451 total_tax += line_tax
452 untax_amount = line.untax_amount or line.amount
453 voucher_line_pool.write(cr, uid, [line.id], {'amount':line_total, 'untax_amount':untax_amount})
455 self.write(cr, uid, [voucher.id], {'amount':total, 'tax_amount':total_tax})
458 def onchange_price(self, cr, uid, ids, line_ids, tax_id, partner_id=False, context=None):
459 context = context or {}
460 tax_pool = self.pool.get('account.tax')
461 partner_pool = self.pool.get('res.partner')
462 position_pool = self.pool.get('account.fiscal.position')
463 line_pool = self.pool.get('account.voucher.line')
472 line_ids = resolve_o2m_operations(cr, uid, line_pool, line_ids, ["amount"], context)
475 for line in line_ids:
477 line_amount = line.get('amount',0.0)
480 tax = [tax_pool.browse(cr, uid, tax_id, context=context)]
482 partner = partner_pool.browse(cr, uid, partner_id, context=context) or False
483 taxes = position_pool.map_tax(cr, uid, partner and partner.property_account_position or False, tax)
484 tax = tax_pool.browse(cr, uid, taxes, context=context)
486 if not tax[0].price_include:
487 for tax_line in tax_pool.compute_all(cr, uid, tax, line_amount, 1).get('taxes', []):
488 total_tax += tax_line.get('amount')
490 voucher_total += line_amount
491 total = voucher_total + total_tax
494 'amount': total or voucher_total,
495 'tax_amount': total_tax
501 def onchange_term_id(self, cr, uid, ids, term_id, amount):
502 term_pool = self.pool.get('account.payment.term')
505 default = {'date_due':False}
506 if term_id and amount:
507 terms = term_pool.compute(cr, uid, term_id, amount)
509 due_date = terms[-1][0]
513 return {'value':default}
515 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):
517 Returns a dict that contains new values and context
519 @param partner_id: latest value from user input for field partner_id
520 @param args: other arguments
521 @param context: context arguments, like lang, time zone
523 @return: Returns a dict which contains new values, and context
529 if not partner_id or not journal_id:
532 partner_pool = self.pool.get('res.partner')
533 journal_pool = self.pool.get('account.journal')
535 journal = journal_pool.browse(cr, uid, journal_id, context=context)
536 partner = partner_pool.browse(cr, uid, partner_id, context=context)
539 if journal.type in ('sale','sale_refund'):
540 account_id = partner.property_account_receivable.id
542 elif journal.type in ('purchase', 'purchase_refund','expense'):
543 account_id = partner.property_account_payable.id
546 if not journal.default_credit_account_id or not journal.default_debit_account_id:
547 raise osv.except_osv(_('Error!'), _('Please define default credit/debit accounts on the journal "%s".') % (journal.name))
548 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
551 default['value']['account_id'] = account_id
552 default['value']['type'] = ttype or tr_type
554 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)
555 default['value'].update(vals.get('value'))
559 def onchange_rate(self, cr, uid, ids, rate, amount, currency_id, payment_rate_currency_id, company_id, context=None):
560 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)}}
561 if rate and amount and currency_id:
562 company_currency = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id
563 #context should contain the date, the payment currency and the payment rate specified on the voucher
564 amount_in_company_currency = self.pool.get('res.currency').compute(cr, uid, currency_id, company_currency.id, amount, context=context)
565 res['value']['paid_amount_in_company_currency'] = amount_in_company_currency
568 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):
572 ctx.update({'date': date})
573 #read the voucher rate with the right date in the context
574 currency_id = currency_id or self.pool.get('res.company').browse(cr, uid, company_id, context=ctx).currency_id.id
575 voucher_rate = self.pool.get('res.currency').read(cr, uid, currency_id, ['rate'], context=ctx)['rate']
577 'voucher_special_currency': payment_rate_currency_id,
578 'voucher_special_currency_rate': rate * voucher_rate})
579 res = self.recompute_voucher_lines(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=ctx)
580 vals = self.onchange_rate(cr, uid, ids, rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
581 for key in vals.keys():
582 res[key].update(vals[key])
585 def recompute_payment_rate(self, cr, uid, ids, vals, currency_id, date, ttype, journal_id, amount, context=None):
588 #on change of the journal, we need to set also the default value for payment_rate and payment_rate_currency_id
589 currency_obj = self.pool.get('res.currency')
590 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
591 company_id = journal.company_id.id
593 currency_id = currency_id or journal.company_id.currency_id.id
594 payment_rate_currency_id = currency_id
596 ctx.update({'date': date})
598 if ttype == 'receipt':
599 o2m_to_loop = 'line_cr_ids'
600 elif ttype == 'payment':
601 o2m_to_loop = 'line_dr_ids'
602 if o2m_to_loop and 'value' in vals and o2m_to_loop in vals['value']:
603 for voucher_line in vals['value'][o2m_to_loop]:
604 if voucher_line['currency_id'] != currency_id:
605 # we take as default value for the payment_rate_currency_id, the currency of the first invoice that
606 # is not in the voucher currency
607 payment_rate_currency_id = voucher_line['currency_id']
608 tmp = currency_obj.browse(cr, uid, payment_rate_currency_id, context=ctx).rate
609 payment_rate = tmp / currency_obj.browse(cr, uid, currency_id, context=ctx).rate
611 vals['value'].update({
612 'payment_rate': payment_rate,
613 'currency_id': currency_id,
614 'payment_rate_currency_id': payment_rate_currency_id
616 #read the voucher rate with the right date in the context
617 voucher_rate = self.pool.get('res.currency').read(cr, uid, currency_id, ['rate'], context=ctx)['rate']
619 'voucher_special_currency_rate': payment_rate * voucher_rate,
620 'voucher_special_currency': payment_rate_currency_id})
621 res = self.onchange_rate(cr, uid, ids, payment_rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
622 for key in res.keys():
623 vals[key].update(res[key])
626 def basic_onchange_partner(self, cr, uid, ids, partner_id, journal_id, ttype, context=None):
627 partner_pool = self.pool.get('res.partner')
628 journal_pool = self.pool.get('account.journal')
629 res = {'value': {'account_id': False}}
630 if not partner_id or not journal_id:
633 journal = journal_pool.browse(cr, uid, journal_id, context=context)
634 partner = partner_pool.browse(cr, uid, partner_id, context=context)
636 if journal.type in ('sale','sale_refund'):
637 account_id = partner.property_account_receivable.id
638 elif journal.type in ('purchase', 'purchase_refund','expense'):
639 account_id = partner.property_account_payable.id
641 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
643 res['value']['account_id'] = account_id
646 def onchange_partner_id(self, cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=None):
651 #TODO: comment me and use me directly in the sales/purchases views
652 res = self.basic_onchange_partner(cr, uid, ids, partner_id, journal_id, ttype, context=context)
653 if ttype in ['sale', 'purchase']:
656 # 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
657 ctx.update({'date': date})
658 vals = self.recompute_voucher_lines(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=ctx)
659 vals2 = self.recompute_payment_rate(cr, uid, ids, vals, currency_id, date, ttype, journal_id, amount, context=context)
660 for key in vals.keys():
661 res[key].update(vals[key])
662 for key in vals2.keys():
663 res[key].update(vals2[key])
664 #TODO: can probably be removed now
665 #TODO: onchange_partner_id() should not returns [pre_line, line_dr_ids, payment_rate...] for type sale, and not
666 # [pre_line, line_cr_ids, payment_rate...] for type purchase.
667 # We should definitively split account.voucher object in two and make distinct on_change functions. In the
668 # meanwhile, bellow lines must be there because the fields aren't present in the view, what crashes if the
669 # onchange returns a value for them
671 del(res['value']['line_dr_ids'])
672 del(res['value']['pre_line'])
673 del(res['value']['payment_rate'])
674 elif ttype == 'purchase':
675 del(res['value']['line_cr_ids'])
676 del(res['value']['pre_line'])
677 del(res['value']['payment_rate'])
680 def recompute_voucher_lines(self, cr, uid, ids, partner_id, journal_id, price, currency_id, ttype, date, context=None):
682 Returns a dict that contains new values and context
684 @param partner_id: latest value from user input for field partner_id
685 @param args: other arguments
686 @param context: context arguments, like lang, time zone
688 @return: Returns a dict which contains new values, and context
690 def _remove_noise_in_o2m():
691 """if the line is partially reconciled, then we must pay attention to display it only once and
693 This function returns True if the line is considered as noise and should not be displayed
695 if line.reconcile_partial_id:
696 if currency_id == line.currency_id.id:
697 if line.amount_residual_currency <= 0:
700 if line.amount_residual <= 0:
706 context_multi_currency = context.copy()
708 currency_pool = self.pool.get('res.currency')
709 move_line_pool = self.pool.get('account.move.line')
710 partner_pool = self.pool.get('res.partner')
711 journal_pool = self.pool.get('account.journal')
712 line_pool = self.pool.get('account.voucher.line')
716 'value': {'line_dr_ids': [] ,'line_cr_ids': [] ,'pre_line': False,},
720 line_ids = ids and line_pool.search(cr, uid, [('voucher_id', '=', ids[0])]) or False
722 line_pool.unlink(cr, uid, line_ids)
724 if not partner_id or not journal_id:
727 journal = journal_pool.browse(cr, uid, journal_id, context=context)
728 partner = partner_pool.browse(cr, uid, partner_id, context=context)
729 currency_id = currency_id or journal.company_id.currency_id.id
734 if context.get('account_id'):
735 account_type = self.pool['account.account'].browse(cr, uid, context['account_id'], context=context).type
736 if ttype == 'payment':
738 account_type = 'payable'
739 total_debit = price or 0.0
741 total_credit = price or 0.0
743 account_type = 'receivable'
745 if not context.get('move_line_ids', False):
746 ids = move_line_pool.search(cr, uid, [('state','=','valid'), ('account_id.type', '=', account_type), ('reconcile_id', '=', False), ('partner_id', '=', partner_id)], context=context)
748 ids = context['move_line_ids']
749 invoice_id = context.get('invoice_id', False)
750 company_currency = journal.company_id.currency_id.id
751 move_lines_found = []
753 #order the lines by most old first
755 account_move_lines = move_line_pool.browse(cr, uid, ids, context=context)
757 #compute the total debit/credit and look for a matching open amount or invoice
758 for line in account_move_lines:
759 if _remove_noise_in_o2m():
763 if line.invoice.id == invoice_id:
764 #if the invoice linked to the voucher line is equal to the invoice_id in context
765 #then we assign the amount on that line, whatever the other voucher lines
766 move_lines_found.append(line.id)
767 elif currency_id == company_currency:
768 #otherwise treatments is the same but with other field names
769 if line.amount_residual == price:
770 #if the amount residual is equal the amount voucher, we assign it to that voucher
771 #line, whatever the other voucher lines
772 move_lines_found.append(line.id)
774 #otherwise we will split the voucher amount on each line (by most old first)
775 total_credit += line.credit or 0.0
776 total_debit += line.debit or 0.0
777 elif currency_id == line.currency_id.id:
778 if line.amount_residual_currency == price:
779 move_lines_found.append(line.id)
781 total_credit += line.credit and line.amount_currency or 0.0
782 total_debit += line.debit and line.amount_currency or 0.0
784 remaining_amount = price
785 #voucher line creation
786 for line in account_move_lines:
788 if _remove_noise_in_o2m():
791 if line.currency_id and currency_id == line.currency_id.id:
792 amount_original = abs(line.amount_currency)
793 amount_unreconciled = abs(line.amount_residual_currency)
795 #always use the amount booked in the company currency as the basis of the conversion into the voucher currency
796 amount_original = currency_pool.compute(cr, uid, company_currency, currency_id, line.credit or line.debit or 0.0, context=context_multi_currency)
797 amount_unreconciled = currency_pool.compute(cr, uid, company_currency, currency_id, abs(line.amount_residual), context=context_multi_currency)
798 line_currency_id = line.currency_id and line.currency_id.id or company_currency
800 'name':line.move_id.name,
801 'type': line.credit and 'dr' or 'cr',
802 'move_line_id':line.id,
803 'account_id':line.account_id.id,
804 'amount_original': amount_original,
805 'amount': (line.id in move_lines_found) and min(abs(remaining_amount), amount_unreconciled) or 0.0,
806 'date_original':line.date,
807 'date_due':line.date_maturity,
808 'amount_unreconciled': amount_unreconciled,
809 'currency_id': line_currency_id,
811 remaining_amount -= rs['amount']
812 #in case a corresponding move_line hasn't been found, we now try to assign the voucher amount
813 #on existing invoices: we split voucher amount by most old first, but only for lines in the same currency
814 if not move_lines_found:
815 if currency_id == line_currency_id:
817 amount = min(amount_unreconciled, abs(total_debit))
818 rs['amount'] = amount
819 total_debit -= amount
821 amount = min(amount_unreconciled, abs(total_credit))
822 rs['amount'] = amount
823 total_credit -= amount
825 if rs['amount_unreconciled'] == rs['amount']:
826 rs['reconcile'] = True
828 if rs['type'] == 'cr':
829 default['value']['line_cr_ids'].append(rs)
831 default['value']['line_dr_ids'].append(rs)
833 if len(default['value']['line_cr_ids']) > 0:
834 default['value']['pre_line'] = 1
835 elif len(default['value']['line_dr_ids']) > 0:
836 default['value']['pre_line'] = 1
837 default['value']['writeoff_amount'] = self._compute_writeoff_amount(cr, uid, default['value']['line_dr_ids'], default['value']['line_cr_ids'], price, ttype)
840 def onchange_payment_rate_currency(self, cr, uid, ids, currency_id, payment_rate, payment_rate_currency_id, date, amount, company_id, context=None):
845 #set the default payment rate of the voucher and compute the paid amount in company currency
847 ctx.update({'date': date})
848 #read the voucher rate with the right date in the context
849 voucher_rate = self.pool.get('res.currency').read(cr, uid, currency_id, ['rate'], context=ctx)['rate']
851 'voucher_special_currency_rate': payment_rate * voucher_rate,
852 'voucher_special_currency': payment_rate_currency_id})
853 vals = self.onchange_rate(cr, uid, ids, payment_rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
854 for key in vals.keys():
855 res[key].update(vals[key])
858 def onchange_date(self, cr, uid, ids, date, currency_id, payment_rate_currency_id, amount, company_id, context=None):
860 @param date: latest value from user input for field date
861 @param args: other arguments
862 @param context: context arguments, like lang, time zone
863 @return: Returns a dict which contains new values, and context
868 #set the period of the voucher
869 period_pool = self.pool.get('account.period')
870 currency_obj = self.pool.get('res.currency')
872 ctx.update({'company_id': company_id, 'account_period_prefer_normal': True})
873 voucher_currency_id = currency_id or self.pool.get('res.company').browse(cr, uid, company_id, context=ctx).currency_id.id
874 pids = period_pool.find(cr, uid, date, context=ctx)
876 res['value'].update({'period_id':pids[0]})
877 if payment_rate_currency_id:
878 ctx.update({'date': date})
880 if payment_rate_currency_id != currency_id:
881 tmp = currency_obj.browse(cr, uid, payment_rate_currency_id, context=ctx).rate
882 payment_rate = tmp / currency_obj.browse(cr, uid, voucher_currency_id, context=ctx).rate
883 vals = self.onchange_payment_rate_currency(cr, uid, ids, voucher_currency_id, payment_rate, payment_rate_currency_id, date, amount, company_id, context=context)
884 vals['value'].update({'payment_rate': payment_rate})
885 for key in vals.keys():
886 res[key].update(vals[key])
889 def onchange_journal(self, cr, uid, ids, journal_id, line_ids, tax_id, partner_id, date, amount, ttype, company_id, context=None):
894 journal_pool = self.pool.get('account.journal')
895 journal = journal_pool.browse(cr, uid, journal_id, context=context)
896 account_id = journal.default_credit_account_id or journal.default_debit_account_id
898 if account_id and account_id.tax_ids:
899 tax_id = account_id.tax_ids[0].id
902 if ttype in ('sale', 'purchase'):
903 vals = self.onchange_price(cr, uid, ids, line_ids, tax_id, partner_id, context)
904 vals['value'].update({'tax_id':tax_id,'amount': amount})
907 currency_id = journal.currency.id
909 currency_id = journal.company_id.currency_id.id
910 vals['value'].update({'currency_id': currency_id})
911 #in case we want to register the payment directly from an invoice, it's confusing to allow to switch the journal
912 #without seeing that the amount is expressed in the journal currency, and not in the invoice currency. So to avoid
913 #this common mistake, we simply reset the amount to 0 if the currency is not the invoice currency.
914 if context.get('payment_expected_currency') and currency_id != context.get('payment_expected_currency'):
915 vals['value']['amount'] = 0
918 res = self.onchange_partner_id(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context)
919 for key in res.keys():
920 vals[key].update(res[key])
923 def button_proforma_voucher(self, cr, uid, ids, context=None):
924 self.signal_proforma_voucher(cr, uid, ids)
925 return {'type': 'ir.actions.act_window_close'}
927 def proforma_voucher(self, cr, uid, ids, context=None):
928 self.action_move_line_create(cr, uid, ids, context=context)
931 def action_cancel_draft(self, cr, uid, ids, context=None):
932 self.create_workflow(cr, uid, ids)
933 self.write(cr, uid, ids, {'state':'draft'})
936 def cancel_voucher(self, cr, uid, ids, context=None):
937 reconcile_pool = self.pool.get('account.move.reconcile')
938 move_pool = self.pool.get('account.move')
939 move_line_pool = self.pool.get('account.move.line')
940 for voucher in self.browse(cr, uid, ids, context=context):
941 # refresh to make sure you don't unlink an already removed move
943 for line in voucher.move_ids:
944 # refresh to make sure you don't unreconcile an already unreconciled entry
946 if line.reconcile_id:
947 move_lines = [move_line.id for move_line in line.reconcile_id.line_id]
948 move_lines.remove(line.id)
949 reconcile_pool.unlink(cr, uid, [line.reconcile_id.id])
950 if len(move_lines) >= 2:
951 move_line_pool.reconcile_partial(cr, uid, move_lines, 'auto',context=context)
953 move_pool.button_cancel(cr, uid, [voucher.move_id.id])
954 move_pool.unlink(cr, uid, [voucher.move_id.id])
959 self.write(cr, uid, ids, res)
962 def unlink(self, cr, uid, ids, context=None):
963 for t in self.read(cr, uid, ids, ['state'], context=context):
964 if t['state'] not in ('draft', 'cancel'):
965 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete voucher(s) which are already opened or paid.'))
966 return super(account_voucher, self).unlink(cr, uid, ids, context=context)
968 def onchange_payment(self, cr, uid, ids, pay_now, journal_id, partner_id, ttype='sale'):
973 partner_pool = self.pool.get('res.partner')
974 journal_pool = self.pool.get('account.journal')
975 if pay_now == 'pay_later':
976 partner = partner_pool.browse(cr, uid, partner_id)
977 journal = journal_pool.browse(cr, uid, journal_id)
978 if journal.type in ('sale','sale_refund'):
979 account_id = partner.property_account_receivable.id
980 elif journal.type in ('purchase', 'purchase_refund','expense'):
981 account_id = partner.property_account_payable.id
983 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
985 res['account_id'] = account_id
988 def _sel_context(self, cr, uid, voucher_id, context=None):
990 Select the context to use accordingly if it needs to be multicurrency or not.
992 :param voucher_id: Id of the actual voucher
993 :return: The returned context will be the same as given in parameter if the voucher currency is the same
994 than the company currency, otherwise it's a copy of the parameter with an extra key 'date' containing
995 the date of the voucher.
998 company_currency = self._get_company_currency(cr, uid, voucher_id, context)
999 current_currency = self._get_current_currency(cr, uid, voucher_id, context)
1000 if current_currency <> company_currency:
1001 context_multi_currency = context.copy()
1002 voucher = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context)
1003 context_multi_currency.update({'date': voucher.date})
1004 return context_multi_currency
1007 def first_move_line_get(self, cr, uid, voucher_id, move_id, company_currency, current_currency, context=None):
1009 Return a dict to be use to create the first account move line of given voucher.
1011 :param voucher_id: Id of voucher what we are creating account_move.
1012 :param move_id: Id of account move where this line will be added.
1013 :param company_currency: id of currency of the company to which the voucher belong
1014 :param current_currency: id of currency of the voucher
1015 :return: mapping between fieldname and value of account move line to create
1018 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1019 debit = credit = 0.0
1020 # TODO: is there any other alternative then the voucher type ??
1021 # ANSWER: We can have payment and receipt "In Advance".
1022 # TODO: Make this logic available.
1023 # -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
1024 if voucher.type in ('purchase', 'payment'):
1025 credit = voucher.paid_amount_in_company_currency
1026 elif voucher.type in ('sale', 'receipt'):
1027 debit = voucher.paid_amount_in_company_currency
1028 if debit < 0: credit = -debit; debit = 0.0
1029 if credit < 0: debit = -credit; credit = 0.0
1030 sign = debit - credit < 0 and -1 or 1
1031 #set the first line of the voucher
1033 'name': voucher.name or '/',
1036 'account_id': voucher.account_id.id,
1038 'journal_id': voucher.journal_id.id,
1039 'period_id': voucher.period_id.id,
1040 'partner_id': voucher.partner_id.id,
1041 'currency_id': company_currency <> current_currency and current_currency or False,
1042 'amount_currency': (sign * abs(voucher.amount) # amount < 0 for refunds
1043 if company_currency != current_currency else 0.0),
1044 'date': voucher.date,
1045 'date_maturity': voucher.date_due
1049 def account_move_get(self, cr, uid, voucher_id, context=None):
1051 This method prepare the creation of the account move related to the given voucher.
1053 :param voucher_id: Id of voucher for which we are creating account_move.
1054 :return: mapping between fieldname and value of account move to create
1057 seq_obj = self.pool.get('ir.sequence')
1058 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1060 name = voucher.number
1061 elif voucher.journal_id.sequence_id:
1062 if not voucher.journal_id.sequence_id.active:
1063 raise osv.except_osv(_('Configuration Error !'),
1064 _('Please activate the sequence of selected journal !'))
1066 c.update({'fiscalyear_id': voucher.period_id.fiscalyear_id.id})
1067 name = seq_obj.next_by_id(cr, uid, voucher.journal_id.sequence_id.id, context=c)
1069 raise osv.except_osv(_('Error!'),
1070 _('Please define a sequence on the journal.'))
1071 if not voucher.reference:
1072 ref = name.replace('/','')
1074 ref = voucher.reference
1078 'journal_id': voucher.journal_id.id,
1079 'narration': voucher.narration,
1080 'date': voucher.date,
1082 'period_id': voucher.period_id.id,
1086 def _get_exchange_lines(self, cr, uid, line, move_id, amount_residual, company_currency, current_currency, context=None):
1088 Prepare the two lines in company currency due to currency rate difference.
1090 :param line: browse record of the voucher.line for which we want to create currency rate difference accounting
1092 :param move_id: Account move wher the move lines will be.
1093 :param amount_residual: Amount to be posted.
1094 :param company_currency: id of currency of the company to which the voucher belong
1095 :param current_currency: id of currency of the voucher
1096 :return: the account move line and its counterpart to create, depicted as mapping between fieldname and value
1097 :rtype: tuple of dict
1099 if amount_residual > 0:
1100 account_id = line.voucher_id.company_id.expense_currency_exchange_account_id
1102 raise osv.except_osv(_('Insufficient Configuration!'),_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
1104 account_id = line.voucher_id.company_id.income_currency_exchange_account_id
1106 raise osv.except_osv(_('Insufficient Configuration!'),_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
1107 # Even if the amount_currency is never filled, we need to pass the foreign currency because otherwise
1108 # the receivable/payable account may have a secondary currency, which render this field mandatory
1109 if line.account_id.currency_id:
1110 account_currency_id = line.account_id.currency_id.id
1112 account_currency_id = company_currency <> current_currency and current_currency or False
1114 'journal_id': line.voucher_id.journal_id.id,
1115 'period_id': line.voucher_id.period_id.id,
1116 'name': _('change')+': '+(line.name or '/'),
1117 'account_id': line.account_id.id,
1119 'partner_id': line.voucher_id.partner_id.id,
1120 'currency_id': account_currency_id,
1121 'amount_currency': 0.0,
1123 'credit': amount_residual > 0 and amount_residual or 0.0,
1124 'debit': amount_residual < 0 and -amount_residual or 0.0,
1125 'date': line.voucher_id.date,
1127 move_line_counterpart = {
1128 'journal_id': line.voucher_id.journal_id.id,
1129 'period_id': line.voucher_id.period_id.id,
1130 'name': _('change')+': '+(line.name or '/'),
1131 'account_id': account_id.id,
1133 'amount_currency': 0.0,
1134 'partner_id': line.voucher_id.partner_id.id,
1135 'currency_id': account_currency_id,
1137 'debit': amount_residual > 0 and amount_residual or 0.0,
1138 'credit': amount_residual < 0 and -amount_residual or 0.0,
1139 'date': line.voucher_id.date,
1141 return (move_line, move_line_counterpart)
1143 def _convert_amount(self, cr, uid, amount, voucher_id, context=None):
1145 This function convert the amount given in company currency. It takes either the rate in the voucher (if the
1146 payment_rate_currency_id is relevant) either the rate encoded in the system.
1148 :param amount: float. The amount to convert
1149 :param voucher: id of the voucher on which we want the conversion
1150 :param context: to context to use for the conversion. It may contain the key 'date' set to the voucher date
1151 field in order to select the good rate to use.
1152 :return: the amount in the currency of the voucher's company
1157 currency_obj = self.pool.get('res.currency')
1158 voucher = self.browse(cr, uid, voucher_id, context=context)
1159 return currency_obj.compute(cr, uid, voucher.currency_id.id, voucher.company_id.currency_id.id, amount, context=context)
1161 def voucher_move_line_create(self, cr, uid, voucher_id, line_total, move_id, company_currency, current_currency, context=None):
1163 Create one account move line, on the given account move, per voucher line where amount is not 0.0.
1164 It returns Tuple with tot_line what is total of difference between debit and credit and
1165 a list of lists with ids to be reconciled with this format (total_deb_cred,list_of_lists).
1167 :param voucher_id: Voucher id what we are working with
1168 :param line_total: Amount of the first line, which correspond to the amount we should totally split among all voucher lines.
1169 :param move_id: Account move wher those lines will be joined.
1170 :param company_currency: id of currency of the company to which the voucher belong
1171 :param current_currency: id of currency of the voucher
1172 :return: Tuple build as (remaining amount not allocated on voucher lines, list of account_move_line created in this method)
1173 :rtype: tuple(float, list of int)
1177 move_line_obj = self.pool.get('account.move.line')
1178 currency_obj = self.pool.get('res.currency')
1179 tax_obj = self.pool.get('account.tax')
1180 tot_line = line_total
1183 date = self.read(cr, uid, voucher_id, ['date'], context=context)['date']
1184 ctx = context.copy()
1185 ctx.update({'date': date})
1186 voucher = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context=ctx)
1187 voucher_currency = voucher.journal_id.currency or voucher.company_id.currency_id
1189 'voucher_special_currency_rate': voucher_currency.rate * voucher.payment_rate ,
1190 'voucher_special_currency': voucher.payment_rate_currency_id and voucher.payment_rate_currency_id.id or False,})
1191 prec = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')
1192 for line in voucher.line_ids:
1193 #create one move line per voucher line where amount is not 0.0
1194 # AND (second part of the clause) only if the original move line was not having debit = credit = 0 (which is a legal value)
1195 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)):
1197 # convert the amount set on the voucher line into the currency of the voucher's company
1198 # 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
1199 amount = self._convert_amount(cr, uid, line.untax_amount or line.amount, voucher.id, context=ctx)
1200 # if the amount encoded in voucher is equal to the amount unreconciled, we need to compute the
1201 # currency rate difference
1202 if line.amount == line.amount_unreconciled:
1203 if not line.move_line_id:
1204 raise osv.except_osv(_('Wrong voucher line'),_("The invoice you are willing to pay is not valid anymore."))
1205 sign = line.type =='dr' and -1 or 1
1206 currency_rate_difference = sign * (line.move_line_id.amount_residual - amount)
1208 currency_rate_difference = 0.0
1210 'journal_id': voucher.journal_id.id,
1211 'period_id': voucher.period_id.id,
1212 'name': line.name or '/',
1213 'account_id': line.account_id.id,
1215 'partner_id': voucher.partner_id.id,
1216 '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,
1217 'analytic_account_id': line.account_analytic_id and line.account_analytic_id.id or False,
1221 'date': voucher.date
1225 if line.type == 'dr':
1230 if (line.type=='dr'):
1232 move_line['debit'] = amount
1235 move_line['credit'] = amount
1237 if voucher.tax_id and voucher.type in ('sale', 'purchase'):
1239 'account_tax_id': voucher.tax_id.id,
1242 if move_line.get('account_tax_id', False):
1243 tax_data = tax_obj.browse(cr, uid, [move_line['account_tax_id']], context=context)[0]
1244 if not (tax_data.base_code_id and tax_data.tax_code_id):
1245 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))
1247 # compute the amount in foreign currency
1248 foreign_currency_diff = 0.0
1249 amount_currency = False
1250 if line.move_line_id:
1251 # We want to set it on the account move line as soon as the original line had a foreign currency
1252 if line.move_line_id.currency_id and line.move_line_id.currency_id.id != company_currency:
1253 # we compute the amount in that foreign currency.
1254 if line.move_line_id.currency_id.id == current_currency:
1255 # if the voucher and the voucher line share the same currency, there is no computation to do
1256 sign = (move_line['debit'] - move_line['credit']) < 0 and -1 or 1
1257 amount_currency = sign * (line.amount)
1259 # if the rate is specified on the voucher, it will be used thanks to the special keys in the context
1260 # otherwise we use the rates of the system
1261 amount_currency = currency_obj.compute(cr, uid, company_currency, line.move_line_id.currency_id.id, move_line['debit']-move_line['credit'], context=ctx)
1262 if line.amount == line.amount_unreconciled:
1263 foreign_currency_diff = line.move_line_id.amount_residual_currency - abs(amount_currency)
1265 move_line['amount_currency'] = amount_currency
1266 voucher_line = move_line_obj.create(cr, uid, move_line)
1267 rec_ids = [voucher_line, line.move_line_id.id]
1269 if not currency_obj.is_zero(cr, uid, voucher.company_id.currency_id, currency_rate_difference):
1270 # Change difference entry in company currency
1271 exch_lines = self._get_exchange_lines(cr, uid, line, move_id, currency_rate_difference, company_currency, current_currency, context=context)
1272 new_id = move_line_obj.create(cr, uid, exch_lines[0],context)
1273 move_line_obj.create(cr, uid, exch_lines[1], context)
1274 rec_ids.append(new_id)
1276 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):
1277 # Change difference entry in voucher currency
1278 move_line_foreign_currency = {
1279 'journal_id': line.voucher_id.journal_id.id,
1280 'period_id': line.voucher_id.period_id.id,
1281 'name': _('change')+': '+(line.name or '/'),
1282 'account_id': line.account_id.id,
1284 'partner_id': line.voucher_id.partner_id.id,
1285 'currency_id': line.move_line_id.currency_id.id,
1286 'amount_currency': -1 * foreign_currency_diff,
1290 'date': line.voucher_id.date,
1292 new_id = move_line_obj.create(cr, uid, move_line_foreign_currency, context=context)
1293 rec_ids.append(new_id)
1294 if line.move_line_id.id:
1295 rec_lst_ids.append(rec_ids)
1296 return (tot_line, rec_lst_ids)
1298 def writeoff_move_line_get(self, cr, uid, voucher_id, line_total, move_id, name, company_currency, current_currency, context=None):
1300 Set a dict to be use to create the writeoff move line.
1302 :param voucher_id: Id of voucher what we are creating account_move.
1303 :param line_total: Amount remaining to be allocated on lines.
1304 :param move_id: Id of account move where this line will be added.
1305 :param name: Description of account move line.
1306 :param company_currency: id of currency of the company to which the voucher belong
1307 :param current_currency: id of currency of the voucher
1308 :return: mapping between fieldname and value of account move line to create
1311 currency_obj = self.pool.get('res.currency')
1314 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1315 current_currency_obj = voucher.currency_id or voucher.journal_id.company_id.currency_id
1317 if not currency_obj.is_zero(cr, uid, current_currency_obj, line_total):
1321 if voucher.payment_option == 'with_writeoff':
1322 account_id = voucher.writeoff_acc_id.id
1323 write_off_name = voucher.comment
1324 elif voucher.partner_id:
1325 if voucher.type in ('sale', 'receipt'):
1326 account_id = voucher.partner_id.property_account_receivable.id
1328 account_id = voucher.partner_id.property_account_payable.id
1330 # fallback on account of voucher
1331 account_id = voucher.account_id.id
1332 sign = voucher.type == 'payment' and -1 or 1
1334 'name': write_off_name or name,
1335 'account_id': account_id,
1337 'partner_id': voucher.partner_id.id,
1338 'date': voucher.date,
1339 'credit': diff > 0 and diff or 0.0,
1340 'debit': diff < 0 and -diff or 0.0,
1341 'amount_currency': company_currency <> current_currency and (sign * -1 * voucher.writeoff_amount) or 0.0,
1342 'currency_id': company_currency <> current_currency and current_currency or False,
1343 'analytic_account_id': voucher.analytic_id and voucher.analytic_id.id or False,
1348 def _get_company_currency(self, cr, uid, voucher_id, context=None):
1350 Get the currency of the actual company.
1352 :param voucher_id: Id of the voucher what i want to obtain company currency.
1353 :return: currency id of the company of the voucher
1356 return self.pool.get('account.voucher').browse(cr,uid,voucher_id,context).journal_id.company_id.currency_id.id
1358 def _get_current_currency(self, cr, uid, voucher_id, context=None):
1360 Get the currency of the voucher.
1362 :param voucher_id: Id of the voucher what i want to obtain current currency.
1363 :return: currency id of the voucher
1366 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1367 return voucher.currency_id.id or self._get_company_currency(cr,uid,voucher.id,context)
1369 def action_move_line_create(self, cr, uid, ids, context=None):
1371 Confirm the vouchers given in ids and create the journal entries for each of them
1375 move_pool = self.pool.get('account.move')
1376 move_line_pool = self.pool.get('account.move.line')
1377 for voucher in self.browse(cr, uid, ids, context=context):
1378 local_context = dict(context, force_company=voucher.journal_id.company_id.id)
1381 company_currency = self._get_company_currency(cr, uid, voucher.id, context)
1382 current_currency = self._get_current_currency(cr, uid, voucher.id, context)
1383 # we select the context to use accordingly if it's a multicurrency case or not
1384 context = self._sel_context(cr, uid, voucher.id, context)
1385 # But for the operations made by _convert_amount, we always need to give the date in the context
1386 ctx = context.copy()
1387 ctx.update({'date': voucher.date})
1388 # Create the account move record.
1389 move_id = move_pool.create(cr, uid, self.account_move_get(cr, uid, voucher.id, context=context), context=context)
1390 # Get the name of the account_move just created
1391 name = move_pool.browse(cr, uid, move_id, context=context).name
1392 # Create the first line of the voucher
1393 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)
1394 move_line_brw = move_line_pool.browse(cr, uid, move_line_id, context=context)
1395 line_total = move_line_brw.debit - move_line_brw.credit
1397 if voucher.type == 'sale':
1398 line_total = line_total - self._convert_amount(cr, uid, voucher.tax_amount, voucher.id, context=ctx)
1399 elif voucher.type == 'purchase':
1400 line_total = line_total + self._convert_amount(cr, uid, voucher.tax_amount, voucher.id, context=ctx)
1401 # Create one move line per voucher line where amount is not 0.0
1402 line_total, rec_list_ids = self.voucher_move_line_create(cr, uid, voucher.id, line_total, move_id, company_currency, current_currency, context)
1404 # Create the writeoff line if needed
1405 ml_writeoff = self.writeoff_move_line_get(cr, uid, voucher.id, line_total, move_id, name, company_currency, current_currency, local_context)
1407 move_line_pool.create(cr, uid, ml_writeoff, local_context)
1408 # We post the voucher.
1409 self.write(cr, uid, [voucher.id], {
1414 if voucher.journal_id.entry_posted:
1415 move_pool.post(cr, uid, [move_id], context={})
1416 # We automatically reconcile the account move lines.
1418 for rec_ids in rec_list_ids:
1419 if len(rec_ids) >= 2:
1420 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)
1423 def copy(self, cr, uid, id, default=None, context=None):
1430 'line_cr_ids': False,
1431 'line_dr_ids': False,
1434 if 'date' not in default:
1435 default['date'] = time.strftime('%Y-%m-%d')
1436 return super(account_voucher, self).copy(cr, uid, id, default, context)
1439 class account_voucher_line(osv.osv):
1440 _name = 'account.voucher.line'
1441 _description = 'Voucher Lines'
1442 _order = "move_line_id"
1444 # If the payment is in the same currency than the invoice, we keep the same amount
1445 # Otherwise, we compute from invoice currency to payment currency
1446 def _compute_balance(self, cr, uid, ids, name, args, context=None):
1447 currency_pool = self.pool.get('res.currency')
1449 for line in self.browse(cr, uid, ids, context=context):
1450 ctx = context.copy()
1451 ctx.update({'date': line.voucher_id.date})
1452 voucher_rate = self.pool.get('res.currency').read(cr, uid, line.voucher_id.currency_id.id, ['rate'], context=ctx)['rate']
1454 'voucher_special_currency': line.voucher_id.payment_rate_currency_id and line.voucher_id.payment_rate_currency_id.id or False,
1455 'voucher_special_currency_rate': line.voucher_id.payment_rate * voucher_rate})
1457 company_currency = line.voucher_id.journal_id.company_id.currency_id.id
1458 voucher_currency = line.voucher_id.currency_id and line.voucher_id.currency_id.id or company_currency
1459 move_line = line.move_line_id or False
1462 res['amount_original'] = 0.0
1463 res['amount_unreconciled'] = 0.0
1464 elif move_line.currency_id and voucher_currency==move_line.currency_id.id:
1465 res['amount_original'] = abs(move_line.amount_currency)
1466 res['amount_unreconciled'] = abs(move_line.amount_residual_currency)
1468 #always use the amount booked in the company currency as the basis of the conversion into the voucher currency
1469 res['amount_original'] = currency_pool.compute(cr, uid, company_currency, voucher_currency, move_line.credit or move_line.debit or 0.0, context=ctx)
1470 res['amount_unreconciled'] = currency_pool.compute(cr, uid, company_currency, voucher_currency, abs(move_line.amount_residual), context=ctx)
1472 rs_data[line.id] = res
1475 def _currency_id(self, cr, uid, ids, name, args, context=None):
1477 This function returns the currency id of a voucher line. It's either the currency of the
1478 associated move line (if any) or the currency of the voucher or the company currency.
1481 for line in self.browse(cr, uid, ids, context=context):
1482 move_line = line.move_line_id
1484 res[line.id] = move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id
1486 res[line.id] = line.voucher_id.currency_id and line.voucher_id.currency_id.id or line.voucher_id.company_id.currency_id.id
1490 'voucher_id':fields.many2one('account.voucher', 'Voucher', required=1, ondelete='cascade'),
1491 'name':fields.char('Description', size=256),
1492 'account_id':fields.many2one('account.account','Account', required=True),
1493 'partner_id':fields.related('voucher_id', 'partner_id', type='many2one', relation='res.partner', string='Partner'),
1494 'untax_amount':fields.float('Untax Amount'),
1495 'amount':fields.float('Amount', digits_compute=dp.get_precision('Account')),
1496 'reconcile': fields.boolean('Full Reconcile'),
1497 'type':fields.selection([('dr','Debit'),('cr','Credit')], 'Dr/Cr'),
1498 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'),
1499 'move_line_id': fields.many2one('account.move.line', 'Journal Item'),
1500 'date_original': fields.related('move_line_id','date', type='date', relation='account.move.line', string='Date', readonly=1),
1501 'date_due': fields.related('move_line_id','date_maturity', type='date', relation='account.move.line', string='Due Date', readonly=1),
1502 'amount_original': fields.function(_compute_balance, multi='dc', type='float', string='Original Amount', store=True, digits_compute=dp.get_precision('Account')),
1503 'amount_unreconciled': fields.function(_compute_balance, multi='dc', type='float', string='Open Balance', store=True, digits_compute=dp.get_precision('Account')),
1504 'company_id': fields.related('voucher_id','company_id', relation='res.company', type='many2one', string='Company', store=True, readonly=True),
1505 'currency_id': fields.function(_currency_id, string='Currency', type='many2one', relation='res.currency', readonly=True),
1511 def onchange_reconcile(self, cr, uid, ids, reconcile, amount, amount_unreconciled, context=None):
1512 vals = {'amount': 0.0}
1514 vals = { 'amount': amount_unreconciled}
1515 return {'value': vals}
1517 def onchange_amount(self, cr, uid, ids, amount, amount_unreconciled, context=None):
1520 vals['reconcile'] = (amount == amount_unreconciled)
1521 return {'value': vals}
1523 def onchange_move_line_id(self, cr, user, ids, move_line_id, context=None):
1525 Returns a dict that contains new values and context
1527 @param move_line_id: latest value from user input for field move_line_id
1528 @param args: other arguments
1529 @param context: context arguments, like lang, time zone
1531 @return: Returns a dict which contains new values, and context
1534 move_line_pool = self.pool.get('account.move.line')
1536 move_line = move_line_pool.browse(cr, user, move_line_id, context=context)
1537 if move_line.credit:
1542 'account_id': move_line.account_id.id,
1544 'currency_id': move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id,
1550 def default_get(self, cr, user, fields_list, context=None):
1552 Returns default values for fields
1553 @param fields_list: list of fields, for which default values are required to be read
1554 @param context: context arguments, like lang, time zone
1556 @return: Returns a dict that contains default values for fields
1560 journal_id = context.get('journal_id', False)
1561 partner_id = context.get('partner_id', False)
1562 journal_pool = self.pool.get('account.journal')
1563 partner_pool = self.pool.get('res.partner')
1564 values = super(account_voucher_line, self).default_get(cr, user, fields_list, context=context)
1565 if (not journal_id) or ('account_id' not in fields_list):
1567 journal = journal_pool.browse(cr, user, journal_id, context=context)
1570 if journal.type in ('sale', 'sale_refund'):
1571 account_id = journal.default_credit_account_id and journal.default_credit_account_id.id or False
1573 elif journal.type in ('purchase', 'expense', 'purchase_refund'):
1574 account_id = journal.default_debit_account_id and journal.default_debit_account_id.id or False
1577 partner = partner_pool.browse(cr, user, partner_id, context=context)
1578 if context.get('type') == 'payment':
1580 account_id = partner.property_account_payable.id
1581 elif context.get('type') == 'receipt':
1582 account_id = partner.property_account_receivable.id
1585 'account_id':account_id,
1590 def resolve_o2m_operations(cr, uid, target_osv, operations, fields, context):
1592 for operation in operations:
1594 if not isinstance(operation, (list, tuple)):
1595 result = target_osv.read(cr, uid, operation, fields, context=context)
1596 elif operation[0] == 0:
1597 # may be necessary to check if all the fields are here and get the default values?
1598 result = operation[2]
1599 elif operation[0] == 1:
1600 result = target_osv.read(cr, uid, operation[1], fields, context=context)
1601 if not result: result = {}
1602 result.update(operation[2])
1603 elif operation[0] == 4:
1604 result = target_osv.read(cr, uid, operation[1], fields, context=context)
1606 results.append(result)
1610 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: