183 lines
11 KiB
HTML
183 lines
11 KiB
HTML
{% 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 %}
|