Auto-create and repair subscription engagements
This commit is contained in:
@@ -224,8 +224,169 @@ def _engagement_is_effective(row: ClientServiceSubscription, effective_from: dat
|
|||||||
return comparison_date is None or comparison_date >= effective_from
|
return comparison_date is None or comparison_date >= effective_from
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _current_period_for_financial_year(financial_year: str, recurrence_type: str | None) -> str:
|
||||||
|
"""Return the current period inside the selected FY for recurring services.
|
||||||
|
|
||||||
|
For the active FY this means the current calendar month / FY quarter. The
|
||||||
|
caller still passes the value through normalize_period_label, so the same
|
||||||
|
validation used by the existing engagement creation flow remains in force.
|
||||||
|
"""
|
||||||
|
recurrence = (recurrence_type or "").strip().lower().replace("-", "_").replace(" ", "_")
|
||||||
|
today = date.today()
|
||||||
|
fy = normalize_financial_year(financial_year)
|
||||||
|
start_year = int(fy.split("-", 1)[0])
|
||||||
|
end_year = start_year + 1
|
||||||
|
|
||||||
|
if recurrence == "monthly":
|
||||||
|
if today.year < start_year or (today.year == start_year and today.month < 4):
|
||||||
|
year, month = start_year, 4
|
||||||
|
elif today.year > end_year or (today.year == end_year and today.month > 3):
|
||||||
|
year, month = end_year, 3
|
||||||
|
else:
|
||||||
|
year, month = today.year, today.month
|
||||||
|
return normalize_period_label(
|
||||||
|
f"{year:04d}-{month:02d}",
|
||||||
|
financial_year=fy,
|
||||||
|
recurrence_type=recurrence,
|
||||||
|
)
|
||||||
|
|
||||||
|
if recurrence == "quarterly":
|
||||||
|
if today.year < start_year or (today.year == start_year and today.month < 4):
|
||||||
|
quarter = "Q1"
|
||||||
|
elif today.year > end_year or (today.year == end_year and today.month > 3):
|
||||||
|
quarter = "Q4"
|
||||||
|
elif today.month in (4, 5, 6):
|
||||||
|
quarter = "Q1"
|
||||||
|
elif today.month in (7, 8, 9):
|
||||||
|
quarter = "Q2"
|
||||||
|
elif today.month in (10, 11, 12):
|
||||||
|
quarter = "Q3"
|
||||||
|
else:
|
||||||
|
quarter = "Q4"
|
||||||
|
return normalize_period_label(
|
||||||
|
quarter,
|
||||||
|
financial_year=fy,
|
||||||
|
recurrence_type=recurrence,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_current_engagement_for_plan(
|
||||||
|
db,
|
||||||
|
*,
|
||||||
|
plan: ClientServicePlan,
|
||||||
|
tenant_id: int,
|
||||||
|
financial_year: str,
|
||||||
|
actor_user_id: int,
|
||||||
|
) -> tuple[ClientServiceSubscription, bool]:
|
||||||
|
"""Create the current engagement for one plan, reusing existing production logic.
|
||||||
|
|
||||||
|
Returns (engagement, created). Existing engagements are never duplicated or
|
||||||
|
overwritten. No task/review/evidence history is modified.
|
||||||
|
"""
|
||||||
|
selected_financial_year = normalize_financial_year(financial_year)
|
||||||
|
recurrence_type = getattr(plan, "recurrence_type", None) or getattr(plan.catalogue, "recurrence_type", None)
|
||||||
|
requested_period = _current_period_for_financial_year(selected_financial_year, recurrence_type)
|
||||||
|
|
||||||
|
existing = db.execute(
|
||||||
|
select(ClientServiceSubscription).where(
|
||||||
|
ClientServiceSubscription.tenant_id == tenant_id,
|
||||||
|
ClientServiceSubscription.service_catalogue_id == plan.service_catalogue_id,
|
||||||
|
ClientServiceSubscription.scope_key == plan.scope_key,
|
||||||
|
ClientServiceSubscription.financial_year == selected_financial_year,
|
||||||
|
ClientServiceSubscription.period_label == requested_period,
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if existing:
|
||||||
|
if existing.service_plan_id is None:
|
||||||
|
existing.service_plan_id = plan.id
|
||||||
|
return existing, False
|
||||||
|
|
||||||
|
client = db.get(Client, plan.client_id)
|
||||||
|
if not client or client.tenant_id != tenant_id:
|
||||||
|
raise ValueError("client")
|
||||||
|
|
||||||
|
firm_selection = get_enabled_firm_service(
|
||||||
|
db,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
service_catalogue_id=plan.service_catalogue_id,
|
||||||
|
)
|
||||||
|
if not firm_selection:
|
||||||
|
raise ValueError("service")
|
||||||
|
|
||||||
|
engagement_type = getattr(firm_selection.catalogue, "engagement_type", None) or "non_assurance"
|
||||||
|
review_required = review_partner_required_for_engagement(
|
||||||
|
db,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
engagement_type=engagement_type,
|
||||||
|
)
|
||||||
|
review_partner_id = plan.default_review_partner_user_id if review_required else None
|
||||||
|
performing_partner_id = plan.default_performing_partner_user_id or plan.default_partner_user_id
|
||||||
|
|
||||||
|
if review_required and review_partner_id is None:
|
||||||
|
raise ValueError("review_partner_required")
|
||||||
|
if review_required and review_partner_id in {plan.default_partner_user_id, performing_partner_id}:
|
||||||
|
raise ValueError("review_partner_independence")
|
||||||
|
|
||||||
|
engagement = ClientServiceSubscription(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
branch_id=plan.branch_id or getattr(client, "branch_id", None),
|
||||||
|
service_plan_id=plan.id,
|
||||||
|
client_id=client.id,
|
||||||
|
scope_type=plan.scope_type,
|
||||||
|
scope_key=plan.scope_key,
|
||||||
|
business_unit_id=plan.business_unit_id,
|
||||||
|
client_branch_id=plan.client_branch_id,
|
||||||
|
registration_id=plan.registration_id,
|
||||||
|
service_catalogue_id=plan.service_catalogue_id,
|
||||||
|
firm_service_selection_id=firm_selection.id,
|
||||||
|
assigned_partner_user_id=plan.default_partner_user_id,
|
||||||
|
performing_partner_user_id=performing_partner_id,
|
||||||
|
assigned_manager_user_id=plan.default_manager_user_id,
|
||||||
|
assigned_staff_user_id=plan.default_staff_user_id,
|
||||||
|
review_partner_user_id=review_partner_id,
|
||||||
|
financial_year=selected_financial_year,
|
||||||
|
period_label=requested_period,
|
||||||
|
assessment_year=assessment_year_from_financial_year(selected_financial_year),
|
||||||
|
engagement_type=engagement_type,
|
||||||
|
start_date=plan.effective_from,
|
||||||
|
end_date=plan.effective_to,
|
||||||
|
status="active",
|
||||||
|
remarks=plan.remarks,
|
||||||
|
is_active=True,
|
||||||
|
created_by_user_id=actor_user_id,
|
||||||
|
updated_by_user_id=actor_user_id,
|
||||||
|
)
|
||||||
|
db.add(engagement)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
apply_due_date_rule_to_subscription(db, engagement, force=True)
|
||||||
|
ensure_engagement_quality_workflow(
|
||||||
|
db,
|
||||||
|
subscription=engagement,
|
||||||
|
actor_user_id=actor_user_id,
|
||||||
|
create_declarations=False,
|
||||||
|
)
|
||||||
|
enforce_quality_gate_on_subscription(engagement)
|
||||||
|
generate_tasks_for_subscription_if_ready(
|
||||||
|
db,
|
||||||
|
subscription=engagement,
|
||||||
|
user_id=actor_user_id,
|
||||||
|
)
|
||||||
|
return engagement, True
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
def subscription_master_list(request: Request, q: str = "", include_inactive: bool = False):
|
def subscription_master_list(
|
||||||
|
request: Request,
|
||||||
|
q: str = "",
|
||||||
|
include_inactive: bool = False,
|
||||||
|
repaired: int = 0,
|
||||||
|
skipped: int = 0,
|
||||||
|
repair_error: str = "",
|
||||||
|
):
|
||||||
db = CommonSessionLocal()
|
db = CommonSessionLocal()
|
||||||
try:
|
try:
|
||||||
user = get_current_user(request, db=db)
|
user = get_current_user(request, db=db)
|
||||||
@@ -295,6 +456,10 @@ def subscription_master_list(request: Request, q: str = "", include_inactive: bo
|
|||||||
q=q,
|
q=q,
|
||||||
include_inactive=include_inactive,
|
include_inactive=include_inactive,
|
||||||
can_edit_subscription="clients.edit" in set(get_user_permissions(db, user.id)),
|
can_edit_subscription="clients.edit" in set(get_user_permissions(db, user.id)),
|
||||||
|
repaired=repaired,
|
||||||
|
skipped=skipped,
|
||||||
|
repair_error=repair_error,
|
||||||
|
financial_year=_active_financial_year(request),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
@@ -302,6 +467,85 @@ def subscription_master_list(request: Request, q: str = "", include_inactive: bo
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/generate-missing")
|
||||||
|
def subscription_generate_missing_submit(
|
||||||
|
request: Request,
|
||||||
|
financial_year: str = Form(""),
|
||||||
|
csrf_token: str = Form(...),
|
||||||
|
):
|
||||||
|
validate_csrf(request, csrf_token)
|
||||||
|
db = CommonSessionLocal()
|
||||||
|
created_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
try:
|
||||||
|
user = get_current_user(request, db=db)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse("/login", 303)
|
||||||
|
try:
|
||||||
|
require_permission(db, user, "clients.edit")
|
||||||
|
except Exception:
|
||||||
|
return _denied()
|
||||||
|
|
||||||
|
tenant_id = _tenant_id(request, user)
|
||||||
|
branch_id = _branch_id(request, user)
|
||||||
|
selected_financial_year = normalize_financial_year(financial_year or _active_financial_year(request))
|
||||||
|
locked = redirect_if_financial_year_locked(
|
||||||
|
db,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
year_code=selected_financial_year,
|
||||||
|
redirect_url="/services/subscriptions?repair_error=financial_year",
|
||||||
|
)
|
||||||
|
if locked:
|
||||||
|
return locked
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
select(ClientServicePlan)
|
||||||
|
.options(
|
||||||
|
selectinload(ClientServicePlan.client),
|
||||||
|
selectinload(ClientServicePlan.catalogue),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
ClientServicePlan.tenant_id == tenant_id,
|
||||||
|
ClientServicePlan.is_active.is_(True),
|
||||||
|
ClientServicePlan.status == "active",
|
||||||
|
)
|
||||||
|
.order_by(ClientServicePlan.id)
|
||||||
|
)
|
||||||
|
if branch_id:
|
||||||
|
stmt = stmt.where(ClientServicePlan.branch_id == branch_id)
|
||||||
|
|
||||||
|
plans = db.execute(stmt).scalars().all()
|
||||||
|
for plan in plans:
|
||||||
|
try:
|
||||||
|
_engagement, created = _ensure_current_engagement_for_plan(
|
||||||
|
db,
|
||||||
|
plan=plan,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
financial_year=selected_financial_year,
|
||||||
|
actor_user_id=user.id,
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
created_count += 1
|
||||||
|
else:
|
||||||
|
skipped_count += 1
|
||||||
|
except ValueError:
|
||||||
|
# A plan with an invalid/missing service setup or unmet assurance
|
||||||
|
# review-partner gate is left untouched; the rest continue safely.
|
||||||
|
skipped_count += 1
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/services/subscriptions?repaired={created_count}&skipped={skipped_count}",
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/bulk")
|
@router.get("/bulk")
|
||||||
def subscription_bulk_page(
|
def subscription_bulk_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -1192,8 +1436,83 @@ def subscription_master_edit_submit(
|
|||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{plan_id}/generate-current")
|
||||||
|
def subscription_generate_current_submit(
|
||||||
|
request: Request,
|
||||||
|
plan_id: int,
|
||||||
|
financial_year: str = Form(""),
|
||||||
|
csrf_token: str = Form(...),
|
||||||
|
):
|
||||||
|
validate_csrf(request, csrf_token)
|
||||||
|
db = CommonSessionLocal()
|
||||||
|
try:
|
||||||
|
user = get_current_user(request, db=db)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse("/login", 303)
|
||||||
|
try:
|
||||||
|
require_permission(db, user, "clients.edit")
|
||||||
|
except Exception:
|
||||||
|
return _denied()
|
||||||
|
|
||||||
|
tenant_id = _tenant_id(request, user)
|
||||||
|
branch_id = _branch_id(request, user)
|
||||||
|
plan = _load_plan(db, plan_id=plan_id, tenant_id=tenant_id, branch_id=branch_id)
|
||||||
|
if not plan:
|
||||||
|
return RedirectResponse("/services/subscriptions", 303)
|
||||||
|
if not plan.is_active or plan.status != "active":
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/services/subscriptions/{plan.id}?generation_error=inactive",
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
|
||||||
|
selected_financial_year = normalize_financial_year(financial_year or _active_financial_year(request))
|
||||||
|
locked = redirect_if_financial_year_locked(
|
||||||
|
db,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
year_code=selected_financial_year,
|
||||||
|
redirect_url=f"/services/subscriptions/{plan.id}?generation_error=financial_year",
|
||||||
|
)
|
||||||
|
if locked:
|
||||||
|
return locked
|
||||||
|
|
||||||
|
try:
|
||||||
|
_engagement, created = _ensure_current_engagement_for_plan(
|
||||||
|
db,
|
||||||
|
plan=plan,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
financial_year=selected_financial_year,
|
||||||
|
actor_user_id=user.id,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
db.rollback()
|
||||||
|
code = str(exc) or "validation"
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/services/subscriptions/{plan.id}?generation_error={code}",
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
flag = "generated=1" if created else "existing=1"
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/services/subscriptions/{plan.id}?{flag}",
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{plan_id}")
|
@router.get("/{plan_id}")
|
||||||
def subscription_master_detail(request: Request, plan_id: int):
|
def subscription_master_detail(
|
||||||
|
request: Request,
|
||||||
|
plan_id: int,
|
||||||
|
generated: int = 0,
|
||||||
|
existing: int = 0,
|
||||||
|
generation_error: str = "",
|
||||||
|
):
|
||||||
db = CommonSessionLocal()
|
db = CommonSessionLocal()
|
||||||
try:
|
try:
|
||||||
user = get_current_user(request, db=db)
|
user = get_current_user(request, db=db)
|
||||||
@@ -1228,6 +1547,10 @@ def subscription_master_detail(request: Request, plan_id: int):
|
|||||||
plan=plan,
|
plan=plan,
|
||||||
engagements=engagements,
|
engagements=engagements,
|
||||||
can_edit_subscription="clients.edit" in permissions,
|
can_edit_subscription="clients.edit" in permissions,
|
||||||
|
generated=generated,
|
||||||
|
existing=existing,
|
||||||
|
generation_error=generation_error,
|
||||||
|
financial_year=_active_financial_year(request),
|
||||||
**_registration_scope_context(
|
**_registration_scope_context(
|
||||||
db,
|
db,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
|
|||||||
@@ -33,6 +33,17 @@
|
|||||||
Map Scope
|
Map Scope
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if can_edit_subscription and plan.is_active and plan.status == 'active' %}
|
||||||
|
<form method="post" action="/services/subscriptions/{{ plan.id }}/generate-current"
|
||||||
|
onsubmit="return confirm('Generate the current engagement for FY {{ financial_year }}? Existing matching engagements will be reused.');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<input type="hidden" name="financial_year" value="{{ financial_year }}">
|
||||||
|
<button type="submit"
|
||||||
|
class="rounded-xl border border-emerald-300 px-4 py-2 text-sm font-medium text-emerald-700 hover:bg-emerald-50">
|
||||||
|
Generate Current Engagement
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
<a href="/services/engagements/new?client_id={{ plan.client_id }}"
|
<a href="/services/engagements/new?client_id={{ plan.client_id }}"
|
||||||
class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">
|
class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">
|
||||||
Create Engagement
|
Create Engagement
|
||||||
@@ -41,6 +52,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% if generated %}
|
||||||
|
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">
|
||||||
|
Current FY engagement generated successfully using the subscription defaults. Due dates, quality workflow and task generation were processed through the existing engagement engine.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if existing %}
|
||||||
|
<div class="rounded-xl border border-sky-200 bg-sky-50 px-4 py-3 text-sm text-sky-800">
|
||||||
|
A matching engagement already exists for the current FY/period, so no duplicate was created.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if generation_error %}
|
||||||
|
<div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">
|
||||||
|
{% if generation_error == 'financial_year' %}
|
||||||
|
Engagement generation is not allowed because FY {{ financial_year }} is locked.
|
||||||
|
{% elif generation_error == 'review_partner_required' %}
|
||||||
|
This assurance service requires a Review Partner before an engagement can be generated.
|
||||||
|
{% elif generation_error == 'review_partner_independence' %}
|
||||||
|
The Review Partner must differ from the Engagement Partner and Performing Partner.
|
||||||
|
{% elif generation_error == 'inactive' %}
|
||||||
|
This subscription is inactive and cannot generate an engagement.
|
||||||
|
{% elif generation_error == 'service' %}
|
||||||
|
The underlying firm service is not enabled, so the engagement was not generated.
|
||||||
|
{% else %}
|
||||||
|
The current engagement could not be generated. Please verify the subscription scope and defaults.
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -13,11 +13,39 @@
|
|||||||
Persistent client services shown by their assigned scope. Period-wise engagements are generated and tracked separately.
|
Persistent client services shown by their assigned scope. Period-wise engagements are generated and tracked separately.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
{% if can_edit_subscription %}
|
||||||
|
<form method="post" action="/services/subscriptions/generate-missing"
|
||||||
|
onsubmit="return confirm('Generate the current missing engagement for all active subscriptions in FY {{ financial_year }}? Existing engagements will not be duplicated.');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<input type="hidden" name="financial_year" value="{{ financial_year }}">
|
||||||
|
<button type="submit"
|
||||||
|
class="rounded-xl border border-emerald-300 px-4 py-2 text-sm font-medium text-emerald-700 hover:bg-emerald-50">
|
||||||
|
Generate Missing Engagements
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
<a href="/services/engagements/new"
|
<a href="/services/engagements/new"
|
||||||
class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">
|
class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">
|
||||||
Assign Service
|
Assign Service
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if repaired or skipped %}
|
||||||
|
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">
|
||||||
|
Missing-engagement repair completed for FY {{ financial_year }}. {{ repaired }} engagement(s) created and {{ skipped }} subscription(s) skipped because an engagement already existed or the subscription was not ready for generation.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if repair_error %}
|
||||||
|
<div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">
|
||||||
|
{% if repair_error == 'financial_year' %}
|
||||||
|
Engagement generation is not allowed because FY {{ financial_year }} is locked.
|
||||||
|
{% else %}
|
||||||
|
Missing engagements could not be generated.
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form method="get" class="flex flex-wrap items-end gap-3 rounded-2xl bg-white p-4 shadow-soft">
|
<form method="get" class="flex flex-wrap items-end gap-3 rounded-2xl bg-white p-4 shadow-soft">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user