dodil/erp-brick-gl
DODIL ERP brick: general ledger
src/brick_gl/application/inputs.py
"""The bounded inputs of this brick's use cases (BRICKS.md: an unbounded int or str is a promise the engine cannot keep).
Money is NUMERIC(18,2) as an exact string; a rate NUMERIC(19,6); a statistical quantity NUMERIC(19,4)."""
from datetime import date
from decimal import Decimal, InvalidOperation
from typing import Annotated, Any, Optional
from erp_platform.types import BigId, text
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, WithJsonSchema
Code = text(64, min_length=1)
Short = text(8, min_length=1)
Currency = text(3)
Reason = text(500, min_length=1)
Memo = text(2000)
# The doors check SIZE only. The shape of a code, a period, a year or a date is checked by the use case itself (brick.checked), AFTER
# the permission: a caller who may not do a thing is told 403, never what is wrong with the body they sent.
CompanyCode = text(4)
LedgerCode = text(8)
PeriodId = text(7)
FiscalYearName = text(6)
IsoDate = text(32)
Limit = Annotated[int, Field(ge=1, le=500)]
def _dec(v: Any, what: str, places: int, limit: Decimal, negative: bool) -> Decimal:
if isinstance(v, bool) or isinstance(v, float):
raise ValueError(f"{what} is an exact number: give it as a string such as \"100.00\"")
try:
d = v if isinstance(v, Decimal) else Decimal(str(v).strip())
except (InvalidOperation, ValueError):
raise ValueError(f"{v!r} is not a number") from None
if not d.is_finite():
raise ValueError(f"{what} is a finite number")
if d.as_tuple().exponent < -places:
raise ValueError(f"{what} has at most {places} decimal places")
if abs(d) >= limit:
raise ValueError(f"{what} is below {limit:,.0f}")
if d < 0 and not negative:
raise ValueError(f"{what} is zero or more")
return d
def _money(v: Any) -> Decimal:
return _dec(v, "an amount", 2, Decimal("1e16"), False)
def _money_signed(v: Any) -> Decimal:
return _dec(v, "an amount", 2, Decimal("1e16"), True)
def _fx(v: Any) -> Decimal:
d = _dec(v, "a rate", 6, Decimal("1e13"), False)
if d <= 0:
raise ValueError("a rate is a positive number")
return d
def _qty(v: Any) -> Decimal:
return _dec(v, "a key figure", 4, Decimal("1e15"), False)
def _exact(pattern: str, description: str):
return WithJsonSchema({"type": "string", "pattern": pattern, "description": description})
Money = Annotated[Decimal, BeforeValidator(_money), _exact(r"^[0-9]{1,16}(\.[0-9]{1,2})?$", "an amount, an exact decimal with up to 2 places (\"100.00\")")]
SignedMoney = Annotated[Decimal, BeforeValidator(_money_signed), _exact(r"^-?[0-9]{1,16}(\.[0-9]{1,2})?$", "an amount, an exact decimal with up to 2 places")]
FxRate = Annotated[Decimal, BeforeValidator(_fx), _exact(r"^[0-9]{1,13}(\.[0-9]{1,6})?$", "an exchange rate, an exact decimal with up to 6 places (\"0.790000\")")]
KeyFigure = Annotated[Decimal, BeforeValidator(_qty), _exact(r"^[0-9]{1,15}(\.[0-9]{1,4})?$", "a statistical key figure, up to 4 places")]
def day(v: Optional[str]) -> Optional[date]:
return None if v in (None, "") else date.fromisoformat(v)
class _In(BaseModel):
model_config = ConfigDict(extra="ignore")
class LineModel(_In):
"""One side of an entry. Name an account or a purpose; debit OR credit, never both."""
account_id: Optional[BigId] = None
purpose: Optional[Code] = None
qualifier: Optional[Code] = None
tax_code: Optional[Code] = None
bank_account_id: Optional[Code] = None
asset_class: Optional[Code] = None
debit: Optional[Money] = None
credit: Optional[Money] = None
memo: Optional[Memo] = None
bp_id: Optional[BigId] = None
cost_centre: Optional[Code] = None
profit_centre: Optional[Code] = None
partner_company: Optional[CompanyCode] = None
movement_type: Optional[Code] = None
against_document: Optional[text(200)] = None
ic_reference: Optional[text(200)] = None
asset_id: Optional[text(200)] = None
company_code: Optional[CompanyCode] = None
Lines = Annotated[list[LineModel], Field(max_length=20000)]