Source code for aoiro._ledger

from collections.abc import Callable, Sequence
from decimal import Decimal
from itertools import chain
from typing import Any, Literal, Protocol, TypeVar

import attrs
import pandas as pd
from account_codes_jp._common import SUNDRY

AccountSundry = Literal["諸口"]
Account = TypeVar("Account", bound=str)
Currency = TypeVar("Currency", bound=str)


class _LedgerLineBase(Protocol):
    date: pd.Timestamp
    """The date when the transaction occurred."""


[docs] class LedgerLine(_LedgerLineBase, Protocol[Account, Currency]): amount: Decimal """The amount. Must be non-negative.""" currency: Currency """The currency.""" debit_account: Account """The account written on the debit side.""" credit_account: Account """The account written on the credit side."""
[docs] class LedgerElement(Protocol[Account, Currency]): account: Account """The account.""" amount: Decimal """The amount.""" currency: Currency """The currency."""
[docs] class MultiLedgerLine(_LedgerLineBase, Protocol[Account, Currency]): debit: Sequence[LedgerElement[Account, Currency]] """The accounts and amounts on the debit side. Each amount needs to be non-negative.""" credit: Sequence[LedgerElement[Account, Currency]] """The accounts and amounts on the credit side. Each amount needs to be non-negative."""
[docs] class GeneralLedgerLine(_LedgerLineBase, Protocol[Account, Currency]): values: Sequence[LedgerElement[Account, Currency]] """The accounts and amounts. Amounts does not need to be non-negative."""
[docs] @attrs.frozen(kw_only=True) class LedgerLineImpl(LedgerLine[Account, Currency]): date: pd.Timestamp amount: Decimal = attrs.field() currency: Currency debit_account: Account credit_account: Account def __repr__(self) -> str: return ( f"{self.date} {self.amount} {self.currency} " f"{self.debit_account} / {self.credit_account}" ) @amount.validator def _validate_amount(self, attribute: Any, value: Decimal) -> None: if value < Decimal(0): raise ValueError("amount must be non-negative")
[docs] @attrs.frozen(kw_only=True) class LedgerElementImpl(LedgerElement[Account, Currency]): account: Account amount: Decimal currency: Currency def __repr__(self) -> str: return f"{self.account} {self.amount} {self.currency}"
[docs] @attrs.frozen(kw_only=True, auto_detect=True) class MultiLedgerLineImpl(MultiLedgerLine[Account, Currency]): date: pd.Timestamp debit: Sequence[LedgerElement[Account, Currency]] = attrs.field() credit: Sequence[LedgerElement[Account, Currency]] = attrs.field() def __repr__(self) -> str: date = pd.Series([self.date], name="date") debit = pd.DataFrame( self.debit, columns=["debit_account", "amount", "currency"] ) credit = pd.DataFrame( self.credit, columns=["credit_account", "amount", "currency"] ) return ( pd.concat([date, debit, credit], axis=1) .replace({pd.NaT: ""}) .fillna("") .to_string(index=False, header=False) ) @debit.validator def _validate_debit( self, attribute: Any, value: Sequence[LedgerElement[Account, Currency]] ) -> None: if any(el.amount < Decimal(0) for el in value): raise ValueError("amount must be non-negative") @credit.validator def _validate_credit( self, attribute: Any, value: Sequence[LedgerElement[Account, Currency]] ) -> None: if any(el.amount < Decimal(0) for el in value): raise ValueError("amount must be non-negative")
[docs] @attrs.frozen(kw_only=True, auto_detect=True) class GeneralLedgerLineImpl(GeneralLedgerLine[Account, Currency]): date: pd.Timestamp values: Sequence[LedgerElement[Account, Currency]]
[docs] def generalledger_line_to_multiledger_line[Account: str, Currency: str]( line: GeneralLedgerLine[Account, Currency], is_debit: Callable[[Account], bool], / ) -> MultiLedgerLine[Account, Currency]: """ Convert a GeneralLedgerLine to a MultiLedgerLine. Parameters ---------- line : GeneralLedgerLine[Account, Currency] The GeneralLedgerLine to convert. is_debit : Callable[[Account], bool] Whether the account is a debit account. Returns ------- MultiLedgerLine[Account, Currency] The converted MultiLedgerLine. """ debit = [] credit = [] for el in line.values: if is_debit(el.account) == (el.amount > 0): debit.append( LedgerElementImpl( account=el.account, amount=abs(el.amount), currency=el.currency ) ) else: credit.append( LedgerElementImpl( account=el.account, amount=abs(el.amount), currency=el.currency ) ) return MultiLedgerLineImpl(date=line.date, debit=debit, credit=credit)
[docs] def multiledger_line_to_generalledger_line[Account: str, Currency: str]( line: MultiLedgerLine[Account, Currency], / ) -> GeneralLedgerLine[Account, Currency]: """ Convert a MultiLedgerLine to a GeneralLedgerLine. Parameters ---------- line : MultiLedgerLine[Account, Currency] The MultiLedgerLine to convert. Returns ------- GeneralLedgerLine[Account, Currency] The converted GeneralLedgerLine. """ return GeneralLedgerLineImpl( date=line.date, values=[*line.debit, *line.credit], )
[docs] def multiledger_line_to_ledger_line[Account: str, Currency: str]( line: MultiLedgerLine[Account, Currency], / ) -> Sequence[LedgerLine[Account | AccountSundry, Currency]]: """ Convert a MultiLedgerLine to a list of LedgerLine. Parameters ---------- line : MultiLedgerLine[Account, Currency] The MultiLedgerLine to convert. Returns ------- Sequence[LedgerLine[Account | AccountSundry, Currency]] The converted LedgerLines. """ if ( len(line.debit) == len(line.credit) == 1 and line.debit[0].amount == line.credit[0].amount and line.debit[0].currency == line.credit[0].currency ): return [ LedgerLineImpl( date=line.date, amount=line.debit[0].amount, currency=line.debit[0].currency, debit_account=line.debit[0].account, credit_account=line.credit[0].account, ) ] return [ LedgerLineImpl( date=line.date, amount=el.amount, currency=el.currency, debit_account=el.account, credit_account=SUNDRY, ) for el in line.debit ] + [ LedgerLineImpl( date=line.date, amount=el.amount, currency=el.currency, debit_account=SUNDRY, credit_account=el.account, ) for el in line.credit ]
[docs] def generalledger_to_multiledger[Account: str, Currency: str]( lines: Sequence[GeneralLedgerLine[Account, Currency]], is_debit: Callable[[Account], bool], ) -> Sequence[MultiLedgerLine[Account, Currency]]: """ Convert a GeneralLedger to a MultiLedger. Parameters ---------- lines : Sequence[GeneralLedgerLine[Account, Currency]] The GeneralLedger to convert. is_debit : Callable[[Account], bool] Whether the account is a debit account. Returns ------- Sequence[MultiLedgerLine[Account, Currency]] The converted MultiLedger. """ return [generalledger_line_to_multiledger_line(line, is_debit) for line in lines]
[docs] def multiledger_to_generalledger[Account: str, Currency: str]( lines: Sequence[MultiLedgerLine[Account, Currency]], ) -> Sequence[GeneralLedgerLine[Account, Currency]]: """ Convert a MultiLedger to a GeneralLedger. Parameters ---------- lines : Sequence[MultiLedgerLine[Account, Currency]] The MultiLedger to convert. Returns ------- Sequence[GeneralLedgerLine[Account, Currency]] The converted GeneralLedger. """ return [multiledger_line_to_generalledger_line(line) for line in lines]
[docs] def multiledger_to_ledger[Account: str, Currency: str]( lines: Sequence[MultiLedgerLine[Account, Currency]], ) -> Sequence[LedgerLine[Account | AccountSundry, Currency]]: """ Convert a MultiLedger to a Ledger. Parameters ---------- lines : Sequence[MultiLedgerLine[Account, Currency]] The MultiLedger to convert. Returns ------- Sequence[LedgerLine[Account | AccountSundry, Currency]] The converted Ledger. """ return list(chain(*[multiledger_line_to_ledger_line(line) for line in lines]))