507 lines
24 KiB
Python
507 lines
24 KiB
Python
from __future__ import annotations
|
|
|
|
from .shared import (
|
|
ASSET_BUCKETS,
|
|
ASSET_BUCKET_ORDER,
|
|
Any,
|
|
LIABILITY_BUCKETS,
|
|
current_kst_date,
|
|
date,
|
|
db,
|
|
fail,
|
|
new_id,
|
|
normalize_date,
|
|
normalize_liquidity_bucket,
|
|
now_iso,
|
|
ok,
|
|
row_to_dict,
|
|
sqlite3,
|
|
)
|
|
|
|
def _valuation_policy() -> dict[str, Any]:
|
|
return {
|
|
"authority_sources": ["accounts.last_balance", "manual_assets"],
|
|
"snapshot_role": "historical_record_layer",
|
|
"formula": "asset_buckets - loan",
|
|
"asset_buckets": ASSET_BUCKETS,
|
|
"liability_buckets": LIABILITY_BUCKETS,
|
|
"missing_valuation_policy": "exclude_without_transaction_inference",
|
|
}
|
|
|
|
|
|
def _account_valuation_items(conn: sqlite3.Connection) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
|
|
rows = conn.execute("SELECT * FROM accounts WHERE active=1 ORDER BY name").fetchall()
|
|
included: list[dict[str, Any]] = []
|
|
excluded: list[dict[str, Any]] = []
|
|
invalid: list[dict[str, Any]] = []
|
|
for row in rows:
|
|
account = row_to_dict(row)
|
|
raw_bucket = str(account.get("liquidity_bucket") or "other")
|
|
try:
|
|
bucket = normalize_liquidity_bucket(raw_bucket)
|
|
except ValueError:
|
|
bucket = "other"
|
|
invalid.append({"id": account.get("id", ""), "name": account.get("name", ""), "liquidity_bucket": raw_bucket})
|
|
base = {
|
|
"id": account.get("id", ""),
|
|
"source_id": account.get("id", ""),
|
|
"source_type": "account",
|
|
"authority_source": "accounts.last_balance",
|
|
"name": account.get("name", ""),
|
|
"type": account.get("type", ""),
|
|
"liquidity_bucket": bucket,
|
|
"valuation_date": account.get("last_synced_at", ""),
|
|
"last_synced_at": account.get("last_synced_at", ""),
|
|
"source_note": account.get("memo", ""),
|
|
"memo": account.get("memo", ""),
|
|
}
|
|
balance = account.get("last_balance")
|
|
if balance is None:
|
|
excluded.append({**base, "reason": "missing_last_balance"})
|
|
continue
|
|
amount = int(balance)
|
|
included.append({**base, "amount": amount, "last_balance": amount})
|
|
return included, excluded, invalid
|
|
|
|
|
|
def _manual_asset_valuation_items(conn: sqlite3.Connection, active_only: bool = True) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
where = "WHERE active=1" if active_only else ""
|
|
rows = conn.execute(f"SELECT * FROM manual_assets {where} ORDER BY name").fetchall()
|
|
included: list[dict[str, Any]] = []
|
|
excluded: list[dict[str, Any]] = []
|
|
for row in rows:
|
|
asset = row_to_dict(row)
|
|
try:
|
|
bucket = normalize_liquidity_bucket(asset.get("liquidity_bucket"))
|
|
except ValueError:
|
|
bucket = "other"
|
|
base = {
|
|
"id": asset.get("id", ""),
|
|
"source_id": asset.get("id", ""),
|
|
"source_type": "manual_asset",
|
|
"authority_source": "manual_assets",
|
|
"name": asset.get("name", ""),
|
|
"liquidity_bucket": bucket,
|
|
"valuation_date": asset.get("valuation_date", ""),
|
|
"source_note": asset.get("source_note", ""),
|
|
"memo": asset.get("memo", ""),
|
|
"active": asset.get("active", True),
|
|
}
|
|
value = asset.get("valuation_amount")
|
|
if value is None:
|
|
excluded.append({**base, "reason": "missing_valuation_amount"})
|
|
continue
|
|
included.append({**base, "amount": int(value), "valuation_amount": int(value)})
|
|
return included, excluded
|
|
|
|
|
|
def _snapshot_rows_for_latest_run(conn: sqlite3.Connection, basis_date: str) -> tuple[list[sqlite3.Row], dict[str, Any]]:
|
|
rows = conn.execute(
|
|
"SELECT rowid AS _rowid, * FROM asset_snapshots WHERE basis_date=? ORDER BY created_at ASC, name ASC",
|
|
(basis_date,),
|
|
).fetchall()
|
|
if not rows:
|
|
return [], {}
|
|
run_rows = [row for row in rows if "snapshot_run_id" in row.keys() and row["snapshot_run_id"]]
|
|
if not run_rows:
|
|
return rows, {"snapshot_run_id": "", "is_correction": False, "correction_of_run_id": "", "correction_reason": "", "total_snapshot_rows_for_basis_date": len(rows)}
|
|
latest = sorted(run_rows, key=lambda row: (int(row["_rowid"]), row["closed_at"] or row["created_at"] or "", row["snapshot_run_id"] or ""))[-1]
|
|
latest_run_id = latest["snapshot_run_id"]
|
|
selected = [row for row in run_rows if row["snapshot_run_id"] == latest_run_id]
|
|
metadata = {
|
|
"snapshot_run_id": latest_run_id,
|
|
"is_correction": bool(latest["is_correction"]),
|
|
"correction_of_run_id": latest["correction_of_run_id"] or "",
|
|
"correction_reason": latest["correction_reason"] or "",
|
|
"closed_at": latest["closed_at"] or latest["created_at"] or "",
|
|
"total_snapshot_rows_for_basis_date": len(rows),
|
|
}
|
|
return selected, metadata
|
|
|
|
|
|
def _snapshot_items(conn: sqlite3.Connection, basis_date: str) -> list[dict[str, Any]]:
|
|
rows, _metadata = _snapshot_rows_for_latest_run(conn, basis_date)
|
|
items: list[dict[str, Any]] = []
|
|
for row in rows:
|
|
snap = row_to_dict(row)
|
|
try:
|
|
bucket = normalize_liquidity_bucket(snap.get("liquidity_bucket"))
|
|
except ValueError:
|
|
bucket = "other"
|
|
items.append(
|
|
{
|
|
"id": snap.get("id", ""),
|
|
"source_id": snap.get("source_id", ""),
|
|
"source_type": snap.get("source_type", ""),
|
|
"authority_source": "asset_snapshots",
|
|
"valuation_source": snap.get("valuation_source", ""),
|
|
"name": snap.get("name", ""),
|
|
"liquidity_bucket": bucket,
|
|
"amount": int(snap.get("amount") or 0),
|
|
"valuation_date": snap.get("basis_date", ""),
|
|
"source_note": snap.get("source_note", ""),
|
|
"memo": snap.get("memo", ""),
|
|
"snapshot_run_id": snap.get("snapshot_run_id", ""),
|
|
"is_correction": bool(snap.get("is_correction", False)),
|
|
"correction_of_run_id": snap.get("correction_of_run_id", ""),
|
|
"correction_reason": snap.get("correction_reason", ""),
|
|
}
|
|
)
|
|
return items
|
|
|
|
|
|
def _summarize_valuation_items(included_items: list[dict[str, Any]], excluded_items: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
|
bucket_totals = {bucket: 0 for bucket in ASSET_BUCKET_ORDER}
|
|
bucket_counts = {bucket: 0 for bucket in ASSET_BUCKET_ORDER}
|
|
for item in included_items:
|
|
bucket = normalize_liquidity_bucket(item.get("liquidity_bucket"))
|
|
amount = int(item.get("amount") or 0)
|
|
bucket_counts[bucket] += 1
|
|
bucket_totals[bucket] += amount
|
|
total_assets = sum(bucket_totals[bucket] for bucket in ASSET_BUCKETS)
|
|
total_liabilities = sum(abs(bucket_totals[bucket]) for bucket in LIABILITY_BUCKETS)
|
|
net_worth = total_assets - total_liabilities
|
|
buckets = []
|
|
for bucket in ASSET_BUCKET_ORDER:
|
|
amount = bucket_totals[bucket]
|
|
denominator = total_liabilities if bucket in LIABILITY_BUCKETS else total_assets
|
|
buckets.append(
|
|
{
|
|
"liquidity_bucket": bucket,
|
|
"amount": amount,
|
|
"account_count": sum(1 for item in included_items if item.get("source_type") == "account" and item.get("liquidity_bucket") == bucket),
|
|
"manual_asset_count": sum(1 for item in included_items if item.get("source_type") == "manual_asset" and item.get("liquidity_bucket") == bucket),
|
|
"item_count": bucket_counts[bucket],
|
|
"asset_share": round(amount / total_assets, 4) if bucket not in LIABILITY_BUCKETS and total_assets else None,
|
|
"liability_share": round(abs(amount) / total_liabilities, 4) if bucket in LIABILITY_BUCKETS and total_liabilities else None,
|
|
"share": round(abs(amount) / denominator, 4) if denominator else None,
|
|
}
|
|
)
|
|
return {
|
|
"buckets": buckets,
|
|
"bucket_totals": bucket_totals,
|
|
"total_assets": total_assets,
|
|
"total_liabilities": total_liabilities,
|
|
"net_worth": net_worth,
|
|
"included_items": included_items,
|
|
"excluded_items": excluded_items or [],
|
|
"valuation_policy": _valuation_policy(),
|
|
}
|
|
|
|
|
|
def create_manual_asset(name: str, valuation_amount: int | None, valuation_date: str, liquidity_bucket: str, source_note: str = "", memo: str = "", currency: str = "KRW", active: bool = True, db_path: str | None = None) -> dict[str, Any]:
|
|
if not name.strip():
|
|
return fail("validation_error", "manual asset name is required")
|
|
try:
|
|
bucket = normalize_liquidity_bucket(liquidity_bucket)
|
|
amount = int(valuation_amount) if valuation_amount is not None else None
|
|
vdate = normalize_date(valuation_date, default_today=True)
|
|
except Exception as e:
|
|
return fail("validation_error", str(e))
|
|
ts = now_iso()
|
|
asset_id = new_id("asset")
|
|
with db(db_path) as conn:
|
|
conn.execute(
|
|
"""INSERT INTO manual_assets(id,name,liquidity_bucket,valuation_amount,valuation_date,currency,source_note,memo,active,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?)""",
|
|
(asset_id, name.strip(), bucket, amount, vdate, currency, source_note, memo, int(active), ts, ts),
|
|
)
|
|
row = conn.execute("SELECT * FROM manual_assets WHERE id=?", (asset_id,)).fetchone()
|
|
return ok(manual_asset={**row_to_dict(row), "authority_source": "manual_assets"})
|
|
|
|
|
|
def list_manual_assets(active_only: bool = True, db_path: str | None = None) -> dict[str, Any]:
|
|
where = "WHERE active=1" if active_only else ""
|
|
with db(db_path) as conn:
|
|
rows = conn.execute(f"SELECT * FROM manual_assets {where} ORDER BY name").fetchall()
|
|
assets = [{**row_to_dict(row), "authority_source": "manual_assets"} for row in rows]
|
|
return ok(manual_assets=assets, count=len(assets), valuation_policy=_valuation_policy())
|
|
|
|
|
|
def update_manual_asset(asset_id: str, name: str | None = None, valuation_amount: int | None = None, valuation_date: str | None = None, liquidity_bucket: str | None = None, source_note: str | None = None, memo: str | None = None, currency: str | None = None, active: bool | None = None, db_path: str | None = None) -> dict[str, Any]:
|
|
allowed_updates: list[str] = []
|
|
params: list[Any] = []
|
|
try:
|
|
if name is not None:
|
|
if not name.strip():
|
|
return fail("validation_error", "manual asset name is required")
|
|
allowed_updates.append("name=?")
|
|
params.append(name.strip())
|
|
if valuation_amount is not None:
|
|
allowed_updates.append("valuation_amount=?")
|
|
params.append(int(valuation_amount))
|
|
if valuation_date is not None:
|
|
allowed_updates.append("valuation_date=?")
|
|
params.append(normalize_date(valuation_date, default_today=True))
|
|
if liquidity_bucket is not None:
|
|
allowed_updates.append("liquidity_bucket=?")
|
|
params.append(normalize_liquidity_bucket(liquidity_bucket))
|
|
if source_note is not None:
|
|
allowed_updates.append("source_note=?")
|
|
params.append(source_note)
|
|
if memo is not None:
|
|
allowed_updates.append("memo=?")
|
|
params.append(memo)
|
|
if currency is not None:
|
|
allowed_updates.append("currency=?")
|
|
params.append(currency or "KRW")
|
|
if active is not None:
|
|
allowed_updates.append("active=?")
|
|
params.append(int(bool(active)))
|
|
except Exception as e:
|
|
return fail("validation_error", str(e))
|
|
if not allowed_updates:
|
|
return fail("validation_error", "no fields to update")
|
|
allowed_updates.append("updated_at=?")
|
|
params.extend([now_iso(), asset_id])
|
|
with db(db_path) as conn:
|
|
cur = conn.execute(f"UPDATE manual_assets SET {', '.join(allowed_updates)} WHERE id=?", params)
|
|
if cur.rowcount == 0:
|
|
return fail("not_found", "manual asset not found")
|
|
row = conn.execute("SELECT * FROM manual_assets WHERE id=?", (asset_id,)).fetchone()
|
|
return ok(manual_asset={**row_to_dict(row), "authority_source": "manual_assets"})
|
|
|
|
|
|
def deactivate_manual_asset(asset_id: str, hard_delete: bool = False, db_path: str | None = None) -> dict[str, Any]:
|
|
with db(db_path) as conn:
|
|
if hard_delete:
|
|
cur = conn.execute("DELETE FROM manual_assets WHERE id=?", (asset_id,))
|
|
else:
|
|
cur = conn.execute("UPDATE manual_assets SET active=0, updated_at=? WHERE id=?", (now_iso(), asset_id))
|
|
if cur.rowcount == 0:
|
|
return fail("not_found", "manual asset not found")
|
|
row = None if hard_delete else conn.execute("SELECT * FROM manual_assets WHERE id=?", (asset_id,)).fetchone()
|
|
payload = {"manual_asset_id": asset_id, "deactivated": True, "hard_delete": hard_delete}
|
|
if row is not None:
|
|
payload["manual_asset"] = {**row_to_dict(row), "authority_source": "manual_assets"}
|
|
return ok(**payload)
|
|
|
|
|
|
def _snapshot_preview_from_conn(conn: sqlite3.Connection, basis: str) -> dict[str, Any]:
|
|
account_items, account_excluded, invalid_bucket_accounts = _account_valuation_items(conn)
|
|
manual_items, manual_excluded = _manual_asset_valuation_items(conn)
|
|
included = account_items + manual_items
|
|
excluded = account_excluded + manual_excluded
|
|
summary = _summarize_valuation_items(included, excluded)
|
|
existing_rows = conn.execute("SELECT COUNT(*) FROM asset_snapshots WHERE basis_date=?", (basis,)).fetchone()[0]
|
|
warnings: list[dict[str, Any]] = []
|
|
for item in excluded:
|
|
warnings.append({"code": item.get("reason", "excluded_item"), "source_type": item.get("source_type", ""), "source_id": item.get("source_id", ""), "name": item.get("name", "")})
|
|
for item in included:
|
|
item_date = str(item.get("valuation_date") or "")[:10]
|
|
if item_date and item_date < basis:
|
|
warnings.append({"code": "stale_valuation", "source_type": item.get("source_type", ""), "source_id": item.get("source_id", ""), "name": item.get("name", ""), "valuation_date": item_date, "basis_date": basis})
|
|
for item in invalid_bucket_accounts:
|
|
warnings.append({"code": "invalid_liquidity_bucket", **item})
|
|
if existing_rows:
|
|
warnings.append({"code": "existing_snapshot_rows", "basis_date": basis, "snapshot_row_count": int(existing_rows), "policy": "append_only_correction_required"})
|
|
return {
|
|
"basis_date": basis,
|
|
"source": "live_authority",
|
|
"is_closed_snapshot": False,
|
|
"snapshot_count": 0,
|
|
"existing_snapshot_row_count": int(existing_rows),
|
|
"would_be_correction": bool(existing_rows),
|
|
"warnings": warnings,
|
|
**summary,
|
|
}
|
|
|
|
|
|
def preview_asset_snapshot_close(basis_date: str = "", db_path: str | None = None) -> dict[str, Any]:
|
|
try:
|
|
basis = normalize_date(basis_date, default_today=True) if basis_date else current_kst_date().isoformat()
|
|
except Exception as e:
|
|
return fail("validation_error", str(e))
|
|
with db(db_path) as conn:
|
|
preview = _snapshot_preview_from_conn(conn, basis)
|
|
return ok(**preview)
|
|
|
|
|
|
def close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str = "", correction_reason: str = "", db_path: str | None = None) -> dict[str, Any]:
|
|
try:
|
|
basis = normalize_date(basis_date, default_today=True) if basis_date else current_kst_date().isoformat()
|
|
except Exception as e:
|
|
return fail("validation_error", str(e))
|
|
ts = now_iso()
|
|
run_id = new_id("snaprun")
|
|
with db(db_path) as conn:
|
|
preview = _snapshot_preview_from_conn(conn, basis)
|
|
included = preview["included_items"]
|
|
latest_rows, latest_meta = _snapshot_rows_for_latest_run(conn, basis)
|
|
is_correction = bool(preview["existing_snapshot_row_count"])
|
|
if is_correction and not correction_reason.strip():
|
|
return fail("validation_error", "correction_reason is required when closing a basis_date that already has snapshot rows", basis_date=basis, existing_snapshot_row_count=preview["existing_snapshot_row_count"])
|
|
correction_of_run_id = latest_meta.get("snapshot_run_id", "") if latest_rows else ""
|
|
snapshot_ids: list[str] = []
|
|
for item in included:
|
|
snapshot_id = new_id("snap")
|
|
row_note = source_note or item.get("source_note", "") or "month-end close"
|
|
row_memo = memo or item.get("memo", "")
|
|
conn.execute(
|
|
"""INSERT INTO asset_snapshots(id,basis_date,source_type,source_id,name,amount,currency,liquidity_bucket,valuation_source,source_note,memo,snapshot_run_id,correction_of_run_id,correction_reason,is_correction,closed_at,created_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
(
|
|
snapshot_id,
|
|
basis,
|
|
item.get("source_type", ""),
|
|
item.get("source_id", item.get("id", "")),
|
|
item.get("name", ""),
|
|
int(item.get("amount") or 0),
|
|
item.get("currency", "KRW"),
|
|
normalize_liquidity_bucket(item.get("liquidity_bucket")),
|
|
item.get("authority_source", item.get("source_type", "")),
|
|
row_note,
|
|
row_memo,
|
|
run_id,
|
|
correction_of_run_id,
|
|
correction_reason.strip(),
|
|
int(is_correction),
|
|
ts,
|
|
ts,
|
|
),
|
|
)
|
|
snapshot_ids.append(snapshot_id)
|
|
report_items = _snapshot_items(conn, basis)
|
|
summary = _summarize_valuation_items(report_items, [])
|
|
return ok(
|
|
basis_date=basis,
|
|
source="snapshot",
|
|
is_closed_snapshot=True,
|
|
snapshot_run_id=run_id,
|
|
correction_of_run_id=correction_of_run_id,
|
|
correction_reason=correction_reason.strip(),
|
|
is_correction=is_correction,
|
|
closed_at=ts,
|
|
inserted_count=len(snapshot_ids),
|
|
inserted_snapshot_ids=snapshot_ids,
|
|
warnings=preview["warnings"],
|
|
**summary,
|
|
)
|
|
|
|
|
|
def create_asset_snapshot(basis_date: str, source_type: str, source_id: str, name: str, amount: int | None, liquidity_bucket: str, valuation_source: str = "", source_note: str = "", memo: str = "", currency: str = "KRW", db_path: str | None = None) -> dict[str, Any]:
|
|
if source_type not in {"account", "manual_asset"}:
|
|
return fail("validation_error", "source_type must be one of ['account', 'manual_asset']")
|
|
if not name.strip():
|
|
return fail("validation_error", "snapshot item name is required")
|
|
try:
|
|
basis = normalize_date(basis_date, default_today=True)
|
|
bucket = normalize_liquidity_bucket(liquidity_bucket)
|
|
amount_int = int(amount) if amount is not None else None
|
|
except Exception as e:
|
|
return fail("validation_error", str(e))
|
|
ts = now_iso()
|
|
snapshot_id = new_id("snap")
|
|
with db(db_path) as conn:
|
|
conn.execute(
|
|
"""INSERT INTO asset_snapshots(id,basis_date,source_type,source_id,name,amount,currency,liquidity_bucket,valuation_source,source_note,memo,created_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
(snapshot_id, basis, source_type, source_id, name.strip(), amount_int, currency, bucket, valuation_source or source_type, source_note, memo, ts),
|
|
)
|
|
row = conn.execute("SELECT * FROM asset_snapshots WHERE id=?", (snapshot_id,)).fetchone()
|
|
return ok(asset_snapshot=row_to_dict(row))
|
|
|
|
|
|
def get_asset_snapshot_report(basis_date: str = "", db_path: str | None = None) -> dict[str, Any]:
|
|
basis = normalize_date(basis_date, default_today=True) if basis_date else current_kst_date().isoformat()
|
|
with db(db_path) as conn:
|
|
rows, close_metadata = _snapshot_rows_for_latest_run(conn, basis)
|
|
snapshots = _snapshot_items(conn, basis) if rows else []
|
|
if snapshots:
|
|
summary = _summarize_valuation_items(snapshots, [])
|
|
return ok(
|
|
basis_date=basis,
|
|
source="asset_snapshots",
|
|
is_closed_snapshot=True,
|
|
snapshot_count=len(snapshots),
|
|
close_metadata=close_metadata,
|
|
**summary,
|
|
)
|
|
account_items, account_excluded, invalid_bucket_accounts = _account_valuation_items(conn)
|
|
manual_items, manual_excluded = _manual_asset_valuation_items(conn)
|
|
included = account_items + manual_items
|
|
excluded = account_excluded + manual_excluded
|
|
summary = _summarize_valuation_items(included, excluded)
|
|
return ok(
|
|
basis_date=basis,
|
|
source="live_authority",
|
|
is_closed_snapshot=False,
|
|
snapshot_count=0,
|
|
invalid_bucket_accounts=invalid_bucket_accounts,
|
|
**summary,
|
|
)
|
|
|
|
|
|
def get_net_worth_timeseries(start_date: str = "", end_date: str = "", months: int = 12, db_path: str | None = None) -> dict[str, Any]:
|
|
try:
|
|
months_int = max(1, min(int(months), 120))
|
|
end_norm = normalize_date(end_date, default_today=True) if end_date else current_kst_date().isoformat()
|
|
if start_date:
|
|
start_norm = normalize_date(start_date)
|
|
else:
|
|
end_dt = date.fromisoformat(end_norm)
|
|
start_month = end_dt.month - months_int + 1
|
|
year = end_dt.year + (start_month - 1) // 12
|
|
month = (start_month - 1) % 12 + 1
|
|
start_norm = date(year, month, 1).isoformat()
|
|
except Exception as e:
|
|
return fail("validation_error", str(e))
|
|
with db(db_path) as conn:
|
|
basis_rows = conn.execute(
|
|
"""SELECT DISTINCT basis_date FROM asset_snapshots
|
|
WHERE basis_date>=? AND basis_date<=?
|
|
ORDER BY basis_date ASC""",
|
|
(start_norm, end_norm),
|
|
).fetchall()
|
|
points = []
|
|
for row in basis_rows:
|
|
basis = row["basis_date"]
|
|
items = _snapshot_items(conn, basis)
|
|
summary = _summarize_valuation_items(items, [])
|
|
points.append(
|
|
{
|
|
"basis_date": basis,
|
|
"total_assets": summary["total_assets"],
|
|
"total_liabilities": summary["total_liabilities"],
|
|
"net_worth": summary["net_worth"],
|
|
"source": "asset_snapshots",
|
|
"item_count": len(items),
|
|
}
|
|
)
|
|
return ok(start_date=start_norm, end_date=end_norm, months=months_int, points=points, count=len(points), valuation_policy=_valuation_policy())
|
|
|
|
|
|
def get_asset_liquidity_report(db_path: str | None = None) -> dict[str, Any]:
|
|
"""Summarize assets/liabilities from shared valuation authority sources.
|
|
|
|
Existing REQ-005 keys are preserved. When manual_assets is empty, totals and
|
|
source remain equivalent to the original accounts.last_balance behavior.
|
|
"""
|
|
with db(db_path) as conn:
|
|
account_items, account_excluded, invalid_bucket_accounts = _account_valuation_items(conn)
|
|
manual_items, manual_excluded = _manual_asset_valuation_items(conn)
|
|
included = account_items + manual_items
|
|
excluded = account_excluded + manual_excluded
|
|
summary = _summarize_valuation_items(included, excluded)
|
|
source_accounts = [item for item in included if item.get("source_type") == "account"]
|
|
source = "accounts.last_balance" if not manual_items and not manual_excluded else "accounts.last_balance+manual_assets"
|
|
return ok(
|
|
buckets=summary["buckets"],
|
|
accounts=source_accounts,
|
|
manual_assets=manual_items,
|
|
total_assets=summary["total_assets"],
|
|
total_liabilities=summary["total_liabilities"],
|
|
net_worth=summary["net_worth"],
|
|
missing_balance_accounts=[
|
|
{"id": item.get("id", ""), "name": item.get("name", ""), "liquidity_bucket": item.get("liquidity_bucket", "")}
|
|
for item in account_excluded
|
|
],
|
|
missing_valuation_items=excluded,
|
|
invalid_bucket_accounts=invalid_bucket_accounts,
|
|
manual_asset_count=len(manual_items),
|
|
included_items=included,
|
|
excluded_items=excluded,
|
|
valuation_policy=summary["valuation_policy"],
|
|
source=source,
|
|
)
|