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: for accrual_type in AccrualAccount:
self.use_sheet(accrual_type.name.title()) self.use_sheet(accrual_type.name.title())
for index in range(self.COL_COUNT): for index in range(self.COL_COUNT):
stylename = self.style_col1_25 if index else '' if index == 0:
self.sheet.addElement(odf.table.TableColumn(stylename=stylename)) 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.add_row(*(
self.string_cell(name, stylename=self.style_bold) self.string_cell(name, stylename=self.style_bold)
for name in self.COLUMNS 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 See also the BaseSpreadsheet base class for additional documentation about
methods you must and can define, the definition of RT and ST, etc. 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: def __init__(self, rt_wrapper: Optional[rtutil.RT]=None) -> None:
self.rt_wrapper = rt_wrapper self.rt_wrapper = rt_wrapper
self.locale = babel.core.Locale.default('LC_MONETARY') self.locale = babel.core.Locale.default('LC_MONETARY')
@ -576,6 +591,29 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
### Styles ### 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( def _build_currency_style(
self, self,
root: odf.element.Element, root: odf.element.Element,
@ -875,20 +913,6 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
aligned_style.addElement(odf.style.ParagraphProperties(textalign=textalign)) aligned_style.addElement(odf.style.ParagraphProperties(textalign=textalign))
setattr(self, f'style_{textalign}text', aligned_style) 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 ### Rows and cells
def add_row(self, *cells: odf.table.TableCell, **attrs: Any) -> odf.table.TableRow: def add_row(self, *cells: odf.table.TableCell, **attrs: Any) -> odf.table.TableRow:

View file

@ -62,6 +62,7 @@ from typing import (
Sequence, Sequence,
TextIO, TextIO,
Tuple, Tuple,
Union,
) )
from pathlib import Path from pathlib import Path
@ -101,11 +102,6 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
('Assets', ['rt-id', 'receipt', 'approval', 'bank-statement']), ('Assets', ['rt-id', 'receipt', 'approval', 'bank-statement']),
('Liabilities', ['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. # Excel 2003 was limited to 65,536 rows per worksheet.
# While we can probably count on all our users supporting more modern # While we can probably count on all our users supporting more modern
# formats (Excel 2007 supports over 1 million rows per worksheet), # 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.required_sheet_names = sheet_names
self.sheet_size = sheet_size 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 @classmethod
def _group_tally( def _group_tally(
cls, cls,
@ -249,7 +258,7 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
] ]
for col_name in self.sheet_columns: for col_name in self.sheet_columns:
self.sheet.addElement(odf.table.TableColumn( 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.add_row(*(
self.string_cell(col_name, stylename=self.style_bold) 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: if balance_accounts[0] not in self.required_sheet_names:
balance_accounts[0] = 'Equity:Funds' balance_accounts[0] = 'Equity:Funds'
self.use_sheet("Balance") self.use_sheet("Balance")
column_style = self.replace_child( self.sheet.addElement(odf.table.TableColumn(stylename=self.column_style(3)))
self.document.automaticstyles, odf.style.Style, name='col_3', self.sheet.addElement(odf.table.TableColumn(stylename=self.column_style(1.5)))
)
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.add_row( self.add_row(
self.string_cell("Account", stylename=self.style_bold), self.string_cell("Account", stylename=self.style_bold),
self.string_cell("Balance", stylename=self.style_bold), self.string_cell("Balance", stylename=self.style_bold),

View file

@ -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.2.3', version='1.2.4',
author='Software Freedom Conservancy', author='Software Freedom Conservancy',
author_email='info@sfconservancy.org', author_email='info@sfconservancy.org',
license='GNU AGPLv3+', 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 len(sheets) == 1
assert sheets[0].getAttribute('name') == 'One' 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', [ @pytest.mark.parametrize('currency_code', [
'USD', 'USD',
'EUR', 'EUR',
@ -261,11 +299,6 @@ def test_ods_currency_style_cache_considers_properties(ods_writer):
assert plain.getAttribute('datastylename') != bold.getAttribute('datastylename') assert plain.getAttribute('datastylename') != bold.getAttribute('datastylename')
@pytest.mark.parametrize('attr_name,child_type,checked_attr', [ @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_bold', odf.style.TextProperties, 'fontweight'),
('style_centertext', odf.style.ParagraphProperties, 'textalign'), ('style_centertext', odf.style.ParagraphProperties, 'textalign'),
('style_dividerline', odf.style.TableCellProperties, 'borderbottom'), ('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'), ('style_starttext', odf.style.ParagraphProperties, 'textalign'),
]) ])
def test_ods_writer_style(ods_writer, attr_name, child_type, checked_attr): def test_ods_writer_style(ods_writer, attr_name, child_type, checked_attr):
if child_type is odf.style.TableColumnProperties: root = ods_writer.document.styles
root = ods_writer.document.automaticstyles
else:
root = ods_writer.document.styles
style = getattr(ods_writer, attr_name) style = getattr(ods_writer, attr_name)
actual = get_child(root, odf.style.Style, name=style.getAttribute('name')) actual = get_child(root, odf.style.Style, name=style.getAttribute('name'))
assert actual is style assert actual is style