Files
arrr-erp/scripts/repair_missing_engagement_tasks.py
T
2026-08-07 15:33:49 +05:30

151 lines
5.2 KiB
Python

from __future__ import annotations
import argparse
from collections import Counter
from sqlalchemy import func, select
# Standalone utilities do not pass through FastAPI startup automatically.
# Importing startup loads the complete SQLAlchemy model registry (including
# ConsultantProfile and all relationship targets) before ORM queries compile.
import app.core.startup # noqa: F401
from app.core.db.common import CommonSessionLocal
from app.modules.services.execution import generate_tasks_for_subscription_if_ready
from app.modules.services.models import (
ClientServiceSubscription,
ClientServiceTaskInstance,
FirmServiceTaskTemplate,
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Generate missing task instances for existing active engagements. "
"Dry-run is the default; pass --apply to commit."
)
)
parser.add_argument("--apply", action="store_true", help="Commit generated tasks.")
parser.add_argument("--tenant-id", type=int, default=None, help="Optional tenant filter.")
parser.add_argument(
"--subscription-id",
type=int,
action="append",
default=[],
help="Repair only this engagement/subscription ID. May be repeated.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
db = CommonSessionLocal()
try:
existing_task_count = (
select(
ClientServiceTaskInstance.subscription_id.label("subscription_id"),
func.count(ClientServiceTaskInstance.id).label("task_count"),
)
.group_by(ClientServiceTaskInstance.subscription_id)
.subquery()
)
stmt = (
select(ClientServiceSubscription)
.outerjoin(
existing_task_count,
existing_task_count.c.subscription_id == ClientServiceSubscription.id,
)
.where(
ClientServiceSubscription.status == "active",
ClientServiceSubscription.is_active.is_(True),
func.coalesce(existing_task_count.c.task_count, 0) == 0,
)
.order_by(ClientServiceSubscription.tenant_id, ClientServiceSubscription.id)
)
if args.tenant_id is not None:
stmt = stmt.where(ClientServiceSubscription.tenant_id == args.tenant_id)
if args.subscription_id:
stmt = stmt.where(ClientServiceSubscription.id.in_(args.subscription_id))
engagements = db.execute(stmt).scalars().all()
print(f"Candidate active engagements with zero task instances: {len(engagements)}")
summary = Counter()
total_created = 0
for engagement in engagements:
template_count = db.execute(
select(func.count(FirmServiceTaskTemplate.id)).where(
FirmServiceTaskTemplate.tenant_id == engagement.tenant_id,
FirmServiceTaskTemplate.service_catalogue_id == engagement.service_catalogue_id,
FirmServiceTaskTemplate.is_active.is_(True),
)
).scalar_one()
actor_user_id = (
engagement.updated_by_user_id
or engagement.created_by_user_id
or engagement.assigned_partner_user_id
or engagement.assigned_manager_user_id
or engagement.assigned_staff_user_id
)
if not template_count:
summary["no_active_templates"] += 1
print(
f"SKIP engagement={engagement.id}: "
"no active Firm Service Task Templates."
)
continue
if not actor_user_id:
summary["no_actor_user"] += 1
print(
f"SKIP engagement={engagement.id}: "
"no valid existing actor/team user available for audit fields."
)
continue
created = generate_tasks_for_subscription_if_ready(
db,
subscription=engagement,
user_id=int(actor_user_id),
)
if created:
total_created += created
summary["repaired"] += 1
print(
f"READY engagement={engagement.id}: "
f"{created} task instance(s) generated from {template_count} active template(s)."
)
else:
summary["quality_gate_or_no_change"] += 1
print(
f"NO CHANGE engagement={engagement.id}: "
"AQMM acceptance gate is pending or no task was required."
)
if args.apply:
db.commit()
print(f"COMMITTED. Total task instances generated: {total_created}")
else:
db.rollback()
print(f"DRY RUN ONLY. Would generate {total_created} task instance(s). No database changes committed.")
print("Summary:")
for key in sorted(summary):
print(f" {key}: {summary[key]}")
return 0
except Exception:
db.rollback()
raise
finally:
db.close()
if __name__ == "__main__":
raise SystemExit(main())