mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-02 10:19:58 -05:00
755faa22c3
Implement comprehensive budget monitoring and forecasting feature with: Database & Models: - Add BudgetAlert model for tracking project budget alerts - Create migration 039_add_budget_alerts_table with proper indexes - Support alert types: 80_percent, 100_percent, over_budget - Add acknowledgment tracking with user and timestamp Budget Forecasting Utilities: - Implement burn rate calculation (daily/weekly/monthly) - Add completion date estimation based on burn rate - Create resource allocation analysis per team member - Build cost trend analysis with configurable granularity - Add automatic budget alert detection with deduplication Routes & API: - Create budget_alerts blueprint with dashboard and detail views - Add API endpoints for burn rate, completion estimates, and trends - Implement resource allocation and cost trend API endpoints - Add alert acknowledgment and manual budget check endpoints - Fix log_event() calls to use keyword arguments UI Templates: - Design modern budget dashboard with Tailwind CSS - Create detailed project budget analysis page with charts - Add gradient stat cards with color-coded status indicators - Implement responsive layouts with full dark mode support - Add smooth animations and toast notifications - Integrate Chart.js for cost trend visualization Project Integration: - Add Budget Alerts link to Finance navigation menu - Enhance project view page with budget overview card - Show budget progress bars with status indicators - Add Budget Analysis button to project header and dashboard - Display real-time budget status with color-coded badges Visual Enhancements: - Use gradient backgrounds for stat cards (blue/green/yellow/red) - Add status badges with icons (healthy/warning/critical/over) - Implement smooth progress bars with embedded percentages - Support responsive grid layouts for all screen sizes - Ensure proper type conversion (Decimal to float) in templates Scheduled Tasks: - Register budget alert checking job (runs every 6 hours) - Integrate with existing APScheduler tasks - Add logging for alert creation and monitoring This feature provides project managers with real-time budget insights, predictive analytics, and proactive alerts to prevent budget overruns.
239 lines
8.7 KiB
Python
239 lines
8.7 KiB
Python
"""Scheduled background tasks for the application"""
|
|
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from flask import current_app
|
|
from app import db
|
|
from app.models import Invoice, User, TimeEntry, Project, BudgetAlert
|
|
from app.utils.email import send_overdue_invoice_notification, send_weekly_summary
|
|
from app.utils.budget_forecasting import check_budget_alerts
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def check_overdue_invoices():
|
|
"""Check for overdue invoices and send notifications
|
|
|
|
This task should be run daily to check for invoices that are past their due date
|
|
and send notifications to users who have overdue invoice notifications enabled.
|
|
"""
|
|
try:
|
|
logger.info("Checking for overdue invoices...")
|
|
|
|
# Get all invoices that are overdue and not paid/cancelled
|
|
today = datetime.utcnow().date()
|
|
overdue_invoices = Invoice.query.filter(
|
|
Invoice.due_date < today,
|
|
Invoice.status.in_(['draft', 'sent'])
|
|
).all()
|
|
|
|
logger.info(f"Found {len(overdue_invoices)} overdue invoices")
|
|
|
|
notifications_sent = 0
|
|
for invoice in overdue_invoices:
|
|
# Update invoice status to overdue if it's not already
|
|
if invoice.status != 'overdue':
|
|
invoice.status = 'overdue'
|
|
db.session.commit()
|
|
|
|
# Get users to notify (creator and admins)
|
|
users_to_notify = set()
|
|
|
|
# Add the invoice creator
|
|
if invoice.creator:
|
|
users_to_notify.add(invoice.creator)
|
|
|
|
# Add all admins
|
|
admins = User.query.filter_by(role='admin', is_active=True).all()
|
|
users_to_notify.update(admins)
|
|
|
|
# Send notifications
|
|
for user in users_to_notify:
|
|
if user.email and user.email_notifications and user.notification_overdue_invoices:
|
|
try:
|
|
send_overdue_invoice_notification(invoice, user)
|
|
notifications_sent += 1
|
|
logger.info(f"Sent overdue notification for invoice {invoice.invoice_number} to {user.username}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to send notification to {user.username}: {e}")
|
|
|
|
logger.info(f"Sent {notifications_sent} overdue invoice notifications")
|
|
return notifications_sent
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error checking overdue invoices: {e}")
|
|
return 0
|
|
|
|
|
|
def send_weekly_summaries():
|
|
"""Send weekly time tracking summaries to users
|
|
|
|
This task should be run weekly (e.g., Sunday evening or Monday morning)
|
|
to send time tracking summaries to users who have opted in.
|
|
"""
|
|
try:
|
|
logger.info("Sending weekly summaries...")
|
|
|
|
# Get users who want weekly summaries
|
|
users = User.query.filter_by(
|
|
is_active=True,
|
|
email_notifications=True,
|
|
notification_weekly_summary=True
|
|
).all()
|
|
|
|
logger.info(f"Found {len(users)} users with weekly summaries enabled")
|
|
|
|
# Calculate date range (last 7 days)
|
|
end_date = datetime.utcnow().date()
|
|
start_date = end_date - timedelta(days=7)
|
|
|
|
summaries_sent = 0
|
|
for user in users:
|
|
if not user.email:
|
|
continue
|
|
|
|
try:
|
|
# Get time entries for this user in the past week
|
|
entries = TimeEntry.query.filter(
|
|
TimeEntry.user_id == user.id,
|
|
TimeEntry.start_time >= datetime.combine(start_date, datetime.min.time()),
|
|
TimeEntry.start_time < datetime.combine(end_date + timedelta(days=1), datetime.min.time()),
|
|
TimeEntry.end_time.isnot(None)
|
|
).all()
|
|
|
|
if not entries:
|
|
logger.info(f"No entries for {user.username}, skipping")
|
|
continue
|
|
|
|
# Calculate hours worked
|
|
hours_worked = sum(e.duration_hours for e in entries)
|
|
|
|
# Group by project
|
|
projects_map = {}
|
|
for entry in entries:
|
|
if entry.project:
|
|
project_name = entry.project.name
|
|
if project_name not in projects_map:
|
|
projects_map[project_name] = {'name': project_name, 'hours': 0}
|
|
projects_map[project_name]['hours'] += entry.duration_hours
|
|
|
|
projects_data = sorted(projects_map.values(), key=lambda x: x['hours'], reverse=True)
|
|
|
|
# Send email
|
|
send_weekly_summary(
|
|
user=user,
|
|
start_date=start_date.strftime('%Y-%m-%d'),
|
|
end_date=end_date.strftime('%Y-%m-%d'),
|
|
hours_worked=hours_worked,
|
|
projects_data=projects_data
|
|
)
|
|
|
|
summaries_sent += 1
|
|
logger.info(f"Sent weekly summary to {user.username}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send weekly summary to {user.username}: {e}")
|
|
|
|
logger.info(f"Sent {summaries_sent} weekly summaries")
|
|
return summaries_sent
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error sending weekly summaries: {e}")
|
|
return 0
|
|
|
|
|
|
def check_project_budget_alerts():
|
|
"""Check all active projects for budget alerts
|
|
|
|
This task should be run periodically (e.g., every 6 hours) to check
|
|
project budgets and create alerts when thresholds are exceeded.
|
|
"""
|
|
try:
|
|
logger.info("Checking project budget alerts...")
|
|
|
|
# Get all active projects with budgets
|
|
projects = Project.query.filter(
|
|
Project.budget_amount.isnot(None),
|
|
Project.status == 'active'
|
|
).all()
|
|
|
|
logger.info(f"Found {len(projects)} active projects with budgets")
|
|
|
|
total_alerts_created = 0
|
|
for project in projects:
|
|
try:
|
|
# Check for budget alerts
|
|
alerts_to_create = check_budget_alerts(project.id)
|
|
|
|
# Create alerts
|
|
for alert_data in alerts_to_create:
|
|
alert = BudgetAlert.create_alert(
|
|
project_id=alert_data['project_id'],
|
|
alert_type=alert_data['type'],
|
|
budget_consumed_percent=alert_data['budget_consumed_percent'],
|
|
budget_amount=alert_data['budget_amount'],
|
|
consumed_amount=alert_data['consumed_amount']
|
|
)
|
|
total_alerts_created += 1
|
|
logger.info(f"Created {alert_data['type']} alert for project {project.name}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error checking budget alerts for project {project.id}: {e}")
|
|
|
|
logger.info(f"Created {total_alerts_created} budget alerts")
|
|
return total_alerts_created
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error checking project budget alerts: {e}")
|
|
return 0
|
|
|
|
|
|
def register_scheduled_tasks(scheduler):
|
|
"""Register all scheduled tasks with APScheduler
|
|
|
|
Args:
|
|
scheduler: APScheduler instance
|
|
"""
|
|
try:
|
|
# Check overdue invoices daily at 9 AM
|
|
scheduler.add_job(
|
|
func=check_overdue_invoices,
|
|
trigger='cron',
|
|
hour=9,
|
|
minute=0,
|
|
id='check_overdue_invoices',
|
|
name='Check for overdue invoices',
|
|
replace_existing=True
|
|
)
|
|
logger.info("Registered overdue invoices check task")
|
|
|
|
# Send weekly summaries every Monday at 8 AM
|
|
scheduler.add_job(
|
|
func=send_weekly_summaries,
|
|
trigger='cron',
|
|
day_of_week='mon',
|
|
hour=8,
|
|
minute=0,
|
|
id='send_weekly_summaries',
|
|
name='Send weekly time summaries',
|
|
replace_existing=True
|
|
)
|
|
logger.info("Registered weekly summaries task")
|
|
|
|
# Check budget alerts every 6 hours
|
|
scheduler.add_job(
|
|
func=check_project_budget_alerts,
|
|
trigger='cron',
|
|
hour='*/6',
|
|
minute=0,
|
|
id='check_budget_alerts',
|
|
name='Check project budget alerts',
|
|
replace_existing=True
|
|
)
|
|
logger.info("Registered budget alerts check task")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error registering scheduled tasks: {e}")
|
|
|