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'], (str("%.2f" % r['amount']) or '')) for r in self.read(cr, uid, ids, ['amount'], 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 'active': fields.boolean('Active', help="By default, reconciliation vouchers made on draft bank statements are set as inactive, which allow to hide the customer/supplier payment while the bank statement isn't confirmed."),
331 'type':fields.selection([
333 ('purchase','Purchase'),
334 ('payment','Payment'),
335 ('receipt','Receipt'),
336 ],'Default Type', readonly=True, states={'draft':[('readonly',False)]}),
337 'name':fields.char('Memo', size=256, readonly=True, states={'draft':[('readonly',False)]}),
338 'date':fields.date('Date', readonly=True, select=True, states={'draft':[('readonly',False)]}, help="Effective date for accounting entries"),
339 'journal_id':fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
340 'account_id':fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}),
341 'line_ids':fields.one2many('account.voucher.line','voucher_id','Voucher Lines', readonly=True, states={'draft':[('readonly',False)]}),
342 'line_cr_ids':fields.one2many('account.voucher.line','voucher_id','Credits',
343 domain=[('type','=','cr')], context={'default_type':'cr'}, readonly=True, states={'draft':[('readonly',False)]}),
344 'line_dr_ids':fields.one2many('account.voucher.line','voucher_id','Debits',
345 domain=[('type','=','dr')], context={'default_type':'dr'}, readonly=True, states={'draft':[('readonly',False)]}),
346 'period_id': fields.many2one('account.period', 'Period', required=True, readonly=True, states={'draft':[('readonly',False)]}),
347 'narration':fields.text('Notes', readonly=True, states={'draft':[('readonly',False)]}),
348 'currency_id': fields.function(_get_journal_currency, type='many2one', relation='res.currency', string='Currency', readonly=True, required=True),
349 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}),
350 'state':fields.selection(
352 ('cancel','Cancelled'),
353 ('proforma','Pro-forma'),
355 ], 'Status', readonly=True, size=32, track_visibility='onchange',
356 help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed Voucher. \
357 \n* The \'Pro-forma\' when voucher is in Pro-forma status,voucher does not have an voucher number. \
358 \n* The \'Posted\' status is used when user create voucher,a voucher number is generated and voucher entries are created in account \
359 \n* The \'Cancelled\' status is used when user cancel voucher.'),
360 'amount': fields.float('Total', digits_compute=dp.get_precision('Account'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
361 'tax_amount':fields.float('Tax Amount', digits_compute=dp.get_precision('Account'), readonly=True),
362 'reference': fields.char('Ref #', size=64, readonly=True, states={'draft':[('readonly',False)]}, help="Transaction reference number."),
363 'number': fields.char('Number', size=32, readonly=True,),
364 'move_id':fields.many2one('account.move', 'Account Entry'),
365 'move_ids': fields.related('move_id','line_id', type='one2many', relation='account.move.line', string='Journal Items', readonly=True),
366 'partner_id':fields.many2one('res.partner', 'Partner', change_default=1, readonly=True, states={'draft':[('readonly',False)]}),
367 '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'),
368 'paid': fields.function(_check_paid, string='Paid', type='boolean', help="The Voucher has been totally paid."),
369 'pay_now':fields.selection([
370 ('pay_now','Pay Directly'),
371 ('pay_later','Pay Later or Group Funds'),
372 ],'Payment', select=True, readonly=True, states={'draft':[('readonly',False)]}),
373 'tax_id': fields.many2one('account.tax', 'Tax', readonly=True, states={'draft':[('readonly',False)]}, domain=[('price_include','=', False)], help="Only for tax excluded from price"),
374 'pre_line':fields.boolean('Previous Payments ?', required=False),
375 'date_due': fields.date('Due Date', readonly=True, select=True, states={'draft':[('readonly',False)]}),
376 'payment_option':fields.selection([
377 ('without_writeoff', 'Keep Open'),
378 ('with_writeoff', 'Reconcile Payment Balance'),
379 ], '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)"),
380 'writeoff_acc_id': fields.many2one('account.account', 'Counterpart Account', readonly=True, states={'draft': [('readonly', False)]}),
381 'comment': fields.char('Counterpart Comment', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
382 'analytic_id': fields.many2one('account.analytic.account','Write-Off Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
383 '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."),
384 'payment_rate_currency_id': fields.many2one('res.currency', 'Payment Rate Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}),
385 'payment_rate': fields.float('Exchange Rate', digits=(12,6), required=True, readonly=True, states={'draft': [('readonly', False)]},
386 help='The specific rate that will be used, in this voucher, between the selected currency (in \'Payment Rate Currency\' field) and the voucher currency.'),
387 'paid_amount_in_company_currency': fields.function(_paid_amount_in_company_currency, string='Paid Amount in Company Currency', type='float', readonly=True),
388 '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'),
389 '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"),
393 'period_id': _get_period,
394 'partner_id': _get_partner,
395 'journal_id':_get_journal,
396 'currency_id': _get_currency,
397 'reference': _get_reference,
398 'narration':_get_narration,
399 'amount': _get_amount,
402 'pay_now': 'pay_now',
404 'date': lambda *a: time.strftime('%Y-%m-%d'),
405 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.voucher',context=c),
407 'payment_option': 'without_writeoff',
408 'comment': _('Write-Off'),
410 'payment_rate_currency_id': _get_payment_rate_currency,
413 def compute_tax(self, cr, uid, ids, context=None):
414 tax_pool = self.pool.get('account.tax')
415 partner_pool = self.pool.get('res.partner')
416 position_pool = self.pool.get('account.fiscal.position')
417 voucher_line_pool = self.pool.get('account.voucher.line')
418 voucher_pool = self.pool.get('account.voucher')
419 if context is None: context = {}
421 for voucher in voucher_pool.browse(cr, uid, ids, context=context):
423 for line in voucher.line_ids:
424 voucher_amount += line.untax_amount or line.amount
425 line.amount = line.untax_amount or line.amount
426 voucher_line_pool.write(cr, uid, [line.id], {'amount':line.amount, 'untax_amount':line.untax_amount})
428 if not voucher.tax_id:
429 self.write(cr, uid, [voucher.id], {'amount':voucher_amount, 'tax_amount':0.0})
432 tax = [tax_pool.browse(cr, uid, voucher.tax_id.id, context=context)]
433 partner = partner_pool.browse(cr, uid, voucher.partner_id.id, context=context) or False
434 taxes = position_pool.map_tax(cr, uid, partner and partner.property_account_position or False, tax)
435 tax = tax_pool.browse(cr, uid, taxes, context=context)
437 total = voucher_amount
440 if not tax[0].price_include:
441 for line in voucher.line_ids:
442 for tax_line in tax_pool.compute_all(cr, uid, tax, line.amount, 1).get('taxes', []):
443 total_tax += tax_line.get('amount', 0.0)
446 for line in voucher.line_ids:
450 for tax_line in tax_pool.compute_all(cr, uid, tax, line.untax_amount or line.amount, 1).get('taxes', []):
451 line_tax += tax_line.get('amount', 0.0)
452 line_total += tax_line.get('price_unit')
453 total_tax += line_tax
454 untax_amount = line.untax_amount or line.amount
455 voucher_line_pool.write(cr, uid, [line.id], {'amount':line_total, 'untax_amount':untax_amount})
457 self.write(cr, uid, [voucher.id], {'amount':total, 'tax_amount':total_tax})
460 def onchange_price(self, cr, uid, ids, line_ids, tax_id, partner_id=False, context=None):
461 context = context or {}
462 tax_pool = self.pool.get('account.tax')
463 partner_pool = self.pool.get('res.partner')
464 position_pool = self.pool.get('account.fiscal.position')
465 line_pool = self.pool.get('account.voucher.line')
474 line_ids = resolve_o2m_operations(cr, uid, line_pool, line_ids, ["amount"], context)
477 for line in line_ids:
479 line_amount = line.get('amount',0.0)
482 tax = [tax_pool.browse(cr, uid, tax_id, context=context)]
484 partner = partner_pool.browse(cr, uid, partner_id, context=context) or False
485 taxes = position_pool.map_tax(cr, uid, partner and partner.property_account_position or False, tax)
486 tax = tax_pool.browse(cr, uid, taxes, context=context)
488 if not tax[0].price_include:
489 for tax_line in tax_pool.compute_all(cr, uid, tax, line_amount, 1).get('taxes', []):
490 total_tax += tax_line.get('amount')
492 voucher_total += line_amount
493 total = voucher_total + total_tax
496 'amount': total or voucher_total,
497 'tax_amount': total_tax
503 def onchange_term_id(self, cr, uid, ids, term_id, amount):
504 term_pool = self.pool.get('account.payment.term')
507 default = {'date_due':False}
508 if term_id and amount:
509 terms = term_pool.compute(cr, uid, term_id, amount)
511 due_date = terms[-1][0]
515 return {'value':default}
517 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):
519 Returns a dict that contains new values and context
521 @param partner_id: latest value from user input for field partner_id
522 @param args: other arguments
523 @param context: context arguments, like lang, time zone
525 @return: Returns a dict which contains new values, and context
531 if not partner_id or not journal_id:
534 partner_pool = self.pool.get('res.partner')
535 journal_pool = self.pool.get('account.journal')
537 journal = journal_pool.browse(cr, uid, journal_id, context=context)
538 partner = partner_pool.browse(cr, uid, partner_id, context=context)
541 if journal.type in ('sale','sale_refund'):
542 account_id = partner.property_account_receivable.id
544 elif journal.type in ('purchase', 'purchase_refund','expense'):
545 account_id = partner.property_account_payable.id
548 if not journal.default_credit_account_id or not journal.default_debit_account_id:
549 raise osv.except_osv(_('Error!'), _('Please define default credit/debit accounts on the journal "%s".') % (journal.name))
550 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
553 default['value']['account_id'] = account_id
554 default['value']['type'] = ttype or tr_type
556 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)
557 default['value'].update(vals.get('value'))
561 def onchange_rate(self, cr, uid, ids, rate, amount, currency_id, payment_rate_currency_id, company_id, context=None):
562 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)}}
563 if rate and amount and currency_id:
564 company_currency = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id
565 #context should contain the date, the payment currency and the payment rate specified on the voucher
566 amount_in_company_currency = self.pool.get('res.currency').compute(cr, uid, currency_id, company_currency.id, amount, context=context)
567 res['value']['paid_amount_in_company_currency'] = amount_in_company_currency
570 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):
574 ctx.update({'date': date})
575 #read the voucher rate with the right date in the context
576 currency_id = currency_id or self.pool.get('res.company').browse(cr, uid, company_id, context=ctx).currency_id.id
577 voucher_rate = self.pool.get('res.currency').read(cr, uid, currency_id, ['rate'], context=ctx)['rate']
579 'voucher_special_currency': payment_rate_currency_id,
580 'voucher_special_currency_rate': rate * voucher_rate})
581 res = self.recompute_voucher_lines(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=ctx)
582 vals = self.onchange_rate(cr, uid, ids, rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
583 for key in vals.keys():
584 res[key].update(vals[key])
587 def recompute_payment_rate(self, cr, uid, ids, vals, currency_id, date, ttype, journal_id, amount, context=None):
590 #on change of the journal, we need to set also the default value for payment_rate and payment_rate_currency_id
591 currency_obj = self.pool.get('res.currency')
592 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
593 company_id = journal.company_id.id
595 currency_id = currency_id or journal.company_id.currency_id.id
596 payment_rate_currency_id = currency_id
598 ctx.update({'date': date})
600 if ttype == 'receipt':
601 o2m_to_loop = 'line_cr_ids'
602 elif ttype == 'payment':
603 o2m_to_loop = 'line_dr_ids'
604 if o2m_to_loop and 'value' in vals and o2m_to_loop in vals['value']:
605 for voucher_line in vals['value'][o2m_to_loop]:
606 if voucher_line['currency_id'] != currency_id:
607 # we take as default value for the payment_rate_currency_id, the currency of the first invoice that
608 # is not in the voucher currency
609 payment_rate_currency_id = voucher_line['currency_id']
610 tmp = currency_obj.browse(cr, uid, payment_rate_currency_id, context=ctx).rate
611 payment_rate = tmp / currency_obj.browse(cr, uid, currency_id, context=ctx).rate
613 vals['value'].update({
614 'payment_rate': payment_rate,
615 'currency_id': currency_id,
616 'payment_rate_currency_id': payment_rate_currency_id
618 #read the voucher rate with the right date in the context
619 voucher_rate = self.pool.get('res.currency').read(cr, uid, currency_id, ['rate'], context=ctx)['rate']
621 'voucher_special_currency_rate': payment_rate * voucher_rate,
622 'voucher_special_currency': payment_rate_currency_id})
623 res = self.onchange_rate(cr, uid, ids, payment_rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
624 for key in res.keys():
625 vals[key].update(res[key])
628 def basic_onchange_partner(self, cr, uid, ids, partner_id, journal_id, ttype, context=None):
629 partner_pool = self.pool.get('res.partner')
630 journal_pool = self.pool.get('account.journal')
631 res = {'value': {'account_id': False}}
632 if not partner_id or not journal_id:
635 journal = journal_pool.browse(cr, uid, journal_id, context=context)
636 partner = partner_pool.browse(cr, uid, partner_id, context=context)
638 if journal.type in ('sale','sale_refund'):
639 account_id = partner.property_account_receivable.id
640 elif journal.type in ('purchase', 'purchase_refund','expense'):
641 account_id = partner.property_account_payable.id
643 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
645 res['value']['account_id'] = account_id
648 def onchange_partner_id(self, cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=None):
653 #TODO: comment me and use me directly in the sales/purchases views
654 res = self.basic_onchange_partner(cr, uid, ids, partner_id, journal_id, ttype, context=context)
655 if ttype in ['sale', 'purchase']:
658 # 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
659 ctx.update({'date': date})
660 vals = self.recompute_voucher_lines(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context=ctx)
661 vals2 = self.recompute_payment_rate(cr, uid, ids, vals, currency_id, date, ttype, journal_id, amount, context=context)
662 for key in vals.keys():
663 res[key].update(vals[key])
664 for key in vals2.keys():
665 res[key].update(vals2[key])
666 #TODO: can probably be removed now
667 #TODO: onchange_partner_id() should not returns [pre_line, line_dr_ids, payment_rate...] for type sale, and not
668 # [pre_line, line_cr_ids, payment_rate...] for type purchase.
669 # We should definitively split account.voucher object in two and make distinct on_change functions. In the
670 # meanwhile, bellow lines must be there because the fields aren't present in the view, what crashes if the
671 # onchange returns a value for them
673 del(res['value']['line_dr_ids'])
674 del(res['value']['pre_line'])
675 del(res['value']['payment_rate'])
676 elif ttype == 'purchase':
677 del(res['value']['line_cr_ids'])
678 del(res['value']['pre_line'])
679 del(res['value']['payment_rate'])
682 def recompute_voucher_lines(self, cr, uid, ids, partner_id, journal_id, price, currency_id, ttype, date, context=None):
684 Returns a dict that contains new values and context
686 @param partner_id: latest value from user input for field partner_id
687 @param args: other arguments
688 @param context: context arguments, like lang, time zone
690 @return: Returns a dict which contains new values, and context
692 def _remove_noise_in_o2m():
693 """if the line is partially reconciled, then we must pay attention to display it only once and
695 This function returns True if the line is considered as noise and should not be displayed
697 if line.reconcile_partial_id:
698 if currency_id == line.currency_id.id:
699 if line.amount_residual_currency <= 0:
702 if line.amount_residual <= 0:
708 context_multi_currency = context.copy()
710 currency_pool = self.pool.get('res.currency')
711 move_line_pool = self.pool.get('account.move.line')
712 partner_pool = self.pool.get('res.partner')
713 journal_pool = self.pool.get('account.journal')
714 line_pool = self.pool.get('account.voucher.line')
718 'value': {'line_dr_ids': [] ,'line_cr_ids': [] ,'pre_line': False,},
722 line_ids = ids and line_pool.search(cr, uid, [('voucher_id', '=', ids[0])]) or False
724 line_pool.unlink(cr, uid, line_ids)
726 if not partner_id or not journal_id:
729 journal = journal_pool.browse(cr, uid, journal_id, context=context)
730 partner = partner_pool.browse(cr, uid, partner_id, context=context)
731 currency_id = currency_id or journal.company_id.currency_id.id
736 if context.get('account_id'):
737 account_type = self.pool['account.account'].browse(cr, uid, context['account_id'], context=context).type
738 if ttype == 'payment':
740 account_type = 'payable'
741 total_debit = price or 0.0
743 total_credit = price or 0.0
745 account_type = 'receivable'
747 if not context.get('move_line_ids', False):
748 ids = move_line_pool.search(cr, uid, [('state','=','valid'), ('account_id.type', '=', account_type), ('reconcile_id', '=', False), ('partner_id', '=', partner_id)], context=context)
750 ids = context['move_line_ids']
751 invoice_id = context.get('invoice_id', False)
752 company_currency = journal.company_id.currency_id.id
753 move_lines_found = []
755 #order the lines by most old first
757 account_move_lines = move_line_pool.browse(cr, uid, ids, context=context)
759 #compute the total debit/credit and look for a matching open amount or invoice
760 for line in account_move_lines:
761 if _remove_noise_in_o2m():
765 if line.invoice.id == invoice_id:
766 #if the invoice linked to the voucher line is equal to the invoice_id in context
767 #then we assign the amount on that line, whatever the other voucher lines
768 move_lines_found.append(line.id)
769 elif currency_id == company_currency:
770 #otherwise treatments is the same but with other field names
771 if line.amount_residual == price:
772 #if the amount residual is equal the amount voucher, we assign it to that voucher
773 #line, whatever the other voucher lines
774 move_lines_found.append(line.id)
776 #otherwise we will split the voucher amount on each line (by most old first)
777 total_credit += line.credit or 0.0
778 total_debit += line.debit or 0.0
779 elif currency_id == line.currency_id.id:
780 if line.amount_residual_currency == price:
781 move_lines_found.append(line.id)
783 total_credit += line.credit and line.amount_currency or 0.0
784 total_debit += line.debit and line.amount_currency or 0.0
786 remaining_amount = price
787 #voucher line creation
788 for line in account_move_lines:
790 if _remove_noise_in_o2m():
793 if line.currency_id and currency_id == line.currency_id.id:
794 amount_original = abs(line.amount_currency)
795 amount_unreconciled = abs(line.amount_residual_currency)
797 #always use the amount booked in the company currency as the basis of the conversion into the voucher currency
798 amount_original = currency_pool.compute(cr, uid, company_currency, currency_id, line.credit or line.debit or 0.0, context=context_multi_currency)
799 amount_unreconciled = currency_pool.compute(cr, uid, company_currency, currency_id, abs(line.amount_residual), context=context_multi_currency)
800 line_currency_id = line.currency_id and line.currency_id.id or company_currency
802 'name':line.move_id.name,
803 'type': line.credit and 'dr' or 'cr',
804 'move_line_id':line.id,
805 'account_id':line.account_id.id,
806 'amount_original': amount_original,
807 'amount': (line.id in move_lines_found) and min(abs(remaining_amount), amount_unreconciled) or 0.0,
808 'date_original':line.date,
809 'date_due':line.date_maturity,
810 'amount_unreconciled': amount_unreconciled,
811 'currency_id': line_currency_id,
813 remaining_amount -= rs['amount']
814 #in case a corresponding move_line hasn't been found, we now try to assign the voucher amount
815 #on existing invoices: we split voucher amount by most old first, but only for lines in the same currency
816 if not move_lines_found:
817 if currency_id == line_currency_id:
819 amount = min(amount_unreconciled, abs(total_debit))
820 rs['amount'] = amount
821 total_debit -= amount
823 amount = min(amount_unreconciled, abs(total_credit))
824 rs['amount'] = amount
825 total_credit -= amount
827 if rs['amount_unreconciled'] == rs['amount']:
828 rs['reconcile'] = True
830 if rs['type'] == 'cr':
831 default['value']['line_cr_ids'].append(rs)
833 default['value']['line_dr_ids'].append(rs)
835 if len(default['value']['line_cr_ids']) > 0:
836 default['value']['pre_line'] = 1
837 elif len(default['value']['line_dr_ids']) > 0:
838 default['value']['pre_line'] = 1
839 default['value']['writeoff_amount'] = self._compute_writeoff_amount(cr, uid, default['value']['line_dr_ids'], default['value']['line_cr_ids'], price, ttype)
842 def onchange_payment_rate_currency(self, cr, uid, ids, currency_id, payment_rate, payment_rate_currency_id, date, amount, company_id, context=None):
847 #set the default payment rate of the voucher and compute the paid amount in company currency
849 ctx.update({'date': date})
850 #read the voucher rate with the right date in the context
851 voucher_rate = self.pool.get('res.currency').read(cr, uid, currency_id, ['rate'], context=ctx)['rate']
853 'voucher_special_currency_rate': payment_rate * voucher_rate,
854 'voucher_special_currency': payment_rate_currency_id})
855 vals = self.onchange_rate(cr, uid, ids, payment_rate, amount, currency_id, payment_rate_currency_id, company_id, context=ctx)
856 for key in vals.keys():
857 res[key].update(vals[key])
860 def onchange_date(self, cr, uid, ids, date, currency_id, payment_rate_currency_id, amount, company_id, context=None):
862 @param date: latest value from user input for field date
863 @param args: other arguments
864 @param context: context arguments, like lang, time zone
865 @return: Returns a dict which contains new values, and context
870 #set the period of the voucher
871 period_pool = self.pool.get('account.period')
872 currency_obj = self.pool.get('res.currency')
874 ctx.update({'company_id': company_id, 'account_period_prefer_normal': True})
875 voucher_currency_id = currency_id or self.pool.get('res.company').browse(cr, uid, company_id, context=ctx).currency_id.id
876 pids = period_pool.find(cr, uid, date, context=ctx)
878 res['value'].update({'period_id':pids[0]})
879 if payment_rate_currency_id:
880 ctx.update({'date': date})
882 if payment_rate_currency_id != currency_id:
883 tmp = currency_obj.browse(cr, uid, payment_rate_currency_id, context=ctx).rate
884 payment_rate = tmp / currency_obj.browse(cr, uid, voucher_currency_id, context=ctx).rate
885 vals = self.onchange_payment_rate_currency(cr, uid, ids, voucher_currency_id, payment_rate, payment_rate_currency_id, date, amount, company_id, context=context)
886 vals['value'].update({'payment_rate': payment_rate})
887 for key in vals.keys():
888 res[key].update(vals[key])
891 def onchange_journal(self, cr, uid, ids, journal_id, line_ids, tax_id, partner_id, date, amount, ttype, company_id, context=None):
896 journal_pool = self.pool.get('account.journal')
897 journal = journal_pool.browse(cr, uid, journal_id, context=context)
898 account_id = journal.default_credit_account_id or journal.default_debit_account_id
900 if account_id and account_id.tax_ids:
901 tax_id = account_id.tax_ids[0].id
904 if ttype in ('sale', 'purchase'):
905 vals = self.onchange_price(cr, uid, ids, line_ids, tax_id, partner_id, context)
906 vals['value'].update({'tax_id':tax_id,'amount': amount})
909 currency_id = journal.currency.id
911 currency_id = journal.company_id.currency_id.id
912 vals['value'].update({'currency_id': currency_id})
913 #in case we want to register the payment directly from an invoice, it's confusing to allow to switch the journal
914 #without seeing that the amount is expressed in the journal currency, and not in the invoice currency. So to avoid
915 #this common mistake, we simply reset the amount to 0 if the currency is not the invoice currency.
916 if context.get('payment_expected_currency') and currency_id != context.get('payment_expected_currency'):
917 vals['value']['amount'] = 0
920 res = self.onchange_partner_id(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context)
921 for key in res.keys():
922 vals[key].update(res[key])
925 def button_proforma_voucher(self, cr, uid, ids, context=None):
926 self.signal_proforma_voucher(cr, uid, ids)
927 return {'type': 'ir.actions.act_window_close'}
929 def proforma_voucher(self, cr, uid, ids, context=None):
930 self.action_move_line_create(cr, uid, ids, context=context)
933 def action_cancel_draft(self, cr, uid, ids, context=None):
934 self.create_workflow(cr, uid, ids)
935 self.write(cr, uid, ids, {'state':'draft'})
938 def cancel_voucher(self, cr, uid, ids, context=None):
939 reconcile_pool = self.pool.get('account.move.reconcile')
940 move_pool = self.pool.get('account.move')
941 move_line_pool = self.pool.get('account.move.line')
942 for voucher in self.browse(cr, uid, ids, context=context):
943 # refresh to make sure you don't unlink an already removed move
945 for line in voucher.move_ids:
946 # refresh to make sure you don't unreconcile an already unreconciled entry
948 if line.reconcile_id:
949 move_lines = [move_line.id for move_line in line.reconcile_id.line_id]
950 move_lines.remove(line.id)
951 reconcile_pool.unlink(cr, uid, [line.reconcile_id.id])
952 if len(move_lines) >= 2:
953 move_line_pool.reconcile_partial(cr, uid, move_lines, 'auto',context=context)
955 move_pool.button_cancel(cr, uid, [voucher.move_id.id])
956 move_pool.unlink(cr, uid, [voucher.move_id.id])
961 self.write(cr, uid, ids, res)
964 def unlink(self, cr, uid, ids, context=None):
965 for t in self.read(cr, uid, ids, ['state'], context=context):
966 if t['state'] not in ('draft', 'cancel'):
967 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete voucher(s) which are already opened or paid.'))
968 return super(account_voucher, self).unlink(cr, uid, ids, context=context)
970 def onchange_payment(self, cr, uid, ids, pay_now, journal_id, partner_id, ttype='sale'):
975 partner_pool = self.pool.get('res.partner')
976 journal_pool = self.pool.get('account.journal')
977 if pay_now == 'pay_later':
978 partner = partner_pool.browse(cr, uid, partner_id)
979 journal = journal_pool.browse(cr, uid, journal_id)
980 if journal.type in ('sale','sale_refund'):
981 account_id = partner.property_account_receivable.id
982 elif journal.type in ('purchase', 'purchase_refund','expense'):
983 account_id = partner.property_account_payable.id
985 account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id
987 res['account_id'] = account_id
990 def _sel_context(self, cr, uid, voucher_id, context=None):
992 Select the context to use accordingly if it needs to be multicurrency or not.
994 :param voucher_id: Id of the actual voucher
995 :return: The returned context will be the same as given in parameter if the voucher currency is the same
996 than the company currency, otherwise it's a copy of the parameter with an extra key 'date' containing
997 the date of the voucher.
1000 company_currency = self._get_company_currency(cr, uid, voucher_id, context)
1001 current_currency = self._get_current_currency(cr, uid, voucher_id, context)
1002 if current_currency <> company_currency:
1003 context_multi_currency = context.copy()
1004 voucher = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context)
1005 context_multi_currency.update({'date': voucher.date})
1006 return context_multi_currency
1009 def first_move_line_get(self, cr, uid, voucher_id, move_id, company_currency, current_currency, context=None):
1011 Return a dict to be use to create the first account move line of given voucher.
1013 :param voucher_id: Id of voucher what we are creating account_move.
1014 :param move_id: Id of account move where this line will be added.
1015 :param company_currency: id of currency of the company to which the voucher belong
1016 :param current_currency: id of currency of the voucher
1017 :return: mapping between fieldname and value of account move line to create
1020 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1021 debit = credit = 0.0
1022 # TODO: is there any other alternative then the voucher type ??
1023 # ANSWER: We can have payment and receipt "In Advance".
1024 # TODO: Make this logic available.
1025 # -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
1026 if voucher.type in ('purchase', 'payment'):
1027 credit = voucher.paid_amount_in_company_currency
1028 elif voucher.type in ('sale', 'receipt'):
1029 debit = voucher.paid_amount_in_company_currency
1030 if debit < 0: credit = -debit; debit = 0.0
1031 if credit < 0: debit = -credit; credit = 0.0
1032 sign = debit - credit < 0 and -1 or 1
1033 #set the first line of the voucher
1035 'name': voucher.name or '/',
1038 'account_id': voucher.account_id.id,
1040 'journal_id': voucher.journal_id.id,
1041 'period_id': voucher.period_id.id,
1042 'partner_id': voucher.partner_id.id,
1043 'currency_id': company_currency <> current_currency and current_currency or False,
1044 'amount_currency': (sign * abs(voucher.amount) # amount < 0 for refunds
1045 if company_currency != current_currency else 0.0),
1046 'date': voucher.date,
1047 'date_maturity': voucher.date_due
1051 def account_move_get(self, cr, uid, voucher_id, context=None):
1053 This method prepare the creation of the account move related to the given voucher.
1055 :param voucher_id: Id of voucher for which we are creating account_move.
1056 :return: mapping between fieldname and value of account move to create
1059 seq_obj = self.pool.get('ir.sequence')
1060 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1062 name = voucher.number
1063 elif voucher.journal_id.sequence_id:
1064 if not voucher.journal_id.sequence_id.active:
1065 raise osv.except_osv(_('Configuration Error !'),
1066 _('Please activate the sequence of selected journal !'))
1068 c.update({'fiscalyear_id': voucher.period_id.fiscalyear_id.id})
1069 name = seq_obj.next_by_id(cr, uid, voucher.journal_id.sequence_id.id, context=c)
1071 raise osv.except_osv(_('Error!'),
1072 _('Please define a sequence on the journal.'))
1073 if not voucher.reference:
1074 ref = name.replace('/','')
1076 ref = voucher.reference
1080 'journal_id': voucher.journal_id.id,
1081 'narration': voucher.narration,
1082 'date': voucher.date,
1084 'period_id': voucher.period_id.id,
1088 def _get_exchange_lines(self, cr, uid, line, move_id, amount_residual, company_currency, current_currency, context=None):
1090 Prepare the two lines in company currency due to currency rate difference.
1092 :param line: browse record of the voucher.line for which we want to create currency rate difference accounting
1094 :param move_id: Account move wher the move lines will be.
1095 :param amount_residual: Amount to be posted.
1096 :param company_currency: id of currency of the company to which the voucher belong
1097 :param current_currency: id of currency of the voucher
1098 :return: the account move line and its counterpart to create, depicted as mapping between fieldname and value
1099 :rtype: tuple of dict
1101 if amount_residual > 0:
1102 account_id = line.voucher_id.company_id.expense_currency_exchange_account_id
1104 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."))
1106 account_id = line.voucher_id.company_id.income_currency_exchange_account_id
1108 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."))
1109 # Even if the amount_currency is never filled, we need to pass the foreign currency because otherwise
1110 # the receivable/payable account may have a secondary currency, which render this field mandatory
1111 if line.account_id.currency_id:
1112 account_currency_id = line.account_id.currency_id.id
1114 account_currency_id = company_currency <> current_currency and current_currency or False
1116 'journal_id': line.voucher_id.journal_id.id,
1117 'period_id': line.voucher_id.period_id.id,
1118 'name': _('change')+': '+(line.name or '/'),
1119 'account_id': line.account_id.id,
1121 'partner_id': line.voucher_id.partner_id.id,
1122 'currency_id': account_currency_id,
1123 'amount_currency': 0.0,
1125 'credit': amount_residual > 0 and amount_residual or 0.0,
1126 'debit': amount_residual < 0 and -amount_residual or 0.0,
1127 'date': line.voucher_id.date,
1129 move_line_counterpart = {
1130 'journal_id': line.voucher_id.journal_id.id,
1131 'period_id': line.voucher_id.period_id.id,
1132 'name': _('change')+': '+(line.name or '/'),
1133 'account_id': account_id.id,
1135 'amount_currency': 0.0,
1136 'partner_id': line.voucher_id.partner_id.id,
1137 'currency_id': account_currency_id,
1139 'debit': amount_residual > 0 and amount_residual or 0.0,
1140 'credit': amount_residual < 0 and -amount_residual or 0.0,
1141 'date': line.voucher_id.date,
1143 return (move_line, move_line_counterpart)
1145 def _convert_amount(self, cr, uid, amount, voucher_id, context=None):
1147 This function convert the amount given in company currency. It takes either the rate in the voucher (if the
1148 payment_rate_currency_id is relevant) either the rate encoded in the system.
1150 :param amount: float. The amount to convert
1151 :param voucher: id of the voucher on which we want the conversion
1152 :param context: to context to use for the conversion. It may contain the key 'date' set to the voucher date
1153 field in order to select the good rate to use.
1154 :return: the amount in the currency of the voucher's company
1159 currency_obj = self.pool.get('res.currency')
1160 voucher = self.browse(cr, uid, voucher_id, context=context)
1161 return currency_obj.compute(cr, uid, voucher.currency_id.id, voucher.company_id.currency_id.id, amount, context=context)
1163 def voucher_move_line_create(self, cr, uid, voucher_id, line_total, move_id, company_currency, current_currency, context=None):
1165 Create one account move line, on the given account move, per voucher line where amount is not 0.0.
1166 It returns Tuple with tot_line what is total of difference between debit and credit and
1167 a list of lists with ids to be reconciled with this format (total_deb_cred,list_of_lists).
1169 :param voucher_id: Voucher id what we are working with
1170 :param line_total: Amount of the first line, which correspond to the amount we should totally split among all voucher lines.
1171 :param move_id: Account move wher those lines will be joined.
1172 :param company_currency: id of currency of the company to which the voucher belong
1173 :param current_currency: id of currency of the voucher
1174 :return: Tuple build as (remaining amount not allocated on voucher lines, list of account_move_line created in this method)
1175 :rtype: tuple(float, list of int)
1179 move_line_obj = self.pool.get('account.move.line')
1180 currency_obj = self.pool.get('res.currency')
1181 tax_obj = self.pool.get('account.tax')
1182 tot_line = line_total
1185 date = self.read(cr, uid, voucher_id, ['date'], context=context)['date']
1186 ctx = context.copy()
1187 ctx.update({'date': date})
1188 voucher = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context=ctx)
1189 voucher_currency = voucher.journal_id.currency or voucher.company_id.currency_id
1191 'voucher_special_currency_rate': voucher_currency.rate * voucher.payment_rate ,
1192 'voucher_special_currency': voucher.payment_rate_currency_id and voucher.payment_rate_currency_id.id or False,})
1193 prec = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')
1194 for line in voucher.line_ids:
1195 #create one move line per voucher line where amount is not 0.0
1196 # AND (second part of the clause) only if the original move line was not having debit = credit = 0 (which is a legal value)
1197 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)):
1199 # convert the amount set on the voucher line into the currency of the voucher's company
1200 # 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
1201 amount = self._convert_amount(cr, uid, line.untax_amount or line.amount, voucher.id, context=ctx)
1202 # if the amount encoded in voucher is equal to the amount unreconciled, we need to compute the
1203 # currency rate difference
1204 if line.amount == line.amount_unreconciled:
1205 if not line.move_line_id:
1206 raise osv.except_osv(_('Wrong voucher line'),_("The invoice you are willing to pay is not valid anymore."))
1207 sign = line.type =='dr' and -1 or 1
1208 currency_rate_difference = sign * (line.move_line_id.amount_residual - amount)
1210 currency_rate_difference = 0.0
1212 'journal_id': voucher.journal_id.id,
1213 'period_id': voucher.period_id.id,
1214 'name': line.name or '/',
1215 'account_id': line.account_id.id,
1217 'partner_id': voucher.partner_id.id,
1218 '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,
1219 'analytic_account_id': line.account_analytic_id and line.account_analytic_id.id or False,
1223 'date': voucher.date
1227 if line.type == 'dr':
1232 if (line.type=='dr'):
1234 move_line['debit'] = amount
1237 move_line['credit'] = amount
1239 if voucher.tax_id and voucher.type in ('sale', 'purchase'):
1241 'account_tax_id': voucher.tax_id.id,
1244 if move_line.get('account_tax_id', False):
1245 tax_data = tax_obj.browse(cr, uid, [move_line['account_tax_id']], context=context)[0]
1246 if not (tax_data.base_code_id and tax_data.tax_code_id):
1247 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))
1249 # compute the amount in foreign currency
1250 foreign_currency_diff = 0.0
1251 amount_currency = False
1252 if line.move_line_id:
1253 # We want to set it on the account move line as soon as the original line had a foreign currency
1254 if line.move_line_id.currency_id and line.move_line_id.currency_id.id != company_currency:
1255 # we compute the amount in that foreign currency.
1256 if line.move_line_id.currency_id.id == current_currency:
1257 # if the voucher and the voucher line share the same currency, there is no computation to do
1258 sign = (move_line['debit'] - move_line['credit']) < 0 and -1 or 1
1259 amount_currency = sign * (line.amount)
1261 # if the rate is specified on the voucher, it will be used thanks to the special keys in the context
1262 # otherwise we use the rates of the system
1263 amount_currency = currency_obj.compute(cr, uid, company_currency, line.move_line_id.currency_id.id, move_line['debit']-move_line['credit'], context=ctx)
1264 if line.amount == line.amount_unreconciled:
1265 foreign_currency_diff = line.move_line_id.amount_residual_currency - abs(amount_currency)
1267 move_line['amount_currency'] = amount_currency
1268 voucher_line = move_line_obj.create(cr, uid, move_line)
1269 rec_ids = [voucher_line, line.move_line_id.id]
1271 if not currency_obj.is_zero(cr, uid, voucher.company_id.currency_id, currency_rate_difference):
1272 # Change difference entry in company currency
1273 exch_lines = self._get_exchange_lines(cr, uid, line, move_id, currency_rate_difference, company_currency, current_currency, context=context)
1274 new_id = move_line_obj.create(cr, uid, exch_lines[0],context)
1275 move_line_obj.create(cr, uid, exch_lines[1], context)
1276 rec_ids.append(new_id)
1278 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):
1279 # Change difference entry in voucher currency
1280 move_line_foreign_currency = {
1281 'journal_id': line.voucher_id.journal_id.id,
1282 'period_id': line.voucher_id.period_id.id,
1283 'name': _('change')+': '+(line.name or '/'),
1284 'account_id': line.account_id.id,
1286 'partner_id': line.voucher_id.partner_id.id,
1287 'currency_id': line.move_line_id.currency_id.id,
1288 'amount_currency': -1 * foreign_currency_diff,
1292 'date': line.voucher_id.date,
1294 new_id = move_line_obj.create(cr, uid, move_line_foreign_currency, context=context)
1295 rec_ids.append(new_id)
1296 if line.move_line_id.id:
1297 rec_lst_ids.append(rec_ids)
1298 return (tot_line, rec_lst_ids)
1300 def writeoff_move_line_get(self, cr, uid, voucher_id, line_total, move_id, name, company_currency, current_currency, context=None):
1302 Set a dict to be use to create the writeoff move line.
1304 :param voucher_id: Id of voucher what we are creating account_move.
1305 :param line_total: Amount remaining to be allocated on lines.
1306 :param move_id: Id of account move where this line will be added.
1307 :param name: Description of account move line.
1308 :param company_currency: id of currency of the company to which the voucher belong
1309 :param current_currency: id of currency of the voucher
1310 :return: mapping between fieldname and value of account move line to create
1313 currency_obj = self.pool.get('res.currency')
1316 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1317 current_currency_obj = voucher.currency_id or voucher.journal_id.company_id.currency_id
1319 if not currency_obj.is_zero(cr, uid, current_currency_obj, line_total):
1323 if voucher.payment_option == 'with_writeoff':
1324 account_id = voucher.writeoff_acc_id.id
1325 write_off_name = voucher.comment
1326 elif voucher.partner_id:
1327 if voucher.type in ('sale', 'receipt'):
1328 account_id = voucher.partner_id.property_account_receivable.id
1330 account_id = voucher.partner_id.property_account_payable.id
1332 # fallback on account of voucher
1333 account_id = voucher.account_id.id
1334 sign = voucher.type == 'payment' and -1 or 1
1336 'name': write_off_name or name,
1337 'account_id': account_id,
1339 'partner_id': voucher.partner_id.id,
1340 'date': voucher.date,
1341 'credit': diff > 0 and diff or 0.0,
1342 'debit': diff < 0 and -diff or 0.0,
1343 'amount_currency': company_currency <> current_currency and (sign * -1 * voucher.writeoff_amount) or 0.0,
1344 'currency_id': company_currency <> current_currency and current_currency or False,
1345 'analytic_account_id': voucher.analytic_id and voucher.analytic_id.id or False,
1350 def _get_company_currency(self, cr, uid, voucher_id, context=None):
1352 Get the currency of the actual company.
1354 :param voucher_id: Id of the voucher what i want to obtain company currency.
1355 :return: currency id of the company of the voucher
1358 return self.pool.get('account.voucher').browse(cr,uid,voucher_id,context).journal_id.company_id.currency_id.id
1360 def _get_current_currency(self, cr, uid, voucher_id, context=None):
1362 Get the currency of the voucher.
1364 :param voucher_id: Id of the voucher what i want to obtain current currency.
1365 :return: currency id of the voucher
1368 voucher = self.pool.get('account.voucher').browse(cr,uid,voucher_id,context)
1369 return voucher.currency_id.id or self._get_company_currency(cr,uid,voucher.id,context)
1371 def action_move_line_create(self, cr, uid, ids, context=None):
1373 Confirm the vouchers given in ids and create the journal entries for each of them
1377 move_pool = self.pool.get('account.move')
1378 move_line_pool = self.pool.get('account.move.line')
1379 for voucher in self.browse(cr, uid, ids, context=context):
1380 local_context = dict(context, force_company=voucher.journal_id.company_id.id)
1383 company_currency = self._get_company_currency(cr, uid, voucher.id, context)
1384 current_currency = self._get_current_currency(cr, uid, voucher.id, context)
1385 # we select the context to use accordingly if it's a multicurrency case or not
1386 context = self._sel_context(cr, uid, voucher.id, context)
1387 # But for the operations made by _convert_amount, we always need to give the date in the context
1388 ctx = context.copy()
1389 ctx.update({'date': voucher.date})
1390 # Create the account move record.
1391 move_id = move_pool.create(cr, uid, self.account_move_get(cr, uid, voucher.id, context=context), context=context)
1392 # Get the name of the account_move just created
1393 name = move_pool.browse(cr, uid, move_id, context=context).name
1394 # Create the first line of the voucher
1395 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)
1396 move_line_brw = move_line_pool.browse(cr, uid, move_line_id, context=context)
1397 line_total = move_line_brw.debit - move_line_brw.credit
1399 if voucher.type == 'sale':
1400 line_total = line_total - self._convert_amount(cr, uid, voucher.tax_amount, voucher.id, context=ctx)
1401 elif voucher.type == 'purchase':
1402 line_total = line_total + self._convert_amount(cr, uid, voucher.tax_amount, voucher.id, context=ctx)
1403 # Create one move line per voucher line where amount is not 0.0
1404 line_total, rec_list_ids = self.voucher_move_line_create(cr, uid, voucher.id, line_total, move_id, company_currency, current_currency, context)
1406 # Create the writeoff line if needed
1407 ml_writeoff = self.writeoff_move_line_get(cr, uid, voucher.id, line_total, move_id, name, company_currency, current_currency, local_context)
1409 move_line_pool.create(cr, uid, ml_writeoff, local_context)
1410 # We post the voucher.
1411 self.write(cr, uid, [voucher.id], {
1416 if voucher.journal_id.entry_posted:
1417 move_pool.post(cr, uid, [move_id], context={})
1418 # We automatically reconcile the account move lines.
1420 for rec_ids in rec_list_ids:
1421 if len(rec_ids) >= 2:
1422 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)
1425 def copy(self, cr, uid, id, default=None, context=None):
1432 'line_cr_ids': False,
1433 'line_dr_ids': False,
1436 if 'date' not in default:
1437 default['date'] = time.strftime('%Y-%m-%d')
1438 return super(account_voucher, self).copy(cr, uid, id, default, context)
1441 class account_voucher_line(osv.osv):
1442 _name = 'account.voucher.line'
1443 _description = 'Voucher Lines'
1444 _order = "move_line_id"
1446 # If the payment is in the same currency than the invoice, we keep the same amount
1447 # Otherwise, we compute from invoice currency to payment currency
1448 def _compute_balance(self, cr, uid, ids, name, args, context=None):
1449 currency_pool = self.pool.get('res.currency')
1451 for line in self.browse(cr, uid, ids, context=context):
1452 ctx = context.copy()
1453 ctx.update({'date': line.voucher_id.date})
1454 voucher_rate = self.pool.get('res.currency').read(cr, uid, line.voucher_id.currency_id.id, ['rate'], context=ctx)['rate']
1456 'voucher_special_currency': line.voucher_id.payment_rate_currency_id and line.voucher_id.payment_rate_currency_id.id or False,
1457 'voucher_special_currency_rate': line.voucher_id.payment_rate * voucher_rate})
1459 company_currency = line.voucher_id.journal_id.company_id.currency_id.id
1460 voucher_currency = line.voucher_id.currency_id and line.voucher_id.currency_id.id or company_currency
1461 move_line = line.move_line_id or False
1464 res['amount_original'] = 0.0
1465 res['amount_unreconciled'] = 0.0
1466 elif move_line.currency_id and voucher_currency==move_line.currency_id.id:
1467 res['amount_original'] = abs(move_line.amount_currency)
1468 res['amount_unreconciled'] = abs(move_line.amount_residual_currency)
1470 #always use the amount booked in the company currency as the basis of the conversion into the voucher currency
1471 res['amount_original'] = currency_pool.compute(cr, uid, company_currency, voucher_currency, move_line.credit or move_line.debit or 0.0, context=ctx)
1472 res['amount_unreconciled'] = currency_pool.compute(cr, uid, company_currency, voucher_currency, abs(move_line.amount_residual), context=ctx)
1474 rs_data[line.id] = res
1477 def _currency_id(self, cr, uid, ids, name, args, context=None):
1479 This function returns the currency id of a voucher line. It's either the currency of the
1480 associated move line (if any) or the currency of the voucher or the company currency.
1483 for line in self.browse(cr, uid, ids, context=context):
1484 move_line = line.move_line_id
1486 res[line.id] = move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id
1488 res[line.id] = line.voucher_id.currency_id and line.voucher_id.currency_id.id or line.voucher_id.company_id.currency_id.id
1492 'voucher_id':fields.many2one('account.voucher', 'Voucher', required=1, ondelete='cascade'),
1493 'name':fields.char('Description', size=256),
1494 'account_id':fields.many2one('account.account','Account', required=True),
1495 'partner_id':fields.related('voucher_id', 'partner_id', type='many2one', relation='res.partner', string='Partner'),
1496 'untax_amount':fields.float('Untax Amount'),
1497 'amount':fields.float('Amount', digits_compute=dp.get_precision('Account')),
1498 'reconcile': fields.boolean('Full Reconcile'),
1499 'type':fields.selection([('dr','Debit'),('cr','Credit')], 'Dr/Cr'),
1500 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'),
1501 'move_line_id': fields.many2one('account.move.line', 'Journal Item'),
1502 'date_original': fields.related('move_line_id','date', type='date', relation='account.move.line', string='Date', readonly=1),
1503 'date_due': fields.related('move_line_id','date_maturity', type='date', relation='account.move.line', string='Due Date', readonly=1),
1504 'amount_original': fields.function(_compute_balance, multi='dc', type='float', string='Original Amount', store=True, digits_compute=dp.get_precision('Account')),
1505 'amount_unreconciled': fields.function(_compute_balance, multi='dc', type='float', string='Open Balance', store=True, digits_compute=dp.get_precision('Account')),
1506 'company_id': fields.related('voucher_id','company_id', relation='res.company', type='many2one', string='Company', store=True, readonly=True),
1507 'currency_id': fields.function(_currency_id, string='Currency', type='many2one', relation='res.currency', readonly=True),
1513 def onchange_reconcile(self, cr, uid, ids, reconcile, amount, amount_unreconciled, context=None):
1514 vals = {'amount': 0.0}
1516 vals = { 'amount': amount_unreconciled}
1517 return {'value': vals}
1519 def onchange_amount(self, cr, uid, ids, amount, amount_unreconciled, context=None):
1522 vals['reconcile'] = (amount == amount_unreconciled)
1523 return {'value': vals}
1525 def onchange_move_line_id(self, cr, user, ids, move_line_id, context=None):
1527 Returns a dict that contains new values and context
1529 @param move_line_id: latest value from user input for field move_line_id
1530 @param args: other arguments
1531 @param context: context arguments, like lang, time zone
1533 @return: Returns a dict which contains new values, and context
1536 move_line_pool = self.pool.get('account.move.line')
1538 move_line = move_line_pool.browse(cr, user, move_line_id, context=context)
1539 if move_line.credit:
1544 'account_id': move_line.account_id.id,
1546 'currency_id': move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id,
1552 def default_get(self, cr, user, fields_list, context=None):
1554 Returns default values for fields
1555 @param fields_list: list of fields, for which default values are required to be read
1556 @param context: context arguments, like lang, time zone
1558 @return: Returns a dict that contains default values for fields
1562 journal_id = context.get('journal_id', False)
1563 partner_id = context.get('partner_id', False)
1564 journal_pool = self.pool.get('account.journal')
1565 partner_pool = self.pool.get('res.partner')
1566 values = super(account_voucher_line, self).default_get(cr, user, fields_list, context=context)
1567 if (not journal_id) or ('account_id' not in fields_list):
1569 journal = journal_pool.browse(cr, user, journal_id, context=context)
1572 if journal.type in ('sale', 'sale_refund'):
1573 account_id = journal.default_credit_account_id and journal.default_credit_account_id.id or False
1575 elif journal.type in ('purchase', 'expense', 'purchase_refund'):
1576 account_id = journal.default_debit_account_id and journal.default_debit_account_id.id or False
1579 partner = partner_pool.browse(cr, user, partner_id, context=context)
1580 if context.get('type') == 'payment':
1582 account_id = partner.property_account_payable.id
1583 elif context.get('type') == 'receipt':
1584 account_id = partner.property_account_receivable.id
1587 'account_id':account_id,
1592 class account_bank_statement(osv.osv):
1593 _inherit = 'account.bank.statement'
1595 def button_confirm_bank(self, cr, uid, ids, context=None):
1596 voucher_obj = self.pool.get('account.voucher')
1598 for statement in self.browse(cr, uid, ids, context=context):
1599 voucher_ids += [line.voucher_id.id for line in statement.line_ids if line.voucher_id]
1601 voucher_obj.write(cr, uid, voucher_ids, {'active': True}, context=context)
1602 return super(account_bank_statement, self).button_confirm_bank(cr, uid, ids, context=context)
1604 def button_cancel(self, cr, uid, ids, context=None):
1605 voucher_obj = self.pool.get('account.voucher')
1606 for st in self.browse(cr, uid, ids, context=context):
1608 for line in st.line_ids:
1610 voucher_ids.append(line.voucher_id.id)
1611 voucher_obj.cancel_voucher(cr, uid, voucher_ids, context)
1612 return super(account_bank_statement, self).button_cancel(cr, uid, ids, context=context)
1614 def create_move_from_st_line(self, cr, uid, st_line_id, company_currency_id, next_number, context=None):
1615 voucher_obj = self.pool.get('account.voucher')
1616 move_line_obj = self.pool.get('account.move.line')
1617 bank_st_line_obj = self.pool.get('account.bank.statement.line')
1618 st_line = bank_st_line_obj.browse(cr, uid, st_line_id, context=context)
1619 if st_line.voucher_id:
1620 voucher_obj.write(cr, uid, [st_line.voucher_id.id],
1621 {'number': next_number,
1622 'date': st_line.date,
1623 'period_id': st_line.statement_id.period_id.id},
1625 if st_line.voucher_id.state == 'cancel':
1626 voucher_obj.action_cancel_draft(cr, uid, [st_line.voucher_id.id], context=context)
1627 voucher_obj.signal_proforma_voucher(cr, uid, [st_line.voucher_id.id])
1629 v = voucher_obj.browse(cr, uid, st_line.voucher_id.id, context=context)
1630 bank_st_line_obj.write(cr, uid, [st_line_id], {
1631 'move_ids': [(4, v.move_id.id, False)]
1634 return move_line_obj.write(cr, uid, [x.id for x in v.move_ids], {'statement_id': st_line.statement_id.id}, context=context)
1635 return super(account_bank_statement, self).create_move_from_st_line(cr, uid, st_line.id, company_currency_id, next_number, context=context)
1637 def write(self, cr, uid, ids, vals, context=None):
1638 # Restrict to modify the journal if we already have some voucher of reconciliation created/generated.
1639 # Because the voucher keeps in memory the journal it was created with.
1640 for bk_st in self.browse(cr, uid, ids, context=context):
1641 if vals.get('journal_id') and bk_st.line_ids:
1642 if any([x.voucher_id and True or False for x in bk_st.line_ids]):
1643 raise osv.except_osv(_('Unable to Change Journal!'), _('You can not change the journal as you already reconciled some statement lines!'))
1644 return super(account_bank_statement, self).write(cr, uid, ids, vals, context=context)
1647 class account_bank_statement_line(osv.osv):
1648 _inherit = 'account.bank.statement.line'
1650 def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
1651 res = super(account_bank_statement_line, self).onchange_partner_id(cr, uid, ids, partner_id, context=context)
1652 if 'value' not in res:
1654 res['value'].update({'voucher_id' : False})
1657 def onchange_amount(self, cr, uid, ids, amount, context=None):
1658 return {'value' : {'voucher_id' : False}}
1660 def _amount_reconciled(self, cursor, user, ids, name, args, context=None):
1664 for line in self.browse(cursor, user, ids, context=context):
1666 res[line.id] = line.voucher_id.amount#
1671 def _check_amount(self, cr, uid, ids, context=None):
1672 for obj in self.browse(cr, uid, ids, context=context):
1674 diff = abs(obj.amount) - abs(obj.voucher_id.amount)
1675 if not self.pool.get('res.currency').is_zero(cr, uid, obj.statement_id.currency, diff):
1680 (_check_amount, 'The amount of the voucher must be the same amount as the one on the statement line.', ['amount']),
1684 'amount_reconciled': fields.function(_amount_reconciled,
1685 string='Amount reconciled', type='float'),
1686 'voucher_id': fields.many2one('account.voucher', 'Reconciliation'),
1689 def unlink(self, cr, uid, ids, context=None):
1690 voucher_obj = self.pool.get('account.voucher')
1691 statement_line = self.browse(cr, uid, ids, context=context)
1693 for st_line in statement_line:
1694 if st_line.voucher_id:
1695 unlink_ids.append(st_line.voucher_id.id)
1696 voucher_obj.unlink(cr, uid, unlink_ids, context=context)
1697 return super(account_bank_statement_line, self).unlink(cr, uid, ids, context=context)
1700 def resolve_o2m_operations(cr, uid, target_osv, operations, fields, context):
1702 for operation in operations:
1704 if not isinstance(operation, (list, tuple)):
1705 result = target_osv.read(cr, uid, operation, fields, context=context)
1706 elif operation[0] == 0:
1707 # may be necessary to check if all the fields are here and get the default values?
1708 result = operation[2]
1709 elif operation[0] == 1:
1710 result = target_osv.read(cr, uid, operation[1], fields, context=context)
1711 if not result: result = {}
1712 result.update(operation[2])
1713 elif operation[0] == 4:
1714 result = target_osv.read(cr, uid, operation[1], fields, context=context)
1716 results.append(result)
1720 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: