[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:
Mitali Patel
2017-09-15 13:24:02 +02:00
committed by qdp-odoo
parent be93768608
commit 79038055f5
10 changed files with 384 additions and 309 deletions
@@ -1,10 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import math
from odoo import models, fields, api, _ from odoo import models, fields, api, _
from odoo.tools import amount_to_text_en, float_round
from odoo.exceptions import UserError, ValidationError from odoo.exceptions import UserError, ValidationError
@@ -29,7 +26,7 @@ class AccountRegisterPayments(models.TransientModel):
def _onchange_amount(self): def _onchange_amount(self):
if hasattr(super(AccountRegisterPayments, self), '_onchange_amount'): if hasattr(super(AccountRegisterPayments, self), '_onchange_amount'):
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): def _prepare_payment_vals(self, invoices):
res = super(AccountRegisterPayments, self)._prepare_payment_vals(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 " 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.") "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') @api.onchange('journal_id')
def _onchange_journal_id(self): def _onchange_journal_id(self):
if hasattr(super(AccountPayment, self), '_onchange_journal_id'): if hasattr(super(AccountPayment, self), '_onchange_journal_id'):
@@ -69,7 +57,7 @@ class AccountPayment(models.Model):
@api.onchange('amount') @api.onchange('amount')
def _onchange_amount(self): def _onchange_amount(self):
res = super(AccountPayment, self)._onchange_amount() 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 return res
def _check_communication(self, payment_method_id, communication): def _check_communication(self, payment_method_id, communication):
+1 -1
View File
@@ -27,7 +27,7 @@ class HrExpenseRegisterPaymentWizard(models.TransientModel):
def _onchange_amount(self): def _onchange_amount(self):
if hasattr(super(HrExpenseRegisterPaymentWizard, self), '_onchange_amount'): if hasattr(super(HrExpenseRegisterPaymentWizard, self), '_onchange_amount'):
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): def _get_payment_vals(self):
res = super(HrExpenseRegisterPaymentWizard, self)._get_payment_vals() res = super(HrExpenseRegisterPaymentWizard, self)._get_payment_vals()
@@ -4,7 +4,6 @@
import time import time
from datetime import datetime from datetime import datetime
from odoo.tools import amount_to_text_en
from odoo import api, models 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') res['to_name'] = to_date.strftime('%d') + '-' + to_date.strftime('%B') + '-' + to_date.strftime('%Y')
return res return res
def convert(self, amount, cur):
return amount_to_text_en.amount_to_text(amount, 'en', cur)
def get_bysal_total(self): def get_bysal_total(self):
return self.total_bysal return self.total_bysal
@@ -55,7 +51,6 @@ class payroll_advice_report(models.AbstractModel):
'docs': advice, 'docs': advice,
'time': time, 'time': time,
'get_month': self.get_month, 'get_month': self.get_month,
'convert': self.convert,
'get_detail': self.get_detail, 'get_detail': self.get_detail,
'get_bysal_total': self.get_bysal_total, 'get_bysal_total': self.get_bysal_total,
} }
+37
View File
@@ -2,10 +2,17 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import json import json
import logging
import math import math
import re import re
import time 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, _ from odoo import api, fields, models, tools, _
CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?') 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', 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.") string='Symbol Position', help="Determines where the currency symbol should be placed after or before the amount.")
date = fields.Date(compute='_compute_date') 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 = [ _sql_constraints = [
('unique_name', 'unique (name)', 'The currency code must be unique!'), ('unique_name', 'unique (name)', 'The currency code must be unique!'),
@@ -79,6 +88,34 @@ class Currency(models.Model):
def name_get(self): def name_get(self):
return [(currency.id, tools.ustr(currency.name)) for currency in 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 @api.multi
def round(self, amount): def round(self, amount):
"""Return ``amount`` rounded according to ``self``'s rounding rules. """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> <field name="rate">264.784483</field>
</record> </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"> <record forcecreate="0" id="rateEUR" model="res.currency.rate">
<field name="currency_id" ref="EUR" /> <field name="currency_id" ref="EUR" />
<field name="name">2010-01-01</field> <field name="name">2010-01-01</field>
+9 -3
View File
@@ -140,9 +140,15 @@
options='{"terminology": "active"}'/> options='{"terminology": "active"}'/>
</button> </button>
</div> </div>
<group col="4"> <group>
<field name="name"/> <group>
<field name="rate"/> <field name="name"/>
<field name="rate"/>
</group>
<group>
<field name="currency_unit_label"/>
<field name="currency_subunit_label"/>
</group>
</group> </group>
<group groups="base.group_no_one"> <group groups="base.group_no_one">
-2
View File
@@ -11,8 +11,6 @@ from .convert import *
from .translate import * from .translate import *
from .graph import graph from .graph import graph
from .image import * from .image import *
from .amount_to_text import *
from .amount_to_text_en import *
from .yaml_import import * from .yaml_import import *
from .sql import * from .sql import *
from .float_utils import * from .float_utils import *
-174
View File
@@ -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)
-96
View File
@@ -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)