reports: Add BaseODS.column_style() method.
Use this to provide more consistent column styles throughout the reports.
This commit is contained in:
parent
cf2833ee20
commit
8b8bdc0225
5 changed files with 102 additions and 39 deletions
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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),
|
||||
|
|
2
setup.py
2
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.2.3',
|
||||
version='1.2.4',
|
||||
author='Software Freedom Conservancy',
|
||||
author_email='info@sfconservancy.org',
|
||||
license='GNU AGPLv3+',
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue