progress
This commit is contained in:
651
app/services/file_management.py
Normal file
651
app/services/file_management.py
Normal file
@@ -0,0 +1,651 @@
|
||||
"""
|
||||
Enhanced file management service
|
||||
Handles file closure, status workflows, transfers, and archival
|
||||
"""
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from datetime import datetime, date, timezone
|
||||
from decimal import Decimal
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import and_, func, or_, desc
|
||||
|
||||
from app.models import (
|
||||
File, Ledger, FileStatus, FileType, Rolodex, Employee,
|
||||
BillingStatement, Timer, TimeEntry, User, FileStatusHistory,
|
||||
FileTransferHistory, FileArchiveInfo
|
||||
)
|
||||
from app.utils.logging import app_logger
|
||||
|
||||
logger = app_logger
|
||||
|
||||
|
||||
class FileManagementError(Exception):
|
||||
"""Exception raised when file management operations fail"""
|
||||
pass
|
||||
|
||||
|
||||
class FileStatusWorkflow:
|
||||
"""Define valid file status transitions and business rules"""
|
||||
|
||||
# Define valid status transitions
|
||||
VALID_TRANSITIONS = {
|
||||
"NEW": ["ACTIVE", "INACTIVE", "FOLLOW_UP"],
|
||||
"ACTIVE": ["INACTIVE", "FOLLOW_UP", "PENDING_CLOSURE", "ARCHIVED"],
|
||||
"INACTIVE": ["ACTIVE", "FOLLOW_UP", "PENDING_CLOSURE", "ARCHIVED"],
|
||||
"FOLLOW_UP": ["ACTIVE", "INACTIVE", "PENDING_CLOSURE", "ARCHIVED"],
|
||||
"PENDING_CLOSURE": ["ACTIVE", "INACTIVE", "CLOSED"],
|
||||
"CLOSED": ["ARCHIVED", "ACTIVE"], # Allow reopening
|
||||
"ARCHIVED": [] # Final state - no transitions
|
||||
}
|
||||
|
||||
# Statuses that require special validation
|
||||
CLOSURE_STATUSES = {"PENDING_CLOSURE", "CLOSED"}
|
||||
FINAL_STATUSES = {"ARCHIVED"}
|
||||
ACTIVE_STATUSES = {"NEW", "ACTIVE", "FOLLOW_UP", "PENDING_CLOSURE"}
|
||||
|
||||
@classmethod
|
||||
def can_transition(cls, from_status: str, to_status: str) -> bool:
|
||||
"""Check if status transition is valid"""
|
||||
return to_status in cls.VALID_TRANSITIONS.get(from_status, [])
|
||||
|
||||
@classmethod
|
||||
def get_valid_transitions(cls, from_status: str) -> List[str]:
|
||||
"""Get list of valid status transitions from current status"""
|
||||
return cls.VALID_TRANSITIONS.get(from_status, [])
|
||||
|
||||
@classmethod
|
||||
def requires_closure_validation(cls, status: str) -> bool:
|
||||
"""Check if status requires closure validation"""
|
||||
return status in cls.CLOSURE_STATUSES
|
||||
|
||||
@classmethod
|
||||
def is_active_status(cls, status: str) -> bool:
|
||||
"""Check if status indicates an active file"""
|
||||
return status in cls.ACTIVE_STATUSES
|
||||
|
||||
|
||||
class FileManagementService:
|
||||
"""Service for advanced file management operations"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.workflow = FileStatusWorkflow()
|
||||
|
||||
def change_file_status(
|
||||
self,
|
||||
file_no: str,
|
||||
new_status: str,
|
||||
user_id: int,
|
||||
notes: Optional[str] = None,
|
||||
validate_transition: bool = True
|
||||
) -> File:
|
||||
"""Change file status with workflow validation"""
|
||||
|
||||
file_obj = self.db.query(File).filter(File.file_no == file_no).first()
|
||||
if not file_obj:
|
||||
raise FileManagementError(f"File {file_no} not found")
|
||||
|
||||
current_status = file_obj.status
|
||||
|
||||
# Validate status transition
|
||||
if validate_transition and not self.workflow.can_transition(current_status, new_status):
|
||||
valid_transitions = self.workflow.get_valid_transitions(current_status)
|
||||
raise FileManagementError(
|
||||
f"Invalid status transition from '{current_status}' to '{new_status}'. "
|
||||
f"Valid transitions: {valid_transitions}"
|
||||
)
|
||||
|
||||
# Special validation for closure statuses
|
||||
if self.workflow.requires_closure_validation(new_status):
|
||||
self._validate_file_closure(file_obj)
|
||||
|
||||
# Update file status
|
||||
old_status = file_obj.status
|
||||
file_obj.status = new_status
|
||||
|
||||
# Set closure date if closing
|
||||
if new_status == "CLOSED" and not file_obj.closed:
|
||||
file_obj.closed = date.today()
|
||||
elif new_status != "CLOSED" and file_obj.closed:
|
||||
# Clear closure date if reopening
|
||||
file_obj.closed = None
|
||||
|
||||
# Create status history record
|
||||
self._create_status_history(file_no, old_status, new_status, user_id, notes)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(file_obj)
|
||||
|
||||
logger.info(f"Changed file {file_no} status from '{old_status}' to '{new_status}' by user {user_id}")
|
||||
return file_obj
|
||||
|
||||
def close_file(
|
||||
self,
|
||||
file_no: str,
|
||||
user_id: int,
|
||||
force_close: bool = False,
|
||||
final_payment_amount: Optional[float] = None,
|
||||
closing_notes: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Close a file with automated closure process"""
|
||||
|
||||
file_obj = self.db.query(File).filter(File.file_no == file_no).first()
|
||||
if not file_obj:
|
||||
raise FileManagementError(f"File {file_no} not found")
|
||||
|
||||
if file_obj.status == "CLOSED":
|
||||
raise FileManagementError("File is already closed")
|
||||
|
||||
closure_summary = {
|
||||
"file_no": file_no,
|
||||
"closure_date": date.today(),
|
||||
"actions_taken": [],
|
||||
"warnings": [],
|
||||
"final_balance": 0.0,
|
||||
"trust_balance": file_obj.trust_bal or 0.0
|
||||
}
|
||||
|
||||
try:
|
||||
# Step 1: Validate closure readiness
|
||||
validation_result = self._validate_file_closure(file_obj, force_close)
|
||||
closure_summary["warnings"].extend(validation_result.get("warnings", []))
|
||||
|
||||
if validation_result.get("blocking_issues") and not force_close:
|
||||
raise FileManagementError(
|
||||
f"Cannot close file: {'; '.join(validation_result['blocking_issues'])}"
|
||||
)
|
||||
|
||||
# Step 2: Handle outstanding balances
|
||||
outstanding_balance = self._calculate_outstanding_balance(file_obj)
|
||||
closure_summary["final_balance"] = outstanding_balance
|
||||
|
||||
if outstanding_balance > 0 and final_payment_amount:
|
||||
# Create payment entry to close outstanding balance
|
||||
payment_entry = self._create_final_payment_entry(
|
||||
file_obj, final_payment_amount, user_id
|
||||
)
|
||||
closure_summary["actions_taken"].append(
|
||||
f"Created final payment entry: ${final_payment_amount:.2f}"
|
||||
)
|
||||
outstanding_balance -= final_payment_amount
|
||||
|
||||
# Step 3: Stop any active timers
|
||||
active_timers = self._stop_active_timers(file_no, user_id)
|
||||
if active_timers:
|
||||
closure_summary["actions_taken"].append(
|
||||
f"Stopped {len(active_timers)} active timer(s)"
|
||||
)
|
||||
|
||||
# Step 4: Mark unbilled time entries as non-billable if any
|
||||
unbilled_entries = self._handle_unbilled_time_entries(file_no, user_id)
|
||||
if unbilled_entries:
|
||||
closure_summary["actions_taken"].append(
|
||||
f"Marked {len(unbilled_entries)} time entries as non-billable"
|
||||
)
|
||||
|
||||
# Step 5: Update file status
|
||||
file_obj.status = "CLOSED"
|
||||
file_obj.closed = date.today()
|
||||
|
||||
# Step 6: Create closure history record
|
||||
self._create_status_history(
|
||||
file_no,
|
||||
file_obj.status,
|
||||
"CLOSED",
|
||||
user_id,
|
||||
closing_notes or "File closed via automated closure process"
|
||||
)
|
||||
|
||||
self.db.commit()
|
||||
|
||||
closure_summary["actions_taken"].append("File status updated to CLOSED")
|
||||
|
||||
logger.info(f"Successfully closed file {file_no} by user {user_id}")
|
||||
return closure_summary
|
||||
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"Failed to close file {file_no}: {str(e)}")
|
||||
raise FileManagementError(f"File closure failed: {str(e)}")
|
||||
|
||||
def reopen_file(
|
||||
self,
|
||||
file_no: str,
|
||||
user_id: int,
|
||||
new_status: str = "ACTIVE",
|
||||
notes: Optional[str] = None
|
||||
) -> File:
|
||||
"""Reopen a closed file"""
|
||||
|
||||
file_obj = self.db.query(File).filter(File.file_no == file_no).first()
|
||||
if not file_obj:
|
||||
raise FileManagementError(f"File {file_no} not found")
|
||||
|
||||
if file_obj.status != "CLOSED":
|
||||
raise FileManagementError("Only closed files can be reopened")
|
||||
|
||||
if file_obj.status == "ARCHIVED":
|
||||
raise FileManagementError("Archived files cannot be reopened")
|
||||
|
||||
# Validate new status
|
||||
if not self.workflow.can_transition("CLOSED", new_status):
|
||||
valid_transitions = self.workflow.get_valid_transitions("CLOSED")
|
||||
raise FileManagementError(
|
||||
f"Invalid reopening status '{new_status}'. Valid options: {valid_transitions}"
|
||||
)
|
||||
|
||||
# Update file
|
||||
old_status = file_obj.status
|
||||
file_obj.status = new_status
|
||||
file_obj.closed = None # Clear closure date
|
||||
|
||||
# Create status history
|
||||
self._create_status_history(
|
||||
file_no,
|
||||
old_status,
|
||||
new_status,
|
||||
user_id,
|
||||
notes or "File reopened"
|
||||
)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(file_obj)
|
||||
|
||||
logger.info(f"Reopened file {file_no} to status '{new_status}' by user {user_id}")
|
||||
return file_obj
|
||||
|
||||
def transfer_file(
|
||||
self,
|
||||
file_no: str,
|
||||
new_attorney_id: str,
|
||||
user_id: int,
|
||||
transfer_reason: Optional[str] = None
|
||||
) -> File:
|
||||
"""Transfer file to a different attorney"""
|
||||
|
||||
file_obj = self.db.query(File).filter(File.file_no == file_no).first()
|
||||
if not file_obj:
|
||||
raise FileManagementError(f"File {file_no} not found")
|
||||
|
||||
# Validate new attorney exists
|
||||
new_attorney = self.db.query(Employee).filter(Employee.empl_num == new_attorney_id).first()
|
||||
if not new_attorney:
|
||||
raise FileManagementError(f"Attorney {new_attorney_id} not found")
|
||||
|
||||
if not new_attorney.active:
|
||||
raise FileManagementError(f"Attorney {new_attorney_id} is not active")
|
||||
|
||||
old_attorney = file_obj.empl_num
|
||||
if old_attorney == new_attorney_id:
|
||||
raise FileManagementError("File is already assigned to this attorney")
|
||||
|
||||
# Update file assignment
|
||||
file_obj.empl_num = new_attorney_id
|
||||
|
||||
# Update hourly rate if attorney has a default rate
|
||||
if new_attorney.rate_per_hour and new_attorney.rate_per_hour > 0:
|
||||
file_obj.rate_per_hour = new_attorney.rate_per_hour
|
||||
|
||||
# Create transfer history record
|
||||
self._create_transfer_history(
|
||||
file_no,
|
||||
old_attorney,
|
||||
new_attorney_id,
|
||||
user_id,
|
||||
transfer_reason
|
||||
)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(file_obj)
|
||||
|
||||
logger.info(f"Transferred file {file_no} from {old_attorney} to {new_attorney_id} by user {user_id}")
|
||||
return file_obj
|
||||
|
||||
def archive_file(
|
||||
self,
|
||||
file_no: str,
|
||||
user_id: int,
|
||||
archive_location: Optional[str] = None,
|
||||
notes: Optional[str] = None
|
||||
) -> File:
|
||||
"""Archive a closed file"""
|
||||
|
||||
file_obj = self.db.query(File).filter(File.file_no == file_no).first()
|
||||
if not file_obj:
|
||||
raise FileManagementError(f"File {file_no} not found")
|
||||
|
||||
if file_obj.status != "CLOSED":
|
||||
raise FileManagementError("Only closed files can be archived")
|
||||
|
||||
# Check for any recent activity (within last 30 days)
|
||||
recent_activity = self._check_recent_activity(file_no, days=30)
|
||||
if recent_activity:
|
||||
raise FileManagementError(
|
||||
f"File has recent activity and cannot be archived: {recent_activity}"
|
||||
)
|
||||
|
||||
# Update file status
|
||||
old_status = file_obj.status
|
||||
file_obj.status = "ARCHIVED"
|
||||
|
||||
# Create archive history record
|
||||
archive_notes = notes or f"File archived to location: {archive_location or 'Standard archive'}"
|
||||
self._create_status_history(file_no, old_status, "ARCHIVED", user_id, archive_notes)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(file_obj)
|
||||
|
||||
logger.info(f"Archived file {file_no} by user {user_id}")
|
||||
return file_obj
|
||||
|
||||
def get_file_status_history(self, file_no: str) -> List[Dict[str, Any]]:
|
||||
"""Get status change history for a file"""
|
||||
history = self.db.query(FileStatusHistory).filter(
|
||||
FileStatusHistory.file_no == file_no
|
||||
).options(
|
||||
joinedload(FileStatusHistory.changed_by)
|
||||
).order_by(FileStatusHistory.change_date.desc()).all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": h.id,
|
||||
"old_status": h.old_status,
|
||||
"new_status": h.new_status,
|
||||
"change_date": h.change_date,
|
||||
"changed_by": h.changed_by_name or (h.changed_by.username if h.changed_by else "System"),
|
||||
"notes": h.notes,
|
||||
"system_generated": h.system_generated
|
||||
}
|
||||
for h in history
|
||||
]
|
||||
|
||||
def get_files_by_status(
|
||||
self,
|
||||
status: str,
|
||||
attorney_id: Optional[str] = None,
|
||||
limit: int = 100
|
||||
) -> List[File]:
|
||||
"""Get files by status with optional attorney filter"""
|
||||
query = self.db.query(File).filter(File.status == status)
|
||||
|
||||
if attorney_id:
|
||||
query = query.filter(File.empl_num == attorney_id)
|
||||
|
||||
return query.options(
|
||||
joinedload(File.owner)
|
||||
).order_by(File.opened.desc()).limit(limit).all()
|
||||
|
||||
def get_closure_candidates(self, days_inactive: int = 90) -> List[Dict[str, Any]]:
|
||||
"""Get files that are candidates for closure"""
|
||||
cutoff_date = datetime.now(timezone.utc).date() - datetime.timedelta(days=days_inactive)
|
||||
|
||||
# Files with no recent ledger activity
|
||||
files_query = self.db.query(File).filter(
|
||||
File.status.in_(["ACTIVE", "FOLLOW_UP"]),
|
||||
File.opened < cutoff_date
|
||||
)
|
||||
|
||||
candidates = []
|
||||
for file_obj in files_query.all():
|
||||
# Check for recent ledger activity
|
||||
recent_activity = self.db.query(Ledger).filter(
|
||||
Ledger.file_no == file_obj.file_no,
|
||||
Ledger.date >= cutoff_date
|
||||
).first()
|
||||
|
||||
if not recent_activity:
|
||||
outstanding_balance = self._calculate_outstanding_balance(file_obj)
|
||||
candidates.append({
|
||||
"file_no": file_obj.file_no,
|
||||
"client_name": f"{file_obj.owner.first or ''} {file_obj.owner.last}".strip() if file_obj.owner else "Unknown",
|
||||
"attorney": file_obj.empl_num,
|
||||
"opened_date": file_obj.opened,
|
||||
"last_activity": None, # Could be enhanced to find actual last activity
|
||||
"outstanding_balance": outstanding_balance,
|
||||
"status": file_obj.status
|
||||
})
|
||||
|
||||
return sorted(candidates, key=lambda x: x["opened_date"])
|
||||
|
||||
def bulk_status_update(
|
||||
self,
|
||||
file_numbers: List[str],
|
||||
new_status: str,
|
||||
user_id: int,
|
||||
notes: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Update status for multiple files"""
|
||||
results = {
|
||||
"successful": [],
|
||||
"failed": [],
|
||||
"total": len(file_numbers)
|
||||
}
|
||||
|
||||
for file_no in file_numbers:
|
||||
try:
|
||||
self.change_file_status(file_no, new_status, user_id, notes)
|
||||
results["successful"].append(file_no)
|
||||
except Exception as e:
|
||||
results["failed"].append({
|
||||
"file_no": file_no,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
logger.info(f"Bulk status update: {len(results['successful'])} successful, {len(results['failed'])} failed")
|
||||
return results
|
||||
|
||||
# Private helper methods
|
||||
|
||||
def _validate_file_closure(self, file_obj: File, force: bool = False) -> Dict[str, Any]:
|
||||
"""Validate if file is ready for closure"""
|
||||
validation_result = {
|
||||
"can_close": True,
|
||||
"blocking_issues": [],
|
||||
"warnings": []
|
||||
}
|
||||
|
||||
# Check for active timers
|
||||
active_timers = self.db.query(Timer).filter(
|
||||
Timer.file_no == file_obj.file_no,
|
||||
Timer.status.in_(["running", "paused"])
|
||||
).count()
|
||||
|
||||
if active_timers > 0:
|
||||
validation_result["warnings"].append(f"{active_timers} active timer(s) will be stopped")
|
||||
|
||||
# Check for unbilled time entries
|
||||
unbilled_entries = self.db.query(TimeEntry).filter(
|
||||
TimeEntry.file_no == file_obj.file_no,
|
||||
TimeEntry.billed == False,
|
||||
TimeEntry.is_billable == True
|
||||
).count()
|
||||
|
||||
if unbilled_entries > 0:
|
||||
validation_result["warnings"].append(f"{unbilled_entries} unbilled time entries exist")
|
||||
|
||||
# Check for outstanding balance
|
||||
outstanding_balance = self._calculate_outstanding_balance(file_obj)
|
||||
if outstanding_balance > 0:
|
||||
validation_result["warnings"].append(f"Outstanding balance: ${outstanding_balance:.2f}")
|
||||
|
||||
# Check for pending billing statements
|
||||
pending_statements = self.db.query(BillingStatement).filter(
|
||||
BillingStatement.file_no == file_obj.file_no,
|
||||
BillingStatement.status.in_(["draft", "pending_approval"])
|
||||
).count()
|
||||
|
||||
if pending_statements > 0:
|
||||
validation_result["blocking_issues"].append(f"{pending_statements} pending billing statement(s)")
|
||||
validation_result["can_close"] = False
|
||||
|
||||
return validation_result
|
||||
|
||||
def _calculate_outstanding_balance(self, file_obj: File) -> float:
|
||||
"""Calculate outstanding balance for a file"""
|
||||
# Sum all charges minus payments
|
||||
charges = self.db.query(func.sum(Ledger.amount)).filter(
|
||||
Ledger.file_no == file_obj.file_no,
|
||||
Ledger.t_type.in_(["1", "2", "3", "4"]) # Fee and cost types
|
||||
).scalar() or 0.0
|
||||
|
||||
payments = self.db.query(func.sum(Ledger.amount)).filter(
|
||||
Ledger.file_no == file_obj.file_no,
|
||||
Ledger.t_type == "5" # Payment type
|
||||
).scalar() or 0.0
|
||||
|
||||
return max(0.0, charges - payments)
|
||||
|
||||
def _stop_active_timers(self, file_no: str, user_id: int) -> List[Timer]:
|
||||
"""Stop all active timers for a file"""
|
||||
from app.services.timers import TimerService
|
||||
|
||||
active_timers = self.db.query(Timer).filter(
|
||||
Timer.file_no == file_no,
|
||||
Timer.status.in_(["running", "paused"])
|
||||
).all()
|
||||
|
||||
timer_service = TimerService(self.db)
|
||||
stopped_timers = []
|
||||
|
||||
for timer in active_timers:
|
||||
try:
|
||||
timer_service.stop_timer(timer.id, timer.user_id)
|
||||
stopped_timers.append(timer)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to stop timer {timer.id}: {str(e)}")
|
||||
|
||||
return stopped_timers
|
||||
|
||||
def _handle_unbilled_time_entries(self, file_no: str, user_id: int) -> List[TimeEntry]:
|
||||
"""Handle unbilled time entries during file closure"""
|
||||
unbilled_entries = self.db.query(TimeEntry).filter(
|
||||
TimeEntry.file_no == file_no,
|
||||
TimeEntry.billed == False,
|
||||
TimeEntry.is_billable == True
|
||||
).all()
|
||||
|
||||
# Mark as non-billable to prevent future billing
|
||||
for entry in unbilled_entries:
|
||||
entry.is_billable = False
|
||||
entry.notes = (entry.notes or "") + " [Marked non-billable during file closure]"
|
||||
|
||||
return unbilled_entries
|
||||
|
||||
def _create_final_payment_entry(
|
||||
self,
|
||||
file_obj: File,
|
||||
amount: float,
|
||||
user_id: int
|
||||
) -> Ledger:
|
||||
"""Create a final payment entry to close outstanding balance"""
|
||||
# Get next item number
|
||||
max_item = self.db.query(func.max(Ledger.item_no)).filter(
|
||||
Ledger.file_no == file_obj.file_no
|
||||
).scalar() or 0
|
||||
|
||||
payment_entry = Ledger(
|
||||
file_no=file_obj.file_no,
|
||||
item_no=max_item + 1,
|
||||
date=date.today(),
|
||||
t_code="FINAL",
|
||||
t_type="5", # Payment type
|
||||
empl_num=f"user_{user_id}",
|
||||
amount=amount,
|
||||
billed="Y", # Mark as billed
|
||||
note="Final payment - file closure"
|
||||
)
|
||||
|
||||
self.db.add(payment_entry)
|
||||
return payment_entry
|
||||
|
||||
def _check_recent_activity(self, file_no: str, days: int = 30) -> Optional[str]:
|
||||
"""Check for recent activity that would prevent archival"""
|
||||
cutoff_date = datetime.now(timezone.utc).date() - datetime.timedelta(days=days)
|
||||
|
||||
# Check for recent ledger entries
|
||||
recent_ledger = self.db.query(Ledger).filter(
|
||||
Ledger.file_no == file_no,
|
||||
Ledger.date >= cutoff_date
|
||||
).first()
|
||||
|
||||
if recent_ledger:
|
||||
return f"Recent ledger activity on {recent_ledger.date}"
|
||||
|
||||
# Check for recent billing statements
|
||||
recent_statement = self.db.query(BillingStatement).filter(
|
||||
BillingStatement.file_no == file_no,
|
||||
BillingStatement.created_at >= cutoff_date
|
||||
).first()
|
||||
|
||||
if recent_statement:
|
||||
return f"Recent billing statement on {recent_statement.created_at.date()}"
|
||||
|
||||
return None
|
||||
|
||||
def _create_status_history(
|
||||
self,
|
||||
file_no: str,
|
||||
old_status: str,
|
||||
new_status: str,
|
||||
user_id: int,
|
||||
notes: Optional[str] = None
|
||||
):
|
||||
"""Create a status history record"""
|
||||
# Get user info for caching
|
||||
user = self.db.query(User).filter(User.id == user_id).first()
|
||||
user_name = user.username if user else f"user_{user_id}"
|
||||
|
||||
history_record = FileStatusHistory(
|
||||
file_no=file_no,
|
||||
old_status=old_status,
|
||||
new_status=new_status,
|
||||
changed_by_user_id=user_id,
|
||||
changed_by_name=user_name,
|
||||
notes=notes,
|
||||
system_generated=False
|
||||
)
|
||||
|
||||
self.db.add(history_record)
|
||||
|
||||
logger.info(
|
||||
f"File {file_no} status changed: {old_status} -> {new_status} by {user_name}"
|
||||
+ (f" - {notes}" if notes else "")
|
||||
)
|
||||
|
||||
def _create_transfer_history(
|
||||
self,
|
||||
file_no: str,
|
||||
old_attorney: str,
|
||||
new_attorney: str,
|
||||
user_id: int,
|
||||
reason: Optional[str] = None
|
||||
):
|
||||
"""Create a transfer history record"""
|
||||
# Get user info for caching
|
||||
user = self.db.query(User).filter(User.id == user_id).first()
|
||||
user_name = user.username if user else f"user_{user_id}"
|
||||
|
||||
# Get current file for rate info
|
||||
file_obj = self.db.query(File).filter(File.file_no == file_no).first()
|
||||
old_rate = file_obj.rate_per_hour if file_obj else None
|
||||
|
||||
# Get new attorney rate
|
||||
new_attorney_obj = self.db.query(Employee).filter(Employee.empl_num == new_attorney).first()
|
||||
new_rate = new_attorney_obj.rate_per_hour if new_attorney_obj else None
|
||||
|
||||
transfer_record = FileTransferHistory(
|
||||
file_no=file_no,
|
||||
old_attorney_id=old_attorney,
|
||||
new_attorney_id=new_attorney,
|
||||
authorized_by_user_id=user_id,
|
||||
authorized_by_name=user_name,
|
||||
reason=reason,
|
||||
old_hourly_rate=old_rate,
|
||||
new_hourly_rate=new_rate
|
||||
)
|
||||
|
||||
self.db.add(transfer_record)
|
||||
|
||||
logger.info(
|
||||
f"File {file_no} transferred: {old_attorney} -> {new_attorney} by {user_name}"
|
||||
+ (f" - {reason}" if reason else "")
|
||||
)
|
||||
Reference in New Issue
Block a user