[verified] Implement account valuation history for issue 12
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
from .account_valuation_service import _public_candidate
|
||||
|
||||
from .shared import (
|
||||
ASSET_BUCKETS,
|
||||
ASSET_BUCKET_ORDER,
|
||||
@@ -29,19 +34,36 @@ def _valuation_policy() -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _account_valuation_items(conn: sqlite3.Connection) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
def _evidence_state_hash(item: dict[str, Any]) -> str:
|
||||
"""Bind idempotency to evidence state without exposing sensitive text."""
|
||||
fields = (
|
||||
"source_type", "source_id", "name", "amount", "currency",
|
||||
"liquidity_bucket", "authority_source", "valuation_id",
|
||||
"valuation_date", "verification_status", "source_note", "memo",
|
||||
"active", "carried_forward",
|
||||
)
|
||||
payload = {key: item.get(key) for key in fields}
|
||||
return hashlib.sha256(
|
||||
json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def _account_valuation_items(conn: sqlite3.Connection, basis_date: str = "", selection_policy: str = "exact") -> 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)
|
||||
account_type = str(account.get("type") or "other").lower()
|
||||
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})
|
||||
if account_type == "loan":
|
||||
bucket = "loan"
|
||||
base = {
|
||||
"id": account.get("id", ""),
|
||||
"source_id": account.get("id", ""),
|
||||
@@ -56,6 +78,62 @@ def _account_valuation_items(conn: sqlite3.Connection) -> tuple[list[dict[str, A
|
||||
"memo": account.get("memo", ""),
|
||||
}
|
||||
balance = account.get("last_balance")
|
||||
if basis_date:
|
||||
valuation_rows = conn.execute(
|
||||
"SELECT * FROM account_valuations WHERE account_id=? AND currency=? ORDER BY basis_date DESC, recorded_at DESC, id DESC",
|
||||
(account["id"], account.get("currency") or "KRW"),
|
||||
).fetchall()
|
||||
leaves = [v for v in valuation_rows if conn.execute("SELECT 1 FROM account_valuations WHERE supersedes_id=?", (v["id"],)).fetchone() is None]
|
||||
if account_type == "investment":
|
||||
# Older imports recorded investment balances as cash_balance;
|
||||
# keep that as a compatibility fallback after the canonical
|
||||
# total_value/balance purposes.
|
||||
purposes = ("total_value", "balance", "cash_balance")
|
||||
elif account_type == "loan":
|
||||
purposes = ("loan_balance", "loan", "balance")
|
||||
else:
|
||||
purposes = ("cash_balance", "balance")
|
||||
selected = None
|
||||
selected_purpose = None
|
||||
candidate_rows = []
|
||||
for purpose in purposes:
|
||||
scoped = [v for v in leaves if v["valuation_kind"] == purpose]
|
||||
if selection_policy == "exact":
|
||||
verified = [v for v in scoped if v["verification_status"] == "verified" and v["basis_date"] == basis_date]
|
||||
else:
|
||||
eligible = [v for v in scoped if v["verification_status"] == "verified" and v["basis_date"] <= basis_date]
|
||||
latest = max((v["basis_date"] for v in eligible), default="")
|
||||
verified = [v for v in eligible if v["basis_date"] == latest]
|
||||
if len(verified) > 1:
|
||||
invalid.append({**base, "reason": "ambiguous_valuation", "valuation_kind": purpose})
|
||||
verified = []
|
||||
break
|
||||
if verified:
|
||||
selected, selected_purpose = verified[0], purpose
|
||||
break
|
||||
candidate_rows.extend(v for v in scoped if v["verification_status"] != "verified" and v["basis_date"] <= basis_date and (v["basis_date"] == basis_date if selection_policy == "exact" else True))
|
||||
if selected is None:
|
||||
excluded.append({**base, "reason": "missing_verified_exact_valuation" if selection_policy == "exact" else "missing_verified_valuation", "unverified_candidates": [_public_candidate(v) for v in candidate_rows]})
|
||||
continue
|
||||
valuation = selected
|
||||
if account_type == "loan" and (
|
||||
selected_purpose not in {"loan", "loan_balance"}
|
||||
or not valuation["representative_liability_definition"]
|
||||
):
|
||||
excluded.append({
|
||||
**base,
|
||||
"reason": "missing_representative_liability_definition",
|
||||
"valuation_id": valuation["id"],
|
||||
"valuation_kind": selected_purpose,
|
||||
"valuation_date": valuation["basis_date"],
|
||||
"verification_status": valuation["verification_status"],
|
||||
})
|
||||
continue
|
||||
balance = valuation["amount"]
|
||||
base.update({"valuation_id": valuation["id"], "verification_status": "verified", "valuation_kind": selected_purpose, "valuation_date": valuation["basis_date"], "currency": valuation["currency"], "carried_forward": valuation["basis_date"] != basis_date})
|
||||
if candidate_rows:
|
||||
base["unverified_candidates"] = [_public_candidate(v) for v in candidate_rows if v["basis_date"] <= basis_date]
|
||||
|
||||
if balance is None:
|
||||
excluded.append({**base, "reason": "missing_last_balance"})
|
||||
continue
|
||||
@@ -64,13 +142,16 @@ def _account_valuation_items(conn: sqlite3.Connection) -> tuple[list[dict[str, A
|
||||
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]]]:
|
||||
def _manual_asset_valuation_items(conn: sqlite3.Connection, active_only: bool = True, basis_date: str = "", selection_policy: str = "exact") -> 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)
|
||||
if (asset.get("currency") or "KRW").upper() != "KRW":
|
||||
excluded.append({"id": asset.get("id", ""), "source_id": asset.get("id", ""), "source_type": "manual_asset", "reason": "non_krw_without_fx", "currency": asset.get("currency")})
|
||||
continue
|
||||
try:
|
||||
bucket = normalize_liquidity_bucket(asset.get("liquidity_bucket"))
|
||||
except ValueError:
|
||||
@@ -88,6 +169,12 @@ def _manual_asset_valuation_items(conn: sqlite3.Connection, active_only: bool =
|
||||
"active": asset.get("active", True),
|
||||
}
|
||||
value = asset.get("valuation_amount")
|
||||
if basis_date:
|
||||
valuation_date = str(asset.get("valuation_date") or "")[:10]
|
||||
if not valuation_date or valuation_date > basis_date or (selection_policy == "exact" and valuation_date != basis_date):
|
||||
excluded.append({**base, "reason": "missing_exact_manual_asset_valuation" if selection_policy == "exact" else "future_or_unknown_manual_asset_valuation"})
|
||||
continue
|
||||
base["carried_forward"] = valuation_date != basis_date
|
||||
if value is None:
|
||||
excluded.append({**base, "reason": "missing_valuation_amount"})
|
||||
continue
|
||||
@@ -96,26 +183,12 @@ def _manual_asset_valuation_items(conn: sqlite3.Connection, active_only: bool =
|
||||
|
||||
|
||||
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),
|
||||
}
|
||||
rows = conn.execute("SELECT rowid AS _rowid, * FROM asset_snapshots WHERE basis_date=? ORDER BY created_at ASC, name ASC", (basis_date,)).fetchall()
|
||||
run = conn.execute("SELECT * FROM asset_snapshot_runs WHERE basis_date=? ORDER BY rowid DESC LIMIT 1", (basis_date,)).fetchone()
|
||||
if not run:
|
||||
return rows, {"snapshot_run_id": "", "is_correction": False, "correction_of_run_id": "", "correction_reason": "", "total_snapshot_rows_for_basis_date": len(rows)} if rows else {}
|
||||
selected = [row for row in rows if row["snapshot_run_id"] == run["id"]]
|
||||
metadata = {"snapshot_run_id": run["id"], "is_correction": bool(run["correction_of_run_id"]), "correction_of_run_id": run["correction_of_run_id"] or "", "correction_reason": run["correction_reason"] or "", "close_status": run["close_status"], "selection_policy": run["selection_policy"], "approved_by": run["approved_by"], "approved_at": run["approved_at"], "approval_reason": run["approval_reason"], "approval_reference": run["approval_reference"], "applied_evidence": json.loads(run["applied_evidence"] or "[]"), "excluded_evidence": json.loads(run["excluded_evidence"] or "[]"), "carried_evidence": json.loads(run["carried_evidence"] or "[]"), "closed_at": run["created_at"] or "", "total_snapshot_rows_for_basis_date": len(rows)}
|
||||
return selected, metadata
|
||||
|
||||
|
||||
@@ -277,16 +350,18 @@ def deactivate_manual_asset(asset_id: str, hard_delete: bool = False, db_path: s
|
||||
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)
|
||||
def _snapshot_preview_from_conn(conn: sqlite3.Connection, basis: str, selection_policy: str = "exact") -> dict[str, Any]:
|
||||
account_items, account_excluded, invalid_bucket_accounts = _account_valuation_items(conn, basis, selection_policy)
|
||||
manual_items, manual_excluded = _manual_asset_valuation_items(conn, basis_date=basis, selection_policy=selection_policy)
|
||||
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", "")})
|
||||
warnings.append({"code": item.get("reason", "excluded_item"), "source_type": item.get("source_type", ""), "source_id": item.get("source_id", ""), "currency": item.get("currency", "KRW")})
|
||||
if item.get("unverified_candidates"):
|
||||
warnings.append({"code": "unverified_candidate", "source_type": item.get("source_type", ""), "source_id": item.get("source_id", ""), "candidates": [{k: candidate.get(k) for k in ("id", "basis_date", "currency", "valuation_kind", "verification_status")} for candidate in item["unverified_candidates"]]})
|
||||
for item in included:
|
||||
item_date = str(item.get("valuation_date") or "")[:10]
|
||||
if item_date and item_date < basis:
|
||||
@@ -307,17 +382,24 @@ def _snapshot_preview_from_conn(conn: sqlite3.Connection, basis: str) -> dict[st
|
||||
}
|
||||
|
||||
|
||||
def preview_asset_snapshot_close(basis_date: str = "", db_path: str | None = None) -> dict[str, Any]:
|
||||
def preview_asset_snapshot_close(basis_date: str = "", selection_policy: str = "exact", db_path: str | None = None) -> dict[str, Any]:
|
||||
if selection_policy not in {"exact", "as_of"}:
|
||||
return fail("validation_error", "selection_policy must be exact or as_of")
|
||||
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)
|
||||
preview = _snapshot_preview_from_conn(conn, basis, selection_policy)
|
||||
preview["selection_policy"] = selection_policy
|
||||
blocking = [w for w in preview["warnings"] if w.get("code", "").startswith(("missing_", "future_", "unknown_", "unverified", "ambiguous_", "stale_"))]
|
||||
preview["close_status"] = "complete_exact" if selection_policy == "exact" and not blocking else "incomplete_draft"
|
||||
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]:
|
||||
def close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str = "", correction_reason: str = "", selection_policy: str = "exact", idempotency_key: str = "", allow_missing_exclusions: bool = False, allow_as_of_carry_forward: bool = False, approved_by: str = "", approval_reason: str = "", approval_reference: str = "", db_path: str | None = None) -> dict[str, Any]:
|
||||
if selection_policy not in {"exact", "as_of"}:
|
||||
return fail("validation_error", "selection_policy must be exact or as_of")
|
||||
try:
|
||||
basis = normalize_date(basis_date, default_today=True) if basis_date else current_kst_date().isoformat()
|
||||
except Exception as e:
|
||||
@@ -325,21 +407,39 @@ def close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str
|
||||
ts = now_iso()
|
||||
run_id = new_id("snaprun")
|
||||
with db(db_path) as conn:
|
||||
preview = _snapshot_preview_from_conn(conn, basis)
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
preview = _snapshot_preview_from_conn(conn, basis, selection_policy)
|
||||
missing_exclusions = [w for w in preview["warnings"] if w.get("code", "").startswith(("missing_", "future_", "unknown_", "unverified", "ambiguous_"))]
|
||||
carried_items = [item for item in preview["included_items"] if item.get("carried_forward")]
|
||||
conditional = bool(missing_exclusions or carried_items)
|
||||
fingerprint_payload = {"basis_date": basis, "source_note": source_note, "memo": memo, "correction_reason": correction_reason, "selection_policy": selection_policy, "allow_missing_exclusions": allow_missing_exclusions, "allow_as_of_carry_forward": allow_as_of_carry_forward, "approved_by": approved_by, "approval_reason": approval_reason, "approval_reference": approval_reference, "included": [{**{k: i.get(k) for k in ("source_type", "source_id", "amount", "currency", "valuation_id", "valuation_date", "verification_status")}, "state_hash": _evidence_state_hash(i)} for i in preview["included_items"]], "excluded": [{**{k: i.get(k) for k in ("source_type", "source_id", "reason", "valuation_date", "verification_status")}, "state_hash": _evidence_state_hash(i)} for i in preview["excluded_items"]]}
|
||||
fingerprint = hashlib.sha256(json.dumps(fingerprint_payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()).hexdigest()
|
||||
if missing_exclusions and (not allow_missing_exclusions or not approved_by.strip() or not approval_reason.strip() or not approval_reference.strip()):
|
||||
return fail("conditional_approval_required", "missing exclusions require explicit approval metadata and allow_missing_exclusions")
|
||||
if carried_items and (not allow_as_of_carry_forward or not approved_by.strip() or not approval_reason.strip() or not approval_reference.strip()):
|
||||
return fail("conditional_approval_required", "conditional close requires explicit approval metadata and flags")
|
||||
if idempotency_key:
|
||||
existing_run = conn.execute("SELECT * FROM asset_snapshot_runs WHERE idempotency_key=?", (idempotency_key,)).fetchone()
|
||||
if existing_run:
|
||||
if existing_run["request_fingerprint"] == fingerprint:
|
||||
return ok(reused=True, snapshot_run_id=existing_run["id"], close_status=existing_run["close_status"], basis_date=basis)
|
||||
return fail("idempotency_conflict", "snapshot close idempotency payload differs")
|
||||
included = preview["included_items"]
|
||||
latest_rows, latest_meta = _snapshot_rows_for_latest_run(conn, basis)
|
||||
is_correction = bool(preview["existing_snapshot_row_count"])
|
||||
# A conditional close may legitimately contain zero snapshot rows.
|
||||
# The run record, not row count, is the authoritative close marker.
|
||||
is_correction = bool(latest_meta.get("snapshot_run_id"))
|
||||
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 ""
|
||||
return fail("validation_error", "correction_reason is required when closing a basis_date that already has a snapshot run", basis_date=basis, existing_snapshot_row_count=preview["existing_snapshot_row_count"])
|
||||
correction_of_run_id = latest_meta.get("snapshot_run_id", "")
|
||||
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(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
"""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,valuation_id,selection_policy,verification_status,evidence_kind,exclusion_reason,created_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
snapshot_id,
|
||||
basis,
|
||||
@@ -357,12 +457,24 @@ def close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str
|
||||
correction_reason.strip(),
|
||||
int(is_correction),
|
||||
ts,
|
||||
item.get("valuation_id", ""),
|
||||
selection_policy,
|
||||
item.get("verification_status", "unverified"),
|
||||
"carried_forward" if item.get("carried_forward") else "applied",
|
||||
"",
|
||||
ts,
|
||||
),
|
||||
)
|
||||
snapshot_ids.append(snapshot_id)
|
||||
report_items = _snapshot_items(conn, basis)
|
||||
summary = _summarize_valuation_items(report_items, [])
|
||||
summary = _summarize_valuation_items(included, [])
|
||||
applied_evidence = [{k: item.get(k) for k in ("source_type", "source_id", "amount", "currency", "valuation_id", "valuation_date", "verification_status")} for item in included]
|
||||
excluded_evidence = [{k: item.get(k) for k in ("source_type", "source_id", "reason", "currency")} for item in preview.get("excluded_items", [])]
|
||||
carried_evidence = [{k: item.get(k) for k in ("source_type", "source_id", "amount", "currency", "valuation_id", "valuation_date", "verification_status")} for item in included if item.get("carried_forward")]
|
||||
try:
|
||||
conn.execute("INSERT INTO asset_snapshot_runs(id,basis_date,close_status,selection_policy,idempotency_key,approved_by,approved_at,approval_reason,approval_reference,correction_of_run_id,correction_reason,applied_evidence,excluded_evidence,carried_evidence,request_fingerprint,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (run_id, basis, "conditional" if conditional else "complete_exact", selection_policy, idempotency_key, approved_by, ts if approved_by else "", approval_reason, approval_reference, correction_of_run_id, correction_reason.strip(), json.dumps(applied_evidence, ensure_ascii=False, sort_keys=True), json.dumps(excluded_evidence, ensure_ascii=False, sort_keys=True), json.dumps(carried_evidence, ensure_ascii=False, sort_keys=True), fingerprint, ts))
|
||||
except sqlite3.IntegrityError as exc:
|
||||
conn.rollback()
|
||||
return fail("snapshot_conflict", "another snapshot close already owns this basis date", basis_date=basis, detail=str(exc))
|
||||
return ok(
|
||||
basis_date=basis,
|
||||
source="snapshot",
|
||||
@@ -372,6 +484,8 @@ def close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str
|
||||
correction_reason=correction_reason.strip(),
|
||||
is_correction=is_correction,
|
||||
closed_at=ts,
|
||||
close_status="conditional" if conditional else "complete_exact",
|
||||
selection_policy=selection_policy,
|
||||
inserted_count=len(snapshot_ids),
|
||||
inserted_snapshot_ids=snapshot_ids,
|
||||
warnings=preview["warnings"],
|
||||
@@ -402,12 +516,14 @@ def create_asset_snapshot(basis_date: str, source_type: str, source_id: str, nam
|
||||
return ok(asset_snapshot=row_to_dict(row))
|
||||
|
||||
|
||||
def get_asset_snapshot_report(basis_date: str = "", db_path: str | None = None) -> dict[str, Any]:
|
||||
def get_asset_snapshot_report(basis_date: str = "", selection_policy: str = "exact", db_path: str | None = None) -> dict[str, Any]:
|
||||
if selection_policy not in {"exact", "as_of"}:
|
||||
return fail("validation_error", "selection_policy must be exact or as_of")
|
||||
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:
|
||||
if close_metadata:
|
||||
summary = _summarize_valuation_items(snapshots, [])
|
||||
return ok(
|
||||
basis_date=basis,
|
||||
@@ -415,16 +531,20 @@ def get_asset_snapshot_report(basis_date: str = "", db_path: str | None = None)
|
||||
is_closed_snapshot=True,
|
||||
snapshot_count=len(snapshots),
|
||||
close_metadata=close_metadata,
|
||||
selection_policy=close_metadata.get("selection_policy", selection_policy),
|
||||
close_status=close_metadata.get("close_status", "complete_exact"),
|
||||
**summary,
|
||||
)
|
||||
account_items, account_excluded, invalid_bucket_accounts = _account_valuation_items(conn)
|
||||
manual_items, manual_excluded = _manual_asset_valuation_items(conn)
|
||||
account_items, account_excluded, invalid_bucket_accounts = _account_valuation_items(conn, basis, selection_policy)
|
||||
manual_items, manual_excluded = _manual_asset_valuation_items(conn, basis_date=basis, selection_policy=selection_policy)
|
||||
included = account_items + manual_items
|
||||
excluded = account_excluded + manual_excluded
|
||||
summary = _summarize_valuation_items(included, excluded)
|
||||
return ok(
|
||||
basis_date=basis,
|
||||
source="live_authority",
|
||||
selection_policy=selection_policy,
|
||||
close_status="incomplete_draft" if excluded else "complete_exact",
|
||||
is_closed_snapshot=False,
|
||||
snapshot_count=0,
|
||||
invalid_bucket_accounts=invalid_bucket_accounts,
|
||||
|
||||
Reference in New Issue
Block a user