All checks were successful
Build and Push Docker Image / build (push) Successful in 4m14s
Reference numbers (unified scheme PREFIX-YYYYMM-HOSP-NNNN, e.g. CMP-202606-HHN-0001): - new ReferenceSequence model + generate_reference() helper (apps/core) - Complaint/Inquiry/Observation/Appreciation/Suggestion emit unified refs via save() - prefix-based auto-routing in public track API (CMP/INQ/OBS trackable; APR/SGT internal-only) - removed legacy CMP-/INQ- generators in ui_views, integrations, px_sources - migrations: core.0003_referencesequence, appreciation.0006, feedback.0008, observations.0012 - unit tests (format, sanitization, monthly reset, 40-thread concurrency) QA audit: - isolated E2E hospital sandbox mirroring HH-N + 10 role users (create_e2e_isolated_env) - feedback-modules-audit.spec.ts + audit helper (headed, run-to-completion) - reports/feedback-modules-qa-report.md Also bundles accumulated in-progress work across complaints, observations, organizations, templates, and other modules.
140 lines
5.3 KiB
Python
140 lines
5.3 KiB
Python
import logging
|
|
|
|
from celery import shared_task
|
|
from django.utils import timezone
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@shared_task
|
|
def process_scheduled_reports():
|
|
from apps.reports.models import ReportSchedule
|
|
|
|
now = timezone.now()
|
|
|
|
for schedule in ReportSchedule.objects.filter(is_active=True, next_run_at__isnull=True):
|
|
schedule.next_run_at = schedule.calculate_next_run()
|
|
if schedule.next_run_at:
|
|
schedule.save(update_fields=["next_run_at"])
|
|
|
|
schedules = ReportSchedule.objects.filter(
|
|
is_active=True,
|
|
next_run_at__lte=now,
|
|
).select_related("report")
|
|
|
|
dispatched = 0
|
|
for schedule in schedules:
|
|
generate_and_deliver_report.delay(str(schedule.id))
|
|
dispatched += 1
|
|
|
|
logger.info(f"Dispatched {dispatched} scheduled reports")
|
|
return {"dispatched": dispatched}
|
|
|
|
|
|
@shared_task
|
|
def generate_and_deliver_report(schedule_id):
|
|
from apps.reports.models import ReportSchedule, GeneratedReport
|
|
from apps.reports.services import ReportBuilderService
|
|
from apps.notifications.services import NotificationService, get_email_header_html
|
|
|
|
try:
|
|
schedule = ReportSchedule.objects.select_related("report").get(id=schedule_id)
|
|
except ReportSchedule.DoesNotExist:
|
|
logger.error(f"ReportSchedule {schedule_id} not found")
|
|
return {"status": "error", "reason": "not_found"}
|
|
|
|
try:
|
|
report = schedule.report
|
|
|
|
report_data = ReportBuilderService.generate_report_data(
|
|
data_source=report.data_source,
|
|
filter_config=report.filter_config,
|
|
column_config=report.column_config,
|
|
grouping_config=report.grouping_config,
|
|
sort_config=report.sort_config,
|
|
)
|
|
summary = ReportBuilderService.generate_summary(
|
|
data_source=report.data_source,
|
|
filter_config=report.filter_config,
|
|
)
|
|
|
|
row_count = len(report_data.get("rows", []))
|
|
|
|
generated = GeneratedReport.objects.create(
|
|
saved_report=report,
|
|
name=report.name,
|
|
data_source=report.data_source,
|
|
filter_config=report.filter_config,
|
|
column_config=report.column_config,
|
|
grouping_config=report.grouping_config,
|
|
chart_config=report.chart_config,
|
|
data=report_data,
|
|
summary=summary,
|
|
chart_data={},
|
|
row_count=row_count,
|
|
hospital=report.hospital,
|
|
)
|
|
|
|
report.last_run_at = timezone.now()
|
|
report.last_run_count = row_count
|
|
report.save(update_fields=["last_run_at", "last_run_count"])
|
|
|
|
schedule.last_run_at = timezone.now()
|
|
schedule.last_error = ""
|
|
|
|
next_run = schedule.calculate_next_run()
|
|
if schedule.frequency == "once":
|
|
schedule.is_active = False
|
|
schedule.next_run_at = None
|
|
else:
|
|
schedule.next_run_at = next_run
|
|
|
|
schedule.save(update_fields=["last_run_at", "next_run_at", "is_active", "last_error"])
|
|
|
|
subject = f"Scheduled Report: {report.name}"
|
|
message = (
|
|
f"Report: {report.name}\n"
|
|
f"Data Source: {report.get_data_source_display()}\n"
|
|
f"Rows: {row_count}\n"
|
|
f"Generated: {timezone.now().strftime('%Y-%m-%d %H:%M')}\n\n"
|
|
f"View the full report in the PX360 dashboard."
|
|
)
|
|
|
|
for recipient in schedule.recipients:
|
|
try:
|
|
NotificationService.send_email(
|
|
email=recipient,
|
|
subject=subject,
|
|
message=message,
|
|
related_object=generated,
|
|
notification_type="report",
|
|
html_message=f"""
|
|
<div style="font-family: 'Segoe UI', Tahoma, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden;">
|
|
{get_email_header_html()}
|
|
<div style="padding: 20px;">
|
|
<h2 style="color: #005696; font-size: 18px; margin: 0 0 12px 0;">Scheduled Report: {report.name}</h2>
|
|
<table style="width: 100%; border-collapse: collapse; margin: 0 0 12px 0;">
|
|
<tr><td style="padding: 4px 0; color: #6b7280; width: 140px;">Data Source:</td><td style="padding: 4px 0;">{report.get_data_source_display()}</td></tr>
|
|
<tr><td style="padding: 4px 0; color: #6b7280;">Rows:</td><td style="padding: 4px 0;">{row_count}</td></tr>
|
|
<tr><td style="padding: 4px 0; color: #6b7280;">Generated:</td><td style="padding: 4px 0;">{timezone.now().strftime('%Y-%m-%d %H:%M')}</td></tr>
|
|
</table>
|
|
<p style="margin: 0; color: #6b7280;">View the full report in the PX360 dashboard.</p>
|
|
</div>
|
|
</div>
|
|
""",
|
|
)
|
|
except Exception as email_err:
|
|
logger.error(f"Failed to send report email to {recipient}: {email_err}")
|
|
|
|
logger.info(f"Generated and delivered report for schedule {schedule_id}")
|
|
return {"status": "success", "generated_report_id": str(generated.id), "row_count": row_count}
|
|
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
logger.error(f"Error generating report for schedule {schedule_id}: {error_msg}", exc_info=True)
|
|
|
|
schedule.last_error = error_msg
|
|
schedule.save(update_fields=["last_error"])
|
|
|
|
return {"status": "error", "reason": error_msg}
|