Files @ 770b22f2f0e7
Branch filter:

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

Brett Smith
reports: Initial budget variance skeleton. RT#12680

This is a *very* rough initial draft of a report. As the docstring mentions,
it's basically counting on the user to provide rewrite rules to provide the
desired representation.

Long-term I'm hoping maybe we can standardize the program metadata enough,
or plan its replacement well enough, that this report can be written against
that directly. But that will take more planning about books structure, and
support from the plugin, before the report can be written that way.
"""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 (
    MetaKey,
    MetaValue,
    Transaction,
)

from typing import (
    MutableMapping,
    Optional,
    Pattern,
    Tuple,
)

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

    # chars is the set of characters we always accept in entity metadata:
    # letters, digits, and ASCII punctuation, except `-` and the Latin 1 supplement
    # (i.e., Roman letters with diacritics: áÁàÀâÂåÅäÄãà çÇ ðÐ ñÑ øØ ß etc.)
    # See the tests for specific cases.
    chars = r'\u0021-\u002c\u002e-\u007e\p{Letter}\p{Digit}--\p{Block=Latin_1_Supplement}'
    ENTITY_RE: Pattern[str] = regex.compile(f'^[{chars}][-{chars}]*$', regex.VERSION1)
    ANONYMOUS_RE: Pattern[str] = regex.compile(r'^[-_.?!\s]*$', regex.VERSION1)
    del chars

    def _check_entity(self,
                      meta: MutableMapping[MetaKey, MetaValue],
                      default: Optional[str]=None,
    ) -> Tuple[Optional[str], Optional[bool]]:
        entity = meta.get(self.METADATA_KEY, default)
        if entity is None:
            return None, None
        elif not isinstance(entity, str):
            return None, False
        elif self.ANONYMOUS_RE.match(entity):
            entity = 'Anonymous'
            meta[self.METADATA_KEY] = entity
            return entity, True
        else:
            return entity, self.ENTITY_RE.match(entity) is not None

    def run(self, txn: Transaction) -> errormod.Iter:
        if not self._run_on_txn(txn):
            return
        txn_entity, txn_entity_ok = self._check_entity(txn.meta, txn.payee)
        if txn_entity_ok is False:
            yield errormod.InvalidMetadataError(txn, self.METADATA_KEY, txn_entity)
        if txn_entity is txn.payee:
            txn.meta[self.METADATA_KEY] = txn.payee
        for post in data.Posting.from_txn(txn):
            if not post.account.is_under(
                    'Assets:Receivable',
                    'Expenses',
                    'Income',
                    'Liabilities:Payable',
            ):
                continue
            entity, entity_ok = self._check_entity(post.meta, txn_entity)
            if entity is txn_entity and entity is not None:
                pass
            elif not entity_ok:
                yield errormod.InvalidMetadataError(txn, self.METADATA_KEY, entity, post)