Changeset - 8b8bdc022508
[Not reviewed]
0 5 0
Brett Smith - 4 years ago 2020-06-17 02:41:13
brettcsmith@brettcsmith.org
reports: Add BaseODS.column_style() method.

Use this to provide more consistent column styles throughout the reports.
5 files changed with 102 insertions and 39 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/accrual.py
Show inline comments
...
 
@@ -306,14 +306,19 @@ class AgingODS(core.BaseODS[AccrualPostings, Optional[data.Account]]):
 
            return None
 

	
 
    def start_spreadsheet(self) -> None:
 
        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
 
            ))
 
            self.lock_first_row()
 

	
conservancy_beancount/reports/core.py
Show inline comments
...
 
@@ -460,12 +460,27 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 
    rows to an OpenDocument spreadsheet. It provides helper methods for
 
    building sheets, rows, and cells.
 

	
 
    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')
 
        self.currency_fmt_key = 'accounting'
 
        self._name_counter = itertools.count(1)
 
        self._currency_style_cache: MutableMapping[str, odf.style.Style] = {}
...
 
@@ -573,12 +588,35 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 
            root, odf.config.ConfigItem, name=name, type=config_type,
 
        )
 
        item.addText(value_s)
 

	
 
    ### 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,
 
            locale: babel.core.Locale,
 
            code: str,
 
            fmt_index: int,
...
 
@@ -872,26 +910,12 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 
                styles, odf.style.Style, name=f'{textalign.title()}Text',
 
            )
 
            aligned_style.setAttribute('family', 'table-cell')
 
            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:
 
        row = odf.table.TableRow(**attrs)
 
        for cell in cells:
 
            row.addElement(cell)
conservancy_beancount/reports/ledger.py
Show inline comments
...
 
@@ -59,12 +59,13 @@ from typing import (
 
    List,
 
    Mapping,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
    Tuple,
 
    Union,
 
)
 

	
 
from pathlib import Path
 

	
 
import odf.table  # type:ignore[import]
 

	
...
 
@@ -98,17 +99,12 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
 
        ('Assets:Receivable', ['project', 'rt-id', 'invoice', 'approval', 'contract', 'purchase-order']),
 
        ('Liabilities:Payable', ['project', 'rt-id', 'invoice', 'approval', 'contract', 'purchase-order']),
 
        ('Assets:PayPal', ['rt-id', 'paypal-id', 'receipt', 'approval']),
 
        ('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),
 
    # keeping the default limit conservative seems good to avoid running into
 
    # other limits (like the number of hyperlinks per worksheet), plus just
 
    # better for human organization and readability.
...
 
@@ -127,12 +123,25 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
 
            sheet_size = self.SHEET_SIZE
 
        super().__init__(rt_wrapper)
 
        self.date_range = ranges.DateRange(start_date, stop_date)
 
        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,
 
            tally_by_account: PostTally,
 
            key: Callable[[data.Account], Optional[str]],
 
    ) -> Dict[str, PostTally]:
...
 
@@ -246,13 +255,13 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
 
        self.sheet_columns: Sequence[str] = [
 
            *self.CORE_COLUMNS,
 
            *(data.Metadata.human_name(meta_key) for meta_key in self.metadata_columns),
 
        ]
 
        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)
 
            for col_name in self.sheet_columns
 
        ))
 
        self.lock_first_row()
...
 
@@ -338,19 +347,14 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
 
        balance_accounts = ['Equity', 'Income', 'Expenses']
 
        # FIXME: This is a hack to exclude non-project Equity accounts from
 
        # project reports.
 
        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),
 
        )
 
        self.lock_first_row()
 
        self.add_row()
setup.py
Show inline comments
...
 
@@ -2,13 +2,13 @@
 

	
 
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+',
 

	
 
    install_requires=[
 
        'babel>=2.6',  # Debian:python3-babel
tests/test_reports_spreadsheet.py
Show inline comments
...
 
@@ -220,12 +220,50 @@ def test_ods_writer_use_sheet_discards_unused_sheets(ods_writer):
 
    ods_writer.use_sheet('Two')
 
    ods_writer.use_sheet('One')
 
    sheets = ods_writer.document.getElementsByType(odf.table.Table)
 
    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',
 
    'BRL',
 
])
 
def test_ods_currency_style(ods_writer, currency_code):
...
 
@@ -258,28 +296,20 @@ def test_ods_currency_style_cache_considers_properties(ods_writer):
 
    bold = ods_writer.currency_style('USD', positive_properties=bold_text)
 
    assert plain is not bold
 
    assert plain.getAttribute('name') != bold.getAttribute('name')
 
    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'),
 
    ('style_endtext', odf.style.ParagraphProperties, 'textalign'),
 
    ('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
 
    child = get_child(actual, child_type)
 
    assert child.getAttribute(checked_attr)
 

	
0 comments (0 inline, 0 general)