reports: Add BaseODS.column_style() method.

Use this to provide more consistent column styles throughout the reports.
This commit is contained in:
Brett Smith 2020-06-16 22:41:13 -04:00
parent cf2833ee20
commit 8b8bdc0225
5 changed files with 102 additions and 39 deletions

View file

@ -309,8 +309,13 @@ class AgingODS(core.BaseODS[AccrualPostings, Optional[data.Account]]):
for accrual_type in AccrualAccount:
self.use_sheet(accrual_type.name.title())
for index in range(self.COL_COUNT):
stylename = self.style_col1_25 if index else ''
self.sheet.addElement(odf.table.TableColumn(stylename=stylename))
if index == 0:
style: Union[str, odf.style.Style] = ''
elif index < 6:
style = self.column_style(1.2)
else:
style = self.column_style(1.5)
self.sheet.addElement(odf.table.TableColumn(stylename=style))
self.add_row(*(
self.string_cell(name, stylename=self.style_bold)
for name in self.COLUMNS

View file

@ -463,6 +463,21 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
See also the BaseSpreadsheet base class for additional documentation about
methods you must and can define, the definition of RT and ST, etc.
"""
# Defined in the XSL spec, "Definitions of Units of Measure"
MEASUREMENT_UNITS = frozenset([
'cm',
'em',
'in',
'mm',
'pc',
'pt',
'px',
])
MEASUREMENT_RE = re.compile(
r'([-+]?(?:\d+\.?|\.\d+|\d+\.\d+))({})'.format('|'.join(MEASUREMENT_UNITS)),
re.ASCII,
)
def __init__(self, rt_wrapper: Optional[rtutil.RT]=None) -> None:
self.rt_wrapper = rt_wrapper
self.locale = babel.core.Locale.default('LC_MONETARY')
@ -576,6 +591,29 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
### Styles
def column_style(self, width: Union[float, str], **attrs: Any) -> odf.style.Style:
if not isinstance(width, str) or (width and not width[-1].isalpha()):
width = f'{width}in'
match = self.MEASUREMENT_RE.fullmatch(width)
if match is None:
raise ValueError(f"invalid width {width!r}")
width_float = float(match.group(1))
if width_float <= 0:
# Per the OpenDocument spec, column-width is a positiveLength.
raise ValueError(f"width {width!r} must be positive")
width = '{:.3g}{}'.format(width_float, match.group(2))
retval = self.ensure_child(
self.document.automaticstyles,
odf.style.Style,
name=f'col_{width.replace(".", "_")}'
)
retval.setAttribute('family', 'table-column')
if retval.firstChild is None:
retval.addElement(odf.style.TableColumnProperties(
columnwidth=width, **attrs
))
return retval
def _build_currency_style(
self,
root: odf.element.Element,
@ -875,20 +913,6 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
aligned_style.addElement(odf.style.ParagraphProperties(textalign=textalign))
setattr(self, f'style_{textalign}text', aligned_style)
self.style_col1: odf.style.Style
self.style_col1_25: odf.style.Style
self.style_col1_5: odf.style.Style
self.style_col1_75: odf.style.Style
self.style_col2: odf.style.Style
for width in ['1', '1.25', '1.5', '1.75', '2']:
width_name = width.replace('.', '_')
column_style = self.replace_child(
self.document.automaticstyles, odf.style.Style, name=f'col_{width_name}',
)
column_style.setAttribute('family', 'table-column')
column_style.addElement(odf.style.TableColumnProperties(columnwidth=f'{width}in'))
setattr(self, f'style_col{width_name}', column_style)
### Rows and cells
def add_row(self, *cells: odf.table.TableCell, **attrs: Any) -> odf.table.TableRow:

View file

@ -62,6 +62,7 @@ from typing import (
Sequence,
TextIO,
Tuple,
Union,
)
from pathlib import Path
@ -101,11 +102,6 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
('Assets', ['rt-id', 'receipt', 'approval', 'bank-statement']),
('Liabilities', ['rt-id', 'receipt', 'approval', 'bank-statement']),
])
COLUMN_STYLES: Mapping[str, str] = {
'Date': '',
'Description': 'col_1_75',
data.Metadata.human_name('paypal-id'): 'col_1_5',
}
# Excel 2003 was limited to 65,536 rows per worksheet.
# While we can probably count on all our users supporting more modern
# formats (Excel 2007 supports over 1 million rows per worksheet),
@ -130,6 +126,19 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
self.required_sheet_names = sheet_names
self.sheet_size = sheet_size
def init_styles(self) -> None:
super().init_styles()
self.amount_column = self.column_style(1.2)
self.default_column = self.column_style(1.5)
self.column_styles: Mapping[str, Union[str, odf.style.Style]] = {
'Date': '',
'Description': self.column_style(2),
'Original Amount': self.amount_column,
'Booked Amount': self.amount_column,
data.Metadata.human_name('project'): self.amount_column,
data.Metadata.human_name('rt-id'): self.amount_column,
}
@classmethod
def _group_tally(
cls,
@ -249,7 +258,7 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
]
for col_name in self.sheet_columns:
self.sheet.addElement(odf.table.TableColumn(
stylename=self.COLUMN_STYLES.get(col_name, 'col_1_25'),
stylename=self.column_styles.get(col_name, self.default_column),
))
self.add_row(*(
self.string_cell(col_name, stylename=self.style_bold)
@ -341,13 +350,8 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
if balance_accounts[0] not in self.required_sheet_names:
balance_accounts[0] = 'Equity:Funds'
self.use_sheet("Balance")
column_style = self.replace_child(
self.document.automaticstyles, odf.style.Style, name='col_3',
)
column_style.setAttribute('family', 'table-column')
column_style.addElement(odf.style.TableColumnProperties(columnwidth='3in'))
for _ in range(2):
self.sheet.addElement(odf.table.TableColumn(stylename=column_style))
self.sheet.addElement(odf.table.TableColumn(stylename=self.column_style(3)))
self.sheet.addElement(odf.table.TableColumn(stylename=self.column_style(1.5)))
self.add_row(
self.string_cell("Account", stylename=self.style_bold),
self.string_cell("Balance", stylename=self.style_bold),

View file

@ -5,7 +5,7 @@ from setuptools import setup
setup(
name='conservancy_beancount',
description="Plugin, library, and reports for reading Conservancy's books",
version='1.2.3',
version='1.2.4',
author='Software Freedom Conservancy',
author_email='info@sfconservancy.org',
license='GNU AGPLv3+',

View file

@ -223,6 +223,44 @@ def test_ods_writer_use_sheet_discards_unused_sheets(ods_writer):
assert len(sheets) == 1
assert sheets[0].getAttribute('name') == 'One'
@pytest.mark.parametrize('width,expect_name', [
('.750', 'col_0_75in'),
(2, 'col_2in'),
('2.2in', 'col_2_2in'),
(3.5, 'col_3_5in'),
('4cm', 'col_4cm'),
])
def test_ods_column_style(ods_writer, width, expect_name):
style = ods_writer.column_style(width)
assert style.getAttribute('name') == expect_name
assert style.getAttribute('family') == 'table-column'
curr_style = get_child(
ods_writer.document.automaticstyles,
odf.style.Style,
name=expect_name,
)
assert get_child(
curr_style,
odf.style.TableColumnProperties,
columnwidth=expect_name[4:].replace('_', '.'),
)
def test_ods_column_style_caches(ods_writer):
int_width = ods_writer.column_style('1in')
float_width = ods_writer.column_style('1.00in')
assert int_width is float_width
@pytest.mark.parametrize('width', [
'1mi',
'0in',
'-1cm',
'in',
'.cm',
])
def test_ods_column_style_invalid_width(ods_writer, width):
with pytest.raises(ValueError):
ods_writer.column_style(width)
@pytest.mark.parametrize('currency_code', [
'USD',
'EUR',
@ -261,11 +299,6 @@ def test_ods_currency_style_cache_considers_properties(ods_writer):
assert plain.getAttribute('datastylename') != bold.getAttribute('datastylename')
@pytest.mark.parametrize('attr_name,child_type,checked_attr', [
('style_col1', odf.style.TableColumnProperties, 'columnwidth'),
('style_col1_25', odf.style.TableColumnProperties, 'columnwidth'),
('style_col1_5', odf.style.TableColumnProperties, 'columnwidth'),
('style_col1_75', odf.style.TableColumnProperties, 'columnwidth'),
('style_col2', odf.style.TableColumnProperties, 'columnwidth'),
('style_bold', odf.style.TextProperties, 'fontweight'),
('style_centertext', odf.style.ParagraphProperties, 'textalign'),
('style_dividerline', odf.style.TableCellProperties, 'borderbottom'),
@ -273,10 +306,7 @@ def test_ods_currency_style_cache_considers_properties(ods_writer):
('style_starttext', odf.style.ParagraphProperties, 'textalign'),
])
def test_ods_writer_style(ods_writer, attr_name, child_type, checked_attr):
if child_type is odf.style.TableColumnProperties:
root = ods_writer.document.automaticstyles
else:
root = ods_writer.document.styles
root = ods_writer.document.styles
style = getattr(ods_writer, attr_name)
actual = get_child(root, odf.style.Style, name=style.getAttribute('name'))
assert actual is style