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.
This commit is contained in:
		
							parent
							
								
									b4f2b506be
								
							
						
					
					
						commit
						de10197af7
					
				
					 3 changed files with 49 additions and 14 deletions
				
			
		| 
						 | 
					@ -737,7 +737,7 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 | 
				
			||||||
            root: odf.element.Element,
 | 
					            root: odf.element.Element,
 | 
				
			||||||
            locale: babel.core.Locale,
 | 
					            locale: babel.core.Locale,
 | 
				
			||||||
            code: str,
 | 
					            code: str,
 | 
				
			||||||
            fmt_index: int,
 | 
					            amount: DecimalCompat=0,
 | 
				
			||||||
            properties: Optional[odf.style.TextProperties]=None,
 | 
					            properties: Optional[odf.style.TextProperties]=None,
 | 
				
			||||||
            *,
 | 
					            *,
 | 
				
			||||||
            fmt_key: Optional[str]=None,
 | 
					            fmt_key: Optional[str]=None,
 | 
				
			||||||
| 
						 | 
					@ -746,11 +746,7 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 | 
				
			||||||
        if fmt_key is None:
 | 
					        if fmt_key is None:
 | 
				
			||||||
            fmt_key = self.currency_fmt_key
 | 
					            fmt_key = self.currency_fmt_key
 | 
				
			||||||
        pattern = locale.currency_formats[fmt_key]
 | 
					        pattern = locale.currency_formats[fmt_key]
 | 
				
			||||||
        fmts = pattern.pattern.split(';')
 | 
					        fmt = get_commodity_format(locale, code, amount, fmt_key)
 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            fmt = fmts[fmt_index]
 | 
					 | 
				
			||||||
        except IndexError:
 | 
					 | 
				
			||||||
            fmt = fmts[0]
 | 
					 | 
				
			||||||
        style = self.replace_child(
 | 
					        style = self.replace_child(
 | 
				
			||||||
            root,
 | 
					            root,
 | 
				
			||||||
            odf.number.CurrencyStyle,
 | 
					            odf.number.CurrencyStyle,
 | 
				
			||||||
| 
						 | 
					@ -823,7 +819,7 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 | 
				
			||||||
                root, locale, code, 0, positive_properties, volatile=True,
 | 
					                root, locale, code, 0, positive_properties, volatile=True,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            curr_style = self._build_currency_style(
 | 
					            curr_style = self._build_currency_style(
 | 
				
			||||||
                root, locale, code, 1, negative_properties,
 | 
					                root, locale, code, -1, negative_properties,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            curr_style.addElement(odf.style.Map(
 | 
					            curr_style.addElement(odf.style.Map(
 | 
				
			||||||
                condition='value()>=0', applystylename=pos_style,
 | 
					                condition='value()>=0', applystylename=pos_style,
 | 
				
			||||||
| 
						 | 
					@ -1133,9 +1129,9 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            return self.currency_cell(amount, **attrs)
 | 
					            return self.currency_cell(amount, **attrs)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            lines = [babel.numbers.format_currency(
 | 
					            lines = [babel.numbers.format_currency(number, currency, get_commodity_format(
 | 
				
			||||||
                number, currency, locale=self.locale, format_type=self.currency_fmt_key,
 | 
					                self.locale, currency, None, self.currency_fmt_key,
 | 
				
			||||||
            ) for number, currency in balance.values()]
 | 
					            )) for number, currency in balance.values()]
 | 
				
			||||||
            attrs['stylename'] = self.merge_styles(
 | 
					            attrs['stylename'] = self.merge_styles(
 | 
				
			||||||
                attrs.get('stylename'), self.style_endtext,
 | 
					                attrs.get('stylename'), self.style_endtext,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
| 
						 | 
					@ -1287,6 +1283,47 @@ def account_balances(
 | 
				
			||||||
        MutableBalance(),
 | 
					        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]:
 | 
					def normalize_amount_func(account_name: str) -> Callable[[T], T]:
 | 
				
			||||||
    """Get a function to normalize amounts for reporting
 | 
					    """Get a function to normalize amounts for reporting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								setup.py
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
										
									
									
									
								
							| 
						 | 
					@ -5,7 +5,7 @@ from setuptools import setup
 | 
				
			||||||
setup(
 | 
					setup(
 | 
				
			||||||
    name='conservancy_beancount',
 | 
					    name='conservancy_beancount',
 | 
				
			||||||
    description="Plugin, library, and reports for reading Conservancy's books",
 | 
					    description="Plugin, library, and reports for reading Conservancy's books",
 | 
				
			||||||
    version='1.9.5',
 | 
					    version='1.9.6',
 | 
				
			||||||
    author='Software Freedom Conservancy',
 | 
					    author='Software Freedom Conservancy',
 | 
				
			||||||
    author_email='info@sfconservancy.org',
 | 
					    author_email='info@sfconservancy.org',
 | 
				
			||||||
    license='GNU AGPLv3+',
 | 
					    license='GNU AGPLv3+',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -549,9 +549,7 @@ def test_ods_writer_balance_cell_multi_currency(ods_writer):
 | 
				
			||||||
    ]]
 | 
					    ]]
 | 
				
			||||||
    balance = core.Balance(amounts)
 | 
					    balance = core.Balance(amounts)
 | 
				
			||||||
    cell = ods_writer.balance_cell(balance)
 | 
					    cell = ods_writer.balance_cell(balance)
 | 
				
			||||||
    assert cell.text == '\0'.join(babel.numbers.format_currency(
 | 
					    assert cell.text == '2,500.00 RUB\0R$3,500.00'
 | 
				
			||||||
        number, currency, locale=EN_US, format_type='accounting',
 | 
					 | 
				
			||||||
    ) for number, currency in amounts)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize('cell_source,style_name', testutil.combine_values(
 | 
					@pytest.mark.parametrize('cell_source,style_name', testutil.combine_values(
 | 
				
			||||||
    CURRENCY_CELL_DATA,
 | 
					    CURRENCY_CELL_DATA,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue