Changeset - de10197af7f5
[Not reviewed]
0 3 0
Brett Smith - 4 years ago 2020-09-04 19:29:10
brettcsmith@brettcsmith.org
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.
3 files changed with 49 insertions and 14 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/core.py
Show inline comments
...
 
@@ -732,30 +732,26 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 
            ))
 
        return retval
 

	
 
    def _build_currency_style(
 
            self,
 
            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,
 
            volatile: bool=False,
 
    ) -> odf.element.Element:
 
        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,
 
            name=f'{code}{next(self._name_counter)}',
 
        )
 
        style.setAttribute('volatile', 'true' if volatile else 'false')
 
        if properties is not None:
 
            style.addElement(properties)
...
 
@@ -818,17 +814,17 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 
        cache_key = '\0'.join(cache_parts)
 
        try:
 
            style = self._style_cache[cache_key]
 
        except KeyError:
 
            pos_style = self._build_currency_style(
 
                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,
 
            ))
 
            style = self.ensure_child(
 
                self.document.styles,
 
                odf.style.Style,
 
                name=f'{curr_style.getAttribute("name")}Cell',
...
 
@@ -1128,19 +1124,19 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 
            return self.float_cell(0, **attrs)
 
        elif balance_currency_count == 1:
 
            amount = next(iter(balance.values()))
 
            attrs['stylename'] = self.merge_styles(
 
                attrs.get('stylename'), self.currency_style(amount.currency),
 
            )
 
            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,
 
            )
 
            return self.multiline_cell(lines, **attrs)
 

	
 
    def currency_cell(self, amount: data.Amount, **attrs: Any) -> odf.table.TableCell:
 
        if 'stylename' not in attrs:
 
            attrs['stylename'] = self.currency_style(amount.currency)
...
 
@@ -1282,16 +1278,57 @@ def account_balances(
 
            in_date_range = False
 
        if in_date_range:
 
            yield (key, groups[key].period_bal)
 
    yield (ENDING_BALANCE_NAME, sum(
 
        (groups[key].stop_bal for key in acct_seq),
 
        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
 

	
 
    Given an account name, return a function that can be used on "amounts"
 
    under that account (including numbers, Amount objects, and Balance objects)
 
    to normalize them for reporting. Right now that means make flipping the
 
    sign for accounts where "normal" postings are negative.
 
    """
setup.py
Show inline comments
 
#!/usr/bin/env python3
 

	
 
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+',
 

	
 
    install_requires=[
 
        'babel>=2.6',  # Debian:python3-babel
 
        'beancount>=2.2',  # Debian:beancount
 
        'GitPython>=2.0',  # Debian:python3-git
tests/test_reports_spreadsheet.py
Show inline comments
...
 
@@ -544,19 +544,17 @@ def test_ods_writer_balance_cell_single_currency(ods_writer):
 

	
 
def test_ods_writer_balance_cell_multi_currency(ods_writer):
 
    amounts = [testutil.Amount(num, code) for num, code in [
 
        (2500, 'RUB'),
 
        (3500, 'BRL'),
 
    ]]
 
    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,
 
    XML_NAMES,
 
))
 
def test_ods_writer_currency_cell(ods_writer, cell_source, style_name):
 
    cell = ods_writer.currency_cell(cell_source, stylename=style_name)
 
    number, currency = cell_source
0 comments (0 inline, 0 general)