Changeset - 760e0a8cd9c7
[Not reviewed]
0 3 0
Brett Smith - 4 years ago 2020-06-14 12:53:27
brettcsmith@brettcsmith.org
reports: Add column width styles to BaseODS.
3 files changed with 45 insertions and 36 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/accrual.py
Show inline comments
...
 
@@ -254,119 +254,107 @@ class AccrualPostings(core.RelatedPostings):
 
        return self if empty else self[start_index + 1:]
 

	
 
    @property
 
    def rt_id(self) -> Union[str, None, Sentinel]:
 
        return self._single_item(self.first_meta_links('rt-id', None))
 

	
 

	
 
class BaseReport:
 
    def __init__(self, out_file: TextIO) -> None:
 
        self.out_file = out_file
 
        self.logger = logger.getChild(type(self).__name__)
 

	
 
    def _report(self, posts: AccrualPostings, index: int) -> Iterable[str]:
 
        raise NotImplementedError("BaseReport._report")
 

	
 
    def run(self, groups: PostGroups) -> None:
 
        for index, invoice in enumerate(groups):
 
            for line in self._report(groups[invoice], index):
 
                print(line, file=self.out_file)
 

	
 

	
 
class AgingODS(core.BaseODS[AccrualPostings, Optional[data.Account]]):
 
    DOC_COLUMNS = [
 
        'rt-id',
 
        'invoice',
 
        'approval',
 
        'contract',
 
        'purchase-order',
 
    ]
 
    COLUMNS = [
 
        'Date',
 
        data.Metadata.human_name('entity'),
 
        'Invoice Amount',
 
        'Booked Amount',
 
        data.Metadata.human_name('project'),
 
        *(data.Metadata.human_name(key) for key in DOC_COLUMNS),
 
    ]
 
    COL_COUNT = len(COLUMNS)
 

	
 
    def __init__(self,
 
                 rt_wrapper: rtutil.RT,
 
                 date: datetime.date,
 
                 logger: logging.Logger,
 
    ) -> None:
 
        super().__init__(rt_wrapper)
 
        self.date = date
 
        self.logger = logger
 

	
 
    def init_styles(self) -> None:
 
        super().init_styles()
 
        self.style_widecol = self.replace_child(
 
            self.document.automaticstyles,
 
            odf.style.Style,
 
            name='WideCol',
 
        )
 
        self.style_widecol.setAttribute('family', 'table-column')
 
        self.style_widecol.addElement(odf.style.TableColumnProperties(
 
            columnwidth='1.25in',
 
        ))
 

	
 
    def section_key(self, row: AccrualPostings) -> Optional[data.Account]:
 
        if isinstance(row.account, str):
 
            return row.account
 
        else:
 
            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_widecol if index else ''
 
                stylename = self.style_col1_25 if index else ''
 
                self.sheet.addElement(odf.table.TableColumn(stylename=stylename))
 
            self.add_row(*(
 
                self.string_cell(name, stylename=self.style_bold)
 
                for name in self.COLUMNS
 
            ))
 
            self.lock_first_row()
 

	
 
    def start_section(self, key: Optional[data.Account]) -> None:
 
        if key is None:
 
            return
 
        self.age_thresholds = list(AccrualAccount.by_account(key).value.aging_thresholds)
 
        self.age_balances = [core.MutableBalance() for _ in self.age_thresholds]
 
        accrual_date = self.date - datetime.timedelta(days=self.age_thresholds[-1])
 
        acct_parts = key.slice_parts()
 
        self.use_sheet(acct_parts[1])
 
        self.add_row()
 
        self.add_row(self.string_cell(
 
            f"{' '.join(acct_parts[2:])} {acct_parts[1]} Aging Report"
 
            f" Accrued by {accrual_date.isoformat()} Unpaid by {self.date.isoformat()}",
 
            stylename=self.merge_styles(self.style_bold, self.style_centertext),
 
            numbercolumnsspanned=self.COL_COUNT,
 
        ))
 
        self.add_row()
 

	
 
    def end_section(self, key: Optional[data.Account]) -> None:
 
        if key is None:
 
            return
 
        total_balance = core.MutableBalance()
 
        text_style = self.merge_styles(self.style_bold, self.style_endtext)
 
        text_span = 4
 
        last_age_text: Optional[str] = None
 
        self.add_row()
 
        for threshold, balance in zip(self.age_thresholds, self.age_balances):
 
            years, days = divmod(threshold, 365)
 
            years_text = f"{years} {'Year' if years == 1 else 'Years'}"
 
            days_text = f"{days} Days"
 
            if years and days:
 
                age_text = f"{years_text} {days_text}"
 
            elif years:
 
                age_text = years_text
 
            else:
 
                age_text = days_text
 
            if last_age_text is None:
 
                age_range = f"Over {age_text}"
 
            else:
 
                age_range = f"{age_text}–{last_age_text}"
 
            self.add_row(
 
                self.string_cell(
conservancy_beancount/reports/core.py
Show inline comments
...
 
@@ -792,127 +792,143 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 

	
 
        If the current active sheet is empty when this method is called, it
 
        will be removed from the spreadsheet.
 
        """
 
        try:
 
            empty_sheet = not self.sheet.hasChildNodes()
 
        except AttributeError:
 
            empty_sheet = False
 
        if empty_sheet:
 
            self.document.spreadsheet.removeChild(self.sheet)
 
        self.sheet = self.ensure_child(
 
            self.document.spreadsheet, odf.table.Table, name=name,
 
        )
 
        return self.sheet
 

	
 
    ### Initialization hooks
 

	
 
    def init_settings(self) -> None:
 
        """Hook called to initialize settings
 

	
 
        This method is called by __init__ to populate
 
        ``self.document.settings``. This implementation creates the barest
 
        skeleton structure necessary to support other methods, in particular
 
        ``lock_first_row``.
 
        """
 
        view_settings = self.ensure_child(
 
            self.document.settings, odf.config.ConfigItemSet, name='ooo:view-settings',
 
        )
 
        views = self.ensure_child(
 
            view_settings, odf.config.ConfigItemMapIndexed, name='Views',
 
        )
 
        self.view = self.ensure_child(views, odf.config.ConfigItemMapEntry)
 
        self.set_config(self.view, 'ViewId', 'view1')
 

	
 
    def init_styles(self) -> None:
 
        """Hook called to initialize settings
 

	
 
        This method is called by __init__ to populate
 
        ``self.document.styles``. This implementation creates basic building
 
        block cell styles often used in financial reports.
 
        """
 
        styles = self.document.styles
 
        self.style_bold = self.ensure_child(
 
            styles, odf.style.Style, name='Bold', family='table-cell',
 
        )
 
        self.ensure_child(
 
            self.style_bold, odf.style.TextProperties, fontweight='bold',
 
        )
 
        self.style_starttext: odf.style.Style
 
        self.style_centertext: odf.style.Style
 
        self.style_endtext: odf.style.Style
 
        for textalign in ['start', 'center', 'end']:
 
            aligned_style = self.replace_child(
 
                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_dividerline = self.ensure_child(
 
            styles, odf.style.Style, name='DividerLine', family='table-cell',
 
        )
 
        self.ensure_child(
 
            self.style_dividerline,
 
            odf.style.TableCellProperties,
 
            borderbottom='1pt solid #0000ff',
 
        )
 

	
 
        date_style = self.replace_child(styles, odf.number.DateStyle, name='ISODate')
 
        date_style.addElement(odf.number.Year(style='long'))
 
        date_style.addElement(odf.number.Text(text='-'))
 
        date_style.addElement(odf.number.Month(style='long'))
 
        date_style.addElement(odf.number.Text(text='-'))
 
        date_style.addElement(odf.number.Day(style='long'))
 
        self.style_date = self.ensure_child(
 
            styles,
 
            odf.style.Style,
 
            name=f'{date_style.getAttribute("name")}Cell',
 
            family='table-cell',
 
            datastylename=date_style,
 
        )
 
        self.style_dividerline = self.ensure_child(
 
            styles, odf.style.Style, name='DividerLine', family='table-cell',
 
        )
 
        self.ensure_child(
 
            self.style_dividerline,
 
            odf.style.TableCellProperties,
 
            borderbottom='1pt solid #0000ff',
 
        )
 

	
 
        self.style_starttext: odf.style.Style
 
        self.style_centertext: odf.style.Style
 
        self.style_endtext: odf.style.Style
 
        for textalign in ['start', 'center', 'end']:
 
            aligned_style = self.replace_child(
 
                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)
 
        self.sheet.addElement(row)
 
        return row
 

	
 
    def balance_cell(self, balance: Balance, **attrs: Any) -> odf.table.TableCell:
 
        if balance.is_zero():
 
            return self.float_cell(0, **attrs)
 
        elif len(balance) == 1:
 
            amount = next(iter(balance.values()))
 
            attrs['stylename'] = self.merge_styles(
 
                attrs.get('stylename'), self.currency_style(amount.currency),
 
            )
 
            return self.currency_cell(amount, **attrs)
 
        else:
 
            lines = [babel.numbers.format_currency(
 
                number, currency, locale=self.locale, format_type=self.currency_fmt_key,
 
            ) for number, currency in balance.values()]
 
            attrs['stylename'] = self.merge_styles(
 
                attrs.get('stylename'), self.style_endtext,
 
            )
 
            return self.multiline_cell(lines, **attrs)
 

	
 
    def currency_cell(self, amount: data.Amount, **attrs: Any) -> odf.table.TableCell:
 
        number, currency = amount
 
        cell = odf.table.TableCell(valuetype='currency', value=number, **attrs)
 
        cell.addElement(odf.text.P(text=babel.numbers.format_currency(
 
            number, currency, locale=self.locale, format_type=self.currency_fmt_key,
 
        )))
 
        return cell
 

	
 
    def date_cell(self, date: datetime.date, **attrs: Any) -> odf.table.TableCell:
 
        attrs.setdefault('stylename', self.style_date)
 
        cell = odf.table.TableCell(valuetype='date', datevalue=date, **attrs)
 
        cell.addElement(odf.text.P(text=date.isoformat()))
 
        return cell
 

	
 
    def float_cell(self, value: Union[int, float, Decimal], **attrs: Any) -> odf.table.TableCell:
 
        cell = odf.table.TableCell(valuetype='float', value=value, **attrs)
 
        cell.addElement(odf.text.P(text=str(value)))
 
        return cell
 

	
 
    def _meta_link_pairs(self, links: Iterable[Optional[str]]) -> Iterator[Tuple[str, str]]:
tests/test_reports_spreadsheet.py
Show inline comments
...
 
@@ -216,109 +216,114 @@ def test_ods_writer_use_sheet_returns_to_prior_sheets(ods_writer):
 
        assert texts == [name]
 

	
 
def test_ods_writer_use_sheet_discards_unused_sheets(ods_writer):
 
    ods_writer.use_sheet('Three')
 
    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('currency_code', [
 
    'USD',
 
    'EUR',
 
    'BRL',
 
])
 
def test_ods_currency_style(ods_writer, currency_code):
 
    style = ods_writer.currency_style(currency_code)
 
    assert style.getAttribute('family') == 'table-cell'
 
    curr_style = get_child(
 
        ods_writer.document.styles,
 
        odf.number.CurrencyStyle,
 
        name=style.getAttribute('datastylename'),
 
    )
 
    check_currency_style(curr_style)
 
    mappings = get_children(curr_style, odf.style.Map)
 
    assert mappings
 
    for mapping in mappings:
 
        check_currency_style(get_child(
 
            ods_writer.document.styles,
 
            odf.number.CurrencyStyle,
 
            name=mapping.getAttribute('applystylename'),
 
        ))
 

	
 
def test_ods_currency_style_caches(ods_writer):
 
    expected = ods_writer.currency_style('USD')
 
    _ = ods_writer.currency_style('EUR')
 
    actual = ods_writer.currency_style('USD')
 
    assert actual is expected
 

	
 
def test_ods_currency_style_cache_considers_properties(ods_writer):
 
    bold_text = odf.style.TextProperties(fontweight='bold')
 
    plain = ods_writer.currency_style('USD')
 
    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
 
    style = getattr(ods_writer, attr_name)
 
    actual = get_child(
 
        ods_writer.document.styles,
 
        odf.style.Style,
 
        name=style.getAttribute('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)
 

	
 
def test_ods_writer_merge_styles(ods_writer):
 
    style = ods_writer.merge_styles(ods_writer.style_bold, ods_writer.style_dividerline)
 
    actual = get_child(
 
        ods_writer.document.styles,
 
        odf.style.Style,
 
        name=style.getAttribute('name'),
 
    )
 
    assert actual is style
 
    assert actual.getAttribute('family') == 'table-cell'
 
    text_props = get_child(actual, odf.style.TextProperties)
 
    assert text_props.getAttribute('fontweight') == 'bold'
 
    cell_props = get_child(actual, odf.style.TableCellProperties)
 
    assert cell_props.getAttribute('borderbottom')
 

	
 
def test_ods_writer_merge_styles_with_children_and_attributes(ods_writer):
 
    jpy_style = ods_writer.currency_style('JPY')
 
    style = ods_writer.merge_styles(ods_writer.style_bold, jpy_style)
 
    actual = get_child(
 
        ods_writer.document.styles,
 
        odf.style.Style,
 
        name=style.getAttribute('name'),
 
    )
 
    assert actual is style
 
    assert actual.getAttribute('family') == 'table-cell'
 
    assert actual.getAttribute('datastylename') == jpy_style.getAttribute('datastylename')
 
    text_props = get_child(actual, odf.style.TextProperties)
 
    assert text_props.getAttribute('fontweight') == 'bold'
 

	
 
def test_ods_writer_merge_styles_caches(ods_writer):
 
    sources = [ods_writer.style_bold, ods_writer.style_dividerline]
 
    style1 = ods_writer.merge_styles(*sources)
 
    style2 = ods_writer.merge_styles(*reversed(sources))
 
    assert style1 is style2
 
    assert get_child(
 
        ods_writer.document.styles,
 
        odf.style.Style,
 
        name=style1.getAttribute('name'),
 
    )
 

	
 
def test_ods_writer_layer_merge_styles(ods_writer):
 
    usd_style = ods_writer.currency_style('USD')
 
    layer1 = ods_writer.merge_styles(ods_writer.style_bold, ods_writer.style_dividerline)
 
    layer2 = ods_writer.merge_styles(layer1, usd_style)
 
    style_name = layer2.getAttribute('name')
0 comments (0 inline, 0 general)