dodil/erp-brick-gl

DODIL ERP brick: general ledger

tests/world.py

"""Small builders the tests share: a set of books (chart, company, account map, a year) and postings through the source route."""
from conftest import ACCOUNTANT, POSTER, api, must
from erp_platform.seam import derived_id

ACCOUNTS = [  # code, name, type, subtype
    ("1100", "Receivables", "asset", "receivable"),
    ("1200", "Bank", "asset", "bank"),
    ("1210", "Bank USD", "asset", "bank"),
    ("1400", "Prepaid", "asset", "other"),
    ("2100", "Accruals", "liability", "other"),
    ("2200", "VAT", "liability", "tax"),
    ("2300", "Payables", "liability", "payable"),
    ("3000", "Retained earnings", "equity", "retained_earnings"),
    ("4000", "Revenue", "income", "other"),
    ("4900", "FX gains", "income", "other"),
    ("6000", "Costs", "expense", "other"),
    ("6100", "Rent", "expense", "other"),
    ("6300", "Insurance", "expense", "other"),
    ("6400", "Software", "expense", "other"),
    ("6900", "FX losses", "expense", "other"),
    ("6990", "Rounding", "expense", "other"),
]
MAP = {"receivable": "1100", "revenue": "4000", "bank": "1200", "retained_earnings": "3000", "fx_gain": "4900", "fx_loss": "6900",
       "rounding": "6990", "bad_debt": "6000", "deposit": "2100", "accrual_balance": "2100", "prepaid": "1400"}


def account_id(code, chart="MAIN"):
    return derived_id("account", code)


def books(company="UK01", currency="GBP", chart="MAIN", year="FY2026", extra_map=None, who=ACCOUNTANT, open_year=True, accounts=True):
    """Chart MAIN with accounts, the account map (first rows, effective today), a company and its first year."""
    if not api("GET", f"/charts/{chart}/accounts", who)[0] == 200:
        must(api("POST", "/charts", who, {"chart_id": chart, "name": chart, "kind": "operational"}))
    if accounts:
        for code, name, typ, sub in ACCOUNTS:
            must(api("POST", "/accounts", who, {"chart_id": chart, "code": code, "name": name, "type": typ, "subtype": sub}))
    must(api("POST", "/companies", who, {"company_code": company, "name": company, "country": "GB", "functional_currency": currency,
                                         "chart_id": chart}))
    for purpose, code in {**MAP, **(extra_map or {})}.items():
        must(api("POST", "/account-map", who, {"chart_id": chart, "purpose": purpose, "account_id": str(account_id(code)),
                                               "valid_from": "2026-01-01"}))
    if open_year:
        must(api("POST", "/fiscal-years/open-year", who, {"fiscal_year": year, "company_code": company}))


def line(account=None, purpose=None, debit=None, credit=None, **kw):
    body = {}
    if account is not None:
        body["account_id"] = str(account_id(account)) if isinstance(account, str) and len(account) <= 6 else str(account)
    if purpose:
        body["purpose"] = purpose
    if debit is not None:
        body["debit"] = str(debit)
    if credit is not None:
        body["credit"] = str(credit)
    return {**body, **kw}


def post(source_document, event, on, lines, source_module="ar", currency="GBP", who=POSTER, **kw):
    body = {"source_module": source_module, "source_document": source_document, "event": event, "journal_date": on, "currency": currency,
            "lines": lines, **kw}
    body.setdefault("journal_id", str(derived_id(source_document, event, kw["ledger_id"]) if kw.get("ledger_id")
                                       else derived_id(source_document, event)))
    return api("POST", "/journal-entry/journals", who, body)


def invoice(doc="INV-1", on="2026-10-15", net="1000.00", vat="200.00", **kw):
    gross = f"{float(net) + float(vat):.2f}"
    return post(doc, "issue", on, [line(purpose="receivable", debit=gross), line(purpose="revenue", credit=net),
                                   {"account_id": str(account_id("2200")), "credit": vat}], **kw)


def close_through(period, company="UK01", who=ACCOUNTANT, first="2026-01"):
    """Close every month from `first` to `period` in order (a month closes only after the one before it)."""
    y, m = int(first[:4]), int(first[5:])
    while f"{y:04d}-{m:02d}" <= period:
        must(api("POST", f"/periods/{y:04d}-{m:02d}/close-period", who, {"company_code": company}))
        y, m = (y, m + 1) if m < 12 else (y + 1, 1)