Changeset - ca38e4517846
[Not reviewed]
0 2 0
Brett Smith - 4 years ago 2021-01-29 19:26:54
brettcsmith@brettcsmith.org
reports: Add BaseODS.row_count() method.
2 files changed with 15 insertions and 0 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/core.py
Show inline comments
...
 
@@ -1217,256 +1217,262 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 
        """
 
        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',
 
        )
 

	
 
        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_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_total = self.border_style(Border.TOP, '1pt')
 
        self.style_endtotal = self.border_style(Border.TOP | Border.BOTTOM, '1pt')
 
        self.style_bottomline = self.merge_styles(
 
            self.style_total,
 
            self.border_style(Border.BOTTOM, '2pt', 'double'),
 
        )
 

	
 
    ### Properties
 

	
 
    def set_common_properties(self,
 
                              repo: Optional[git.Repo]=None,
 
                              command: Optional[Sequence[str]]=sys.argv,
 
    ) -> None:
 
        if repo is None:
 
            git_shahex = '<none>'
 
            git_dirty = True
 
        else:
 
            git_shahex = repo.head.commit.hexsha
 
            git_dirty = repo.is_dirty()
 
        self.set_custom_property('GitSHA', git_shahex)
 
        self.set_custom_property('GitDirty', git_dirty, 'boolean')
 
        if command is not None:
 
            command_s = ' '.join(shlex.quote(s) for s in command)
 
            self.set_custom_property('ReportCommand', command_s)
 

	
 
    def set_custom_property(self,
 
                            name: str,
 
                            value: Any,
 
                            valuetype: Optional[str]=None,
 
    ) -> odf.meta.UserDefined:
 
        if valuetype is None:
 
            if isinstance(value, bool):
 
                valuetype = 'boolean'
 
            elif isinstance(value, (datetime.date, datetime.datetime)):
 
                valuetype = 'date'
 
            elif isinstance(value, (int, float, Decimal)):
 
                valuetype = 'float'
 
        if not isinstance(value, str):
 
            if valuetype == 'boolean':
 
                value = 'true' if value else 'false'
 
            elif valuetype == 'date':
 
                value = value.isoformat()
 
            else:
 
                value = str(value)
 
        retval = self.ensure_child(self.document.meta, odf.meta.UserDefined, name=name)
 
        if valuetype is None:
 
            try:
 
                retval.removeAttribute('valuetype')
 
            except KeyError:
 
                pass
 
        else:
 
            retval.setAttribute('valuetype', valuetype)
 
        retval.childNodes.clear()
 
        retval.addText(value)
 
        return retval
 

	
 
    def set_properties(self, *,
 
                       created: Optional[datetime.datetime]=None,
 
                       generator: str='conservancy_beancount',
 
    ) -> None:
 
        if created is None:
 
            created = datetime.datetime.now()
 
        created_elem = self.ensure_child(self.document.meta, odf.meta.CreationDate)
 
        created_elem.childNodes.clear()
 
        created_elem.addText(created.isoformat())
 
        generator_elem = self.ensure_child(self.document.meta, odf.meta.Generator)
 
        generator_elem.childNodes.clear()
 
        generator_elem.addText(f'{generator}/{cliutil.VERSION} {TOOLSVERSION}')
 

	
 
    ### 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 row_count(self, sheet: Optional[odf.table.Table]=None) -> int:
 
        if sheet is None:
 
            sheet = self.sheet
 
        TableRow = odf.table.TableRow
 
        return sum(1 for cell in sheet.childNodes if cell.isInstanceOf(TableRow))
 

	
 
    def balance_cell(self, balance: Balance, **attrs: Any) -> odf.table.TableCell:
 
        balance = balance.clean_copy() or balance
 
        balance_currency_count = len(balance)
 
        if balance_currency_count == 0:
 
            return self.float_cell(0, **attrs)
 
        elif balance_currency_count == 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, get_commodity_format(
 
                self.locale, currency, None, 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:
 
        if 'stylename' not in attrs:
 
            attrs['stylename'] = self.currency_style(amount.currency)
 
        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]]:
 
        for href in links:
 
            if href is None:
 
                continue
 
            elif self.rt_wrapper is not None:
 
                rt_ids = self.rt_wrapper.parse(href)
 
                rt_href = rt_ids and self.rt_wrapper.url(*rt_ids)
 
            else:
 
                rt_ids = None
 
                rt_href = None
 
            if rt_ids is None or rt_href is None:
 
                # '..' pops the ODS filename off the link path. In other words,
 
                # make the link relative to the directory the ODS is in.
 
                href_path = Path('..', href)
 
                href = urlparse.quote(str(href_path))
 
                text = href_path.name
 
            else:
 
                rt_path = urlparse.urlparse(rt_href).path
 
                if rt_path.endswith('/Ticket/Display.html'):
 
                    text = rtutil.RT.unparse(*rt_ids)
 
                else:
 
                    text = urlparse.unquote(Path(rt_path).name)
 
                href = rt_href
 
            yield (href, text)
 

	
 
    def meta_links_cell(self, links: Iterable[Optional[str]], **attrs: Any) -> odf.table.TableCell:
 
        return self.multilink_cell(self._meta_link_pairs(links), **attrs)
 

	
 
    def multiline_cell(self, lines: Iterable[Any], **attrs: Any) -> odf.table.TableCell:
 
        cell = odf.table.TableCell(valuetype='string', **attrs)
 
        for line in lines:
 
            cell.addElement(odf.text.P(text=str(line)))
 
        return cell
 

	
 
    def multilink_cell(self, links: Iterable[LinkType], **attrs: Any) -> odf.table.TableCell:
 
        cell = odf.table.TableCell(valuetype='string', **attrs)
 
        for link in links:
 
            if isinstance(link, tuple):
 
                href, text = link
 
            else:
 
                href = link
 
                text = None
 
            cell.addElement(odf.text.P())
 
            cell.lastChild.addElement(odf.text.A(
 
                type='simple', href=href, text=text or href,
 
            ))
 
        return cell
 

	
 
    def string_cell(self, text: str, **attrs: Any) -> odf.table.TableCell:
 
        cell = odf.table.TableCell(valuetype='string', **attrs)
 
        cell.addElement(odf.text.P(text=text))
 
        return cell
 

	
 
    def write_row(self, row: RT) -> None:
 
        """Write a single row of input data to the spreadsheet
 

	
 
        This default implementation adds a single row to the spreadsheet,
 
        with one cell per element of the row. The type of each element
 
        determines what kind of cell is created.
 

	
 
        This implementation will help get you started, but you'll probably
 
        want to override it to specify styles.
 
        """
 
        out_row = odf.table.TableRow()
 
        for cell_source in row:
 
            if isinstance(cell_source, (int, float, Decimal)):
 
                cell = self.float_cell(cell_source)
 
            else:
 
                cell = self.string_cell(cell_source)
 
            out_row.addElement(cell)
 
        self.sheet.addElement(out_row)
 

	
 
    def save_file(self, out_file: BinaryIO) -> None:
 
        self.document.write(out_file)
 

	
 
    def save_path(self, path: Path, mode: str='w') -> None:
 
        with path.open(f'{mode}b') as out_file:
 
            out_file = cast(BinaryIO, out_file)
 
            self.save_file(out_file)
 

	
 

	
 
def account_balances(
 
        groups: Mapping[data.Account, PeriodPostings],
 
        order: Optional[Sequence[str]]=None,
 
) -> Iterator[Tuple[str, Balance]]:
 
    """Iterate account balances over a date range
 

	
tests/test_reports_spreadsheet.py
Show inline comments
...
 
@@ -401,256 +401,265 @@ def test_ods_writer_merge_styles_caches(ods_writer):
 
def test_ods_writer_layer_merge_styles(ods_writer):
 
    usd_style = ods_writer.currency_style('USD')
 
    border_style = ods_writer.border_style(core.Border.BOTTOM)
 
    layer1 = ods_writer.merge_styles(ods_writer.style_bold, border_style)
 
    layer2 = ods_writer.merge_styles(layer1, usd_style)
 
    style_name = layer2.getAttribute('name')
 
    assert style_name.count('Merge_') == 1
 
    actual = get_child(
 
        ods_writer.document.styles,
 
        odf.style.Style,
 
        name=style_name,
 
    )
 
    assert actual is layer2
 
    assert actual.getAttribute('family') == 'table-cell'
 
    assert actual.getAttribute('datastylename') == usd_style.getAttribute('datastylename')
 
    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_one_style(ods_writer):
 
    actual = ods_writer.merge_styles(None, ods_writer.style_bold)
 
    assert actual is ods_writer.style_bold
 

	
 
def test_ods_writer_merge_no_styles(ods_writer):
 
    assert ods_writer.merge_styles() is None
 

	
 
def test_ods_writer_merge_nonexistent_style(ods_writer):
 
    name = 'Non Existent Style'
 
    with pytest.raises(ValueError, match=repr(name)):
 
        ods_writer.merge_styles(ods_writer.style_bold, name)
 

	
 
