From de10197af7f5a18d5d607368e25c242388e3e69f 2020-09-04 19:29:10 From: Brett Smith Date: 2020-09-04 19:29:10 Subject: [PATCH] reports: Improve formatting of non-currency commodities. Introduce the get_commodity_format() function, which returns Babel's usual format string for currencies, but returns a version of it "merged" with the locale's currency unit pattern for other commodities. BaseODS then calls this function where needed to format amounts. --- diff --git a/conservancy_beancount/reports/core.py b/conservancy_beancount/reports/core.py index b2ad5eb4a85bc1803998f976fe4ef4f0dafc0af4..cac844cb902556133c06bc18ab9743f39f646322 100644 --- a/conservancy_beancount/reports/core.py +++ b/conservancy_beancount/reports/core.py @@ -737,7 +737,7 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta): root: odf.element.Element, locale: babel.core.Locale, code: str, - fmt_index: int, + amount: DecimalCompat=0, properties: Optional[odf.style.TextProperties]=None, *, fmt_key: Optional[str]=None, @@ -746,11 +746,7 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta): if fmt_key is None: fmt_key = self.currency_fmt_key pattern = locale.currency_formats[fmt_key] - fmts = pattern.pattern.split(';') - try: - fmt = fmts[fmt_index] - except IndexError: - fmt = fmts[0] + fmt = get_commodity_format(locale, code, amount, fmt_key) style = self.replace_child( root, odf.number.CurrencyStyle, @@ -823,7 +819,7 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta): root, locale, code, 0, positive_properties, volatile=True, ) curr_style = self._build_currency_style( - root, locale, code, 1, negative_properties, + root, locale, code, -1, negative_properties, ) curr_style.addElement(odf.style.Map( condition='value()>=0', applystylename=pos_style, @@ -1133,9 +1129,9 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta): ) return self.currency_cell(amount, **attrs) else: - lines = [babel.numbers.format_currency( - number, currency, locale=self.locale, format_type=self.currency_fmt_key, - ) for number, currency in balance.values()] + lines = [babel.numbers.format_currency(number, currency, get_commodity_format( + self.locale, currency, None, self.currency_fmt_key, + )) for number, currency in balance.values()] attrs['stylename'] = self.merge_styles( attrs.get('stylename'), self.style_endtext, ) @@ -1287,6 +1283,47 @@ def account_balances( MutableBalance(), )) +def get_commodity_format(locale: babel.core.Locale, + code: str, + amount: Optional[DecimalCompat]=None, + format_type: str='accounting', +) -> str: + """Return a format string for a commodity + + Typical use looks like:: + + number, code = post.units + fmt = get_commodity_format(locale, code) + units_s = babel.numbers.format_currency(number, code, fmt) + + When the commodity code refers to a real currency, you get the same format + string provided by Babel. + + For other commodities like stock, you get a format code built from the + locale's currency unit pattern. + + If ``amount`` is defined, the format string will be specifically for that + number, whether positive or negative. Otherwise, the format string may + define both positive and negative formats. + """ + fmt: str = locale.currency_formats[format_type].pattern + if amount is not None: + fmt, _, neg_fmt = fmt.partition(';') + if amount < 0 and neg_fmt: + fmt = neg_fmt + symbol = babel.numbers.get_currency_symbol(code, locale) + if symbol != code: + return fmt + else: + long_fmt: str = babel.numbers.get_currency_unit_pattern(code, locale=locale) + return re.sub( + r'[#0,.\s¤]+', + lambda match: long_fmt.format( + match.group(0).replace('¤', '').strip(), '¤¤', + ), + fmt, + ) + def normalize_amount_func(account_name: str) -> Callable[[T], T]: """Get a function to normalize amounts for reporting diff --git a/setup.py b/setup.py index ce21f101b8e4586cf18cb5917a881cc35d0a81df..a590f6aaa9ac3fa928477804e4bd723be3e0e615 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup setup( name='conservancy_beancount', description="Plugin, library, and reports for reading Conservancy's books", - version='1.9.5', + version='1.9.6', author='Software Freedom Conservancy', author_email='info@sfconservancy.org', license='GNU AGPLv3+', diff --git a/tests/test_reports_spreadsheet.py b/tests/test_reports_spreadsheet.py index 9f8e7f7a1eb9ef86590967599f7450cd5010035a..99342ababe84b3736ed4052118e7c20c030aa24c 100644 --- a/tests/test_reports_spreadsheet.py +++ b/tests/test_reports_spreadsheet.py @@ -549,9 +549,7 @@ def test_ods_writer_balance_cell_multi_currency(ods_writer): ]] balance = core.Balance(amounts) cell = ods_writer.balance_cell(balance) - assert cell.text == '\0'.join(babel.numbers.format_currency( - number, currency, locale=EN_US, format_type='accounting', - ) for number, currency in amounts) + assert cell.text == '2,500.00 RUB\0R$3,500.00' @pytest.mark.parametrize('cell_source,style_name', testutil.combine_values( CURRENCY_CELL_DATA,