Spaces:
Sleeping
Sleeping
File size: 4,421 Bytes
c0cb280 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | from datetime import datetime
import logging
from typing import Any, Dict, List, Optional
from accounting.models import (
Bill,
BillStatus,
CategorizationProposal,
FinancialClose,
Invoice,
InvoiceStatus,
JournalEntry,
Transaction,
TransactionStatus,
)
from sqlalchemy import func
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
class CloseChecklistAgent:
"""
Agent responsible for monitoring readiness for the periodic financial close.
"""
def __init__(self, db: Session):
self.db = db
async def run_close_check(self, workspace_id: str, period: str) -> Dict[str, Any]:
"""
Evaluate if the workspace is ready for a financial close for the given period.
"""
results = {
"period": period,
"is_ready": True,
"checklist": [],
"blockers": []
}
# 1. Check for Uncategorized Transactions
uncategorized_count = self.db.query(Transaction).filter(
Transaction.workspace_id == workspace_id,
Transaction.status == TransactionStatus.PENDING
).count()
if uncategorized_count > 0:
results["is_ready"] = False
results["blockers"].append(f"{uncategorized_count} transactions are still pending categorization.")
results["checklist"].append({"task": "Categorize Transactions", "status": "blocked"})
else:
results["checklist"].append({"task": "Categorize Transactions", "status": "complete"})
# 2. Check for Unbalanced Journal Entries
# In our EventSourcedLedger, this shouldn't happen, but good to verify
# SELECT transaction_id, SUM(CASE WHEN type='debit' THEN amount ELSE -amount END) as diff
from sqlalchemy import case
unbalanced = self.db.query(JournalEntry.transaction_id).group_by(JournalEntry.transaction_id).having(
func.abs(func.sum(case((JournalEntry.type == 'debit', JournalEntry.amount), else_=-JournalEntry.amount))) > 0.001
).all()
if unbalanced:
results["is_ready"] = False
results["blockers"].append(f"{len(unbalanced)} transactions are unbalanced in the ledger.")
results["checklist"].append({"task": "Ledger Integrity Check", "status": "blocked"})
else:
results["checklist"].append({"task": "Ledger Integrity Check", "status": "complete"})
# 3. Check for Open Invoices / Bills (Optional for soft close, blocker for hard close)
open_bills = self.db.query(Bill).filter(
Bill.workspace_id == workspace_id,
Bill.status == BillStatus.OPEN
).count()
if open_bills > 0:
results["checklist"].append({"task": "Review Open Bills", "status": "warning", "note": f"{open_bills} bills are still open."})
else:
results["checklist"].append({"task": "Review Open Bills", "status": "complete"})
# Update or create the Close record
close_record = self.db.query(FinancialClose).filter(
FinancialClose.workspace_id == workspace_id,
FinancialClose.period == period
).first()
if not close_record:
close_record = FinancialClose(
workspace_id=workspace_id,
period=period,
metadata_json=results
)
self.db.add(close_record)
else:
close_record.metadata_json = results
self.db.commit()
return results
async def close_period(self, workspace_id: str, period: str, user_id: str) -> Dict[str, Any]:
"""
Permanently close a period if ready.
"""
check = await self.run_close_check(workspace_id, period)
if not check["is_ready"]:
return {"success": False, "message": "Cannot close period. Please resolve blockers.", "blockers": check["blockers"]}
close_record = self.db.query(FinancialClose).filter(
FinancialClose.workspace_id == workspace_id,
FinancialClose.period == period
).first()
close_record.is_closed = True
close_record.closed_at = datetime.utcnow()
close_record.closed_by = user_id
self.db.commit()
return {"success": True, "message": f"Period {period} has been closed successfully."}
|