Prepare ERP source for Gitea deployment
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Platform/SaaS billing module."""
|
||||
@@ -0,0 +1,168 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, Numeric, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.db.common import CommonBase
|
||||
|
||||
|
||||
class PlatformPlan(CommonBase):
|
||||
__tablename__ = "platform_plans"
|
||||
__table_args__ = (UniqueConstraint("code", name="uq_platform_plans_code"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
code: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
target_account_type: Mapped[str] = mapped_column(String(30), nullable=False, default="AUDIT_FIRM", index=True)
|
||||
billing_cycle: Mapped[str] = mapped_column(String(20), nullable=False, default="Monthly", index=True)
|
||||
base_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
gst_rate: Mapped[Decimal] = mapped_column(Numeric(5, 2), nullable=False, default=Decimal("18.00"))
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, index=True)
|
||||
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
|
||||
features = relationship("PlatformPlanFeature", back_populates="plan", cascade="all, delete-orphan", order_by="PlatformPlanFeature.sort_order.asc()")
|
||||
subscriptions = relationship("PlatformSubscription", back_populates="plan")
|
||||
|
||||
|
||||
class PlatformPlanFeature(CommonBase):
|
||||
__tablename__ = "platform_plan_features"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
plan_id: Mapped[int] = mapped_column(ForeignKey("platform_plans.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
feature_code: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
feature_name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
limit_value: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
is_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
|
||||
plan = relationship("PlatformPlan", back_populates="features")
|
||||
|
||||
|
||||
class PlatformBillingAccount(CommonBase):
|
||||
__tablename__ = "platform_billing_accounts"
|
||||
__table_args__ = (UniqueConstraint("account_type", "account_code", name="uq_platform_billing_accounts_type_code"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
account_type: Mapped[str] = mapped_column(String(30), nullable=False, index=True) # AUDIT_FIRM|CLIENT|CONSULTANT|MARKETPLACE_CUSTOMER
|
||||
account_code: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
|
||||
display_name: Mapped[str] = mapped_column(String(220), nullable=False, index=True)
|
||||
|
||||
tenant_id: Mapped[int | None] = mapped_column(ForeignKey("tenants.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
client_id: Mapped[int | None] = mapped_column(ForeignKey("clients.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
consultant_id: Mapped[int | None] = mapped_column(ForeignKey("consultant_profiles.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
|
||||
email: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
mobile: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
gstin: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
pan: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
billing_address: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
state: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="ACTIVE", index=True)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
|
||||
updated_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
|
||||
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
|
||||
subscriptions = relationship("PlatformSubscription", back_populates="account")
|
||||
invoices = relationship("PlatformInvoice", back_populates="account")
|
||||
|
||||
|
||||
class PlatformSubscription(CommonBase):
|
||||
__tablename__ = "platform_subscriptions"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
account_id: Mapped[int] = mapped_column(ForeignKey("platform_billing_accounts.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
plan_id: Mapped[int] = mapped_column(ForeignKey("platform_plans.id", ondelete="RESTRICT"), nullable=False, index=True)
|
||||
subscription_code: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
start_date: Mapped[date] = mapped_column(Date, nullable=False, default=date.today)
|
||||
end_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
billing_cycle: Mapped[str] = mapped_column(String(20), nullable=False, default="Monthly", index=True)
|
||||
amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
gst_rate: Mapped[Decimal] = mapped_column(Numeric(5, 2), nullable=False, default=Decimal("18.00"))
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="ACTIVE", index=True)
|
||||
auto_generate_invoice: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
|
||||
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
|
||||
account = relationship("PlatformBillingAccount", back_populates="subscriptions")
|
||||
plan = relationship("PlatformPlan", back_populates="subscriptions")
|
||||
|
||||
|
||||
class PlatformInvoice(CommonBase):
|
||||
__tablename__ = "platform_invoices"
|
||||
__table_args__ = (UniqueConstraint("invoice_no", name="uq_platform_invoices_invoice_no"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
account_id: Mapped[int] = mapped_column(ForeignKey("platform_billing_accounts.id", ondelete="RESTRICT"), nullable=False, index=True)
|
||||
subscription_id: Mapped[int | None] = mapped_column(ForeignKey("platform_subscriptions.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
invoice_no: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
|
||||
invoice_date: Mapped[date] = mapped_column(Date, nullable=False, default=date.today, index=True)
|
||||
due_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
|
||||
billing_period_from: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
billing_period_to: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
tax_type: Mapped[str] = mapped_column(String(20), nullable=False, default="CGST_SGST")
|
||||
subtotal: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
discount_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
taxable_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
cgst_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
sgst_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
igst_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
total_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="DRAFT", index=True)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
|
||||
posted_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
|
||||
posted_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
|
||||
account = relationship("PlatformBillingAccount", back_populates="invoices")
|
||||
subscription = relationship("PlatformSubscription")
|
||||
lines = relationship("PlatformInvoiceLine", back_populates="invoice", cascade="all, delete-orphan", order_by="PlatformInvoiceLine.sort_order.asc()")
|
||||
|
||||
|
||||
class PlatformInvoiceLine(CommonBase):
|
||||
__tablename__ = "platform_invoice_lines"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("platform_invoices.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
charge_type: Mapped[str] = mapped_column(String(40), nullable=False, default="SUBSCRIPTION", index=True)
|
||||
description: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
reference_type: Mapped[str | None] = mapped_column(String(60), nullable=True)
|
||||
reference_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
quantity: Mapped[Decimal] = mapped_column(Numeric(12, 2), nullable=False, default=Decimal("1.00"))
|
||||
rate: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
discount_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
taxable_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
gst_rate: Mapped[Decimal] = mapped_column(Numeric(5, 2), nullable=False, default=Decimal("18.00"))
|
||||
cgst_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
sgst_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
igst_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
line_total: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00"))
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
|
||||
invoice = relationship("PlatformInvoice", back_populates="lines")
|
||||
|
||||
|
||||
class PlatformPayment(CommonBase):
|
||||
__tablename__ = "platform_payments"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("platform_invoices.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
account_id: Mapped[int] = mapped_column(ForeignKey("platform_billing_accounts.id", ondelete="RESTRICT"), nullable=False, index=True)
|
||||
payment_date: Mapped[date] = mapped_column(Date, nullable=False, default=date.today, index=True)
|
||||
amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False)
|
||||
mode: Mapped[str] = mapped_column(String(30), nullable=False, default="Bank")
|
||||
reference_no: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
|
||||
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-4xl space-y-6"><h1 class="text-2xl font-semibold">New Platform Billing Account</h1><form method="post" class="space-y-4 rounded-2xl border bg-white p-5 shadow-soft"><input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="grid gap-4 md:grid-cols-2"><label class="text-sm">Account Type<select name="account_type" class="mt-1 w-full rounded-xl border px-3 py-2">{% for x in account_types %}<option value="{{ x }}">{{ x }}</option>{% endfor %}</select></label><label class="text-sm">Account Code<input name="account_code" required class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm md:col-span-2">Display Name<input name="display_name" required class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Link Audit Firm<select name="tenant_id" class="mt-1 w-full rounded-xl border px-3 py-2"><option value="">Not linked</option>{% for t in audit_firms %}<option value="{{ t.id }}">{{ t.name }} ({{ t.code }})</option>{% endfor %}</select></label><label class="text-sm">Link Client<select name="client_id" class="mt-1 w-full rounded-xl border px-3 py-2"><option value="">Not linked</option>{% for c in clients %}<option value="{{ c.id }}">{{ c.client_name }} ({{ c.client_code }})</option>{% endfor %}</select></label><label class="text-sm">Link Consultant<select name="consultant_id" class="mt-1 w-full rounded-xl border px-3 py-2"><option value="">Not linked</option>{% for c in consultants %}<option value="{{ c.id }}">{{ c.contact_person }}{% if c.firm_name %} - {{ c.firm_name }}{% endif %}</option>{% endfor %}</select></label><label class="text-sm">Email<input name="email" class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Mobile<input name="mobile" class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">GSTIN<input name="gstin" class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">PAN<input name="pan" class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">State<input name="state" class="mt-1 w-full rounded-xl border px-3 py-2" /></label></div><label class="block text-sm">Billing Address<textarea name="billing_address" class="mt-1 w-full rounded-xl border px-3 py-2"></textarea></label><label class="block text-sm">Notes<textarea name="notes" class="mt-1 w-full rounded-xl border px-3 py-2"></textarea></label><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Save Account</button></form></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6"><div class="flex items-center justify-between"><h1 class="text-2xl font-semibold">Platform Billing Accounts</h1>{% if can_create %}<a href="/platform-billing/accounts/new" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">New Account</a>{% endif %}</div>
|
||||
<form method="get" class="grid gap-3 rounded-2xl border bg-white p-4 shadow-soft md:grid-cols-[1fr_220px_auto]"><input name="q" value="{{ q or '' }}" placeholder="Search account" class="rounded-xl border px-3 py-2 text-sm" /><select name="account_type" class="rounded-xl border px-3 py-2 text-sm"><option value="">All Types</option>{% for x in account_types %}<option value="{{ x }}" {% if selected_account_type==x %}selected{% endif %}>{{ x }}</option>{% endfor %}</select><button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Search</button></form>
|
||||
<div class="overflow-hidden rounded-2xl border bg-white shadow-soft"><table class="min-w-full text-sm"><thead class="bg-slate-50 text-left"><tr><th class="px-4 py-3">Code</th><th class="px-4 py-3">Name</th><th class="px-4 py-3">Type</th><th class="px-4 py-3">Email</th><th class="px-4 py-3">Status</th></tr></thead><tbody class="divide-y">{% for row in rows %}<tr><td class="px-4 py-3 font-medium">{{ row.account_code }}</td><td class="px-4 py-3">{{ row.display_name }}</td><td class="px-4 py-3">{{ row.account_type }}</td><td class="px-4 py-3">{{ row.email or '-' }}</td><td class="px-4 py-3">{{ row.status }}</td></tr>{% else %}<tr><td colspan="5" class="px-4 py-8 text-center text-slate-500">No billing accounts found.</td></tr>{% endfor %}</tbody></table></div></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,86 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Generate Audit Firm Platform Bills</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Generate draft SaaS invoices for Audit Firm subscriptions. Duplicate invoices for the same subscription and period are skipped.</p>
|
||||
</div>
|
||||
<a href="/platform-billing/audit-firm-subscriptions" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm">Back to Audit Firm Subscriptions</a>
|
||||
</div>
|
||||
|
||||
{% if generated or skipped %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<div class="text-sm"><span class="font-semibold text-emerald-700">Generated: {{ generated }}</span> · <span class="font-semibold text-amber-700">Skipped: {{ skipped }}</span></div>
|
||||
{% if errors %}
|
||||
<ul class="mt-3 list-disc space-y-1 pl-5 text-sm text-slate-600">
|
||||
{% for error in errors %}<li>{{ error }}</li>{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if invoices %}
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
{% for invoice in invoices %}<a href="/platform-billing/invoices/{{ invoice.id }}" class="rounded-xl bg-slate-100 px-3 py-2 text-sm text-slate-700">{{ invoice.invoice_no }}</a>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="space-y-5 rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<label class="text-sm font-medium text-slate-700">Period From<input type="date" name="period_from" required class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" /></label>
|
||||
<label class="text-sm font-medium text-slate-700">Period To<input type="date" name="period_to" required class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" /></label>
|
||||
<label class="text-sm font-medium text-slate-700">Invoice Date<input type="date" name="invoice_date" required class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" /></label>
|
||||
<label class="text-sm font-medium text-slate-700">Due Date<input type="date" name="due_date" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" /></label>
|
||||
<label class="text-sm font-medium text-slate-700">Tax Type<select name="tax_type" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2">{% for x in tax_types %}<option value="{{ x }}">{{ x }}</option>{% endfor %}</select></label>
|
||||
<label class="inline-flex items-end gap-2 text-sm text-slate-700"><input type="checkbox" name="include_zero_usage_lines" class="rounded border-slate-300" /> Include zero usage lines</label>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl bg-slate-50 p-4">
|
||||
<h2 class="font-semibold text-slate-900">Optional usage rates</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Base subscription amount comes from the subscription. Add usage rates only when you want to charge extra by count.</p>
|
||||
<div class="mt-4 grid gap-4 md:grid-cols-4">
|
||||
<label class="text-sm">Per Client<input name="client_rate" value="0" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" /></label>
|
||||
<label class="text-sm">Per Employee<input name="employee_rate" value="0" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" /></label>
|
||||
<label class="text-sm">Per Consultant<input name="consultant_rate" value="0" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" /></label>
|
||||
<label class="text-sm">Per Branch<input name="branch_rate" value="0" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" /></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 overflow-hidden">
|
||||
<div class="border-b border-slate-200 bg-slate-50 px-4 py-3 font-semibold text-slate-900">Select Audit Firm subscriptions</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Select</th>
|
||||
<th class="px-4 py-3">Audit Firm</th>
|
||||
<th class="px-4 py-3">Subscription</th>
|
||||
<th class="px-4 py-3">Plan</th>
|
||||
<th class="px-4 py-3 text-right">Base</th>
|
||||
<th class="px-4 py-3">Usage Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for row in rows %}
|
||||
{% set sub = row.subscription %}
|
||||
<tr>
|
||||
<td class="px-4 py-3"><input type="checkbox" name="subscription_ids" value="{{ sub.id }}" class="rounded border-slate-300" {% if sub.status != 'ACTIVE' or not sub.auto_generate_invoice %}disabled{% endif %} /></td>
|
||||
<td class="px-4 py-3 font-medium text-slate-900">{{ sub.account.display_name if sub.account else '-' }}</td>
|
||||
<td class="px-4 py-3">{{ sub.subscription_code }}</td>
|
||||
<td class="px-4 py-3">{{ sub.plan.name if sub.plan else '-' }}</td>
|
||||
<td class="px-4 py-3 text-right">{{ '%.2f'|format(sub.amount or 0) }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">Clients: {{ row.usage.clients }} · Employees: {{ row.usage.employees }} · Consultants: {{ row.usage.consultants }} · Branches: {{ row.usage.branches }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="px-4 py-6 text-center text-slate-500">No Audit Firm subscriptions available.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft">Generate Draft Platform Invoices</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,83 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Audit Firm Subscription Billing</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">PB2: manage SaaS subscriptions and generate platform bills for Audit Firms.</p>
|
||||
{% if synced %}<p class="mt-2 rounded-xl bg-emerald-50 px-3 py-2 text-sm text-emerald-700">Sync completed: {{ synced }}</p>{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if can_manage %}
|
||||
<form method="post" action="/platform-billing/audit-firm-subscriptions/sync-accounts">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<button class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm">Sync Audit Firm Accounts</button>
|
||||
</form>
|
||||
<a href="/platform-billing/subscriptions/new" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm">New Subscription</a>
|
||||
{% endif %}
|
||||
{% if can_generate %}
|
||||
<a href="/platform-billing/audit-firm-subscriptions/generate" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft">Generate Audit Firm Bills</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="flex gap-2 rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<input name="q" value="{{ q }}" placeholder="Search Audit Firm / subscription" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
<button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Search</button>
|
||||
</form>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white shadow-soft overflow-hidden">
|
||||
<div class="border-b border-slate-200 px-5 py-4">
|
||||
<h2 class="font-semibold text-slate-900">Active Audit Firm subscriptions</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Audit Firm</th>
|
||||
<th class="px-4 py-3">Subscription</th>
|
||||
<th class="px-4 py-3">Plan</th>
|
||||
<th class="px-4 py-3">Cycle</th>
|
||||
<th class="px-4 py-3 text-right">Base Amount</th>
|
||||
<th class="px-4 py-3">Usage Count</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for row in rows %}
|
||||
{% set sub = row.subscription %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium text-slate-900">{{ sub.account.display_name if sub.account else '-' }}</td>
|
||||
<td class="px-4 py-3">{{ sub.subscription_code }}</td>
|
||||
<td class="px-4 py-3">{{ sub.plan.name if sub.plan else '-' }}</td>
|
||||
<td class="px-4 py-3">{{ sub.billing_cycle }}</td>
|
||||
<td class="px-4 py-3 text-right">{{ '%.2f'|format(sub.amount or 0) }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">
|
||||
Clients: {{ row.usage.clients }} · Employees: {{ row.usage.employees }} · Consultants: {{ row.usage.consultants }} · Branches: {{ row.usage.branches }}
|
||||
</td>
|
||||
<td class="px-4 py-3">{{ sub.status }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="px-4 py-6 text-center text-slate-500">No Audit Firm subscriptions found. Sync Audit Firm accounts, create a plan, then create subscriptions.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<h2 class="font-semibold text-slate-900">Audit Firm billing accounts</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">These are platform billing accounts linked to your Audit Firm master. They are separate from firm-level client billing.</p>
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
{% for account in audit_firm_accounts %}
|
||||
<div class="rounded-xl border border-slate-200 p-4 text-sm">
|
||||
<div class="font-semibold text-slate-900">{{ account.display_name }}</div>
|
||||
<div class="mt-1 text-slate-500">Code: {{ account.account_code }} · Status: {{ account.status }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-xl bg-slate-50 p-4 text-sm text-slate-500">No Audit Firm billing accounts synced yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,98 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Generate Client Dashboard Platform Bills</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Generate draft platform invoices for client compliance dashboard subscriptions. Existing invoice for the same subscription and period will be skipped.</p>
|
||||
</div>
|
||||
<a href="/platform-billing/client-dashboard-subscriptions" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm">Back to Client Dashboard Billing</a>
|
||||
</div>
|
||||
|
||||
{% if generated or skipped or errors %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<div class="text-sm text-slate-700">Generated: <b>{{ generated }}</b> · Skipped: <b>{{ skipped }}</b></div>
|
||||
{% if errors %}
|
||||
<ul class="mt-3 list-disc space-y-1 pl-5 text-sm text-amber-700">
|
||||
{% for error in errors %}<li>{{ error }}</li>{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if invoices %}
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
{% for invoice in invoices %}<a class="rounded-xl bg-slate-900 px-3 py-2 text-sm font-semibold text-white" href="/platform-billing/invoices/{{ invoice.id }}">{{ invoice.invoice_no }}</a>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Period From</label>
|
||||
<input type="date" name="period_from" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Period To</label>
|
||||
<input type="date" name="period_to" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Invoice Date</label>
|
||||
<input type="date" name="invoice_date" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Due Date</label>
|
||||
<input type="date" name="due_date" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Tax Type</label>
|
||||
<select name="tax_type" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
{% for tax_type in tax_types %}<option value="{{ tax_type }}">{{ tax_type }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<label class="mt-6 inline-flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="include_zero_usage_lines" class="rounded border-slate-300" /> Include zero usage lines
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Extra PAN Rate</label>
|
||||
<input name="pan_rate" value="0" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Extra GSTIN Rate</label>
|
||||
<input name="gstin_rate" value="0" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Per Compliance Module Rate</label>
|
||||
<input name="module_rate" value="0" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-xl border border-slate-200">
|
||||
<div class="border-b border-slate-200 bg-slate-50 px-4 py-3 text-sm font-semibold text-slate-700">Select client dashboard subscriptions</div>
|
||||
<div class="max-h-[520px] overflow-auto divide-y divide-slate-100">
|
||||
{% for row in rows %}
|
||||
{% set sub = row.subscription %}
|
||||
<label class="flex cursor-pointer items-start gap-3 px-4 py-3 text-sm hover:bg-slate-50">
|
||||
<input type="checkbox" name="subscription_ids" value="{{ sub.id }}" class="mt-1 rounded border-slate-300" />
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-slate-900">{{ sub.account.display_name if sub.account else '-' }}</div>
|
||||
<div class="text-slate-500">{{ sub.subscription_code }} · {{ sub.plan.name if sub.plan else '-' }} · Base: {{ '%.2f'|format(sub.amount or 0) }} · {{ sub.billing_cycle }}</div>
|
||||
<div class="text-xs text-slate-400">PAN {{ row.usage.pan_units }} · GSTIN {{ row.usage.gstin_units }} · Modules {{ row.usage.module_count }}{% if row.usage.modules %} — {{ row.usage.modules|join(', ') }}{% endif %}</div>
|
||||
</div>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-xs text-slate-600">{{ sub.status }}</span>
|
||||
</label>
|
||||
{% else %}
|
||||
<div class="px-4 py-6 text-center text-sm text-slate-500">No client dashboard subscriptions available.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex justify-end">
|
||||
<button class="rounded-xl bg-brand-600 px-5 py-2.5 text-sm font-semibold text-white shadow-soft">Generate Draft Platform Invoices</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,145 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Client Compliance Dashboard Billing</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">PB3: manage platform subscriptions and generate bills for client compliance dashboard access.</p>
|
||||
{% if synced %}<p class="mt-2 rounded-xl bg-emerald-50 px-3 py-2 text-sm text-emerald-700">Sync completed: {{ synced }}</p>{% endif %}
|
||||
{% if created %}<p class="mt-2 rounded-xl bg-emerald-50 px-3 py-2 text-sm text-emerald-700">Client dashboard subscription created.</p>{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if can_manage %}
|
||||
<form method="post" action="/platform-billing/client-dashboard-subscriptions/sync-accounts">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<button class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm">Sync Client Accounts</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if can_generate %}
|
||||
<a href="/platform-billing/client-dashboard-subscriptions/generate" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft">Generate Client Dashboard Bills</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if can_manage %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<h2 class="font-semibold text-slate-900">Create Client Dashboard Subscription</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Use CLIENT-target platform plans. If no plan appears, create a plan with Target Account Type = CLIENT.</p>
|
||||
<form method="post" action="/platform-billing/client-dashboard-subscriptions/new" class="mt-4 grid gap-4 md:grid-cols-3">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Client Account</label>
|
||||
<select name="account_id" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">Select client</option>
|
||||
{% for account in client_accounts %}<option value="{{ account.id }}">{{ account.display_name }} ({{ account.account_code }})</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Plan</label>
|
||||
<select name="plan_id" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">Select plan</option>
|
||||
{% for plan in client_plans %}<option value="{{ plan.id }}">{{ plan.name }} - {{ '%.2f'|format(plan.base_amount or 0) }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Subscription Code</label>
|
||||
<input name="subscription_code" required placeholder="CLD-ABC-2026" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Start Date</label>
|
||||
<input type="date" name="start_date" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">End Date</label>
|
||||
<input type="date" name="end_date" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Billing Cycle</label>
|
||||
<select name="billing_cycle" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
{% for cycle in billing_cycles %}<option value="{{ cycle }}">{{ cycle }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Base Amount</label>
|
||||
<input name="amount" value="0" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">GST Rate</label>
|
||||
<input name="gst_rate" value="18" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<label class="mt-6 inline-flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="auto_generate_invoice" checked class="rounded border-slate-300" /> Auto generate invoice
|
||||
</label>
|
||||
<div class="md:col-span-3">
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Notes</label>
|
||||
<input name="notes" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div class="md:col-span-3">
|
||||
<button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Create Subscription</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="get" class="flex gap-2 rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<input name="q" value="{{ q }}" placeholder="Search client / subscription" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
<button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Search</button>
|
||||
</form>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white shadow-soft overflow-hidden">
|
||||
<div class="border-b border-slate-200 px-5 py-4">
|
||||
<h2 class="font-semibold text-slate-900">Active client dashboard subscriptions</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Client</th>
|
||||
<th class="px-4 py-3">Subscription</th>
|
||||
<th class="px-4 py-3">Plan</th>
|
||||
<th class="px-4 py-3">Cycle</th>
|
||||
<th class="px-4 py-3 text-right">Base Amount</th>
|
||||
<th class="px-4 py-3">Dashboard Units</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for row in rows %}
|
||||
{% set sub = row.subscription %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium text-slate-900">{{ sub.account.display_name if sub.account else '-' }}</td>
|
||||
<td class="px-4 py-3">{{ sub.subscription_code }}</td>
|
||||
<td class="px-4 py-3">{{ sub.plan.name if sub.plan else '-' }}</td>
|
||||
<td class="px-4 py-3">{{ sub.billing_cycle }}</td>
|
||||
<td class="px-4 py-3 text-right">{{ '%.2f'|format(sub.amount or 0) }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">
|
||||
PAN: {{ row.usage.pan_units }} · GSTIN: {{ row.usage.gstin_units }} · Modules: {{ row.usage.module_count }}
|
||||
{% if row.usage.modules %}<div class="text-xs text-slate-400">{{ row.usage.modules|join(', ') }}</div>{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3">{{ sub.status }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="px-4 py-6 text-center text-slate-500">No client dashboard subscriptions found. Sync client accounts, create a CLIENT plan, then create subscriptions.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<h2 class="font-semibold text-slate-900">Client billing accounts</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">These accounts are for platform billing of compliance dashboard access. They are separate from the Audit Firm's client invoices.</p>
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
{% for account in client_accounts[:12] %}
|
||||
<div class="rounded-xl border border-slate-200 p-4 text-sm">
|
||||
<div class="font-semibold text-slate-900">{{ account.display_name }}</div>
|
||||
<div class="mt-1 text-slate-500">Code: {{ account.account_code }} · Status: {{ account.status }}</div>
|
||||
<div class="mt-1 text-slate-500">PAN: {{ account.pan or '-' }} · GSTIN: {{ account.gstin or '-' }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-xl bg-slate-50 p-4 text-sm text-slate-500">No client billing accounts synced yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,94 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Generate Consultant Platform Bills</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Generate draft platform invoices for consultant SaaS/tool access subscriptions. Existing invoice for the same subscription and period will be skipped.</p>
|
||||
</div>
|
||||
<a href="/platform-billing/consultant-subscriptions" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm">Back to Consultant Billing</a>
|
||||
</div>
|
||||
|
||||
{% if generated or skipped or errors %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<div class="text-sm text-slate-700">Generated: <b>{{ generated }}</b> · Skipped: <b>{{ skipped }}</b></div>
|
||||
{% if errors %}
|
||||
<ul class="mt-3 list-disc space-y-1 pl-5 text-sm text-amber-700">
|
||||
{% for error in errors %}<li>{{ error }}</li>{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if invoices %}
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
{% for invoice in invoices %}<a class="rounded-xl bg-slate-900 px-3 py-2 text-sm font-semibold text-white" href="/platform-billing/invoices/{{ invoice.id }}">{{ invoice.invoice_no }}</a>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Period From</label>
|
||||
<input type="date" name="period_from" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Period To</label>
|
||||
<input type="date" name="period_to" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Invoice Date</label>
|
||||
<input type="date" name="invoice_date" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Due Date</label>
|
||||
<input type="date" name="due_date" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Tax Type</label>
|
||||
<select name="tax_type" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
{% for tax_type in tax_types %}<option value="{{ tax_type }}">{{ tax_type }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<label class="mt-6 inline-flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="include_zero_usage_lines" class="rounded border-slate-300" /> Include zero usage lines
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Per Managed Client Rate</label>
|
||||
<input name="managed_client_rate" value="0" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Per Portal User Rate</label>
|
||||
<input name="user_account_rate" value="0" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-xl border border-slate-200">
|
||||
<div class="border-b border-slate-200 bg-slate-50 px-4 py-3 text-sm font-semibold text-slate-700">Select consultant subscriptions</div>
|
||||
<div class="max-h-[520px] overflow-auto divide-y divide-slate-100">
|
||||
{% for row in rows %}
|
||||
{% set sub = row.subscription %}
|
||||
<label class="flex cursor-pointer items-start gap-3 px-4 py-3 text-sm hover:bg-slate-50">
|
||||
<input type="checkbox" name="subscription_ids" value="{{ sub.id }}" class="mt-1 rounded border-slate-300" />
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-slate-900">{{ sub.account.display_name if sub.account else '-' }}</div>
|
||||
<div class="text-slate-500">{{ sub.subscription_code }} · {{ sub.plan.name if sub.plan else '-' }} · Base: {{ '%.2f'|format(sub.amount or 0) }} · {{ sub.billing_cycle }}</div>
|
||||
<div class="text-xs text-slate-400">Managed clients {{ row.usage.managed_client_count }} · Portal users {{ row.usage.user_account_count }} · Workspace {{ row.usage.workspace_type }} · Plan {{ row.usage.workspace_plan }}</div>
|
||||
</div>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-xs text-slate-600">{{ sub.status }}</span>
|
||||
</label>
|
||||
{% else %}
|
||||
<div class="px-4 py-6 text-center text-sm text-slate-500">No consultant subscriptions available.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex justify-end">
|
||||
<button class="rounded-xl bg-brand-600 px-5 py-2.5 text-sm font-semibold text-white shadow-soft">Generate Draft Platform Invoices</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,145 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Consultant SaaS/Tool Billing</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">PB4: manage platform subscriptions and generate bills for consultant SaaS/tool access.</p>
|
||||
{% if synced %}<p class="mt-2 rounded-xl bg-emerald-50 px-3 py-2 text-sm text-emerald-700">Sync completed: {{ synced }}</p>{% endif %}
|
||||
{% if created %}<p class="mt-2 rounded-xl bg-emerald-50 px-3 py-2 text-sm text-emerald-700">Consultant subscription created.</p>{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if can_manage %}
|
||||
<form method="post" action="/platform-billing/consultant-subscriptions/sync-accounts">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<button class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm">Sync Consultant Accounts</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if can_generate %}
|
||||
<a href="/platform-billing/consultant-subscriptions/generate" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft">Generate Consultant Bills</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if can_manage %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<h2 class="font-semibold text-slate-900">Create Consultant Subscription</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Use CONSULTANT-target platform plans. If no plan appears, create a plan with Target Account Type = CONSULTANT.</p>
|
||||
<form method="post" action="/platform-billing/consultant-subscriptions/new" class="mt-4 grid gap-4 md:grid-cols-3">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Consultant Account</label>
|
||||
<select name="account_id" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">Select consultant</option>
|
||||
{% for account in consultant_accounts %}<option value="{{ account.id }}">{{ account.display_name }} ({{ account.account_code }})</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Plan</label>
|
||||
<select name="plan_id" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">Select plan</option>
|
||||
{% for plan in consultant_plans %}<option value="{{ plan.id }}">{{ plan.name }} - {{ '%.2f'|format(plan.base_amount or 0) }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Subscription Code</label>
|
||||
<input name="subscription_code" required placeholder="CON-ABC-2026" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Start Date</label>
|
||||
<input type="date" name="start_date" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">End Date</label>
|
||||
<input type="date" name="end_date" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Billing Cycle</label>
|
||||
<select name="billing_cycle" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
{% for cycle in billing_cycles %}<option value="{{ cycle }}">{{ cycle }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Base Amount</label>
|
||||
<input name="amount" value="0" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">GST Rate</label>
|
||||
<input name="gst_rate" value="18" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<label class="mt-6 inline-flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="auto_generate_invoice" checked class="rounded border-slate-300" /> Auto generate invoice
|
||||
</label>
|
||||
<div class="md:col-span-3">
|
||||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Notes</label>
|
||||
<input name="notes" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</div>
|
||||
<div class="md:col-span-3">
|
||||
<button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Create Subscription</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="get" class="flex gap-2 rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<input name="q" value="{{ q }}" placeholder="Search consultant / subscription" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
<button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Search</button>
|
||||
</form>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white shadow-soft overflow-hidden">
|
||||
<div class="border-b border-slate-200 px-5 py-4">
|
||||
<h2 class="font-semibold text-slate-900">Active consultant subscriptions</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Consultant</th>
|
||||
<th class="px-4 py-3">Subscription</th>
|
||||
<th class="px-4 py-3">Plan</th>
|
||||
<th class="px-4 py-3">Cycle</th>
|
||||
<th class="px-4 py-3 text-right">Base Amount</th>
|
||||
<th class="px-4 py-3">Tool Usage</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for row in rows %}
|
||||
{% set sub = row.subscription %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium text-slate-900">{{ sub.account.display_name if sub.account else '-' }}</td>
|
||||
<td class="px-4 py-3">{{ sub.subscription_code }}</td>
|
||||
<td class="px-4 py-3">{{ sub.plan.name if sub.plan else '-' }}</td>
|
||||
<td class="px-4 py-3">{{ sub.billing_cycle }}</td>
|
||||
<td class="px-4 py-3 text-right">{{ '%.2f'|format(sub.amount or 0) }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">
|
||||
Managed Clients: {{ row.usage.managed_client_count }} · Portal Users: {{ row.usage.user_account_count }}
|
||||
<div class="text-xs text-slate-400">Workspace: {{ row.usage.workspace_type }} · Plan: {{ row.usage.workspace_plan }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">{{ sub.status }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="px-4 py-6 text-center text-slate-500">No consultant subscriptions found. Sync consultant accounts, create a CONSULTANT plan, then create subscriptions.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<h2 class="font-semibold text-slate-900">Consultant billing accounts</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">These accounts are for platform billing of consultant SaaS/tool access. They are separate from firm-level billing and consultant master records.</p>
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
{% for account in consultant_accounts[:12] %}
|
||||
<div class="rounded-xl border border-slate-200 p-4 text-sm">
|
||||
<div class="font-semibold text-slate-900">{{ account.display_name }}</div>
|
||||
<div class="mt-1 text-slate-500">Code: {{ account.account_code }} · Status: {{ account.status }}</div>
|
||||
<div class="mt-1 text-slate-500">Email: {{ account.email or '-' }} · Mobile: {{ account.mobile or '-' }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-xl bg-slate-50 p-4 text-sm text-slate-500">No consultant billing accounts synced yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Platform Billing</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">SaaS billing for Audit Firms, compliance-dashboard clients, consultants and future marketplace customers.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if can_manage_plans %}<a href="/platform-billing/plans/new" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm">New Plan</a>{% endif %}
|
||||
{% if can_generate_platform_billing_flag %}<a href="/platform-billing/audit-firm-subscriptions/generate" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft">Generate Audit Firm Bills</a>{% endif %}
|
||||
{% if can_generate_platform_billing_flag %}<a href="/platform-billing/client-dashboard-subscriptions/generate" class="rounded-xl border border-brand-200 bg-white px-4 py-2 text-sm font-semibold text-brand-700 shadow-sm">Generate Client Dashboard Bills</a>{% endif %}
|
||||
{% if can_generate_platform_billing_flag %}<a href="/platform-billing/consultant-subscriptions/generate" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 shadow-sm">Generate Consultant Bills</a>{% endif %}
|
||||
{% if can_create_invoice %}<a href="/platform-billing/invoices/new" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm">New Platform Invoice</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-5">
|
||||
<a href="/platform-billing/accounts" class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-sm text-slate-500">Accounts</div><div class="mt-2 text-3xl font-semibold">{{ accounts|length }}</div></a>
|
||||
<a href="/platform-billing/plans" class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-sm text-slate-500">Plans</div><div class="mt-2 text-3xl font-semibold">{{ plans|length }}</div></a>
|
||||
<a href="/platform-billing/audit-firm-subscriptions" class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-sm text-slate-500">Audit Firm Billing</div><div class="mt-2 text-3xl font-semibold">{{ audit_firm_accounts|length }}</div></a>
|
||||
<a href="/platform-billing/client-dashboard-subscriptions" class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-sm text-slate-500">Client Dashboard Billing</div><div class="mt-2 text-3xl font-semibold">{{ client_dashboard_accounts|length }}</div></a>
|
||||
<a href="/platform-billing/consultant-subscriptions" class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-sm text-slate-500">Consultant Billing</div><div class="mt-2 text-3xl font-semibold">{{ consultant_billing_accounts|length }}</div></a>
|
||||
</div>
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<h2 class="font-semibold">Revenue Channels</h2>
|
||||
<div class="mt-4 grid gap-3 text-sm text-slate-700">
|
||||
<a href="/platform-billing/audit-firm-subscriptions" class="rounded-xl bg-slate-50 p-3 hover:bg-slate-100">Audit Firm subscription billing</a>
|
||||
<a href="/platform-billing/client-dashboard-subscriptions" class="rounded-xl bg-slate-50 p-3 hover:bg-slate-100">Client compliance dashboard access billing</a>
|
||||
<a href="/platform-billing/consultant-subscriptions" class="rounded-xl bg-slate-50 p-3 hover:bg-slate-100">Consultant SaaS/tool access billing</a>
|
||||
<div class="rounded-xl bg-slate-50 p-3">Marketplace lead / commission billing foundation</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<h2 class="font-semibold">Quick Links</h2>
|
||||
<div class="mt-4 flex flex-wrap gap-2 text-sm">
|
||||
<a href="/platform-billing/accounts" class="rounded-xl border px-3 py-2">Billing Accounts</a>
|
||||
<a href="/platform-billing/plans" class="rounded-xl border px-3 py-2">Plans</a>
|
||||
<a href="/platform-billing/audit-firm-subscriptions" class="rounded-xl border px-3 py-2">Audit Firm Subscriptions</a>
|
||||
<a href="/platform-billing/client-dashboard-subscriptions" class="rounded-xl border px-3 py-2">Client Dashboard Subscriptions</a>
|
||||
<a href="/platform-billing/consultant-subscriptions" class="rounded-xl border px-3 py-2">Consultant Subscriptions</a>
|
||||
<a href="/platform-billing/subscriptions" class="rounded-xl border px-3 py-2">All Subscriptions</a>
|
||||
<a href="/platform-billing/invoices" class="rounded-xl border px-3 py-2">Platform Invoices</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}<div class="max-w-4xl space-y-6"><h1 class="text-2xl font-semibold">New Platform Invoice</h1><form method="post" class="space-y-4 rounded-2xl border bg-white p-5 shadow-soft"><input type="hidden" name="csrf_token" value="{{ csrf_token }}" /><div class="grid gap-4 md:grid-cols-2"><label class="text-sm">Account<select name="account_id" required class="mt-1 w-full rounded-xl border px-3 py-2">{% for a in accounts %}<option value="{{ a.id }}">{{ a.display_name }} ({{ a.account_type }})</option>{% endfor %}</select></label><label class="text-sm">Subscription optional<select name="subscription_id" class="mt-1 w-full rounded-xl border px-3 py-2"><option value="">Manual invoice</option>{% for s in subscriptions %}<option value="{{ s.id }}">{{ s.subscription_code }} - {{ s.account.display_name if s.account else s.account_id }}</option>{% endfor %}</select></label><label class="text-sm">Invoice No<input name="invoice_no" required class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Tax Type<select name="tax_type" class="mt-1 w-full rounded-xl border px-3 py-2">{% for x in tax_types %}<option value="{{ x }}">{{ x }}</option>{% endfor %}</select></label><label class="text-sm">Invoice Date<input type="date" name="invoice_date" required class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Due Date<input type="date" name="due_date" class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Period From<input type="date" name="billing_period_from" class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Period To<input type="date" name="billing_period_to" class="mt-1 w-full rounded-xl border px-3 py-2" /></label></div><div class="rounded-2xl border bg-slate-50 p-4"><h2 class="font-semibold">Invoice Line</h2><div class="mt-3 grid gap-4 md:grid-cols-2"><label class="text-sm">Charge Type<select name="charge_type" class="mt-1 w-full rounded-xl border px-3 py-2">{% for x in charge_types %}<option value="{{ x }}">{{ x }}</option>{% endfor %}</select></label><label class="text-sm">GST Rate<input name="gst_rate" value="18" class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm md:col-span-2">Description<input name="description" required class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Quantity<input name="quantity" value="1" class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Rate<input name="rate" value="0" class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Discount<input name="discount_amount" value="0" class="mt-1 w-full rounded-xl border px-3 py-2" /></label></div></div><label class="block text-sm">Notes<textarea name="notes" class="mt-1 w-full rounded-xl border px-3 py-2"></textarea></label><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Create Draft Invoice</button></form></div>{% endblock %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}<div class="space-y-6"><div class="flex items-center justify-between"><div><h1 class="text-2xl font-semibold">Platform Invoice {{ invoice.invoice_no }}</h1><p class="text-sm text-slate-500">{{ invoice.account.display_name if invoice.account else invoice.account_id }} • {{ invoice.status }}</p></div><div class="flex gap-2">{% if can_post and invoice.status == 'DRAFT' %}<form method="post" action="/platform-billing/invoices/{{ invoice.id }}/post"><input type="hidden" name="csrf_token" value="{{ csrf_token }}" /><button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Post Invoice</button></form>{% endif %}</div></div><div class="rounded-2xl border bg-white p-5 shadow-soft"><div class="grid gap-3 text-sm md:grid-cols-4"><div><span class="text-slate-500">Invoice Date</span><div class="font-medium">{{ invoice.invoice_date }}</div></div><div><span class="text-slate-500">Due Date</span><div class="font-medium">{{ invoice.due_date or '-' }}</div></div><div><span class="text-slate-500">Tax Type</span><div class="font-medium">{{ invoice.tax_type }}</div></div><div><span class="text-slate-500">Total</span><div class="font-semibold">₹ {{ '%.2f'|format(invoice.total_amount or 0) }}</div></div></div></div><div class="overflow-hidden rounded-2xl border bg-white shadow-soft"><table class="min-w-full text-sm"><thead class="bg-slate-50 text-left"><tr><th class="px-4 py-3">Description</th><th class="px-4 py-3">Charge</th><th class="px-4 py-3 text-right">Taxable</th><th class="px-4 py-3 text-right">Tax</th><th class="px-4 py-3 text-right">Total</th></tr></thead><tbody class="divide-y">{% for line in invoice.lines %}<tr><td class="px-4 py-3">{{ line.description }}</td><td class="px-4 py-3">{{ line.charge_type }}</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(line.taxable_amount or 0) }}</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format((line.cgst_amount or 0) + (line.sgst_amount or 0) + (line.igst_amount or 0)) }}</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(line.line_total or 0) }}</td></tr>{% endfor %}</tbody></table></div>{% if can_record_payment %}<form method="post" action="/platform-billing/invoices/{{ invoice.id }}/payments" class="grid gap-3 rounded-2xl border bg-white p-5 shadow-soft md:grid-cols-4"><input type="hidden" name="csrf_token" value="{{ csrf_token }}" /><input name="amount" placeholder="Amount" required class="rounded-xl border px-3 py-2 text-sm" /><input name="mode" value="Bank" class="rounded-xl border px-3 py-2 text-sm" /><input name="reference_no" placeholder="Reference No" class="rounded-xl border px-3 py-2 text-sm" /><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Record Payment</button></form>{% endif %}</div>{% endblock %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}<div class="space-y-6"><div class="flex items-center justify-between"><h1 class="text-2xl font-semibold">Platform Invoices</h1>{% if can_create %}<a href="/platform-billing/invoices/new" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">New Platform Invoice</a>{% endif %}</div><form method="get" class="rounded-2xl border bg-white p-4 shadow-soft"><input name="q" value="{{ q or '' }}" placeholder="Search invoice/account" class="w-full rounded-xl border px-3 py-2 text-sm" /></form><div class="overflow-hidden rounded-2xl border bg-white shadow-soft"><table class="min-w-full text-sm"><thead class="bg-slate-50 text-left"><tr><th class="px-4 py-3">Invoice No</th><th class="px-4 py-3">Date</th><th class="px-4 py-3">Account</th><th class="px-4 py-3 text-right">Total</th><th class="px-4 py-3">Status</th><th class="px-4 py-3"></th></tr></thead><tbody class="divide-y">{% for row in rows %}<tr><td class="px-4 py-3 font-medium">{{ row.invoice_no }}</td><td class="px-4 py-3">{{ row.invoice_date }}</td><td class="px-4 py-3">{{ row.account.display_name if row.account else row.account_id }}</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(row.total_amount or 0) }}</td><td class="px-4 py-3">{{ row.status }}</td><td class="px-4 py-3 text-right"><a href="/platform-billing/invoices/{{ row.id }}" class="text-brand-600 hover:underline">View</a></td></tr>{% else %}<tr><td colspan="6" class="px-4 py-8 text-center text-slate-500">No platform invoices found.</td></tr>{% endfor %}</tbody></table></div></div>{% endblock %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-3xl space-y-6"><h1 class="text-2xl font-semibold">New Platform Plan</h1><form method="post" class="space-y-4 rounded-2xl border bg-white p-5 shadow-soft"><input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="grid gap-4 md:grid-cols-2"><label class="text-sm">Plan Code<input name="code" required class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Plan Name<input name="name" required class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Target Account<select name="target_account_type" class="mt-1 w-full rounded-xl border px-3 py-2">{% for x in account_types %}<option value="{{ x }}">{{ x }}</option>{% endfor %}</select></label><label class="text-sm">Billing Cycle<select name="billing_cycle" class="mt-1 w-full rounded-xl border px-3 py-2">{% for x in billing_cycles %}<option value="{{ x }}">{{ x }}</option>{% endfor %}</select></label><label class="text-sm">Base Amount<input name="base_amount" value="0" class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">GST Rate<input name="gst_rate" value="18" class="mt-1 w-full rounded-xl border px-3 py-2" /></label></div><label class="block text-sm">Description<textarea name="description" class="mt-1 w-full rounded-xl border px-3 py-2"></textarea></label><label class="block text-sm">Features / Limits (one per line)<textarea name="feature_text" rows="5" class="mt-1 w-full rounded-xl border px-3 py-2"></textarea></label><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Save Plan</button></form></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between"><h1 class="text-2xl font-semibold">Platform Plans</h1>{% if can_manage %}<a href="/platform-billing/plans/new" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">New Plan</a>{% endif %}</div>
|
||||
<form method="get" class="rounded-2xl border bg-white p-4 shadow-soft"><input name="q" value="{{ q or '' }}" placeholder="Search plan" class="w-full rounded-xl border px-3 py-2 text-sm" /></form>
|
||||
<div class="overflow-hidden rounded-2xl border bg-white shadow-soft"><table class="min-w-full text-sm"><thead class="bg-slate-50 text-left"><tr><th class="px-4 py-3">Code</th><th class="px-4 py-3">Name</th><th class="px-4 py-3">For</th><th class="px-4 py-3">Cycle</th><th class="px-4 py-3 text-right">Base</th><th class="px-4 py-3">Status</th></tr></thead><tbody class="divide-y">{% for row in rows %}<tr><td class="px-4 py-3 font-medium">{{ row.code }}</td><td class="px-4 py-3">{{ row.name }}</td><td class="px-4 py-3">{{ row.target_account_type }}</td><td class="px-4 py-3">{{ row.billing_cycle }}</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(row.base_amount or 0) }}</td><td class="px-4 py-3">{{ 'Active' if row.is_active else 'Inactive' }}</td></tr>{% else %}<tr><td colspan="6" class="px-4 py-8 text-center text-slate-500">No plans found.</td></tr>{% endfor %}</tbody></table></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}<div class="max-w-3xl space-y-6"><h1 class="text-2xl font-semibold">New Platform Subscription</h1><form method="post" class="space-y-4 rounded-2xl border bg-white p-5 shadow-soft"><input type="hidden" name="csrf_token" value="{{ csrf_token }}" /><div class="grid gap-4 md:grid-cols-2"><label class="text-sm">Account<select name="account_id" required class="mt-1 w-full rounded-xl border px-3 py-2">{% for a in accounts %}<option value="{{ a.id }}">{{ a.display_name }} ({{ a.account_type }})</option>{% endfor %}</select></label><label class="text-sm">Plan<select name="plan_id" required class="mt-1 w-full rounded-xl border px-3 py-2">{% for p in plans %}<option value="{{ p.id }}">{{ p.name }}</option>{% endfor %}</select></label><label class="text-sm">Subscription Code<input name="subscription_code" required class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Billing Cycle<select name="billing_cycle" class="mt-1 w-full rounded-xl border px-3 py-2">{% for x in billing_cycles %}<option value="{{ x }}">{{ x }}</option>{% endfor %}</select></label><label class="text-sm">Start Date<input type="date" name="start_date" required class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">End Date<input type="date" name="end_date" class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">Amount<input name="amount" value="0" class="mt-1 w-full rounded-xl border px-3 py-2" /></label><label class="text-sm">GST Rate<input name="gst_rate" value="18" class="mt-1 w-full rounded-xl border px-3 py-2" /></label></div><label class="inline-flex items-center gap-2 text-sm"><input type="checkbox" name="auto_generate_invoice" checked /> Auto generate invoice later</label><label class="block text-sm">Notes<textarea name="notes" class="mt-1 w-full rounded-xl border px-3 py-2"></textarea></label><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Save Subscription</button></form></div>{% endblock %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}<div class="space-y-6"><div class="flex items-center justify-between"><h1 class="text-2xl font-semibold">Platform Subscriptions</h1>{% if can_manage %}<a href="/platform-billing/subscriptions/new" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">New Subscription</a>{% endif %}</div><form method="get" class="rounded-2xl border bg-white p-4 shadow-soft"><input name="q" value="{{ q or '' }}" placeholder="Search subscription/account" class="w-full rounded-xl border px-3 py-2 text-sm" /></form><div class="overflow-hidden rounded-2xl border bg-white shadow-soft"><table class="min-w-full text-sm"><thead class="bg-slate-50 text-left"><tr><th class="px-4 py-3">Code</th><th class="px-4 py-3">Account</th><th class="px-4 py-3">Plan</th><th class="px-4 py-3">Cycle</th><th class="px-4 py-3 text-right">Amount</th><th class="px-4 py-3">Status</th></tr></thead><tbody class="divide-y">{% for row in rows %}<tr><td class="px-4 py-3 font-medium">{{ row.subscription_code }}</td><td class="px-4 py-3">{{ row.account.display_name if row.account else row.account_id }}</td><td class="px-4 py-3">{{ row.plan.name if row.plan else row.plan_id }}</td><td class="px-4 py-3">{{ row.billing_cycle }}</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(row.amount or 0) }}</td><td class="px-4 py-3">{{ row.status }}</td></tr>{% else %}<tr><td colspan="6" class="px-4 py-8 text-center text-slate-500">No subscriptions found.</td></tr>{% endfor %}</tbody></table></div></div>{% endblock %}
|
||||
@@ -0,0 +1,684 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from app.core.db.common import CommonSessionLocal
|
||||
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
|
||||
from app.core.security.session_auth import get_current_user
|
||||
from app.core.templating import templates
|
||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||
from app.modules.core.rbac.permission_guard import require_permission
|
||||
from app.modules.platform_billing.services import (
|
||||
ACCOUNT_TYPES,
|
||||
BILLING_CYCLES,
|
||||
CHARGE_TYPES,
|
||||
TAX_TYPES,
|
||||
create_platform_account,
|
||||
create_platform_invoice,
|
||||
create_platform_plan,
|
||||
create_platform_subscription,
|
||||
generate_audit_firm_subscription_invoices,
|
||||
generate_client_dashboard_subscription_invoices,
|
||||
generate_consultant_subscription_invoices,
|
||||
get_platform_invoice,
|
||||
list_audit_firm_accounts,
|
||||
list_audit_firm_subscription_rows,
|
||||
list_client_dashboard_accounts,
|
||||
list_client_dashboard_plans,
|
||||
list_client_dashboard_subscription_rows,
|
||||
list_consultant_billing_accounts,
|
||||
list_consultant_billing_plans,
|
||||
list_consultant_billing_subscription_rows,
|
||||
list_platform_accounts,
|
||||
list_platform_invoices,
|
||||
list_platform_plans,
|
||||
list_platform_subscriptions,
|
||||
list_reference_audit_firms,
|
||||
list_reference_clients,
|
||||
list_reference_consultants,
|
||||
parse_date,
|
||||
post_platform_invoice,
|
||||
record_platform_payment,
|
||||
sync_audit_firm_billing_accounts,
|
||||
sync_client_dashboard_billing_accounts,
|
||||
sync_consultant_billing_accounts,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/platform-billing", tags=["platform-billing-ui"])
|
||||
|
||||
|
||||
def _redirect_denied():
|
||||
return RedirectResponse(url="/system-settings", status_code=303)
|
||||
|
||||
|
||||
def _has_perm(db, user, code: str) -> bool:
|
||||
try:
|
||||
require_permission(db, user, code)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _require_user(request: Request, db, permission_code: str):
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return None, RedirectResponse(url="/login", status_code=303)
|
||||
try:
|
||||
require_permission(db, user, permission_code)
|
||||
except Exception:
|
||||
return user, _redirect_denied()
|
||||
return user, None
|
||||
|
||||
|
||||
def _base_ctx(request: Request, db, user, **ctx):
|
||||
base = {
|
||||
"request": request,
|
||||
"current_user": user,
|
||||
"current_user_roles": get_user_roles(db, user.id),
|
||||
"current_user_permissions": get_user_permissions(db, user.id),
|
||||
"csrf_token": get_or_create_csrf_token(request),
|
||||
"account_types": ACCOUNT_TYPES,
|
||||
"billing_cycles": BILLING_CYCLES,
|
||||
"tax_types": TAX_TYPES,
|
||||
"charge_types": CHARGE_TYPES,
|
||||
}
|
||||
base.update(ctx)
|
||||
return base
|
||||
|
||||
|
||||
def _render(request: Request, template: str, db, user, **ctx):
|
||||
return templates.TemplateResponse(template, _base_ctx(request, db, user, **ctx))
|
||||
|
||||
|
||||
@router.get("")
|
||||
def dashboard(request: Request):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.view")
|
||||
if response:
|
||||
return response
|
||||
return _render(
|
||||
request,
|
||||
"modules/platform_billing/templates/platform_billing/dashboard.html",
|
||||
db,
|
||||
user,
|
||||
title="Platform Billing",
|
||||
plans=list_platform_plans(db)[:5],
|
||||
accounts=list_platform_accounts(db)[:5],
|
||||
audit_firm_accounts=list_audit_firm_accounts(db)[:5],
|
||||
client_dashboard_accounts=list_client_dashboard_accounts(db)[:5],
|
||||
consultant_billing_accounts=list_consultant_billing_accounts(db)[:5],
|
||||
subscriptions=list_platform_subscriptions(db)[:5],
|
||||
invoices=list_platform_invoices(db)[:5],
|
||||
can_manage_plans=_has_perm(db, user, "platform_plans.manage"),
|
||||
can_manage_subscriptions=_has_perm(db, user, "platform_subscriptions.manage"),
|
||||
can_generate_platform_billing_flag=_has_perm(db, user, "platform_billing.generate"),
|
||||
can_create_invoice=_has_perm(db, user, "platform_billing.create"),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/plans")
|
||||
def plans_list(request: Request, q: str = ""):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.view")
|
||||
if response:
|
||||
return response
|
||||
return _render(request, "modules/platform_billing/templates/platform_billing/plans/list.html", db, user, title="Platform Plans", rows=list_platform_plans(db, q=q), q=q, can_manage=_has_perm(db, user, "platform_plans.manage"))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/plans/new")
|
||||
def plan_new(request: Request):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_plans.manage")
|
||||
if response:
|
||||
return response
|
||||
return _render(request, "modules/platform_billing/templates/platform_billing/plans/create.html", db, user, title="New Platform Plan")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/plans/new")
|
||||
def plan_create(request: Request, csrf_token: str = Form(...), code: str = Form(...), name: str = Form(...), target_account_type: str = Form(...), billing_cycle: str = Form(...), base_amount: str = Form("0"), gst_rate: str = Form("18"), description: str = Form(""), feature_text: str = Form("")):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_plans.manage")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
create_platform_plan(db, code=code, name=name, target_account_type=target_account_type, billing_cycle=billing_cycle, base_amount=base_amount, gst_rate=gst_rate, description=description, feature_text=feature_text)
|
||||
return RedirectResponse(url="/platform-billing/plans", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/accounts")
|
||||
def accounts_list(request: Request, q: str = "", account_type: str = ""):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.view")
|
||||
if response:
|
||||
return response
|
||||
return _render(request, "modules/platform_billing/templates/platform_billing/accounts/list.html", db, user, title="Platform Billing Accounts", rows=list_platform_accounts(db, q=q, account_type=account_type), q=q, selected_account_type=account_type, can_create=_has_perm(db, user, "platform_billing.create"))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/accounts/new")
|
||||
def account_new(request: Request):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.create")
|
||||
if response:
|
||||
return response
|
||||
return _render(request, "modules/platform_billing/templates/platform_billing/accounts/create.html", db, user, title="New Platform Billing Account", audit_firms=list_reference_audit_firms(db), clients=list_reference_clients(db), consultants=list_reference_consultants(db))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/accounts/new")
|
||||
def account_create(request: Request, csrf_token: str = Form(...), account_type: str = Form(...), account_code: str = Form(...), display_name: str = Form(...), tenant_id: str = Form(""), client_id: str = Form(""), consultant_id: str = Form(""), email: str = Form(""), mobile: str = Form(""), gstin: str = Form(""), pan: str = Form(""), billing_address: str = Form(""), state: str = Form(""), notes: str = Form("")):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.create")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
create_platform_account(db, account_type=account_type, account_code=account_code, display_name=display_name, tenant_id=int(tenant_id) if tenant_id else None, client_id=int(client_id) if client_id else None, consultant_id=int(consultant_id) if consultant_id else None, email=email, mobile=mobile, gstin=gstin, pan=pan, billing_address=billing_address, state=state, notes=notes, user_id=user.id)
|
||||
return RedirectResponse(url="/platform-billing/accounts", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/audit-firm-subscriptions")
|
||||
def audit_firm_subscriptions(request: Request, q: str = "", synced: str = ""):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.view")
|
||||
if response:
|
||||
return response
|
||||
return _render(
|
||||
request,
|
||||
"modules/platform_billing/templates/platform_billing/audit_firms/list.html",
|
||||
db,
|
||||
user,
|
||||
title="Audit Firm Subscription Billing",
|
||||
rows=list_audit_firm_subscription_rows(db, q=q),
|
||||
audit_firm_accounts=list_audit_firm_accounts(db, q=q),
|
||||
q=q,
|
||||
synced=synced,
|
||||
can_manage=_has_perm(db, user, "platform_subscriptions.manage"),
|
||||
can_generate=_has_perm(db, user, "platform_billing.generate"),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/audit-firm-subscriptions/sync-accounts")
|
||||
def audit_firm_accounts_sync(request: Request, csrf_token: str = Form(...)):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
result = sync_audit_firm_billing_accounts(db, user_id=user.id)
|
||||
return RedirectResponse(url=f"/platform-billing/audit-firm-subscriptions?synced=created-{result['created']}-updated-{result['updated']}", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/audit-firm-subscriptions/generate")
|
||||
def audit_firm_generate_form(request: Request, generated: int = 0, skipped: int = 0):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.generate")
|
||||
if response:
|
||||
return response
|
||||
return _render(
|
||||
request,
|
||||
"modules/platform_billing/templates/platform_billing/audit_firms/generate.html",
|
||||
db,
|
||||
user,
|
||||
title="Generate Audit Firm Platform Bills",
|
||||
rows=list_audit_firm_subscription_rows(db),
|
||||
generated=generated,
|
||||
skipped=skipped,
|
||||
errors=[],
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/audit-firm-subscriptions/generate")
|
||||
def audit_firm_generate_submit(request: Request, csrf_token: str = Form(...), subscription_ids: list[int] = Form(default=[]), period_from: str = Form(...), period_to: str = Form(...), invoice_date: str = Form(...), due_date: str = Form(""), tax_type: str = Form("CGST_SGST"), client_rate: str = Form("0"), employee_rate: str = Form("0"), consultant_rate: str = Form("0"), branch_rate: str = Form("0"), include_zero_usage_lines: str = Form("off")):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.generate")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
result = generate_audit_firm_subscription_invoices(
|
||||
db,
|
||||
subscription_ids=subscription_ids,
|
||||
period_from=parse_date(period_from),
|
||||
period_to=parse_date(period_to),
|
||||
invoice_date=parse_date(invoice_date),
|
||||
due_date=parse_date(due_date),
|
||||
tax_type=tax_type,
|
||||
client_rate=client_rate,
|
||||
employee_rate=employee_rate,
|
||||
consultant_rate=consultant_rate,
|
||||
branch_rate=branch_rate,
|
||||
include_zero_usage_lines=include_zero_usage_lines == "on",
|
||||
user_id=user.id,
|
||||
)
|
||||
return _render(
|
||||
request,
|
||||
"modules/platform_billing/templates/platform_billing/audit_firms/generate.html",
|
||||
db,
|
||||
user,
|
||||
title="Generate Audit Firm Platform Bills",
|
||||
rows=list_audit_firm_subscription_rows(db),
|
||||
generated=result["created"],
|
||||
skipped=result["skipped"],
|
||||
errors=result["errors"],
|
||||
invoices=result["invoices"],
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/client-dashboard-subscriptions")
|
||||
def client_dashboard_subscriptions(request: Request, q: str = "", synced: str = "", created: str = ""):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.view")
|
||||
if response:
|
||||
return response
|
||||
return _render(
|
||||
request,
|
||||
"modules/platform_billing/templates/platform_billing/clients/list.html",
|
||||
db,
|
||||
user,
|
||||
title="Client Compliance Dashboard Billing",
|
||||
rows=list_client_dashboard_subscription_rows(db, q=q),
|
||||
client_accounts=list_client_dashboard_accounts(db, q=q),
|
||||
client_plans=list_client_dashboard_plans(db),
|
||||
q=q,
|
||||
synced=synced,
|
||||
created=created,
|
||||
can_manage=_has_perm(db, user, "platform_subscriptions.manage"),
|
||||
can_generate=_has_perm(db, user, "platform_billing.generate"),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/client-dashboard-subscriptions/sync-accounts")
|
||||
def client_dashboard_accounts_sync(request: Request, csrf_token: str = Form(...)):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
result = sync_client_dashboard_billing_accounts(db, user_id=user.id)
|
||||
return RedirectResponse(url=f"/platform-billing/client-dashboard-subscriptions?synced=created-{result['created']}-updated-{result['updated']}", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/client-dashboard-subscriptions/new")
|
||||
def client_dashboard_subscription_create(request: Request, csrf_token: str = Form(...), account_id: int = Form(...), plan_id: int = Form(...), subscription_code: str = Form(...), start_date: str = Form(...), end_date: str = Form(""), billing_cycle: str = Form("Monthly"), amount: str = Form("0"), gst_rate: str = Form("18"), auto_generate_invoice: str = Form("on"), notes: str = Form("")):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
create_platform_subscription(
|
||||
db,
|
||||
account_id=account_id,
|
||||
plan_id=plan_id,
|
||||
subscription_code=subscription_code,
|
||||
start_date=parse_date(start_date),
|
||||
end_date=parse_date(end_date),
|
||||
billing_cycle=billing_cycle,
|
||||
amount=amount,
|
||||
gst_rate=gst_rate,
|
||||
auto_generate_invoice=auto_generate_invoice == "on",
|
||||
notes=notes,
|
||||
user_id=user.id,
|
||||
)
|
||||
return RedirectResponse(url="/platform-billing/client-dashboard-subscriptions?created=1", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/client-dashboard-subscriptions/generate")
|
||||
def client_dashboard_generate_form(request: Request, generated: int = 0, skipped: int = 0):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.generate")
|
||||
if response:
|
||||
return response
|
||||
return _render(
|
||||
request,
|
||||
"modules/platform_billing/templates/platform_billing/clients/generate.html",
|
||||
db,
|
||||
user,
|
||||
title="Generate Client Dashboard Platform Bills",
|
||||
rows=list_client_dashboard_subscription_rows(db),
|
||||
generated=generated,
|
||||
skipped=skipped,
|
||||
errors=[],
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/client-dashboard-subscriptions/generate")
|
||||
def client_dashboard_generate_submit(request: Request, csrf_token: str = Form(...), subscription_ids: list[int] = Form(default=[]), period_from: str = Form(...), period_to: str = Form(...), invoice_date: str = Form(...), due_date: str = Form(""), tax_type: str = Form("CGST_SGST"), pan_rate: str = Form("0"), gstin_rate: str = Form("0"), module_rate: str = Form("0"), include_zero_usage_lines: str = Form("off")):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.generate")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
result = generate_client_dashboard_subscription_invoices(
|
||||
db,
|
||||
subscription_ids=subscription_ids,
|
||||
period_from=parse_date(period_from),
|
||||
period_to=parse_date(period_to),
|
||||
invoice_date=parse_date(invoice_date),
|
||||
due_date=parse_date(due_date),
|
||||
tax_type=tax_type,
|
||||
pan_rate=pan_rate,
|
||||
gstin_rate=gstin_rate,
|
||||
module_rate=module_rate,
|
||||
include_zero_usage_lines=include_zero_usage_lines == "on",
|
||||
user_id=user.id,
|
||||
)
|
||||
return _render(
|
||||
request,
|
||||
"modules/platform_billing/templates/platform_billing/clients/generate.html",
|
||||
db,
|
||||
user,
|
||||
title="Generate Client Dashboard Platform Bills",
|
||||
rows=list_client_dashboard_subscription_rows(db),
|
||||
generated=result["created"],
|
||||
skipped=result["skipped"],
|
||||
errors=result["errors"],
|
||||
invoices=result["invoices"],
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/consultant-subscriptions")
|
||||
def consultant_subscriptions(request: Request, q: str = "", synced: str = "", created: str = ""):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.view")
|
||||
if response:
|
||||
return response
|
||||
return _render(
|
||||
request,
|
||||
"modules/platform_billing/templates/platform_billing/consultants/list.html",
|
||||
db,
|
||||
user,
|
||||
title="Consultant SaaS/Tool Billing",
|
||||
rows=list_consultant_billing_subscription_rows(db, q=q),
|
||||
consultant_accounts=list_consultant_billing_accounts(db, q=q),
|
||||
consultant_plans=list_consultant_billing_plans(db),
|
||||
q=q,
|
||||
synced=synced,
|
||||
created=created,
|
||||
can_manage=_has_perm(db, user, "platform_subscriptions.manage"),
|
||||
can_generate=_has_perm(db, user, "platform_billing.generate"),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/consultant-subscriptions/sync-accounts")
|
||||
def consultant_accounts_sync(request: Request, csrf_token: str = Form(...)):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
result = sync_consultant_billing_accounts(db, user_id=user.id)
|
||||
return RedirectResponse(url=f"/platform-billing/consultant-subscriptions?synced=created-{result['created']}-updated-{result['updated']}", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/consultant-subscriptions/new")
|
||||
def consultant_subscription_create(request: Request, csrf_token: str = Form(...), account_id: int = Form(...), plan_id: int = Form(...), subscription_code: str = Form(...), start_date: str = Form(...), end_date: str = Form(""), billing_cycle: str = Form("Monthly"), amount: str = Form("0"), gst_rate: str = Form("18"), auto_generate_invoice: str = Form("on"), notes: str = Form("")):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
create_platform_subscription(
|
||||
db,
|
||||
account_id=account_id,
|
||||
plan_id=plan_id,
|
||||
subscription_code=subscription_code,
|
||||
start_date=parse_date(start_date),
|
||||
end_date=parse_date(end_date),
|
||||
billing_cycle=billing_cycle,
|
||||
amount=amount,
|
||||
gst_rate=gst_rate,
|
||||
auto_generate_invoice=auto_generate_invoice == "on",
|
||||
notes=notes,
|
||||
user_id=user.id,
|
||||
)
|
||||
return RedirectResponse(url="/platform-billing/consultant-subscriptions?created=1", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/consultant-subscriptions/generate")
|
||||
def consultant_generate_form(request: Request, generated: int = 0, skipped: int = 0):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.generate")
|
||||
if response:
|
||||
return response
|
||||
return _render(
|
||||
request,
|
||||
"modules/platform_billing/templates/platform_billing/consultants/generate.html",
|
||||
db,
|
||||
user,
|
||||
title="Generate Consultant Platform Bills",
|
||||
rows=list_consultant_billing_subscription_rows(db),
|
||||
generated=generated,
|
||||
skipped=skipped,
|
||||
errors=[],
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/consultant-subscriptions/generate")
|
||||
def consultant_generate_submit(request: Request, csrf_token: str = Form(...), subscription_ids: list[int] = Form(default=[]), period_from: str = Form(...), period_to: str = Form(...), invoice_date: str = Form(...), due_date: str = Form(""), tax_type: str = Form("CGST_SGST"), managed_client_rate: str = Form("0"), user_account_rate: str = Form("0"), include_zero_usage_lines: str = Form("off")):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.generate")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
result = generate_consultant_subscription_invoices(
|
||||
db,
|
||||
subscription_ids=subscription_ids,
|
||||
period_from=parse_date(period_from),
|
||||
period_to=parse_date(period_to),
|
||||
invoice_date=parse_date(invoice_date),
|
||||
due_date=parse_date(due_date),
|
||||
tax_type=tax_type,
|
||||
managed_client_rate=managed_client_rate,
|
||||
user_account_rate=user_account_rate,
|
||||
include_zero_usage_lines=include_zero_usage_lines == "on",
|
||||
user_id=user.id,
|
||||
)
|
||||
return _render(
|
||||
request,
|
||||
"modules/platform_billing/templates/platform_billing/consultants/generate.html",
|
||||
db,
|
||||
user,
|
||||
title="Generate Consultant Platform Bills",
|
||||
rows=list_consultant_billing_subscription_rows(db),
|
||||
generated=result["created"],
|
||||
skipped=result["skipped"],
|
||||
errors=result["errors"],
|
||||
invoices=result["invoices"],
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/subscriptions")
|
||||
def subscriptions_list(request: Request, q: str = ""):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.view")
|
||||
if response:
|
||||
return response
|
||||
return _render(request, "modules/platform_billing/templates/platform_billing/subscriptions/list.html", db, user, title="Platform Subscriptions", rows=list_platform_subscriptions(db, q=q), q=q, can_manage=_has_perm(db, user, "platform_subscriptions.manage"))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/subscriptions/new")
|
||||
def subscription_new(request: Request):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
return _render(request, "modules/platform_billing/templates/platform_billing/subscriptions/create.html", db, user, title="New Platform Subscription", accounts=list_platform_accounts(db), plans=list_platform_plans(db))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/subscriptions/new")
|
||||
def subscription_create(request: Request, csrf_token: str = Form(...), account_id: int = Form(...), plan_id: int = Form(...), subscription_code: str = Form(...), start_date: str = Form(...), end_date: str = Form(""), billing_cycle: str = Form(...), amount: str = Form("0"), gst_rate: str = Form("18"), auto_generate_invoice: str = Form("off"), notes: str = Form("")):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
create_platform_subscription(db, account_id=account_id, plan_id=plan_id, subscription_code=subscription_code, start_date=parse_date(start_date), end_date=parse_date(end_date), billing_cycle=billing_cycle, amount=amount, gst_rate=gst_rate, auto_generate_invoice=auto_generate_invoice == "on", notes=notes, user_id=user.id)
|
||||
return RedirectResponse(url="/platform-billing/subscriptions", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/invoices")
|
||||
def invoices_list(request: Request, q: str = ""):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.view")
|
||||
if response:
|
||||
return response
|
||||
return _render(request, "modules/platform_billing/templates/platform_billing/invoices/list.html", db, user, title="Platform Invoices", rows=list_platform_invoices(db, q=q), q=q, can_create=_has_perm(db, user, "platform_billing.create"))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/invoices/new")
|
||||
def invoice_new(request: Request):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.create")
|
||||
if response:
|
||||
return response
|
||||
return _render(request, "modules/platform_billing/templates/platform_billing/invoices/create.html", db, user, title="New Platform Invoice", accounts=list_platform_accounts(db), subscriptions=list_platform_subscriptions(db))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/invoices/new")
|
||||
def invoice_create(request: Request, csrf_token: str = Form(...), account_id: int = Form(...), subscription_id: str = Form(""), invoice_no: str = Form(...), invoice_date: str = Form(...), due_date: str = Form(""), billing_period_from: str = Form(""), billing_period_to: str = Form(""), tax_type: str = Form(...), charge_type: str = Form("SUBSCRIPTION"), description: str = Form(...), quantity: str = Form("1"), rate: str = Form("0"), discount_amount: str = Form("0"), gst_rate: str = Form("18"), notes: str = Form("")):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.create")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
invoice = create_platform_invoice(
|
||||
db,
|
||||
account_id=account_id,
|
||||
subscription_id=int(subscription_id) if subscription_id else None,
|
||||
invoice_no=invoice_no,
|
||||
invoice_date=parse_date(invoice_date),
|
||||
due_date=parse_date(due_date),
|
||||
billing_period_from=parse_date(billing_period_from),
|
||||
billing_period_to=parse_date(billing_period_to),
|
||||
tax_type=tax_type,
|
||||
line_items=[{"charge_type": charge_type, "description": description, "quantity": quantity, "rate": rate, "discount_amount": discount_amount, "gst_rate": gst_rate}],
|
||||
notes=notes,
|
||||
user_id=user.id,
|
||||
)
|
||||
return RedirectResponse(url=f"/platform-billing/invoices/{invoice.id}", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/invoices/{invoice_id}")
|
||||
def invoice_detail(request: Request, invoice_id: int):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.view")
|
||||
if response:
|
||||
return response
|
||||
invoice = get_platform_invoice(db, invoice_id)
|
||||
if not invoice:
|
||||
return RedirectResponse(url="/platform-billing/invoices", status_code=303)
|
||||
return _render(request, "modules/platform_billing/templates/platform_billing/invoices/detail.html", db, user, title=f"Platform Invoice {invoice.invoice_no}", invoice=invoice, can_post=_has_perm(db, user, "platform_billing.post"), can_record_payment=_has_perm(db, user, "platform_billing.payment.create"))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/invoices/{invoice_id}/post")
|
||||
def invoice_post(request: Request, invoice_id: int, csrf_token: str = Form(...)):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.post")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
invoice = get_platform_invoice(db, invoice_id)
|
||||
if invoice and invoice.status == "DRAFT":
|
||||
post_platform_invoice(db, invoice, user.id)
|
||||
return RedirectResponse(url=f"/platform-billing/invoices/{invoice_id}", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/invoices/{invoice_id}/payments")
|
||||
def payment_create(request: Request, invoice_id: int, csrf_token: str = Form(...), amount: str = Form(...), mode: str = Form("Bank"), reference_no: str = Form(""), notes: str = Form("")):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "platform_billing.payment.create")
|
||||
if response:
|
||||
return response
|
||||
validate_csrf(request, csrf_token)
|
||||
invoice = get_platform_invoice(db, invoice_id)
|
||||
if invoice:
|
||||
record_platform_payment(db, invoice=invoice, amount=amount, mode=mode, reference_no=reference_no, notes=notes, user_id=user.id)
|
||||
return RedirectResponse(url=f"/platform-billing/invoices/{invoice_id}", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
Reference in New Issue
Block a user