def test_ods_writer_merge_conflicting_styles(ods_writer):
 
    sources = [ods_writer.currency_style(code) for code in ['USD', 'EUR']]
 
    with pytest.raises(ValueError, match='conflicting datastylename'):
 
        ods_writer.merge_styles(*sources)
 

	
 
def test_ods_writer_date_style(ods_writer):
 
    data_style_name = ods_writer.style_date.getAttribute('datastylename')
 
    actual = get_child(
 
        ods_writer.document.styles,
 
        odf.style.Style,
 
        family='table-cell',
 
        datastylename=data_style_name,
 
    )
 
    assert actual is ods_writer.style_date
 
    data_style = get_child(
 
        ods_writer.document.styles,
 
        odf.number.DateStyle,
 
        name=data_style_name,
 
    )
 
    assert len(data_style.childNodes) == 5
 
    year, t1, month, t2, day = data_style.childNodes
 
    assert year.qname[1] == 'year'
 
    assert year.getAttribute('style') == 'long'
 
    assert get_text(t1) == '-'
 
    assert month.qname[1] == 'month'
 
    assert month.getAttribute('style') == 'long'
 
    assert get_text(t2) == '-'
 
    assert day.qname[1] == 'day'
 
    assert day.getAttribute('style') == 'long'
 

	
 
@pytest.mark.parametrize('method_name,split_name,side_name', [
 
    ('lock_first_row', 'Vertical', 'Bottom'),
 
    ('lock_first_column', 'Horizontal', 'Right'),
 
])
 
def test_ods_lock_first_cells(ods_writer, method_name, split_name, side_name):
 
    getattr(ods_writer, method_name)()
 
    view_settings = get_child(
 
        ods_writer.document.settings,
 
        odf.config.ConfigItemSet,
 
        name='ooo:view-settings',
 
    )
 
    views = get_child(view_settings, odf.config.ConfigItemMapIndexed, name='Views')
 
    view1 = get_child(views, odf.config.ConfigItemMapEntry, index=0)
 
    config_map = get_child(view1, odf.config.ConfigItemMapNamed, name='Tables')
 
    sheet_name = ods_writer.sheet.getAttribute('name')
 
    config_entry = get_child(config_map, odf.config.ConfigItemMapEntry, name=sheet_name)
 
    for name, ctype, value in [
 
            (f'Position{side_name}', 'int', '1'),
 
            (f'{split_name}SplitMode', 'short', '2'),
 
            (f'{split_name}SplitPosition', 'short', '1'),
 
    ]:
 
        child = get_child(config_entry, odf.config.ConfigItem, name=name)
 
        assert child.getAttribute('type') == ctype
 
        assert child.firstChild.data == value
 

	
 
@pytest.mark.parametrize('arg', [
 
    None,
 
    'Target Sheet',
 
    odf.table.Table(name='Target Sheet'),
 
])
 
def test_ods_set_open_sheet(ods_writer, arg):
 
    ods_writer.use_sheet('Start Sheet' if arg else 'Target Sheet')
 
    ods_writer.set_open_sheet(arg)
 
    view_settings = get_child(
 
        ods_writer.document.settings,
 
        odf.config.ConfigItemSet,
 
        name='ooo:view-settings',
 
    )
 
    views = get_child(view_settings, odf.config.ConfigItemMapIndexed, name='Views')
 
    view1 = get_child(views, odf.config.ConfigItemMapEntry, index=0)
 
    actual = get_child(view1, odf.config.ConfigItem, name='ActiveTable')
 
    assert actual.text == 'Target Sheet'
 

	
 
@pytest.mark.parametrize('style_name', XML_NAMES_LIST)
 
def test_ods_writer_add_row(ods_writer, style_name):
 
    cell1 = ods_writer.string_cell('one')
 
    cell2 = ods_writer.float_cell(42.0)
 
    row = ods_writer.add_row(cell1, cell2, defaultcellstylename=style_name)
 
    assert ods_writer.sheet.lastChild is row
 
    assert row.getAttribute('defaultcellstylename') == style_name
 
    assert row.firstChild is cell1
 
    assert row.lastChild is cell2
 

	
 
def test_ods_writer_add_row_single_cell(ods_writer):
 
    cell = ods_writer.multilink_cell(LINK_CELL_DATA[:1])
 
    row = ods_writer.add_row(cell)
 
    assert ods_writer.sheet.lastChild is row
 
    assert row.firstChild is cell
 
    assert row.lastChild is cell
 

	
 
def test_ods_writer_add_row_empty(ods_writer):
 
    row = ods_writer.add_row(stylename='blank')
 
    assert ods_writer.sheet.lastChild is row
 
    assert row.firstChild is None
 
    assert row.getAttribute('stylename') == 'blank'
 

	
 
@pytest.mark.parametrize('col_count', range(3))
 
def test_ods_writer_row_count(ods_writer, col_count):
 
    for _ in range(col_count):
 
        ods_writer.sheet.addElement(odf.table.TableColumn())
 
    assert ods_writer.row_count() == 0
 
    for expected in range(1, 4):
 
        ods_writer.sheet.addElement(odf.table.TableRow())
 
        assert ods_writer.row_count() == expected
 

	
 
def test_ods_writer_balance_cell_empty(ods_writer):
 
    balance = core.Balance()
 
    cell = ods_writer.balance_cell(balance)
 
    assert cell.value_type != 'string'
 
    assert float(cell.value) == 0
 

	
 
def test_ods_writer_balance_cell_single_currency(ods_writer):
 
    number = 250
 
    currency = 'EUR'
 
    balance = core.Balance([testutil.Amount(number, currency)])
 
    cell = ods_writer.balance_cell(balance)
 
    assert cell.value_type == 'currency'
 
    assert Decimal(cell.value) == number
 
    assert cell.text == babel.numbers.format_currency(
 
        number, currency, locale=EN_US, format_type='accounting',
 
    )
 

	
 
def test_ods_writer_balance_cell_multi_currency(ods_writer):
 
    amounts = [testutil.Amount(num, code) for num, code in [
 
        (2500, 'RUB'),
 
        (3500, 'BRL'),
 
    ]]
 
    balance = core.Balance(amounts)
 
    cell = ods_writer.balance_cell(balance)
 
    assert cell.text == '2,500.00 RUB\0R$3,500.00'
 

	
 
@pytest.mark.parametrize('cell_source,style_name', testutil.combine_values(
 
    CURRENCY_CELL_DATA,
 
    XML_NAMES,
 
))
 
def test_ods_writer_currency_cell(ods_writer, cell_source, style_name):
 
    cell = ods_writer.currency_cell(cell_source, stylename=style_name)
 
    number, currency = cell_source
 
    assert cell.getAttribute('valuetype') == 'currency'
 
    assert cell.getAttribute('value') == str(number)
 
    assert cell.getAttribute('stylename') == style_name
 
    expected = babel.numbers.format_currency(
 
        number, currency, locale=EN_US, format_type='accounting',
 
    )
 
    assert get_text(cell) == expected
 

	
 
@pytest.mark.parametrize('currency', [
 
    'EUR',
 
    'CHF',
 
    'GBP',
 
])
 
