Files @ 072937eff508
Branch filter:

Location: NPO-Accounting/conservancy_beancount/conservancy_beancount/plugin/meta_entity.py

Brett Smith
books.Loader: New loading strategy.

The old loading strategy didn't load options, which yielded some
spurious errors. It also created awkward duplication of plugin
information in the code as well as the books.

Implement a new loading strategy that works by reading one of the
"main files" under the books/ subdirectory and includes entries
for additional FYs beyond that.

This is still not ideal in a lot of ways. In particular, Beancount can't
cache any results, causing any load to be slower than it theoretically could
be. I expect more commits to follow. But some of them might require
restructuring the books, and that should happen separately.
"""meta_entity - Validate entity metadata"""
# Copyright © 2020  Brett Smith
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

# Type stubs aren't available for regex.
# Fortunately, we're using it in a way that's API-compatible with the re
# module. We mitigate the lack of type stubs by providing type declarations
# for returned objects. This way, the only thing that isn't type checked are
# the calls to regex functions.
import regex  # type:ignore[import]

from . import core
from .. import data
from .. import errors as errormod
from ..beancount_types import (
    Transaction,
)

from typing import (
    Pattern,
)

class MetaEntity(core.TransactionHook):
    METADATA_KEY = 'entity'
    HOOK_GROUPS = frozenset(['posting', 'metadata', METADATA_KEY])

    # alnum is the set of characters we always accept in entity metadata:
    # letters and digits, minus the Latin 1 supplement (i.e., Roman letters
    # with diacritics: áÁàÀâÂåÅäÄãà çÇ ðÐ ñÑ øØ ß etc.)
    # See the tests for specific cases.
    alnum = r'\p{Letter}\p{Digit}--\p{Block=Latin_1_Supplement}'
    # A regexp that would be reasonably stricter would be:
    #   f'^[{alnum}][.{alnum}]*(?:-[.{alnum}])*$'
    # However, current producers fail that regexp in a few different ways.
    # See the tests for specific cases.
    ENTITY_RE: Pattern[str] = regex.compile(f'^[{alnum}][-.{alnum}]*$', regex.VERSION1)
    del alnum

    def run(self, txn: Transaction) -> errormod.Iter:
        if data.is_opening_balance_txn(txn):
            return
        txn_entity = txn.meta.get(self.METADATA_KEY, txn.payee)
        if txn_entity is None:
            txn_entity_ok = None
        elif isinstance(txn_entity, str):
            txn_entity_ok = bool(self.ENTITY_RE.match(txn_entity))
        else:
            txn_entity_ok = False
        if txn_entity_ok is False:
            yield errormod.InvalidMetadataError(txn, self.METADATA_KEY, txn_entity)
        for post in data.Posting.from_txn(txn):
            if not post.account.is_under(
                    'Assets:Receivable',
                    'Expenses',
                    'Income',
                    'Liabilities:Payable',
            ):
                continue
            entity = post.meta.get(self.METADATA_KEY)
            if entity is None:
                yield errormod.InvalidMetadataError(txn, self.METADATA_KEY, entity, post)
            elif entity is txn_entity:
                pass
            elif not self.ENTITY_RE.match(entity):
                yield errormod.InvalidMetadataError(txn, self.METADATA_KEY, entity, post)