Customer 360: extended Client fields, auto-migrate, updated Rolodex CRUD/templates, QDRO routes/views, importer mapping

QDRO links appear in rolodex_view.html case rows and case.html header when QDRO data exists, matching legacy flows.
This commit is contained in:
HotSwapp
2025-10-13 14:04:35 -05:00
parent 4cd35c66fd
commit 2e2380552e
32 changed files with 194632 additions and 9 deletions

View File

@@ -29,7 +29,7 @@ import structlog
from structlog import contextvars as structlog_contextvars
from .database import create_tables, get_db, get_database_url
from .models import User, Case, Client, Phone, Transaction, Document, Payment, ImportLog
from .models import User, Case, Client, Phone, Transaction, Document, Payment, ImportLog, Qdros
from .auth import authenticate_user, get_current_user_from_session
from .reporting import (
build_phone_book_pdf,
@@ -633,16 +633,37 @@ def import_rolodex_data(db: Session, file_path: str) -> Dict[str, Any]:
result['errors'].append(f"Row {row_num}: Client with ID '{rolodex_id}' already exists")
continue
# Parse DOB (YYYY-MM-DD or MM/DD/YY variants)
dob_raw = row.get('DOB', '').strip()
dob_val = None
for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%m/%d/%y"):
if not dob_raw:
break
try:
dob_val = datetime.strptime(dob_raw, fmt).date()
break
except ValueError:
continue
client = Client(
rolodex_id=rolodex_id,
prefix=row.get('Prefix', '').strip() or None,
first_name=row.get('First', '').strip() or None,
middle_initial=row.get('Middle', '').strip() or None,
middle_name=row.get('Middle', '').strip() or None,
last_name=row.get('Last', '').strip() or None,
suffix=row.get('Suffix', '').strip() or None,
title=row.get('Title', '').strip() or None,
company=row.get('Title', '').strip() or None,
address=row.get('A1', '').strip() or None,
city=row.get('City', '').strip() or None,
state=row.get('St', '').strip() or None,
zip_code=row.get('Zip', '').strip() or None
state=(row.get('Abrev', '').strip() or row.get('St', '').strip() or None),
zip_code=row.get('Zip', '').strip() or None,
group=row.get('Group', '').strip() or None,
email=row.get('Email', '').strip() or None,
dob=dob_val,
ssn=row.get('SS#', '').strip() or None,
legal_status=row.get('Legal_Status', '').strip() or None,
memo=row.get('Memo', '').strip() or None,
)
db.add(client)
@@ -2298,6 +2319,9 @@ async def case_detail(
logger.info("case_detail", case_id=case_obj.id, file_no=case_obj.file_no)
# Determine if QDRO entries exist for this case's file number
has_qdro = db.query(Qdros).filter(Qdros.file_no == case_obj.file_no).count() > 0
# Get any errors from session and clear them
errors = request.session.pop("case_update_errors", None)
@@ -2322,6 +2346,7 @@ async def case_detail(
"saved": saved,
"errors": errors or [],
"totals": totals,
"has_qdro": has_qdro,
},
)
@@ -2658,13 +2683,23 @@ async def rolodex_view(client_id: int, request: Request, db: Session = Depends(g
@app.post("/rolodex/create")
async def rolodex_create(
request: Request,
prefix: str = Form(None),
first_name: str = Form(None),
middle_name: str = Form(None),
last_name: str = Form(None),
suffix: str = Form(None),
title: str = Form(None),
company: str = Form(None),
address: str = Form(None),
city: str = Form(None),
state: str = Form(None),
zip_code: str = Form(None),
group: str = Form(None),
email: str = Form(None),
dob: str = Form(None),
ssn: str = Form(None),
legal_status: str = Form(None),
memo: str = Form(None),
rolodex_id: str = Form(None),
db: Session = Depends(get_db),
):
@@ -2672,14 +2707,32 @@ async def rolodex_create(
if not user:
return RedirectResponse(url="/login", status_code=302)
# Parse date
dob_dt = None
if dob and dob.strip():
try:
dob_dt = datetime.strptime(dob.strip(), "%Y-%m-%d").date()
except ValueError:
dob_dt = None
client = Client(
prefix=(prefix or "").strip() or None,
first_name=(first_name or "").strip() or None,
middle_name=(middle_name or "").strip() or None,
last_name=(last_name or "").strip() or None,
suffix=(suffix or "").strip() or None,
title=(title or "").strip() or None,
company=(company or "").strip() or None,
address=(address or "").strip() or None,
city=(city or "").strip() or None,
state=(state or "").strip() or None,
zip_code=(zip_code or "").strip() or None,
group=(group or "").strip() or None,
email=(email or "").strip() or None,
dob=dob_dt,
ssn=(ssn or "").strip() or None,
legal_status=(legal_status or "").strip() or None,
memo=(memo or "").strip() or None,
rolodex_id=(rolodex_id or "").strip() or None,
)
db.add(client)
@@ -2693,13 +2746,23 @@ async def rolodex_create(
async def rolodex_update(
client_id: int,
request: Request,
prefix: str = Form(None),
first_name: str = Form(None),
middle_name: str = Form(None),
last_name: str = Form(None),
suffix: str = Form(None),
title: str = Form(None),
company: str = Form(None),
address: str = Form(None),
city: str = Form(None),
state: str = Form(None),
zip_code: str = Form(None),
group: str = Form(None),
email: str = Form(None),
dob: str = Form(None),
ssn: str = Form(None),
legal_status: str = Form(None),
memo: str = Form(None),
rolodex_id: str = Form(None),
db: Session = Depends(get_db),
):
@@ -2711,13 +2774,27 @@ async def rolodex_update(
if not client:
raise HTTPException(status_code=404, detail="Client not found")
client.prefix = (prefix or "").strip() or None
client.first_name = (first_name or "").strip() or None
client.middle_name = (middle_name or "").strip() or None
client.last_name = (last_name or "").strip() or None
client.suffix = (suffix or "").strip() or None
client.title = (title or "").strip() or None
client.company = (company or "").strip() or None
client.address = (address or "").strip() or None
client.city = (city or "").strip() or None
client.state = (state or "").strip() or None
client.zip_code = (zip_code or "").strip() or None
client.group = (group or "").strip() or None
client.email = (email or "").strip() or None
if dob and dob.strip():
try:
client.dob = datetime.strptime(dob.strip(), "%Y-%m-%d").date()
except ValueError:
pass
client.ssn = (ssn or "").strip() or None
client.legal_status = (legal_status or "").strip() or None
client.memo = (memo or "").strip() or None
client.rolodex_id = (rolodex_id or "").strip() or None
db.commit()
@@ -3625,3 +3702,67 @@ async def api_list_ledger(
items=items,
pagination=Pagination(page=page, page_size=page_size, total=total, total_pages=total_pages),
)
# ------------------------------
# QDRO Views
# ------------------------------
@app.get("/qdro/{file_no}")
async def qdro_versions(
request: Request,
file_no: str,
db: Session = Depends(get_db),
):
user = get_current_user_from_session(request.session)
if not user:
return RedirectResponse(url="/login", status_code=302)
versions = (
db.query(Qdros)
.filter(Qdros.file_no == file_no)
.order_by(Qdros.version.asc())
.all()
)
return templates.TemplateResponse(
"qdro.html",
{
"request": request,
"user": user,
"file_no": file_no,
"versions": versions,
"qdro": None,
},
)
@app.get("/qdro/{file_no}/{version}")
async def qdro_detail(
request: Request,
file_no: str,
version: str,
db: Session = Depends(get_db),
):
user = get_current_user_from_session(request.session)
if not user:
return RedirectResponse(url="/login", status_code=302)
q = db.query(Qdros).filter(Qdros.file_no == file_no, Qdros.version == version).first()
if not q:
return RedirectResponse(url=f"/qdro/{file_no}", status_code=302)
versions = (
db.query(Qdros)
.filter(Qdros.file_no == file_no)
.order_by(Qdros.version.asc())
.all()
)
return templates.TemplateResponse(
"qdro.html",
{
"request": request,
"user": user,
"file_no": file_no,
"versions": versions,
"qdro": q,
},
)