def test_ods_writer_currency_cell_default_style(ods_writer, currency):
 
    amount = testutil.Amount(1000, currency)
 
    expected_stylename = ods_writer.currency_style(currency).getAttribute('name')
 
    cell = ods_writer.currency_cell(amount)
 
    assert cell.getAttribute('valuetype') == 'currency'
 
    assert cell.getAttribute('value') == '1000'
 
    assert cell.getAttribute('stylename') == expected_stylename
 

	
 
@pytest.mark.parametrize('date,style_name', testutil.combine_values(
 
    [datetime.date(1980, 2, 5), datetime.date(2030, 10, 30)],
 
    XML_NAMES_LIST,
 
))
 
def test_ods_writer_date_cell(ods_writer, date, style_name):
 
    if style_name is None:
 
        expect_style = ods_writer.style_date.getAttribute('name')
 
        cell = ods_writer.date_cell(date)
 
    else:
 
        expect_style = style_name
 
        cell = ods_writer.date_cell(date, stylename=style_name)
 
    date_s = date.isoformat()
 
    assert cell.getAttribute('valuetype') == 'date'
 
    assert cell.getAttribute('datevalue') == date_s
 
    assert cell.getAttribute('stylename') == expect_style
 
    assert get_text(cell) == date_s
 

	
 
@pytest.mark.parametrize('cell_source,style_name', testutil.combine_values(
 
    NUMERIC_CELL_DATA,
 
    XML_NAMES,
 
))
 
def test_ods_writer_float_cell(ods_writer, cell_source, style_name):
 
    cell = ods_writer.float_cell(cell_source, stylename=style_name)
 
    assert cell.getAttribute('valuetype') == 'float'
 
    assert cell.getAttribute('stylename') == style_name
 
    expected = str(cell_source)
 
    assert cell.getAttribute('value') == expected
 
    assert get_text(cell) == expected
 

	
 
def test_ods_writer_meta_links_cell(ods_writer):
 
    rt_client = testutil.RTClient()
 
    ods_writer.rt_wrapper = rtutil.RT(rt_client)
 
    rt_url = rt_client.DEFAULT_URL[:-10]
 
    meta_links = [
 
        'rt://ticket/1',
 
        'rt://ticket/2/attachments/9',
 
        'rt:1/5',
 
        'Invoices/0123.pdf',
 
        'Invoice #789.pdf',
 
    ]
 
    cell = ods_writer.meta_links_cell(meta_links, stylename='meta1')
 
    assert cell.getAttribute('valuetype') == 'string'
 
    assert cell.getAttribute('stylename') == 'meta1'
 
    children = iter(get_children(cell, odf.text.A))
 
    child = next(children)
 
    assert child.getAttribute('type') == 'simple'
 
    expect_url = f'{rt_url}/Ticket/Display.html?id=1'
 
    assert child.getAttribute('href') == expect_url
 
    assert get_text(child) == 'rt:1'
 
    child = next(children)
 
    assert child.getAttribute('type') == 'simple'
 
    expect_url = f'{rt_url}/Ticket/Display.html?id=2#txn-7'
 
    assert child.getAttribute('href') == expect_url
 
    assert get_text(child) == 'rt:2/9'
 
    child = next(children)
 
    assert child.getAttribute('type') == 'simple'
 
    expect_url = f'{rt_url}/Ticket/Attachment/1/5/photo.jpg'
 
    assert child.getAttribute('href') == expect_url
 
    assert get_text(child) == 'photo.jpg'
 
    child = next(children)
 
    assert child.getAttribute('type') == 'simple'
 
    expect_url = f'../{meta_links[3]}'
 
    assert child.getAttribute('href') == expect_url
 
    assert get_text(child) == '0123.pdf'
 
    child = next(children)
 
    assert child.getAttribute('type') == 'simple'
 
    assert child.getAttribute('href') == '../Invoice%20%23789.pdf'
 
    assert get_text(child) == 'Invoice #789.pdf'
 

	
 
def test_ods_writer_multiline_cell(ods_writer):
 
    cell = ods_writer.multiline_cell(iter(STRING_CELL_DATA))
 
    assert cell.getAttribute('valuetype') == 'string'
 
    children = get_children(cell, odf.text.P)
 
    for expected, child in itertools.zip_longest(STRING_CELL_DATA, children):
0 comments (0 inline, 0 general)