0.2.43: historical NAV backfill without touching current holdings

The batch history import now also records each quarter's NAV in the
fund's valuation history: the old file's HLD rows are matched by issuer
and security name against the book as it exists today, matched rows
write that quarter's valuations, unmatched rows are counted and
reported, and nothing outside the round is created or modified. A
manually signed quarter is never overwritten.

The single-file wizard automatically takes the same history-only path
when the file is older than the fund's newest round. Previously that
import would regress position cost basis to the old file's values and
resurrect since-exited positions, corrupting the fund's Invested total.
This commit is contained in:
Jonathan Kirkwood
2026-08-11 12:32:57 -05:00
parent ae967494bd
commit ebafcf19d9
11 changed files with 376 additions and 10 deletions
@@ -25,7 +25,9 @@ from ten31portal.database import get_session
from ten31portal.models import (
CapitalAccountStatement, Entity, EntityAccess, User, UserRole,
)
from ten31portal.routers.import_router import _open_workbook, _enav_as_of
from ten31portal.routers.import_router import (
_open_workbook, _enav_as_of, _parse_schedule_xlsx, upsert_history_round,
)
from ten31portal.schemas import (
BatchCapitalFileResult, BatchCapitalImportResult,
CapitalImportCommit, CapitalImportPreview, ImportInvestorPreview, ImportValueRow,
@@ -433,7 +435,8 @@ def batch_import(
fname = upload.filename or "(unnamed)"
res = BatchCapitalFileResult(filename=fname)
try:
wb = _open_workbook(storage.read_capped(upload), password)
file_bytes = storage.read_capped(upload)
wb = _open_workbook(file_bytes, password)
if "ALLOC SI" not in wb.sheetnames:
raise HTTPException(status_code=422, detail="No ALLOC SI tab found in this workbook.")
as_of, roster = _parse_alloc_si(wb)
@@ -488,11 +491,31 @@ def batch_import(
res.matched += 1
res.statements_written += 1
# NAV history leg: record this quarter's valuation round from the file's HLD
# sheet, matched against today's book only (holdings are never modified). An
# HLD problem must not lose the member statements above, so it runs in a
# savepoint and reports per-file.
try:
with session.begin_nested():
_, _, _, positions_prev, _ = _parse_schedule_xlsx(file_bytes, password)
if positions_prev:
hist = upsert_history_round(entity_id, as_of, positions_prev, admin, session)
res.nav_status = hist["status"]
res.nav_matched = hist["matched"]
res.nav_unmatched = hist["unmatched"]
res.nav_cents = hist["nav_cents"]
else:
res.nav_status = "no-hld"
except Exception: # noqa: BLE001 — the savepoint rolled back; members still land
res.nav_status = "error"
record_audit(session, admin.id, "import_batch", "capital_account", entity_id, {
"file": fname,
"as_of_date": str(as_of),
"matched": res.matched,
"skipped": len(res.skipped),
"nav_status": res.nav_status,
"nav_cents": res.nav_cents,
})
session.commit()
total_statements += res.statements_written