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:
|
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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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),
|
||||||
|
|
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.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+',
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue