Prepare ERP source for Gitea deployment
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
{% include "modules/clients/templates/clients/_client_tabs.html" %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-slate-900">Invoice {{ invoice.invoice_no }}</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Issued on {{ invoice.invoice_date.strftime('%d-%m-%Y') if invoice.invoice_date else '-' }}{% if invoice.due_date %} • Due {{ invoice.due_date.strftime('%d-%m-%Y') }}{% endif %}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/client/billing" class="af-btn af-btn-secondary">Back to Bills</a>
|
||||
<a href="/client/billing/{{ invoice.id }}/print" class="af-btn af-btn-secondary">Print / Save PDF</a>
|
||||
{% if invoice.balance_amount and invoice.balance_amount > 0 %}<a href="/client/billing/{{ invoice.id }}/pay-now" class="af-btn af-btn-primary">Pay Now</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="grid gap-4 md:grid-cols-4">
|
||||
<div class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">Invoice Total</div><div class="mt-2 text-2xl font-semibold">₹ {{ '%.2f'|format(invoice.total_amount or 0) }}</div></div>
|
||||
<div class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">Received</div><div class="mt-2 text-2xl font-semibold text-emerald-700">₹ {{ '%.2f'|format(invoice.amount_received or 0) }}</div></div>
|
||||
<div class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">TDS</div><div class="mt-2 text-2xl font-semibold text-slate-900">₹ {{ '%.2f'|format(invoice.tds_deducted or 0) }}</div></div>
|
||||
<div class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">Balance</div><div class="mt-2 text-2xl font-semibold text-amber-700">₹ {{ '%.2f'|format(invoice.balance_amount or 0) }}</div></div>
|
||||
</section>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div class="af-card">
|
||||
<div class="flex items-center justify-between gap-3"><h3 class="text-lg font-semibold text-slate-900">Invoice Lines</h3><span class="af-badge {% if invoice.status == 'PAID' %}af-badge-success{% else %}af-badge-warning{% endif %}">{{ invoice.status.replace('_', ' ') }}</span></div>
|
||||
<div class="mt-5 overflow-x-auto rounded-2xl border border-slate-200">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500"><tr><th class="px-4 py-3">Description</th><th class="px-4 py-3">SAC</th><th class="px-4 py-3 text-right">Taxable</th><th class="px-4 py-3 text-right">GST</th><th class="px-4 py-3 text-right">Total</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-100 bg-white">
|
||||
{% for line in invoice.lines %}
|
||||
<tr><td class="px-4 py-3 font-medium text-slate-900 whitespace-pre-line">{{ line.description }}</td><td class="px-4 py-3 text-slate-600">{{ line.sac_code or '-' }}</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 font-semibold">₹ {{ '%.2f'|format(line.line_total or 0) }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="space-y-6">
|
||||
<div class="af-card">
|
||||
<h3 class="text-base font-semibold text-slate-900">Payment Status</h3>
|
||||
<div class="mt-4 space-y-3 text-sm">
|
||||
<div class="flex justify-between"><span class="text-slate-500">Status</span><span class="font-semibold">{{ invoice.status.replace('_', ' ') }}</span></div>
|
||||
<div class="flex justify-between"><span class="text-slate-500">Due Amount</span><span class="font-semibold text-amber-700">₹ {{ '%.2f'|format(invoice.balance_amount or 0) }}</span></div>
|
||||
{% if invoice.balance_amount and invoice.balance_amount > 0 %}<a href="/client/billing/{{ invoice.id }}/pay-now" class="mt-2 w-full justify-center af-btn af-btn-primary">Pay Now</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="af-card">
|
||||
<h3 class="text-base font-semibold text-slate-900">Receipts</h3>
|
||||
<div class="mt-4 space-y-3 text-sm">
|
||||
{% for p in invoice.payments %}
|
||||
{% if p.status == 'RECEIVED' %}
|
||||
<a href="/client/billing/receipts/{{ p.id }}" class="block rounded-2xl border border-slate-200 p-3 hover:bg-slate-50"><div class="font-semibold text-brand-700">{{ p.receipt_no }}</div><div class="mt-1 text-xs text-slate-500">{{ p.payment_date.strftime('%d-%m-%Y') if p.payment_date else '-' }} • ₹ {{ '%.2f'|format(p.amount_received or 0) }}</div></a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-slate-300 p-4 text-slate-500">No receipts recorded yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,75 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
{% include "modules/clients/templates/clients/_client_tabs.html" %}
|
||||
<div class="space-y-6">
|
||||
<section class="rounded-3xl bg-gradient-to-r from-brand-700 to-slate-900 p-6 text-white shadow-soft">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-brand-100">Client Portal</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold">My Bills & Payments</h2>
|
||||
<p class="mt-2 max-w-3xl text-sm text-brand-100">View invoices issued by your audit firm, download receipts and use Pay Now for pending bills.</p>
|
||||
<p class="mt-1 text-xs text-brand-100">Active FY: {{ active_financial_year or 'All Years' }}</p>
|
||||
</div>
|
||||
{% if billing_latest_due_invoice %}
|
||||
<a href="/client/billing/{{ billing_latest_due_invoice.id }}/pay-now" class="rounded-xl bg-white px-4 py-2 text-sm font-semibold text-brand-700 hover:bg-brand-50">Pay Latest Due</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-4 md:grid-cols-3">
|
||||
<div class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">Outstanding</div><div class="mt-2 text-3xl font-semibold text-amber-700">₹ {{ '%.2f'|format(billing_outstanding_amount or 0) }}</div><div class="mt-1 text-xs text-slate-500">{{ billing_open_count or 0 }} open bill(s)</div></div>
|
||||
<div class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">Total Invoices</div><div class="mt-2 text-3xl font-semibold text-slate-900">{{ billing_total_count or 0 }}</div><div class="mt-1 text-xs text-slate-500">Issued by firm</div></div>
|
||||
<div class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">Paid</div><div class="mt-2 text-3xl font-semibold text-emerald-700">{{ billing_paid_count or 0 }}</div><div class="mt-1 text-xs text-slate-500">Completed payments</div></div>
|
||||
</section>
|
||||
|
||||
<div class="af-card">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-900">Invoices</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">Draft and cancelled invoices are not shown in the client portal.</p>
|
||||
</div>
|
||||
<form method="get" action="/client/billing" class="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<input name="q" value="{{ q or '' }}" placeholder="Search invoice no" class="rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
<select name="include_paid" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="yes" {% if include_paid != 'no' %}selected{% endif %}>All invoices</option>
|
||||
<option value="no" {% if include_paid == 'no' %}selected{% endif %}>Only pending</option>
|
||||
</select>
|
||||
<button class="af-btn af-btn-secondary" type="submit">Filter</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 overflow-x-auto rounded-2xl border border-slate-200">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Invoice</th>
|
||||
<th class="px-4 py-3">Date</th><th class="px-4 py-3">FY</th>
|
||||
<th class="px-4 py-3">Due Date</th>
|
||||
<th class="px-4 py-3 text-right">Total</th>
|
||||
<th class="px-4 py-3 text-right">Balance</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3 text-right">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 bg-white">
|
||||
{% for row in rows %}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="px-4 py-3 font-semibold text-slate-900"><a class="text-brand-700 hover:underline" href="/client/billing/{{ row.id }}">{{ row.invoice_no }}</a></td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ row.invoice_date.strftime('%d-%m-%Y') if row.invoice_date else '-' }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ row.due_date.strftime('%d-%m-%Y') if row.due_date else '-' }}</td>
|
||||
<td class="px-4 py-3 text-right font-medium">₹ {{ '%.2f'|format(row.total_amount or 0) }}</td>
|
||||
<td class="px-4 py-3 text-right font-medium {% if row.balance_amount and row.balance_amount > 0 %}text-amber-700{% else %}text-emerald-700{% endif %}">₹ {{ '%.2f'|format(row.balance_amount or 0) }}</td>
|
||||
<td class="px-4 py-3"><span class="af-badge {% if row.status == 'PAID' %}af-badge-success{% elif row.status == 'OVERDUE' %}af-badge-danger{% else %}af-badge-warning{% endif %}">{{ row.status.replace('_', ' ') }}</span></td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
{% if row.balance_amount and row.balance_amount > 0 %}<a href="/client/billing/{{ row.id }}/pay-now" class="af-btn af-btn-primary">Pay Now</a>{% else %}<a href="/client/billing/{{ row.id }}" class="af-btn af-btn-secondary">View</a>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="px-4 py-8 text-center text-slate-500">No invoices found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,94 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
{% include "modules/clients/templates/clients/_client_tabs.html" %}
|
||||
<div class="mx-auto max-w-4xl space-y-6">
|
||||
<section class="rounded-3xl bg-gradient-to-r from-brand-700 to-slate-900 p-6 text-white shadow-soft">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-brand-100">Pay Now</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold">Invoice {{ invoice.invoice_no }}</h2>
|
||||
<p class="mt-2 text-sm text-brand-100">Pay the outstanding amount using online gateway, UPI or bank transfer. Online gateway receipts are created automatically after successful verification.</p>
|
||||
</section>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<div class="af-card">
|
||||
<h3 class="text-lg font-semibold text-slate-900">Payment Options</h3>
|
||||
<div class="mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
|
||||
<div class="font-semibold">Amount payable: ₹ {{ '%.2f'|format(amount_due or 0) }}</div>
|
||||
<div class="mt-1">Invoice balance only is shown here. TDS or bank charges will be adjusted by the firm while recording receipt.</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% if payumoney_enabled %}
|
||||
<div class="mt-5 rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-emerald-900">Online Payment Gateway</div>
|
||||
<p class="mt-1 text-sm text-emerald-800">Pay securely through PayUMoney / PayU. Receipt will be created automatically after successful confirmation.</p>
|
||||
{% if payumoney_mode != 'LIVE' %}<p class="mt-1 text-xs font-semibold text-amber-700">Currently running in TEST mode.</p>{% endif %}
|
||||
</div>
|
||||
<form method="post" action="/client/billing/{{ invoice.id }}/payumoney/start">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<button type="submit" class="af-btn af-btn-primary whitespace-nowrap">Pay Online</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if cashfree_enabled %}
|
||||
<div class="mt-5 rounded-2xl border border-sky-200 bg-sky-50 p-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-sky-900">Cashfree Payment Gateway</div>
|
||||
<p class="mt-1 text-sm text-sky-800">Pay securely through Cashfree checkout. Receipt will be created automatically after successful confirmation.</p>
|
||||
{% if cashfree_mode != 'LIVE' %}<p class="mt-1 text-xs font-semibold text-amber-700">Currently running in TEST / Sandbox mode.</p>{% endif %}
|
||||
</div>
|
||||
<form method="post" action="/client/billing/{{ invoice.id }}/cashfree/start">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<button type="submit" class="af-btn af-btn-primary whitespace-nowrap">Pay with Cashfree</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if upi_link %}
|
||||
<div class="mt-5 rounded-2xl border border-brand-200 bg-brand-50 p-4">
|
||||
<div class="text-sm font-semibold text-brand-800">UPI Payment</div>
|
||||
<div class="mt-2 text-sm text-slate-700">UPI ID: <span class="font-semibold">{{ upi_id }}</span></div>
|
||||
<a href="{{ upi_link }}" class="mt-4 inline-flex af-btn af-btn-primary">Open UPI App</a>
|
||||
<p class="mt-3 text-xs text-slate-500">This opens a UPI app on supported devices. After payment, share the UTR/reference number with the firm if requested.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-5 rounded-2xl border border-slate-200 p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Bank Transfer</div>
|
||||
<dl class="mt-3 grid gap-3 text-sm sm:grid-cols-2">
|
||||
<div><dt class="text-xs uppercase text-slate-500">Bank</dt><dd class="font-medium">{{ bank_name or '-' }}</dd></div>
|
||||
<div><dt class="text-xs uppercase text-slate-500">Account Name</dt><dd class="font-medium">{{ bank_account_name or '-' }}</dd></div>
|
||||
<div><dt class="text-xs uppercase text-slate-500">Account No.</dt><dd class="font-medium">{{ bank_account_number or '-' }}</dd></div>
|
||||
<div><dt class="text-xs uppercase text-slate-500">IFSC</dt><dd class="font-medium">{{ bank_ifsc or '-' }}</dd></div>
|
||||
</dl>
|
||||
{% if payment_instructions %}<div class="mt-4 whitespace-pre-line rounded-xl bg-slate-50 p-3 text-sm text-slate-700">{{ payment_instructions }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="space-y-6">
|
||||
<div class="af-card">
|
||||
<h3 class="text-base font-semibold text-slate-900">Invoice Summary</h3>
|
||||
<div class="mt-4 space-y-3 text-sm">
|
||||
<div class="flex justify-between"><span class="text-slate-500">Invoice Total</span><span class="font-semibold">₹ {{ '%.2f'|format(invoice.total_amount or 0) }}</span></div>
|
||||
<div class="flex justify-between"><span class="text-slate-500">Received</span><span class="font-semibold">₹ {{ '%.2f'|format(invoice.amount_received or 0) }}</span></div>
|
||||
<div class="flex justify-between"><span class="text-slate-500">TDS</span><span class="font-semibold">₹ {{ '%.2f'|format(invoice.tds_deducted or 0) }}</span></div>
|
||||
<div class="border-t border-slate-200 pt-3 flex justify-between"><span class="text-slate-500">Balance</span><span class="font-semibold text-amber-700">₹ {{ '%.2f'|format(invoice.balance_amount or 0) }}</span></div>
|
||||
</div>
|
||||
<div class="mt-5 grid gap-2">
|
||||
<a href="/client/billing/{{ invoice.id }}" class="af-btn af-btn-secondary justify-center">View Invoice</a>
|
||||
<a href="/client/billing/{{ invoice.id }}/print" class="af-btn af-btn-secondary justify-center">Print / Save PDF</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4 text-xs leading-5 text-slate-500 shadow-soft">
|
||||
{% if payumoney_enabled or cashfree_enabled %}Online gateway confirmation is enabled. UPI/bank transfer can still be used when the client prefers manual payment.{% else %}Online gateway is not enabled yet. This page helps the client pay through UPI/bank details and the firm records receipt manually.{% endif %}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
{% include "modules/clients/templates/clients/_client_tabs.html" %}
|
||||
<div class="mx-auto max-w-3xl space-y-6">
|
||||
<section class="af-card p-6">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] {% if result == 'success' %}text-emerald-700{% else %}text-rose-700{% endif %}">PayUMoney Payment</p>
|
||||
<h1 class="mt-2 text-2xl font-bold text-slate-900">{{ heading }}</h1>
|
||||
<p class="mt-2 text-sm text-slate-600">{{ message }}</p>
|
||||
|
||||
{% if transaction %}
|
||||
<dl class="mt-5 grid gap-3 rounded-2xl bg-slate-50 p-4 text-sm sm:grid-cols-2">
|
||||
<div><dt class="text-xs uppercase text-slate-500">Invoice</dt><dd class="font-semibold">{{ transaction.invoice.invoice_no }}</dd></div>
|
||||
<div><dt class="text-xs uppercase text-slate-500">Amount</dt><dd class="font-semibold">₹ {{ '%.2f'|format(transaction.amount or 0) }}</dd></div>
|
||||
<div><dt class="text-xs uppercase text-slate-500">Txn ID</dt><dd class="font-mono text-xs font-semibold">{{ transaction.txnid }}</dd></div>
|
||||
<div><dt class="text-xs uppercase text-slate-500">Gateway Status</dt><dd class="font-semibold">{{ transaction.gateway_status or transaction.status }}</dd></div>
|
||||
{% if transaction.bank_ref_num %}<div><dt class="text-xs uppercase text-slate-500">Bank Ref.</dt><dd class="font-semibold">{{ transaction.bank_ref_num }}</dd></div>{% endif %}
|
||||
{% if transaction.mihpayid %}<div><dt class="text-xs uppercase text-slate-500">PayU ID</dt><dd class="font-semibold">{{ transaction.mihpayid }}</dd></div>{% endif %}
|
||||
</dl>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-6 flex flex-wrap gap-3">
|
||||
{% if transaction %}<a href="/client/billing/{{ transaction.invoice_id }}" class="af-btn af-btn-primary">View Invoice</a>{% endif %}
|
||||
<a href="/client/billing" class="af-btn af-btn-secondary">Back to My Bills</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,124 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Create GST Invoice</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Prepare a professional tax invoice with SAC, GST breakup, place of supply and firm billing defaults.</p>
|
||||
<p class="mt-1 text-xs text-slate-400">Invoice will be tagged to active FY: <span class="font-semibold text-slate-600">{{ active_financial_year or 'Current FY' }}</span></p>
|
||||
</div>
|
||||
<a href="/billing/settings" class="af-btn af-btn-secondary">Billing Settings</a>
|
||||
</div>
|
||||
|
||||
<form method="post" class="space-y-6 af-card">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
|
||||
<section class="space-y-4">
|
||||
<div class="af-panel-header">
|
||||
<div>
|
||||
<h2 class="text-base font-semibold text-slate-900">Invoice Header</h2>
|
||||
<p class="text-xs text-slate-500">Client, date, GST treatment and billing period.</p>
|
||||
</div>
|
||||
<span class="af-badge af-badge-info">{{ settings.invoice_title or 'Tax Invoice' }}</span>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">Client</span>
|
||||
<select name="client_id" required class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">Select client</option>
|
||||
{% for client in clients %}
|
||||
<option value="{{ client.id }}">{{ client.client_code }} - {{ client.client_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">Invoice Date</span>
|
||||
<input type="date" name="invoice_date" value="{{ today }}" required class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">Due Date</span>
|
||||
<input type="date" name="due_date" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">Billing Period From</span>
|
||||
<input type="date" name="billing_period_from" value="{{ default_billing_period_from or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">Billing Period To</span>
|
||||
<input type="date" name="billing_period_to" value="{{ default_billing_period_to or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">Tax Type</span>
|
||||
<select name="tax_type" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
{% for tax_type in tax_types %}<option value="{{ tax_type }}" {% if settings.default_tax_type == tax_type %}selected{% endif %}>{{ tax_type }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">Place of Supply</span>
|
||||
<input name="place_of_supply" placeholder="State / Union Territory" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">Client State Code</span>
|
||||
<input name="client_state_code" maxlength="2" placeholder="e.g. 33" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="mt-7 inline-flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="reverse_charge" value="yes" class="rounded border-slate-300" />
|
||||
Reverse charge applicable
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<div class="af-panel-header">
|
||||
<div>
|
||||
<h2 class="text-base font-semibold text-slate-900">Invoice Lines</h2>
|
||||
<p class="text-xs text-slate-500">SAC defaults to billing settings if left blank. Blank description rows are ignored.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto rounded-xl border border-slate-200">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase text-slate-500">
|
||||
<tr>
|
||||
<th class="px-3 py-2">Service</th>
|
||||
<th class="px-3 py-2">Description</th>
|
||||
<th class="px-3 py-2">SAC</th>
|
||||
<th class="px-3 py-2">Qty</th>
|
||||
<th class="px-3 py-2">Rate</th>
|
||||
<th class="px-3 py-2">Discount</th>
|
||||
<th class="px-3 py-2">GST %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for i in range(1, 8) %}
|
||||
<tr>
|
||||
<td class="px-3 py-2">
|
||||
<select name="line_service_id" class="w-48 rounded-lg border border-slate-300 px-2 py-1.5">
|
||||
<option value="">No service</option>
|
||||
{% for service in services %}<option value="{{ service.id }}">{{ service.service_code }} - {{ service.service_name }}</option>{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-3 py-2"><input name="line_description" class="w-80 rounded-lg border border-slate-300 px-2 py-1.5" placeholder="Professional fees / service description" /></td>
|
||||
<td class="px-3 py-2"><input name="line_sac_code" value="{{ settings.default_sac_code or '' }}" class="w-24 rounded-lg border border-slate-300 px-2 py-1.5" /></td>
|
||||
<td class="px-3 py-2"><input name="line_quantity" value="1" class="w-20 rounded-lg border border-slate-300 px-2 py-1.5" /></td>
|
||||
<td class="px-3 py-2"><input name="line_rate" value="0" class="w-28 rounded-lg border border-slate-300 px-2 py-1.5" /></td>
|
||||
<td class="px-3 py-2"><input name="line_discount" value="0" class="w-28 rounded-lg border border-slate-300 px-2 py-1.5" /></td>
|
||||
<td class="px-3 py-2"><input name="line_gst_rate" value="{{ settings.default_gst_rate or 18 }}" class="w-20 rounded-lg border border-slate-300 px-2 py-1.5" /></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="block"><span class="text-sm font-medium text-slate-700">Declaration / Notes</span><textarea name="notes" rows="3" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{{ settings.declaration or '' }}</textarea></label>
|
||||
<label class="block"><span class="text-sm font-medium text-slate-700">Terms & Conditions</span><textarea name="terms" rows="3" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{{ settings.terms or '' }}</textarea></label>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<a href="/billing" class="af-btn af-btn-secondary">Cancel</a>
|
||||
<button class="af-btn af-btn-primary">Save Draft Invoice</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,120 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">{{ invoice_ctx.invoice_title }} {{ invoice.invoice_no }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">{{ invoice.client_legal_name or (invoice.client.client_name if invoice.client else '') }} • {{ invoice.invoice_date }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if invoice.status == 'DRAFT' %}
|
||||
<form method="post" action="/billing/{{ invoice.id }}/issue">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<button class="af-btn af-btn-primary">Issue Invoice</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if can_record_payment and invoice.status not in ['DRAFT','CANCELLED','PAID'] %}
|
||||
<a href="/billing/{{ invoice.id }}/payments/new" class="af-btn af-btn-primary">Record Payment</a>
|
||||
{% endif %}
|
||||
<a href="/billing/{{ invoice.id }}/print" target="_blank" class="af-btn af-btn-secondary">Print / PDF</a>
|
||||
<a href="/billing" class="af-btn af-btn-secondary">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="af-card space-y-6">
|
||||
<div class="grid gap-4 md:grid-cols-4 lg:grid-cols-7">
|
||||
<div><div class="text-xs uppercase text-slate-500">Status</div><div class="font-semibold">{{ invoice.status }}</div></div>
|
||||
<div><div class="text-xs uppercase text-slate-500">Due Date</div><div class="font-semibold">{{ invoice.due_date or '-' }}</div></div>
|
||||
<div><div class="text-xs uppercase text-slate-500">Place of Supply</div><div class="font-semibold">{{ invoice.place_of_supply or '-' }}</div></div>
|
||||
<div><div class="text-xs uppercase text-slate-500">Total</div><div class="font-semibold">₹ {{ '%.2f'|format(invoice.total_amount or 0) }}</div></div>
|
||||
<div><div class="text-xs uppercase text-slate-500">Amount Received</div><div class="font-semibold text-emerald-700">₹ {{ '%.2f'|format(invoice.amount_received or 0) }}</div></div>
|
||||
<div><div class="text-xs uppercase text-slate-500">TDS Deducted</div><div class="font-semibold text-blue-700">₹ {{ '%.2f'|format(invoice.tds_deducted or 0) }}</div></div>
|
||||
<div><div class="text-xs uppercase text-slate-500">Balance</div><div class="font-semibold {% if invoice.balance_amount and invoice.balance_amount > 0 %}text-amber-700{% else %}text-emerald-700{% endif %}">₹ {{ '%.2f'|format(invoice.balance_amount or 0) }}</div></div>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="rounded-xl border border-slate-200 p-4">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Supplier</h2>
|
||||
<div class="mt-2 font-semibold text-slate-900">{{ invoice_ctx.firm_name }}</div>
|
||||
<div class="text-sm text-slate-600 whitespace-pre-line">{{ invoice_ctx.firm_address or '-' }}</div>
|
||||
<div class="mt-2 text-sm text-slate-600">GSTIN: {{ invoice_ctx.firm_gstin or '-' }} • PAN: {{ invoice_ctx.firm_pan or '-' }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-200 p-4">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Bill To</h2>
|
||||
<div class="mt-2 font-semibold text-slate-900">{{ invoice.client_legal_name or '-' }}</div>
|
||||
<div class="text-sm text-slate-600">{{ invoice.client_billing_address or '-' }}</div>
|
||||
<div class="mt-2 text-sm text-slate-600">GSTIN: {{ invoice.client_gstin or '-' }} • PAN: {{ invoice.client_pan or '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-soft">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase text-slate-500">
|
||||
<tr><th class="px-4 py-3">Description</th><th class="px-4 py-3">SAC</th><th class="px-4 py-3 text-right">Qty</th><th class="px-4 py-3 text-right">Rate</th><th class="px-4 py-3 text-right">Taxable</th><th class="px-4 py-3 text-right">GST</th><th class="px-4 py-3 text-right">Total</th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for line in invoice.lines %}
|
||||
<tr>
|
||||
<td class="px-4 py-3"><div class="font-medium text-slate-900">{{ line.description }}</div><div class="text-xs text-slate-500">{{ line.service.service_name if line.service else '' }}</div></td>
|
||||
<td class="px-4 py-3">{{ line.sac_code or '-' }}</td>
|
||||
<td class="px-4 py-3 text-right">{{ line.quantity }}</td>
|
||||
<td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(line.rate or 0) }}</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">{{ line.gst_rate }}%</td>
|
||||
<td class="px-4 py-3 text-right font-semibold">₹ {{ '%.2f'|format(line.line_total or 0) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot class="bg-slate-50 text-sm font-semibold">
|
||||
<tr><td colspan="6" class="px-4 py-3 text-right">Subtotal</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(invoice.subtotal or 0) }}</td></tr>
|
||||
<tr><td colspan="6" class="px-4 py-3 text-right">Discount</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(invoice.discount_amount or 0) }}</td></tr>
|
||||
<tr><td colspan="6" class="px-4 py-3 text-right">Taxable Value</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(invoice.taxable_amount or 0) }}</td></tr>
|
||||
<tr><td colspan="6" class="px-4 py-3 text-right">CGST</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(invoice.cgst_amount or 0) }}</td></tr>
|
||||
<tr><td colspan="6" class="px-4 py-3 text-right">SGST</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(invoice.sgst_amount or 0) }}</td></tr>
|
||||
<tr><td colspan="6" class="px-4 py-3 text-right">IGST</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(invoice.igst_amount or 0) }}</td></tr>
|
||||
<tr><td colspan="6" class="px-4 py-3 text-right text-base">Grand Total</td><td class="px-4 py-3 text-right text-base">₹ {{ '%.2f'|format(invoice.total_amount or 0) }}</td></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="af-card space-y-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="font-semibold text-slate-900">Payment History</h2>
|
||||
<p class="text-sm text-slate-500">Receipts, TDS deductions and outstanding balance for this invoice.</p>
|
||||
</div>
|
||||
{% if can_record_payment and invoice.status not in ['DRAFT','CANCELLED','PAID'] %}
|
||||
<a href="/billing/{{ invoice.id }}/payments/new" class="af-btn af-btn-primary">Record Payment</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="overflow-hidden rounded-xl border border-slate-200">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase text-slate-500">
|
||||
<tr><th class="px-4 py-3">Receipt</th><th class="px-4 py-3">Date</th><th class="px-4 py-3">Mode</th><th class="px-4 py-3">Reference</th><th class="px-4 py-3 text-right">Received</th><th class="px-4 py-3 text-right">TDS</th><th class="px-4 py-3"></th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for payment in invoice.payments %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium text-slate-900">{{ payment.receipt_no }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ payment.payment_date }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ payment.mode }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ payment.reference_no or '-' }}</td>
|
||||
<td class="px-4 py-3 text-right font-semibold">₹ {{ '%.2f'|format(payment.amount_received or 0) }}</td>
|
||||
<td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(payment.tds_deducted or 0) }}</td>
|
||||
<td class="px-4 py-3 text-right"><a href="/billing/payments/{{ payment.id }}/receipt" target="_blank" class="text-brand-600 hover:underline">Receipt</a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="px-4 py-6 text-center text-slate-500">No payments recorded yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="af-card"><h2 class="font-semibold text-slate-900">Amount in Words</h2><p class="mt-2 text-sm text-slate-600">{{ invoice.amount_in_words or '-' }}</p></div>
|
||||
<div class="af-card"><h2 class="font-semibold text-slate-900">Bank / UPI Details</h2><p class="mt-2 text-sm text-slate-600 whitespace-pre-line">{% if invoice_ctx.bank_name %}{{ invoice_ctx.bank_name }}{% endif %}{% if invoice_ctx.bank_account_number %}\nA/c: {{ invoice_ctx.bank_account_number }}{% endif %}{% if invoice_ctx.bank_ifsc %}\nIFSC: {{ invoice_ctx.bank_ifsc }}{% endif %}{% if invoice_ctx.upi_id %}\nUPI: {{ invoice_ctx.upi_id }}{% endif %}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Import Fee Structure</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Upload Excel with Fee_Structure and Fee_Services sheets.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/billing/fee-structures/template" class="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 text-sm font-medium text-emerald-800 shadow-sm hover:bg-emerald-100">Download Excel Template</a>
|
||||
<a href="/billing/fee-structures/list" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Fee Structure List</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if result %}
|
||||
<div class="rounded-2xl border {{ 'border-emerald-200 bg-emerald-50 text-emerald-900' if result.success else 'border-red-200 bg-red-50 text-red-900' }} p-4">
|
||||
{% if result.success %}
|
||||
<div class="font-semibold">Import completed</div>
|
||||
<div class="mt-1 text-sm">Created: {{ result.created }} | Updated: {{ result.updated }}</div>
|
||||
{% else %}
|
||||
<div class="font-semibold">Import failed</div>
|
||||
<ul class="mt-2 list-disc pl-5 text-sm">
|
||||
{% for error in result.errors %}<li>{{ error }}</li>{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="space-y-5 rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-slate-700">Excel File</span>
|
||||
<input type="file" name="import_file" accept=".xlsx,.xlsm" required class="mt-1 block w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<div class="rounded-xl bg-slate-50 p-4 text-sm text-slate-600">
|
||||
<div class="font-semibold text-slate-800">How to import using template</div>
|
||||
<ol class="mt-2 list-decimal space-y-1 pl-5">
|
||||
<li>Click <strong>Download Excel Template</strong>.</li>
|
||||
<li>Fill <strong>Fee_Structure</strong> for client-wise package/header details.</li>
|
||||
<li>Fill <strong>Fee_Services</strong> for services included in each package.</li>
|
||||
<li>Upload the completed file here. Imported fee structures can then be used in <strong>Generate Bills</strong>.</li>
|
||||
</ol>
|
||||
<div class="mt-4 font-semibold text-slate-800">Required sheets</div>
|
||||
<div class="mt-1">Fee_Structure: client, billing group, mode, frequency, fee and tax details.</div>
|
||||
<div>Fee_Services: services included in each billing group.</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<a href="/billing/fee-structures/list" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700">Back</a>
|
||||
<button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Import Fee Structure</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,63 @@
|
||||
{% 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">Fee Structure</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Client-wise billing packages with multiple services grouped for future invoice generation.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/billing/generate" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Generate Bills</a>
|
||||
<a href="/billing" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Invoices</a>
|
||||
{% if can_import %}
|
||||
<a href="/billing/fee-structures/template" class="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 text-sm font-medium text-emerald-800 shadow-sm hover:bg-emerald-100">Download Template</a>
|
||||
<a href="/billing/fee-structures/import" class="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-emerald-700">Import Using Template</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<div class="flex gap-3">
|
||||
<input name="q" value="{{ q or '' }}" placeholder="Search group, client code or client name" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none" />
|
||||
<button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-soft">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Group Code</th>
|
||||
<th class="px-4 py-3">Client</th>
|
||||
<th class="px-4 py-3">Package</th>
|
||||
<th class="px-4 py-3">Mode</th>
|
||||
<th class="px-4 py-3">Frequency</th>
|
||||
<th class="px-4 py-3 text-right">Fee</th>
|
||||
<th class="px-4 py-3">Services</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for row in rows %}
|
||||
<tr class="align-top hover:bg-slate-50">
|
||||
<td class="px-4 py-3 font-medium text-slate-900">{{ row.group_code }}</td>
|
||||
<td class="px-4 py-3 text-slate-700">{{ row.client.client_name if row.client else row.client_id }}</td>
|
||||
<td class="px-4 py-3 text-slate-700">{{ row.group_name }}</td>
|
||||
<td class="px-4 py-3"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs">{{ row.billing_mode }}</span></td>
|
||||
<td class="px-4 py-3">{{ row.frequency }}</td>
|
||||
<td class="px-4 py-3 text-right font-semibold">₹ {{ '%.2f'|format(row.fee_amount or 0) }}</td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600">
|
||||
{% for item in row.services %}
|
||||
<div>{{ item.service.service_code if item.service else item.service_id }} - {{ item.line_description or (item.service.service_name if item.service else '') }}</div>
|
||||
{% else %}
|
||||
<span class="text-slate-400">No services mapped</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="px-4 py-8 text-center text-slate-500">No fee structures found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,182 @@
|
||||
{% 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 Draft Invoices</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Create draft GST invoices from fee structures and automatically link matching client service subscriptions / engagements for the selected financial year. Existing invoices for the same fee group and period are skipped by default.</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="/billing" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Invoices</a>
|
||||
<a href="/billing/fee-structures/list" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Fee Structure</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if result %}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Generation Result</h2>
|
||||
<div class="mt-3 grid gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-xl bg-emerald-50 p-3 text-sm text-emerald-800"><div class="text-xs uppercase tracking-wide">Draft invoices created</div><div class="mt-1 text-2xl font-bold">{{ result.created|length }}</div></div>
|
||||
<div class="rounded-xl bg-amber-50 p-3 text-sm text-amber-800"><div class="text-xs uppercase tracking-wide">Skipped</div><div class="mt-1 text-2xl font-bold">{{ result.skipped|length }}</div></div>
|
||||
<div class="rounded-xl bg-rose-50 p-3 text-sm text-rose-800"><div class="text-xs uppercase tracking-wide">Errors</div><div class="mt-1 text-2xl font-bold">{{ result.errors|length }}</div></div>
|
||||
</div>
|
||||
|
||||
{% if result.created %}
|
||||
<div class="mt-4">
|
||||
<div class="text-sm font-semibold text-slate-700">Created Draft Invoices</div>
|
||||
<div class="mt-2 overflow-hidden rounded-xl border border-slate-200">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500"><tr><th class="px-3 py-2">Invoice</th><th class="px-3 py-2">Client</th><th class="px-3 py-2 text-right">Amount</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for invoice in result.created %}
|
||||
<tr>
|
||||
<td class="px-3 py-2"><a href="/billing/{{ invoice.id }}" class="font-semibold text-brand-700 hover:underline">{{ invoice.invoice_no }}</a></td>
|
||||
<td class="px-3 py-2">{{ invoice.client.client_name if invoice.client else invoice.client_id }}</td>
|
||||
<td class="px-3 py-2 text-right">₹ {{ '%.2f'|format(invoice.total_amount or 0) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if result.skipped %}
|
||||
<div class="mt-4 rounded-xl bg-amber-50 p-3 text-sm text-amber-800">
|
||||
<div class="font-semibold">Skipped rows</div>
|
||||
<ul class="mt-1 list-disc space-y-1 pl-5">
|
||||
{% for item in result.skipped %}<li>{{ item }}</li>{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if result.errors %}
|
||||
<div class="mt-4 rounded-xl bg-rose-50 p-3 text-sm text-rose-800">
|
||||
<div class="font-semibold">Errors</div>
|
||||
<ul class="mt-1 list-disc space-y-1 pl-5">
|
||||
{% for item in result.errors %}<li>{{ item }}</li>{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="rounded-2xl border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900">
|
||||
<div class="font-semibold">Engagement-to-invoice refinement</div>
|
||||
<div class="mt-1">This screen continues to use your existing fee-structure billing logic. During generation, the system checks the client, service and active financial year ({{ active_financial_year or 'current FY' }}) and links the invoice / invoice lines to the matching client service subscription wherever available. No duplicate module is created.</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<div class="grid gap-3 lg:grid-cols-6">
|
||||
<div>
|
||||
<label class="text-xs font-semibold uppercase tracking-wide text-slate-500">Frequency</label>
|
||||
<select name="frequency" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none">
|
||||
<option value="">All</option>
|
||||
{% for f in frequencies %}<option value="{{ f }}" {% if frequency == f %}selected{% endif %}>{{ f }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold uppercase tracking-wide text-slate-500">Period From</label>
|
||||
<input type="date" name="billing_period_from" value="{{ billing_period_from }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold uppercase tracking-wide text-slate-500">Period To</label>
|
||||
<input type="date" name="billing_period_to" value="{{ billing_period_to }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold uppercase tracking-wide text-slate-500">Auto Generate</label>
|
||||
<select name="auto_generate_only" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none">
|
||||
<option value="yes" {% if auto_generate_only != 'no' %}selected{% endif %}>Only Yes</option>
|
||||
<option value="no" {% if auto_generate_only == 'no' %}selected{% endif %}>All Active</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lg:col-span-2">
|
||||
<label class="text-xs font-semibold uppercase tracking-wide text-slate-500">Search</label>
|
||||
<div class="mt-1 flex gap-2">
|
||||
<input name="q" value="{{ q or '' }}" placeholder="Client / group code / package" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none" />
|
||||
<button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" class="rounded-2xl border border-slate-200 bg-white shadow-soft">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="frequency" value="{{ frequency or '' }}" />
|
||||
<input type="hidden" name="billing_period_from" value="{{ billing_period_from }}" />
|
||||
<input type="hidden" name="billing_period_to" value="{{ billing_period_to }}" />
|
||||
<input type="hidden" name="auto_generate_only" value="{{ auto_generate_only }}" />
|
||||
<input type="hidden" name="q" value="{{ q or '' }}" />
|
||||
|
||||
<div class="flex flex-col gap-3 border-b border-slate-200 p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<div class="font-semibold text-slate-900">Eligible Fee Structures</div>
|
||||
<div class="text-sm text-slate-500">Select packages and create draft invoices for {{ billing_period_from }} to {{ billing_period_to }}.</div>
|
||||
</div>
|
||||
<label class="inline-flex items-center gap-2 text-sm text-slate-600">
|
||||
<input type="checkbox" name="skip_duplicates" value="yes" checked class="rounded border-slate-300 text-brand-600" />
|
||||
Skip duplicates
|
||||
</label>
|
||||
</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 font-semibold uppercase tracking-wide text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3"><input type="checkbox" onclick="document.querySelectorAll('.fee-check').forEach(cb => cb.checked = this.checked && !cb.disabled)" /></th>
|
||||
<th class="px-4 py-3">Group Code</th>
|
||||
<th class="px-4 py-3">Client</th>
|
||||
<th class="px-4 py-3">Package</th>
|
||||
<th class="px-4 py-3">Services / Engagement Source</th>
|
||||
<th class="px-4 py-3">Mode</th>
|
||||
<th class="px-4 py-3 text-right">Fee</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for row in rows %}
|
||||
{% set duplicate = duplicate_map.get(row.id) %}
|
||||
<tr class="align-top hover:bg-slate-50">
|
||||
<td class="px-4 py-3"><input class="fee-check rounded border-slate-300 text-brand-600" type="checkbox" name="fee_group_ids" value="{{ row.id }}" {% if duplicate %}disabled{% endif %} /></td>
|
||||
<td class="px-4 py-3 font-medium text-slate-900">{{ row.group_code }}</td>
|
||||
<td class="px-4 py-3 text-slate-700">{{ row.client.client_name if row.client else row.client_id }}</td>
|
||||
<td class="px-4 py-3 text-slate-700">
|
||||
<div class="font-medium text-slate-900">{{ row.group_name }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">{{ row.frequency }} billing</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600">
|
||||
{% if row.services %}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% for item in row.services[:4] %}
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1">{{ item.service.service_name if item.service else item.service_id }}</span>
|
||||
{% endfor %}
|
||||
{% if row.services|length > 4 %}<span class="rounded-full bg-slate-100 px-2 py-1">+{{ row.services|length - 4 }}</span>{% endif %}
|
||||
</div>
|
||||
<div class="mt-1 text-[11px] text-slate-400">Matching active subscriptions are linked during generation.</div>
|
||||
{% else %}
|
||||
<span class="text-slate-400">Package line only</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs">{{ row.billing_mode }}</span></td>
|
||||
<td class="px-4 py-3 text-right font-semibold">₹ {{ '%.2f'|format(row.fee_amount or 0) }}</td>
|
||||
<td class="px-4 py-3 text-xs">
|
||||
{% if duplicate %}
|
||||
<span class="rounded-full bg-amber-100 px-2 py-1 font-medium text-amber-800">Already billed: {{ duplicate.invoice_no }}</span>
|
||||
{% else %}
|
||||
<span class="rounded-full bg-emerald-100 px-2 py-1 font-medium text-emerald-800">Ready</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="8" class="px-4 py-8 text-center text-slate-500">No eligible fee structures found for the selected filter.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end border-t border-slate-200 p-4">
|
||||
<button class="rounded-xl bg-brand-600 px-5 py-2 text-sm font-semibold text-white shadow-soft hover:bg-brand-700">Generate Draft Invoices</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,120 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{ invoice_ctx.invoice_title }} {{ invoice.invoice_no }}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@page { size: A4; margin: 14mm; }
|
||||
@media print { .no-print { display: none !important; } body { background: white !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-100 text-slate-900">
|
||||
<div class="no-print mx-auto my-4 flex max-w-5xl justify-end gap-2">
|
||||
<button onclick="window.print()" class="rounded-lg bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Print / Save PDF</button>
|
||||
<a href="/billing/{{ invoice.id }}" class="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700">Back</a>
|
||||
</div>
|
||||
<main class="mx-auto max-w-5xl bg-white p-8 shadow print:shadow-none">
|
||||
<header class="border-b-2 border-slate-900 pb-4">
|
||||
<div class="flex items-start justify-between gap-6">
|
||||
<div>
|
||||
<div class="text-2xl font-bold">{{ invoice_ctx.firm_name }}</div>
|
||||
<div class="mt-1 whitespace-pre-line text-sm text-slate-600">{{ invoice_ctx.firm_address or '' }}</div>
|
||||
<div class="mt-2 text-sm text-slate-700">GSTIN: <b>{{ invoice_ctx.firm_gstin or '-' }}</b> | PAN: <b>{{ invoice_ctx.firm_pan or '-' }}</b></div>
|
||||
<div class="text-sm text-slate-700">Email: {{ invoice_ctx.firm_contact_email or '-' }} | Mobile: {{ invoice_ctx.firm_contact_mobile or '-' }}</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-2xl font-bold uppercase">{{ invoice_ctx.invoice_title }}</div>
|
||||
<div class="mt-2 text-sm">Invoice No: <b>{{ invoice.invoice_no }}</b></div>
|
||||
<div class="text-sm">Invoice Date: <b>{{ invoice.invoice_date }}</b></div>
|
||||
<div class="text-sm">Due Date: <b>{{ invoice.due_date or '-' }}</b></div>
|
||||
<div class="text-sm">Status: <b>{{ invoice.status }}</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="mt-5 grid grid-cols-2 gap-4 text-sm">
|
||||
<div class="rounded-lg border border-slate-300 p-4">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Bill To</div>
|
||||
<div class="mt-2 text-base font-bold">{{ invoice.client_legal_name or '-' }}</div>
|
||||
<div class="mt-1 text-slate-700">{{ invoice.client_billing_address or '-' }}</div>
|
||||
<div class="mt-2">GSTIN: <b>{{ invoice.client_gstin or '-' }}</b></div>
|
||||
<div>PAN: <b>{{ invoice.client_pan or '-' }}</b></div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-slate-300 p-4">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Tax Particulars</div>
|
||||
<div class="mt-2">Place of Supply: <b>{{ invoice.place_of_supply or '-' }}</b></div>
|
||||
<div>Tax Type: <b>{{ invoice.tax_type }}</b></div>
|
||||
<div>Reverse Charge: <b>{{ 'Yes' if invoice.reverse_charge else 'No' }}</b></div>
|
||||
<div>Client State Code: <b>{{ invoice.client_state_code or '-' }}</b></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<table class="mt-5 w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr class="bg-slate-100">
|
||||
<th class="border border-slate-300 px-2 py-2 text-left">#</th>
|
||||
<th class="border border-slate-300 px-2 py-2 text-left">Description</th>
|
||||
<th class="border border-slate-300 px-2 py-2 text-left">SAC</th>
|
||||
<th class="border border-slate-300 px-2 py-2 text-right">Qty</th>
|
||||
<th class="border border-slate-300 px-2 py-2 text-right">Rate</th>
|
||||
<th class="border border-slate-300 px-2 py-2 text-right">Taxable</th>
|
||||
<th class="border border-slate-300 px-2 py-2 text-right">GST %</th>
|
||||
<th class="border border-slate-300 px-2 py-2 text-right">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for line in invoice.lines %}
|
||||
<tr>
|
||||
<td class="border border-slate-300 px-2 py-2">{{ loop.index }}</td>
|
||||
<td class="border border-slate-300 px-2 py-2">{{ line.description }}</td>
|
||||
<td class="border border-slate-300 px-2 py-2">{{ line.sac_code or '-' }}</td>
|
||||
<td class="border border-slate-300 px-2 py-2 text-right">{{ line.quantity }}</td>
|
||||
<td class="border border-slate-300 px-2 py-2 text-right">{{ '%.2f'|format(line.rate or 0) }}</td>
|
||||
<td class="border border-slate-300 px-2 py-2 text-right">{{ '%.2f'|format(line.taxable_amount or 0) }}</td>
|
||||
<td class="border border-slate-300 px-2 py-2 text-right">{{ line.gst_rate }}</td>
|
||||
<td class="border border-slate-300 px-2 py-2 text-right">{{ '%.2f'|format(line.line_total or 0) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr><td colspan="7" class="border border-slate-300 px-2 py-2 text-right font-semibold">Subtotal</td><td class="border border-slate-300 px-2 py-2 text-right font-semibold">{{ '%.2f'|format(invoice.subtotal or 0) }}</td></tr>
|
||||
<tr><td colspan="7" class="border border-slate-300 px-2 py-2 text-right font-semibold">Discount</td><td class="border border-slate-300 px-2 py-2 text-right font-semibold">{{ '%.2f'|format(invoice.discount_amount or 0) }}</td></tr>
|
||||
<tr><td colspan="7" class="border border-slate-300 px-2 py-2 text-right font-semibold">Taxable Value</td><td class="border border-slate-300 px-2 py-2 text-right font-semibold">{{ '%.2f'|format(invoice.taxable_amount or 0) }}</td></tr>
|
||||
<tr><td colspan="7" class="border border-slate-300 px-2 py-2 text-right font-semibold">CGST</td><td class="border border-slate-300 px-2 py-2 text-right font-semibold">{{ '%.2f'|format(invoice.cgst_amount or 0) }}</td></tr>
|
||||
<tr><td colspan="7" class="border border-slate-300 px-2 py-2 text-right font-semibold">SGST</td><td class="border border-slate-300 px-2 py-2 text-right font-semibold">{{ '%.2f'|format(invoice.sgst_amount or 0) }}</td></tr>
|
||||
<tr><td colspan="7" class="border border-slate-300 px-2 py-2 text-right font-semibold">IGST</td><td class="border border-slate-300 px-2 py-2 text-right font-semibold">{{ '%.2f'|format(invoice.igst_amount or 0) }}</td></tr>
|
||||
<tr class="bg-slate-100"><td colspan="7" class="border border-slate-300 px-2 py-2 text-right text-base font-bold">Grand Total</td><td class="border border-slate-300 px-2 py-2 text-right text-base font-bold">₹ {{ '%.2f'|format(invoice.total_amount or 0) }}</td></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<section class="mt-5 grid grid-cols-2 gap-4 text-sm">
|
||||
<div class="rounded-lg border border-slate-300 p-4">
|
||||
<div class="font-semibold">Amount in Words</div>
|
||||
<div class="mt-1">{{ invoice.amount_in_words or '-' }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-slate-300 p-4">
|
||||
<div class="font-semibold">Payment Details</div>
|
||||
<div class="mt-1">Bank: {{ invoice_ctx.bank_name or '-' }}</div>
|
||||
<div>A/c: {{ invoice_ctx.bank_account_number or '-' }}</div>
|
||||
<div>IFSC: {{ invoice_ctx.bank_ifsc or '-' }}</div>
|
||||
<div>UPI: {{ invoice_ctx.upi_id or '-' }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-5 text-sm">
|
||||
{% if invoice_ctx.terms %}<div><b>Terms:</b> {{ invoice_ctx.terms }}</div>{% endif %}
|
||||
{% if invoice_ctx.declaration %}<div class="mt-2"><b>Declaration:</b> {{ invoice_ctx.declaration }}</div>{% endif %}
|
||||
</section>
|
||||
|
||||
<footer class="mt-12 flex items-end justify-between text-sm">
|
||||
<div>{{ invoice_ctx.footer_note or '' }}</div>
|
||||
<div class="text-center">
|
||||
<div class="mb-10">For {{ invoice_ctx.firm_name }}</div>
|
||||
<div class="border-t border-slate-500 px-8 pt-2">{{ invoice_ctx.authorised_signatory_name or 'Authorised Signatory' }}</div>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,81 @@
|
||||
{% 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">Billing Invoices</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Create, issue and print GST-ready client invoices with SAC and tax breakup.</p>
|
||||
<p class="mt-1 text-xs text-slate-400">Showing billing records for active FY: <span class="font-semibold text-slate-600">{{ active_financial_year or 'All Years' }}</span></p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if can_generate %}
|
||||
<a href="/billing/generate" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Generate Bills</a>
|
||||
{% endif %}
|
||||
{% if can_view_fee_structure %}
|
||||
<a href="/billing/fee-structures/list" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Fee Structure</a>
|
||||
{% endif %}
|
||||
{% if can_import_fee_structure %}
|
||||
<a href="/billing/fee-structures/template" class="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 text-sm font-medium text-emerald-800 shadow-sm hover:bg-emerald-100">Download Fee Template</a>
|
||||
<a href="/billing/fee-structures/import" class="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-emerald-700">Import Fee Excel</a>
|
||||
{% endif %}
|
||||
<a href="/billing/payments" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Payments</a>
|
||||
<a href="/billing/settings" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Billing Settings</a>
|
||||
{% if can_create %}
|
||||
<a href="/billing/new" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-brand-700">New Invoice</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<div class="flex gap-3">
|
||||
<input name="q" value="{{ q or '' }}" placeholder="Search invoice no, client code or client name" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none" />
|
||||
<button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
{% if report_summary %}
|
||||
<section class="grid gap-4 md:grid-cols-4">
|
||||
<div class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">Billed</div><div class="mt-2 text-2xl font-semibold text-slate-900">₹ {{ '%.2f'|format(report_summary.total_billed or 0) }}</div><div class="mt-1 text-xs text-slate-500">{{ report_summary.invoice_count }} invoice(s)</div></div>
|
||||
<div class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">Collected + TDS</div><div class="mt-2 text-2xl font-semibold text-emerald-700">₹ {{ '%.2f'|format(report_summary.total_collected_with_tds or 0) }}</div><div class="mt-1 text-xs text-slate-500">{{ report_summary.payment_count }} receipt(s)</div></div>
|
||||
<div class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">Outstanding</div><div class="mt-2 text-2xl font-semibold text-amber-700">₹ {{ '%.2f'|format(report_summary.outstanding or 0) }}</div><div class="mt-1 text-xs text-slate-500">Active issued bills</div></div>
|
||||
<div class="af-metric-card"><div class="text-xs font-semibold uppercase text-slate-500">Status</div><div class="mt-2 text-sm font-semibold text-slate-800">Draft {{ report_summary.draft_count }} · Open {{ report_summary.issued_count }} · Paid {{ report_summary.paid_count }}</div><div class="mt-1 text-xs text-slate-500">FY-filtered billing report</div></div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-soft">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Invoice No</th>
|
||||
<th class="px-4 py-3">Date</th>
|
||||
<th class="px-4 py-3">FY</th>
|
||||
<th class="px-4 py-3">Client</th>
|
||||
<th class="px-4 py-3 text-right">Amount</th>
|
||||
<th class="px-4 py-3 text-right">Received/TDS</th>
|
||||
<th class="px-4 py-3 text-right">Balance</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for row in rows %}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="px-4 py-3 font-medium text-slate-900">{{ row.invoice_no }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ row.invoice_date }}</td>
|
||||
<td class="px-4 py-3 text-xs text-slate-500">{{ row.financial_year or '-' }}</td>
|
||||
<td class="px-4 py-3 text-slate-700">{{ row.client.client_name if row.client else row.client_id }}</td>
|
||||
<td class="px-4 py-3 text-right font-semibold">₹ {{ '%.2f'|format(row.total_amount or 0) }}</td>
|
||||
<td class="px-4 py-3 text-right text-slate-700">₹ {{ '%.2f'|format((row.amount_received or 0) + (row.tds_deducted or 0)) }}</td>
|
||||
<td class="px-4 py-3 text-right font-semibold {% if row.balance_amount and row.balance_amount > 0 %}text-amber-700{% else %}text-emerald-700{% endif %}">₹ {{ '%.2f'|format(row.balance_amount or row.total_amount or 0) }}</td>
|
||||
<td class="px-4 py-3"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs font-medium text-slate-700">{{ row.status }}</span></td>
|
||||
<td class="px-4 py-3 text-right"><div class="flex justify-end gap-3"><a href="/billing/{{ row.id }}" class="text-brand-600 hover:underline">View</a>{% if can_record_payment and row.status not in ['DRAFT','CANCELLED','PAID'] %}<a href="/billing/{{ row.id }}/payments/new" class="text-emerald-700 hover:underline">Payment</a>{% endif %}<a href="/billing/{{ row.id }}/print" target="_blank" class="text-slate-600 hover:underline">Print</a></div></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="9" class="px-4 py-8 text-center text-slate-500">No invoices found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% 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">Payments & Receipts</h1><p class="mt-1 text-sm text-slate-500">Track invoice collections, TDS deductions and receipt printouts.</p><p class="mt-1 text-xs text-slate-400">Showing receipts for active FY: <span class="font-semibold text-slate-600">{{ active_financial_year or 'All Years' }}</span></p></div>
|
||||
<a href="/billing" class="af-btn af-btn-secondary">Invoices</a>
|
||||
</div>
|
||||
<form method="get" class="af-card"><div class="flex gap-3"><input name="q" value="{{ q or '' }}" placeholder="Search receipt, invoice or client" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" /><button class="af-btn af-btn-primary">Search</button></div></form>
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-soft">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase text-slate-500"><tr><th class="px-4 py-3">Receipt</th><th class="px-4 py-3">Invoice</th><th class="px-4 py-3">Client</th><th class="px-4 py-3">Date</th><th class="px-4 py-3">FY</th><th class="px-4 py-3">Mode</th><th class="px-4 py-3 text-right">Received</th><th class="px-4 py-3 text-right">TDS</th><th class="px-4 py-3"></th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for row in rows %}
|
||||
<tr><td class="px-4 py-3 font-medium">{{ row.receipt_no }}</td><td class="px-4 py-3">{{ row.invoice.invoice_no if row.invoice else row.invoice_id }}</td><td class="px-4 py-3">{{ row.client.client_name if row.client else row.client_id }}</td><td class="px-4 py-3">{{ row.payment_date }}</td><td class="px-4 py-3 text-xs text-slate-500">{{ row.financial_year or '-' }}</td><td class="px-4 py-3">{{ row.mode }}</td><td class="px-4 py-3 text-right font-semibold">₹ {{ '%.2f'|format(row.amount_received or 0) }}</td><td class="px-4 py-3 text-right">₹ {{ '%.2f'|format(row.tds_deducted or 0) }}</td><td class="px-4 py-3 text-right"><a href="/billing/payments/{{ row.id }}/receipt" target="_blank" class="text-brand-600 hover:underline">Receipt</a></td></tr>
|
||||
{% else %}
|
||||
<tr><td colspan="9" class="px-4 py-8 text-center text-slate-500">No payments recorded.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6 max-w-4xl">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Record Payment</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Invoice {{ invoice.invoice_no }} • Balance ₹ {{ '%.2f'|format(invoice.balance_amount or invoice.total_amount or 0) }}</p>
|
||||
</div>
|
||||
<form method="post" class="af-card space-y-5">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div><label class="text-sm font-medium text-slate-700">Payment date</label><input type="date" name="payment_date" value="{{ today }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" required /></div>
|
||||
<div><label class="text-sm font-medium text-slate-700">Amount received</label><input type="number" step="0.01" name="amount_received" value="{{ '%.2f'|format(invoice.balance_amount or invoice.total_amount or 0) }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" /></div>
|
||||
<div><label class="text-sm font-medium text-slate-700">TDS deducted</label><input type="number" step="0.01" name="tds_deducted" value="0.00" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" /></div>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div><label class="text-sm font-medium text-slate-700">Bank charges</label><input type="number" step="0.01" name="bank_charges" value="0.00" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" /></div>
|
||||
<div><label class="text-sm font-medium text-slate-700">Mode</label><select name="mode" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{% for mode in payment_modes %}<option value="{{ mode }}">{{ mode }}</option>{% endfor %}</select></div>
|
||||
<div><label class="text-sm font-medium text-slate-700">Reference no.</label><input name="reference_no" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="UTR / cheque / transaction id" /></div>
|
||||
</div>
|
||||
<div><label class="text-sm font-medium text-slate-700">Remarks</label><textarea name="remarks" rows="3" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></textarea></div>
|
||||
<div class="flex justify-end gap-2"><a href="/billing/{{ invoice.id }}" class="af-btn af-btn-secondary">Cancel</a><button class="af-btn af-btn-primary">Save & Print Receipt</button></div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="mx-auto max-w-4xl bg-white p-8 print:p-0">
|
||||
<div class="mb-4 flex justify-end print:hidden"><button onclick="window.print()" class="af-btn af-btn-primary">Print Receipt</button></div>
|
||||
<div class="rounded-2xl border border-slate-300 p-8">
|
||||
<div class="flex items-start justify-between border-b border-slate-200 pb-5">
|
||||
<div><h1 class="text-2xl font-bold text-slate-900">{{ invoice_ctx.firm_name }}</h1><p class="mt-1 whitespace-pre-line text-sm text-slate-600">{{ invoice_ctx.firm_address or '' }}</p><p class="mt-1 text-sm text-slate-600">GSTIN: {{ invoice_ctx.firm_gstin or '-' }} • PAN: {{ invoice_ctx.firm_pan or '-' }}</p></div>
|
||||
<div class="text-right"><div class="text-xl font-bold text-slate-900">Receipt</div><div class="mt-1 text-sm text-slate-600">{{ payment.receipt_no }}</div><div class="text-sm text-slate-600">{{ payment.receipt_date }}</div></div>
|
||||
</div>
|
||||
<div class="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<div><div class="text-xs font-semibold uppercase text-slate-500">Received From</div><div class="mt-1 font-semibold text-slate-900">{{ payment.client.client_name if payment.client else invoice.client_legal_name }}</div><div class="text-sm text-slate-600">Invoice: {{ invoice.invoice_no }}</div></div>
|
||||
<div class="rounded-xl bg-slate-50 p-4"><div class="grid gap-2 text-sm"><div class="flex justify-between"><span>Amount Received</span><strong>₹ {{ '%.2f'|format(payment.amount_received or 0) }}</strong></div><div class="flex justify-between"><span>TDS Deducted</span><strong>₹ {{ '%.2f'|format(payment.tds_deducted or 0) }}</strong></div><div class="flex justify-between"><span>Bank Charges</span><strong>₹ {{ '%.2f'|format(payment.bank_charges or 0) }}</strong></div></div></div>
|
||||
</div>
|
||||
<div class="mt-6 grid gap-4 md:grid-cols-3 text-sm"><div><span class="text-slate-500">Mode</span><div class="font-semibold">{{ payment.mode }}</div></div><div><span class="text-slate-500">Payment Date</span><div class="font-semibold">{{ payment.payment_date }}</div></div><div><span class="text-slate-500">Reference</span><div class="font-semibold">{{ payment.reference_no or '-' }}</div></div></div>
|
||||
{% if payment.remarks %}<div class="mt-6 rounded-xl border border-slate-200 p-4 text-sm text-slate-600">{{ payment.remarks }}</div>{% endif %}
|
||||
<div class="mt-10 flex justify-end"><div class="text-center"><div class="h-12"></div><div class="border-t border-slate-400 px-8 pt-2 text-sm font-semibold">Authorised Signatory</div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,244 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-brand-600">Phase 7R.1</p>
|
||||
<h1 class="text-2xl font-bold text-slate-900">Firm Billing Settings</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Configure firm GST, invoice numbering, payment details and invoice footer defaults.</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm shadow-sm">
|
||||
<div class="font-semibold text-slate-900">{{ tenant_name }}</div>
|
||||
<div class="text-xs text-slate-500">{% if branch_name %}Branch: {{ branch_name }}{% else %}Firm-wide default{% endif %}</div>
|
||||
<div class="mt-2 text-xs text-slate-500">Next invoice preview</div>
|
||||
<div class="font-mono text-sm font-semibold text-brand-700">{{ preview_invoice_no }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<form method="post" action="/billing/settings" class="space-y-6">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
|
||||
<section class="af-card p-5">
|
||||
<div class="mb-4 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-slate-900">Scope</h2>
|
||||
<p class="text-sm text-slate-500">Keep branch-specific settings for branch-wise invoice series, or use firm-wide default if you are working across branches.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="rounded-2xl border border-slate-200 p-4 text-sm">
|
||||
<input type="radio" name="branch_scope" value="active" class="mr-2" {% if branch_scope != 'firm' %}checked{% endif %} />
|
||||
Active branch settings
|
||||
<div class="mt-1 text-xs text-slate-500">Recommended for branch-wise invoice numbering.</div>
|
||||
</label>
|
||||
<label class="rounded-2xl border border-slate-200 p-4 text-sm">
|
||||
<input type="radio" name="branch_scope" value="firm" class="mr-2" {% if branch_scope == 'firm' %}checked{% endif %} />
|
||||
Firm-wide default
|
||||
<div class="mt-1 text-xs text-slate-500">Available when cross-branch billing permission is active.</div>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="af-card p-5">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Firm GST & Contact Details</h2>
|
||||
<div class="mt-4 grid gap-4 md:grid-cols-2">
|
||||
<label class="text-sm font-medium text-slate-700">Legal / Billing Name
|
||||
<input name="legal_name" value="{{ settings.legal_name or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">GSTIN
|
||||
<input name="gstin" value="{{ settings.gstin or '' }}" maxlength="15" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm uppercase" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">PAN
|
||||
<input name="pan" value="{{ settings.pan or '' }}" maxlength="10" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm uppercase" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">State Code
|
||||
<input name="state_code" value="{{ settings.state_code or '' }}" maxlength="2" placeholder="33" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Contact Email
|
||||
<input name="contact_email" value="{{ settings.contact_email or '' }}" type="email" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Contact Mobile
|
||||
<input name="contact_mobile" value="{{ settings.contact_mobile or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700 md:col-span-2">Website
|
||||
<input name="website_url" value="{{ settings.website_url or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700 md:col-span-2">Billing Address
|
||||
<textarea name="billing_address" rows="3" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{{ settings.billing_address or '' }}</textarea>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="af-card p-5">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Invoice Numbering & Tax Defaults</h2>
|
||||
<div class="mt-4 grid gap-4 md:grid-cols-3">
|
||||
<label class="text-sm font-medium text-slate-700">Invoice Title
|
||||
<input name="invoice_title" value="{{ settings.invoice_title or 'Tax Invoice' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Prefix
|
||||
<input name="invoice_prefix" value="{{ settings.invoice_prefix or 'INV' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm uppercase" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Next Number
|
||||
<input name="next_invoice_no" value="{{ settings.next_invoice_no or 1 }}" type="number" min="1" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Padding
|
||||
<input name="padding" value="{{ settings.padding or 4 }}" type="number" min="1" max="10" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700 md:col-span-2">Number Format
|
||||
<input name="invoice_number_format" value="{{ settings.invoice_number_format or '{prefix}/{fy}/{number}' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 font-mono text-sm" />
|
||||
<span class="mt-1 block text-xs text-slate-500">Tokens: {prefix}, {fy}, {number}, {branch_id}</span>
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Default Due Days
|
||||
<input name="default_due_days" value="{{ settings.default_due_days or 15 }}" type="number" min="0" max="365" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Default GST Rate %
|
||||
<input name="default_gst_rate" value="{{ settings.default_gst_rate or '18.00' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Default Tax Type
|
||||
<select name="default_tax_type" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
{% for tax in tax_types %}<option value="{{ tax }}" {% if settings.default_tax_type == tax %}selected{% endif %}>{{ tax }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Default SAC Code
|
||||
<input name="default_sac_code" value="{{ settings.default_sac_code or '' }}" placeholder="9982" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="af-card p-5">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Bank, UPI & Payment Details</h2>
|
||||
<div class="mt-4 grid gap-4 md:grid-cols-2">
|
||||
<label class="text-sm font-medium text-slate-700">Bank Name
|
||||
<input name="bank_name" value="{{ settings.bank_name or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Account Name
|
||||
<input name="bank_account_name" value="{{ settings.bank_account_name or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Account Number
|
||||
<input name="bank_account_number" value="{{ settings.bank_account_number or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">IFSC
|
||||
<input name="bank_ifsc" value="{{ settings.bank_ifsc or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm uppercase" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700 md:col-span-2">UPI ID
|
||||
<input name="upi_id" value="{{ settings.upi_id or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700 md:col-span-2">Additional Bank Details / Payment Instructions
|
||||
<textarea name="bank_details" rows="3" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{{ settings.bank_details or '' }}</textarea>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="af-card p-5">
|
||||
<h2 class="text-lg font-semibold text-slate-900">PayUMoney / PayU Online Payment Gateway</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Enable this only after entering valid PayU/PayUMoney merchant credentials. Test mode posts to PayU test checkout.</p>
|
||||
<div class="mt-4 grid gap-4 md:grid-cols-2">
|
||||
<label class="flex items-center gap-3 rounded-2xl border border-slate-200 p-4 text-sm font-medium text-slate-700 md:col-span-2">
|
||||
<input type="checkbox" name="payumoney_enabled" value="1" {% if settings.payumoney_enabled %}checked{% endif %} />
|
||||
Enable PayUMoney / PayU Pay Now for client portal
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Mode
|
||||
<select name="payumoney_mode" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="TEST" {% if settings.payumoney_mode != 'LIVE' %}selected{% endif %}>TEST / Sandbox</option>
|
||||
<option value="LIVE" {% if settings.payumoney_mode == 'LIVE' %}selected{% endif %}>LIVE / Production</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Merchant ID, optional
|
||||
<input name="payumoney_merchant_id" value="{{ settings.payumoney_merchant_id or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Merchant Key
|
||||
<input name="payumoney_merchant_key" value="{{ settings.payumoney_merchant_key or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" autocomplete="off" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Merchant Salt
|
||||
<input name="payumoney_merchant_salt" value="{{ settings.payumoney_merchant_salt or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" autocomplete="off" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700 md:col-span-2">Product Info Label
|
||||
<input name="payumoney_product_info" value="{{ settings.payumoney_product_info or 'Professional Services Invoice' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-4 rounded-2xl border border-amber-200 bg-amber-50 p-4 text-xs leading-5 text-amber-900">
|
||||
Store separate test and live credentials carefully. Do not enable LIVE until callback testing is completed from an accessible public URL.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="af-card p-5">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Cashfree Online Payment Gateway</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Enable Cashfree only after adding valid Cashfree PG credentials. Sandbox mode uses Cashfree sandbox APIs.</p>
|
||||
<div class="mt-4 grid gap-4 md:grid-cols-2">
|
||||
<label class="flex items-center gap-3 rounded-2xl border border-slate-200 p-4 text-sm font-medium text-slate-700 md:col-span-2">
|
||||
<input type="checkbox" name="cashfree_enabled" value="1" {% if settings.cashfree_enabled %}checked{% endif %} />
|
||||
Enable Cashfree Pay Now for client portal
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Mode
|
||||
<select name="cashfree_mode" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="TEST" {% if settings.cashfree_mode != 'LIVE' %}selected{% endif %}>TEST / Sandbox</option>
|
||||
<option value="LIVE" {% if settings.cashfree_mode == 'LIVE' %}selected{% endif %}>LIVE / Production</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">API Version
|
||||
<input name="cashfree_api_version" value="{{ settings.cashfree_api_version or '2023-08-01' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Client ID / App ID
|
||||
<input name="cashfree_client_id" value="{{ settings.cashfree_client_id or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" autocomplete="off" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Client Secret
|
||||
<input name="cashfree_client_secret" value="{{ settings.cashfree_client_secret or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" autocomplete="off" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700 md:col-span-2">Order Note
|
||||
<input name="cashfree_order_note" value="{{ settings.cashfree_order_note or 'Professional Services Invoice' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-4 rounded-2xl border border-sky-200 bg-sky-50 p-4 text-xs leading-5 text-sky-900">
|
||||
Cashfree checkout creates an order from the server and uses payment_session_id for hosted checkout. Webhook URL: <span class="font-mono">/client/billing/cashfree/webhook</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="af-card p-5">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Invoice Notes, Terms & Signatory</h2>
|
||||
<div class="mt-4 grid gap-4">
|
||||
<label class="text-sm font-medium text-slate-700">Default Terms
|
||||
<textarea name="terms" rows="3" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{{ settings.terms or '' }}</textarea>
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Declaration
|
||||
<textarea name="declaration" rows="3" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{{ settings.declaration or '' }}</textarea>
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Invoice Footer Note
|
||||
<textarea name="footer_note" rows="2" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{{ settings.footer_note or '' }}</textarea>
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Authorised Signatory Name
|
||||
<input name="authorised_signatory_name" value="{{ settings.authorised_signatory_name or '' }}" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<a href="/billing" class="af-btn af-btn-secondary">Back to Invoices</a>
|
||||
{% if can_edit_settings %}
|
||||
<button type="submit" class="af-btn af-btn-primary">Save Billing Settings</button>
|
||||
{% else %}
|
||||
<span class="text-sm text-slate-500">View-only access</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<aside class="space-y-4">
|
||||
<div class="af-card p-5">
|
||||
<h3 class="font-semibold text-slate-900">Why this matters</h3>
|
||||
<ul class="mt-3 space-y-2 text-sm text-slate-600">
|
||||
<li>• GST invoice format will use these details in Phase 7R.2.</li>
|
||||
<li>• Payment and receipt tracking will use bank/UPI details in Phase 7R.4.</li>
|
||||
<li>• Client portal Pay Now uses UPI, PayUMoney and Cashfree settings from Phase 7R.5 / 7R.6 / 7R.6A.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="af-card p-5">
|
||||
<h3 class="font-semibold text-slate-900">Recommended invoice format</h3>
|
||||
<p class="mt-2 rounded-xl bg-slate-50 px-3 py-2 font-mono text-sm text-slate-700">{prefix}/{fy}/{number}</p>
|
||||
<p class="mt-2 text-xs text-slate-500">Example: INV/2026-27/0001</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user