[IMP] base, res.currency: amount_to_text() based on num2words library
Old implementation of tools.amount_to_text() is now deprecated. The new one support more languages Was task 27215. Was PR #16531
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import math
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.tools import amount_to_text_en, float_round
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
@@ -29,7 +26,7 @@ class AccountRegisterPayments(models.TransientModel):
|
||||
def _onchange_amount(self):
|
||||
if hasattr(super(AccountRegisterPayments, self), '_onchange_amount'):
|
||||
super(AccountRegisterPayments, self)._onchange_amount()
|
||||
self.check_amount_in_words = self.env['account.payment']._get_check_amount_in_words(self.amount)
|
||||
self.check_amount_in_words = self.currency_id.amount_to_text(self.amount)
|
||||
|
||||
def _prepare_payment_vals(self, invoices):
|
||||
res = super(AccountRegisterPayments, self)._prepare_payment_vals(invoices)
|
||||
@@ -50,15 +47,6 @@ class AccountPayment(models.Model):
|
||||
help="The selected journal is configured to print check numbers. If your pre-printed check paper already has numbers "
|
||||
"or if the current numbering is wrong, you can change it in the journal configuration page.")
|
||||
|
||||
def _get_check_amount_in_words(self, amount):
|
||||
# TODO: merge, refactor and complete the amount_to_text and amount_to_text_en classes
|
||||
check_amount_in_words = amount_to_text_en.amount_to_text(math.floor(amount), lang='en', currency='')
|
||||
check_amount_in_words = check_amount_in_words.replace(' and Zero Cent', '') # Ugh
|
||||
decimals = amount % 1
|
||||
if decimals >= 10**-2:
|
||||
check_amount_in_words += _(' and %s/100') % str(int(round(float_round(decimals*100, precision_rounding=1))))
|
||||
return check_amount_in_words
|
||||
|
||||
@api.onchange('journal_id')
|
||||
def _onchange_journal_id(self):
|
||||
if hasattr(super(AccountPayment, self), '_onchange_journal_id'):
|
||||
@@ -69,7 +57,7 @@ class AccountPayment(models.Model):
|
||||
@api.onchange('amount')
|
||||
def _onchange_amount(self):
|
||||
res = super(AccountPayment, self)._onchange_amount()
|
||||
self.check_amount_in_words = self._get_check_amount_in_words(self.amount)
|
||||
self.check_amount_in_words = self.currency_id.amount_to_text(self.amount)
|
||||
return res
|
||||
|
||||
def _check_communication(self, payment_method_id, communication):
|
||||
|
||||
@@ -27,7 +27,7 @@ class HrExpenseRegisterPaymentWizard(models.TransientModel):
|
||||
def _onchange_amount(self):
|
||||
if hasattr(super(HrExpenseRegisterPaymentWizard, self), '_onchange_amount'):
|
||||
super(HrExpenseRegisterPaymentWizard, self)._onchange_amount()
|
||||
self.check_amount_in_words = self.env['account.payment']._get_check_amount_in_words(self.amount)
|
||||
self.check_amount_in_words = self.currency_id.amount_to_text(self.amount)
|
||||
|
||||
def _get_payment_vals(self):
|
||||
res = super(HrExpenseRegisterPaymentWizard, self)._get_payment_vals()
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from odoo.tools import amount_to_text_en
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
@@ -23,9 +22,6 @@ class payroll_advice_report(models.AbstractModel):
|
||||
res['to_name'] = to_date.strftime('%d') + '-' + to_date.strftime('%B') + '-' + to_date.strftime('%Y')
|
||||
return res
|
||||
|
||||
def convert(self, amount, cur):
|
||||
return amount_to_text_en.amount_to_text(amount, 'en', cur)
|
||||
|
||||
def get_bysal_total(self):
|
||||
return self.total_bysal
|
||||
|
||||
@@ -55,7 +51,6 @@ class payroll_advice_report(models.AbstractModel):
|
||||
'docs': advice,
|
||||
'time': time,
|
||||
'get_month': self.get_month,
|
||||
'convert': self.convert,
|
||||
'get_detail': self.get_detail,
|
||||
'get_bysal_total': self.get_bysal_total,
|
||||
}
|
||||
|
||||
@@ -2,10 +2,17 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
|
||||
try:
|
||||
from num2words import num2words
|
||||
except ImportError:
|
||||
logging.getLogger(__name__).warning("The num2words python library is not installed, l10n_mx_edi features won't be fully available.")
|
||||
num2words = None
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
|
||||
CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?')
|
||||
@@ -28,6 +35,8 @@ class Currency(models.Model):
|
||||
position = fields.Selection([('after', 'After Amount'), ('before', 'Before Amount')], default='after',
|
||||
string='Symbol Position', help="Determines where the currency symbol should be placed after or before the amount.")
|
||||
date = fields.Date(compute='_compute_date')
|
||||
currency_unit_label = fields.Char(string="Currency Unit", help="Currency Unit Name")
|
||||
currency_subunit_label = fields.Char(string="Currency Subunit", help="Currency Subunit Name")
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_name', 'unique (name)', 'The currency code must be unique!'),
|
||||
@@ -79,6 +88,34 @@ class Currency(models.Model):
|
||||
def name_get(self):
|
||||
return [(currency.id, tools.ustr(currency.name)) for currency in self]
|
||||
|
||||
@api.multi
|
||||
def amount_to_text(self, amount):
|
||||
self.ensure_one()
|
||||
def _num2words(number, lang):
|
||||
try:
|
||||
return num2words(number, lang=lang).title()
|
||||
except NotImplementedError:
|
||||
return num2words(number, lang='en').title()
|
||||
|
||||
if num2words is None:
|
||||
logging.getLogger(__name__).warning("The library 'num2words' is missing, cannot render textual amounts.")
|
||||
return ""
|
||||
|
||||
fractional_value, integer_value = math.modf(amount)
|
||||
fractional_amount = round(abs(fractional_value), self.decimal_places) * (math.pow(10, self.decimal_places))
|
||||
lang_code = self.env.context.get('lang') or self.env.user.lang
|
||||
lang = self.env['res.lang'].search([('code', '=', lang_code)])
|
||||
amount_words = tools.ustr('{amt_value} {amt_word}').format(
|
||||
amt_value=_num2words(int(integer_value), lang=lang.iso_code),
|
||||
amt_word=self.currency_unit_label,
|
||||
)
|
||||
if not self.is_zero(fractional_value):
|
||||
amount_words += ' ' + _('and') + tools.ustr(' {amt_value} {amt_word}').format(
|
||||
amt_value=_num2words(int(fractional_amount), lang=lang.iso_code),
|
||||
amt_word=self.currency_subunit_label,
|
||||
)
|
||||
return amount_words
|
||||
|
||||
@api.multi
|
||||
def round(self, amount):
|
||||
"""Return ``amount`` rounded according to ``self``'s rounding rules.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -763,12 +763,6 @@
|
||||
<field name="rate">264.784483</field>
|
||||
</record>
|
||||
|
||||
<record forcecreate="0" id="rateVUB" model="res.currency.rate">
|
||||
<field name="currency_id" ref="VUB" />
|
||||
<field name="name">2010-01-01</field>
|
||||
<field name="rate">5.3821</field>
|
||||
</record>
|
||||
|
||||
<record forcecreate="0" id="rateEUR" model="res.currency.rate">
|
||||
<field name="currency_id" ref="EUR" />
|
||||
<field name="name">2010-01-01</field>
|
||||
|
||||
@@ -140,9 +140,15 @@
|
||||
options='{"terminology": "active"}'/>
|
||||
</button>
|
||||
</div>
|
||||
<group col="4">
|
||||
<field name="name"/>
|
||||
<field name="rate"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="rate"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="currency_unit_label"/>
|
||||
<field name="currency_subunit_label"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group groups="base.group_no_one">
|
||||
|
||||
@@ -11,8 +11,6 @@ from .convert import *
|
||||
from .translate import *
|
||||
from .graph import graph
|
||||
from .image import *
|
||||
from .amount_to_text import *
|
||||
from .amount_to_text_en import *
|
||||
from .yaml_import import *
|
||||
from .sql import *
|
||||
from .float_utils import *
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# French
|
||||
#-------------------------------------------------------------
|
||||
|
||||
|
||||
from __future__ import print_function
|
||||
to_19_fr = ( u'zéro', 'un', 'deux', 'trois', 'quatre', 'cinq', 'six',
|
||||
'sept', 'huit', 'neuf', 'dix', 'onze', 'douze', 'treize',
|
||||
'quatorze', 'quinze', 'seize', 'dix-sept', 'dix-huit', 'dix-neuf' )
|
||||
tens_fr = ( 'vingt', 'trente', 'quarante', 'Cinquante', 'Soixante', 'Soixante-dix', 'Quatre-vingts', 'Quatre-vingt Dix')
|
||||
denom_fr = ( '',
|
||||
'Mille', 'Millions', 'Milliards', 'Billions', 'Quadrillions',
|
||||
'Quintillion', 'Sextillion', 'Septillion', 'Octillion', 'Nonillion',
|
||||
'Décillion', 'Undecillion', 'Duodecillion', 'Tredecillion', 'Quattuordecillion',
|
||||
'Sexdecillion', 'Septendecillion', 'Octodecillion', 'Icosillion', 'Vigintillion' )
|
||||
|
||||
def _convert_nn_fr(val):
|
||||
""" convert a value < 100 to French
|
||||
"""
|
||||
if val < 20:
|
||||
return to_19_fr[val]
|
||||
for (dcap, dval) in ((k, 20 + (10 * v)) for (v, k) in enumerate(tens_fr)):
|
||||
base = 10
|
||||
if dval in [60, 80]:
|
||||
base = 20
|
||||
if dval + base > val:
|
||||
if val % base:
|
||||
bond = (val % base in [1, 11] and dval != 80) and '-et-' or '-'
|
||||
return dcap + bond + to_19_fr[val % base]
|
||||
return dcap
|
||||
|
||||
def _convert_nnn_fr(val):
|
||||
""" convert a value < 1000 to french
|
||||
|
||||
special cased because it is the level that kicks
|
||||
off the < 100 special case. The rest are more general. This also allows you to
|
||||
get strings in the form of 'forty-five hundred' if called directly.
|
||||
"""
|
||||
word = ''
|
||||
(mod, rem) = (val % 100, val // 100)
|
||||
if rem > 0:
|
||||
word = (rem > 1 and to_19_fr[rem] + ' cents') or 'Cent'
|
||||
if mod > 0:
|
||||
word += ' '
|
||||
if mod > 0:
|
||||
word += _convert_nn_fr(mod)
|
||||
return word
|
||||
|
||||
def french_number(val):
|
||||
if val < 100:
|
||||
return _convert_nn_fr(val)
|
||||
if val < 1000:
|
||||
return _convert_nnn_fr(val)
|
||||
for (didx, dval) in ((v - 1, 1000 ** v) for v in range(len(denom_fr))):
|
||||
if dval > val:
|
||||
mod = 1000 ** didx
|
||||
l = val // mod
|
||||
r = val - (l * mod)
|
||||
ret = _convert_nnn_fr(l) + ' ' + denom_fr[didx]
|
||||
if r > 0:
|
||||
ret = ret + ', ' + french_number(r)
|
||||
return ret
|
||||
|
||||
def amount_to_text_fr(number, currency):
|
||||
number = '%.2f' % number
|
||||
units_name = currency
|
||||
list = str(number).split('.')
|
||||
start_word = french_number(abs(int(list[0])))
|
||||
end_word = french_number(int(list[1]))
|
||||
cents_number = int(list[1])
|
||||
cents_name = (cents_number > 1) and ' Cents' or ' Cent'
|
||||
final_result = start_word +' '+units_name+' '+ end_word +' '+cents_name
|
||||
return final_result
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# Dutch
|
||||
#-------------------------------------------------------------
|
||||
|
||||
to_19_nl = ( 'Nul', 'Een', 'Twee', 'Drie', 'Vier', 'Vijf', 'Zes',
|
||||
'Zeven', 'Acht', 'Negen', 'Tien', 'Elf', 'Twaalf', 'Dertien',
|
||||
'Veertien', 'Vijftien', 'Zestien', 'Zeventien', 'Achttien', 'Negentien' )
|
||||
tens_nl = ( 'Twintig', 'Dertig', 'Veertig', 'Vijftig', 'Zestig', 'Zeventig', 'Tachtig', 'Negentig')
|
||||
denom_nl = ( '',
|
||||
'Duizend', 'Miljoen', 'Miljard', 'Triljoen', 'Quadriljoen',
|
||||
'Quintillion', 'Sextiljoen', 'Septillion', 'Octillion', 'Nonillion',
|
||||
'Decillion', 'Undecillion', 'Duodecillion', 'Tredecillion', 'Quattuordecillion',
|
||||
'Sexdecillion', 'Septendecillion', 'Octodecillion', 'Novemdecillion', 'Vigintillion' )
|
||||
|
||||
def _convert_nn_nl(val):
|
||||
""" convert a value < 100 to Dutch
|
||||
"""
|
||||
if val < 20:
|
||||
return to_19_nl[val]
|
||||
for (dcap, dval) in ((k, 20 + (10 * v)) for (v, k) in enumerate(tens_nl)):
|
||||
if dval + 10 > val:
|
||||
if val % 10:
|
||||
return to_19_nl[val % 10] + '-en-' + dcap
|
||||
return dcap
|
||||
|
||||
def _convert_nnn_nl(val):
|
||||
""" convert a value < 1000 to Dutch
|
||||
|
||||
special cased because it is the level that kicks
|
||||
off the < 100 special case. The rest are more general. This also allows you to
|
||||
get strings in the form of 'forty-five hundred' if called directly.
|
||||
"""
|
||||
word = ''
|
||||
(mod, rem) = (val % 100, val // 100)
|
||||
if rem > 0:
|
||||
word = (rem > 1 and to_19_nl[rem] + 'honderd') or 'Honderd'
|
||||
if mod > 0:
|
||||
word += ' '
|
||||
if mod > 0:
|
||||
word += _convert_nn_nl(mod)
|
||||
return word
|
||||
|
||||
def dutch_number(val):
|
||||
if val < 100:
|
||||
return _convert_nn_nl(val)
|
||||
if val < 1000:
|
||||
return _convert_nnn_nl(val)
|
||||
for (didx, dval) in ((v - 1, 1000 ** v) for v in range(len(denom_nl))):
|
||||
if dval > val:
|
||||
mod = 1000 ** didx
|
||||
l = val // mod
|
||||
r = val - (l * mod)
|
||||
ret = _convert_nnn_nl(l) + ' ' + denom_nl[didx]
|
||||
if r > 0:
|
||||
ret = ret + ', ' + dutch_number(r)
|
||||
return ret
|
||||
|
||||
def amount_to_text_nl(number, currency):
|
||||
number = '%.2f' % number
|
||||
units_name = currency
|
||||
list = str(number).split('.')
|
||||
start_word = dutch_number(int(list[0]))
|
||||
end_word = dutch_number(int(list[1]))
|
||||
cents_number = int(list[1])
|
||||
cents_name = (cents_number > 1) and 'cent' or 'cent'
|
||||
final_result = start_word +' '+units_name+' '+ end_word +' '+cents_name
|
||||
return final_result
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# Generic functions
|
||||
#-------------------------------------------------------------
|
||||
|
||||
_translate_funcs = {'fr' : amount_to_text_fr, 'nl' : amount_to_text_nl}
|
||||
|
||||
def add_amount_to_text_function(lang, func):
|
||||
_translate_funcs[lang] = func
|
||||
|
||||
#TODO: we should use the country AND language (ex: septante VS soixante dix)
|
||||
#TODO: we should use en by default, but the translation func is yet to be implemented
|
||||
def amount_to_text(nbr, lang='fr', currency='euro'):
|
||||
""" Converts an integer to its textual representation, using the language set in the context if any.
|
||||
|
||||
Example::
|
||||
|
||||
1654: mille six cent cinquante-quatre.
|
||||
"""
|
||||
# if nbr > 1000000:
|
||||
##TODO: use logger
|
||||
# print "WARNING: number too large '%d', can't translate it!" % (nbr,)
|
||||
# return str(nbr)
|
||||
|
||||
if lang not in _translate_funcs:
|
||||
#TODO: use logger
|
||||
print("WARNING: no translation function found for lang: '%s'" % (lang,))
|
||||
#TODO: (default should be en) same as above
|
||||
lang = 'fr'
|
||||
return _translate_funcs[lang](abs(nbr), currency)
|
||||
@@ -1,96 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
from .translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
#-------------------------------------------------------------
|
||||
#ENGLISH
|
||||
#-------------------------------------------------------------
|
||||
|
||||
to_19 = ( 'Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six',
|
||||
'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve', 'Thirteen',
|
||||
'Fourteen', 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen' )
|
||||
tens = ( 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety')
|
||||
denom = ( '',
|
||||
'Thousand', 'Million', 'Billion', 'Trillion', 'Quadrillion',
|
||||
'Quintillion', 'Sextillion', 'Septillion', 'Octillion', 'Nonillion',
|
||||
'Decillion', 'Undecillion', 'Duodecillion', 'Tredecillion', 'Quattuordecillion',
|
||||
'Sexdecillion', 'Septendecillion', 'Octodecillion', 'Novemdecillion', 'Vigintillion' )
|
||||
|
||||
def _convert_nn(val):
|
||||
"""convert a value < 100 to English.
|
||||
"""
|
||||
if val < 20:
|
||||
return to_19[val]
|
||||
for (dcap, dval) in ((k, 20 + (10 * v)) for (v, k) in enumerate(tens)):
|
||||
if dval + 10 > val:
|
||||
if val % 10:
|
||||
return dcap + '-' + to_19[val % 10]
|
||||
return dcap
|
||||
|
||||
def _convert_nnn(val):
|
||||
"""
|
||||
convert a value < 1000 to english, special cased because it is the level that kicks
|
||||
off the < 100 special case. The rest are more general. This also allows you to
|
||||
get strings in the form of 'forty-five hundred' if called directly.
|
||||
"""
|
||||
word = ''
|
||||
(mod, rem) = (val % 100, val // 100)
|
||||
if rem > 0:
|
||||
word = to_19[rem] + ' Hundred'
|
||||
if mod > 0:
|
||||
word += ' '
|
||||
if mod > 0:
|
||||
word += _convert_nn(mod)
|
||||
return word
|
||||
|
||||
def english_number(val):
|
||||
if val < 100:
|
||||
return _convert_nn(val)
|
||||
if val < 1000:
|
||||
return _convert_nnn(val)
|
||||
for (didx, dval) in ((v - 1, 1000 ** v) for v in range(len(denom))):
|
||||
if dval > val:
|
||||
mod = 1000 ** didx
|
||||
l = val // mod
|
||||
r = val - (l * mod)
|
||||
ret = _convert_nnn(l) + ' ' + denom[didx]
|
||||
if r > 0:
|
||||
ret = ret + ', ' + english_number(r)
|
||||
return ret
|
||||
|
||||
def amount_to_text(number, currency):
|
||||
number = '%.2f' % number
|
||||
units_name = currency
|
||||
list = str(number).split('.')
|
||||
start_word = english_number(int(list[0]))
|
||||
end_word = english_number(int(list[1]))
|
||||
cents_number = int(list[1])
|
||||
cents_name = (cents_number > 1) and 'Cents' or 'Cent'
|
||||
|
||||
return ' '.join(w for w in [start_word, units_name, (start_word or units_name) and (end_word or cents_name) and 'and', end_word, cents_name] if w)
|
||||
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# Generic functions
|
||||
#-------------------------------------------------------------
|
||||
|
||||
_translate_funcs = {'en' : amount_to_text}
|
||||
|
||||
#TODO: we should use the country AND language (ex: septante VS soixante dix)
|
||||
#TODO: we should use en by default, but the translation func is yet to be implemented
|
||||
def amount_to_text(nbr, lang='en', currency='euro'):
|
||||
""" Converts an integer to its textual representation, using the language set in the context if any.
|
||||
|
||||
Example::
|
||||
|
||||
1654: thousands six cent cinquante-quatre.
|
||||
"""
|
||||
if lang not in _translate_funcs:
|
||||
_logger.warning(_("no translation function found for lang: '%s'"), lang)
|
||||
#TODO: (default should be en) same as above
|
||||
lang = 'en'
|
||||
return _translate_funcs[lang](abs(nbr), currency)
|
||||
Reference in New Issue
Block a user