677 lines
50 KiB
HTML
677 lines
50 KiB
HTML
{% extends "ui/templates/base/layout.html" %}
|
||
{% block content %}
|
||
{% if synced %}<div class="mb-4 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">Tally accounting masters synchronized successfully.</div>{% endif %}
|
||
|
||
{% if transactions_synced %}
|
||
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">Tally accounting transactions synchronized successfully.</div>
|
||
{% endif %}
|
||
<div class="relative space-y-6 xl:pr-80">
|
||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||
<div>
|
||
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">Tools · Accounting</p>
|
||
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Tally Accounting</h1>
|
||
<p class="mt-1 max-w-4xl text-sm text-slate-500">Mirror Tally once into the client SQLite Accounting Mirror, then run accounting and audit analysis from the local .act database. Existing mapping, synchronization and write-back controls remain unchanged.</p>
|
||
</div>
|
||
<a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}&refresh=1{% else %}?refresh=1{% endif %}" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-brand-700">Refresh Tally Companies</a>
|
||
</div>
|
||
|
||
<aside class="rounded-2xl border border-slate-200 bg-white p-3 shadow-lg xl:fixed xl:right-6 xl:top-24 xl:z-20 xl:max-h-[calc(100vh-7rem)] xl:w-72 xl:overflow-y-auto">
|
||
<div class="mb-2 flex items-center justify-between gap-2 px-2">
|
||
<div><div class="text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">Accounting Tools</div><div class="text-sm font-semibold text-slate-900">Menu & Submenus</div></div>
|
||
</div>
|
||
<div class="space-y-2">
|
||
<details class="rounded-xl border border-blue-200 bg-blue-50/40" open><summary class="cursor-pointer px-3 py-2 text-sm font-semibold text-blue-800">Accounting Foundation</summary><div class="space-y-1 border-t border-blue-100 p-2">
|
||
<a href="/tools/accounting/chart-of-accounts{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Chart of Accounts</a>
|
||
<a href="/tools/accounting/master-integrity{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Master Integrity Engine</a>
|
||
<a href="/tools/accounting/stock-items{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Stock Item Intelligence</a>
|
||
<a href="/tools/accounting/opening-balances{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Opening Balance Review</a>
|
||
<a href="/tools/accounting/voucher-engine{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Accounting Voucher Engine</a>
|
||
</div></details>
|
||
|
||
<details class="rounded-xl border border-amber-200 bg-amber-50/30" open><summary class="cursor-pointer px-3 py-2 text-sm font-semibold text-amber-800">Purchases & Expenses</summary><div class="space-y-1 border-t border-amber-100 p-2">
|
||
<a href="/tools/accounting/taxonomy" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Accounting Taxonomy</a>
|
||
<a href="/tools/accounting/historical-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Historical Learning</a>
|
||
<a href="/tools/accounting/ledger-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Ledger Learning</a>
|
||
<a href="/tools/accounting/gstr2b{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">GSTR-2B Intelligence</a>
|
||
<a href="/tools/accounting/gst-reconciliation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg bg-emerald-50 px-3 py-2 text-sm font-semibold text-emerald-800 hover:bg-emerald-100">GST Return Reconciliation</a>
|
||
<a href="/tools/accounting/purchase-enrichment{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">E-Invoice / E-Way Bill</a>
|
||
<a href="/tools/accounting/purchase-review{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Purchase Review</a>
|
||
<a href="/tools/accounting/purchase-posting{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Purchase → Tally</a>
|
||
<a href="/tools/accounting/sundry-creditors{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg bg-rose-50 px-3 py-2 text-sm font-semibold text-rose-800 hover:bg-rose-100">Sundry Creditors – 180 Days</a>
|
||
</div></details>
|
||
|
||
<details class="rounded-xl border border-emerald-200 bg-emerald-50/30"><summary class="cursor-pointer px-3 py-2 text-sm font-semibold text-emerald-800">Cash, Bank & Compliance</summary><div class="space-y-1 border-t border-emerald-100 p-2">
|
||
<a href="/tools/accounting/bank-posting{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Bank Expenses / Payments</a>
|
||
<a href="/tools/accounting/bank-reconciliation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Bank Reconciliation</a>
|
||
<a href="/tools/tally/depreciation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Depreciation (IT)</a>
|
||
<a href="/tools/accounting/cash-payments{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Cash Payment Compliance</a>
|
||
<a href="/tools/accounting/cash-payments/allocation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Cash Payment Entry Allocation</a>
|
||
<a href="/tools/accounting/tds/compliance{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">TDS Compliance Review</a>
|
||
<a href="/tools/accounting/tds/rules" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">TDS Rules</a>
|
||
<a href="/tools/accounting/tds/liability{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">TDS Liability</a>
|
||
<a href="/tools/accounting/tds/reconciliation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">TDS Reconciliation</a>
|
||
</div></details>
|
||
|
||
<details class="rounded-xl border border-emerald-200 bg-emerald-50/30"><summary class="cursor-pointer px-3 py-2 text-sm font-semibold text-emerald-800">Sales & Income</summary><div class="space-y-1 border-t border-emerald-100 p-2">
|
||
<a href="/tools/accounting/sales-income{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Sales & Income Register</a>
|
||
<a href="/tools/accounting/customer-intelligence{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Customer Intelligence</a>
|
||
<a href="/tools/accounting/historical-sales-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Historical Sales Learning</a>
|
||
<a href="/tools/accounting/sales-ledger-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Sales Ledger Learning</a>
|
||
</div></details>
|
||
|
||
<details class="rounded-xl border border-indigo-200 bg-indigo-50/30"><summary class="cursor-pointer px-3 py-2 text-sm font-semibold text-indigo-800">AI & Advanced</summary><div class="space-y-1 border-t border-indigo-100 p-2">
|
||
<a href="/tools/accounting/ai{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Accounting AI</a>
|
||
<a href="/tools/accounting/internal-model" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Internal Model</a>
|
||
</div></details>
|
||
</div>
|
||
</aside>
|
||
|
||
{% if initialized %}
|
||
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">Client accounting storage was initialized successfully.</div>
|
||
{% endif %}
|
||
{% if mapped %}
|
||
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">Tally company mapping was saved successfully.</div>
|
||
{% endif %}
|
||
{% if unmapped %}
|
||
<div class="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm font-medium text-slate-700">Tally company mapping was removed.</div>
|
||
{% endif %}
|
||
{% if command_error %}
|
||
<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">{{ command_error }}</div>
|
||
{% endif %}
|
||
|
||
{% set agent = live_result.agent if live_result else None %}
|
||
{% set tally = live_result.tally if live_result else None %}
|
||
{% set accounting = live_result.accounting if live_result else None %}
|
||
|
||
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||
<div class="rounded-2xl bg-white p-5 shadow-soft">
|
||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">ERP Local Agent</div>
|
||
<div class="mt-2 text-lg font-semibold {% if agent_online %}text-emerald-700{% else %}text-red-700{% endif %}">{{ 'Connected' if agent_online else 'Offline' }}</div>
|
||
<div class="mt-1 text-xs text-slate-500">{{ storage_node.node_name if storage_node else 'No active branch agent' }}</div>
|
||
</div>
|
||
<div class="rounded-2xl bg-white p-5 shadow-soft">
|
||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Mapping Module</div>
|
||
<div class="mt-2 text-lg font-semibold {% if agent and agent.tally_mapping_capability %}text-emerald-700{% else %}text-slate-700{% endif %}">
|
||
{{ 'Available' if agent and agent.tally_mapping_capability else ('Update Agent' if agent_online else 'Unavailable') }}
|
||
</div>
|
||
<div class="mt-1 text-xs text-slate-500">Agent {{ agent.version if agent and agent.version else '-' }}</div>
|
||
</div>
|
||
<div class="rounded-2xl bg-white p-5 shadow-soft">
|
||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">TallyPrime</div>
|
||
<div class="mt-2 text-lg font-semibold {% if tally and tally.connected %}text-emerald-700{% elif tally %}text-red-700{% else %}text-slate-700{% endif %}">
|
||
{{ 'Connected' if tally and tally.connected else ('Not connected' if tally else 'Not checked') }}
|
||
</div>
|
||
<div class="mt-1 text-xs text-slate-500">{{ tally.url if tally and tally.url else '127.0.0.1:9000' }}</div>
|
||
</div>
|
||
<div class="rounded-2xl bg-white p-5 shadow-soft">
|
||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Loaded Companies</div>
|
||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ tally.company_count if tally else '-' }}</div>
|
||
<div class="mt-1 text-xs text-slate-500">GUID-based discovery</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||
<form method="get" action="/tools/tally" class="flex flex-wrap items-end gap-3">
|
||
<label class="min-w-[280px] flex-1 text-sm font-medium text-slate-700">
|
||
ERP Client
|
||
<select name="client_id" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100">
|
||
<option value="">Select client</option>
|
||
{% for client in clients %}
|
||
<option value="{{ client.id }}" {% if selected_client and selected_client.id == client.id %}selected{% endif %}>{{ client.client_name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</label>
|
||
<button type="submit" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Open Client</button>
|
||
</form>
|
||
<p class="mt-3 text-xs text-slate-500">Only clients assigned to the logged-in Partner in the active branch are shown.</p>
|
||
</section>
|
||
|
||
{% if selected_client %}
|
||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||
<div>
|
||
<h2 class="font-semibold text-slate-900">Accounting Storage</h2>
|
||
<p class="mt-1 text-sm text-slate-500">{{ selected_client.client_name }}</p>
|
||
</div>
|
||
<span class="rounded-full px-3 py-1 text-xs font-semibold {% if accounting and accounting.exists %}bg-emerald-50 text-emerald-700{% else %}bg-amber-50 text-amber-700{% endif %}">
|
||
{{ 'Initialized' if accounting and accounting.exists else 'Not initialized' }}
|
||
</span>
|
||
</div>
|
||
|
||
{% if accounting and accounting.exists %}
|
||
<dl class="mt-4 grid gap-4 md:grid-cols-2">
|
||
<div><dt class="text-xs font-semibold uppercase tracking-wide text-slate-500">.act Database</dt><dd class="mt-1 break-all text-sm font-medium text-slate-900">{{ accounting.db_path }}</dd></div>
|
||
<div><dt class="text-xs font-semibold uppercase tracking-wide text-slate-500">Schema Version</dt><dd class="mt-1 text-sm font-medium text-slate-900">{{ accounting.metadata.schema_version or '2' }}</dd></div>
|
||
</dl>
|
||
{% else %}
|
||
<form method="post" action="/tools/tally/initialize" class="mt-4">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||
<input type="hidden" name="client_id" value="{{ selected_client.id }}" />
|
||
<button type="submit" class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800">Create .act Accounting Storage</button>
|
||
</form>
|
||
{% endif %}
|
||
</section>
|
||
|
||
{% if accounting and accounting.exists and tally and tally.connected %}
|
||
{% set active_mappings = (accounting.mappings or []) | selectattr('is_active') | list %}
|
||
{% if active_mappings %}
|
||
{% set current_mapping = active_mappings[0] %}
|
||
{% if selected_client and analysis_history %}
|
||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||
<div class="flex items-center justify-between gap-3"><div><h2 class="font-semibold text-slate-900">Recent Accounting Analysis</h2><p class="mt-1 text-sm text-slate-500">Stored in the client local SQLite accounting database.</p></div></div>
|
||
<div class="mt-3 overflow-x-auto"><table class="min-w-full text-sm"><thead><tr class="text-left text-xs uppercase text-slate-500"><th class="p-2">Run</th><th class="p-2">Analysis</th><th class="p-2">FY / Period</th><th class="p-2">Completed</th><th class="p-2"></th></tr></thead><tbody>{% for row in analysis_history %}<tr class="border-t"><td class="p-2">#{{ row.id }}</td><td class="p-2 font-medium">{{ row.analysis_type|replace('_',' ')|title }}</td><td class="p-2">{{ row.financial_year }} · {{ row.period_from }} → {{ row.period_to }}</td><td class="p-2">{{ row.completed_at_utc }}</td><td class="p-2 text-right"><a class="font-semibold text-brand-700" href="/tools/tally/analysis/{{ row.id }}/download?client_id={{ selected_client.id }}&financial_year={{ row.financial_year }}&format=xlsx">Excel</a> · <a class="font-semibold text-brand-700" href="/tools/tally/analysis/{{ row.id }}/download?client_id={{ selected_client.id }}&financial_year={{ row.financial_year }}&format=pdf">PDF</a></td></tr>{% endfor %}</tbody></table></div>
|
||
</section>
|
||
{% endif %}
|
||
<details class="rounded-2xl border border-slate-200 bg-white shadow-soft">
|
||
<summary class="cursor-pointer list-none px-5 py-4">
|
||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<h2 class="font-semibold text-slate-900">Tally Company Linked</h2>
|
||
<p class="mt-1 text-sm text-slate-500">
|
||
{{ selected_client.client_name }} → {{ current_mapping.company_name }}
|
||
{% if current_mapping.gstin %} · {{ current_mapping.gstin }}{% endif %}
|
||
</p>
|
||
</div>
|
||
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700">Mapped by GUID</span>
|
||
</div>
|
||
<p class="mt-2 text-xs text-slate-500">Mapping is a one-time setup. Open this panel only when you need to change the linked Tally company.</p>
|
||
</summary>
|
||
<div class="border-t border-slate-100 p-5">
|
||
<div>
|
||
<h3 class="font-semibold text-slate-900">Change ERP Scope to Tally Company Mapping</h3>
|
||
<p class="mt-1 text-sm text-slate-500">The Tally GUID remains the permanent identifier. Company name and GSTIN are descriptive snapshots.</p>
|
||
</div>
|
||
|
||
<form method="post" action="/tools/tally/map-company" class="mt-5 grid gap-4 lg:grid-cols-2">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||
<input type="hidden" name="client_id" value="{{ selected_client.id }}" />
|
||
|
||
<label class="text-sm font-medium text-slate-700">
|
||
ERP Scope
|
||
<select name="registration_id" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
||
<option value="">Client level · {{ selected_client.client_name }}</option>
|
||
{% for reg, reg_type in registrations %}
|
||
<option value="{{ reg.id }}">
|
||
{{ reg_type.name }} · {{ reg.registration_number }}
|
||
{% if reg.trade_name %} · {{ reg.trade_name }}{% elif reg.legal_name %} · {{ reg.legal_name }}{% endif %}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
<span class="mt-1 block text-xs font-normal text-slate-500">Choose a GSTIN/other registration when the Tally company belongs to that registration; otherwise use client level.</span>
|
||
</label>
|
||
|
||
<label class="text-sm font-medium text-slate-700">
|
||
Currently Open in TallyPrime
|
||
<select name="tally_guid" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
||
<option value="">Select Tally company</option>
|
||
{% for company in tally.companies %}
|
||
{% if company.guid %}
|
||
<option value="{{ company.guid }}">{{ company.name }}{% if company.gstin %} · {{ company.gstin }}{% endif %}</option>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</select>
|
||
<span class="mt-1 block text-xs font-normal text-slate-500">Companies without a GUID are intentionally not offered for permanent mapping.</span>
|
||
</label>
|
||
|
||
<label class="flex items-start gap-2 lg:col-span-2">
|
||
<input type="checkbox" name="allow_gstin_mismatch" value="true" class="mt-1 rounded border-slate-300" />
|
||
<span class="text-sm text-slate-700">
|
||
Allow GSTIN mismatch after manual verification.
|
||
<span class="block text-xs text-slate-500">For a GSTIN registration, Phase 3 blocks a mapping when both ERP and Tally contain different GSTINs unless this override is explicitly selected.</span>
|
||
</span>
|
||
</label>
|
||
|
||
<div class="lg:col-span-2">
|
||
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Save Tally Mapping</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</details>
|
||
{% else %}
|
||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||
<div>
|
||
<h2 class="font-semibold text-slate-900">Link Tally Company</h2>
|
||
<p class="mt-1 text-sm text-slate-500">One-time setup. The Tally GUID is stored as the permanent identifier; company name and GSTIN are descriptive snapshots.</p>
|
||
</div>
|
||
|
||
<form method="post" action="/tools/tally/map-company" class="mt-5 grid gap-4 lg:grid-cols-2">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||
<input type="hidden" name="client_id" value="{{ selected_client.id }}" />
|
||
|
||
<label class="text-sm font-medium text-slate-700">
|
||
ERP Scope
|
||
<select name="registration_id" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
||
<option value="">Client level · {{ selected_client.client_name }}</option>
|
||
{% for reg, reg_type in registrations %}
|
||
<option value="{{ reg.id }}">
|
||
{{ reg_type.name }} · {{ reg.registration_number }}
|
||
{% if reg.trade_name %} · {{ reg.trade_name }}{% elif reg.legal_name %} · {{ reg.legal_name }}{% endif %}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
<span class="mt-1 block text-xs font-normal text-slate-500">Choose a registration only when the Tally company belongs specifically to that registration; otherwise use client level.</span>
|
||
</label>
|
||
|
||
<label class="text-sm font-medium text-slate-700">
|
||
Currently Open in TallyPrime
|
||
<select name="tally_guid" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
||
<option value="">Select Tally company</option>
|
||
{% for company in tally.companies %}
|
||
{% if company.guid %}
|
||
<option value="{{ company.guid }}">{{ company.name }}{% if company.gstin %} · {{ company.gstin }}{% endif %}</option>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</select>
|
||
</label>
|
||
|
||
<label class="flex items-start gap-2 lg:col-span-2">
|
||
<input type="checkbox" name="allow_gstin_mismatch" value="true" class="mt-1 rounded border-slate-300" />
|
||
<span class="text-sm text-slate-700">
|
||
Allow GSTIN mismatch after manual verification.
|
||
<span class="block text-xs text-slate-500">Use only when the ERP registration and the selected Tally company have intentionally different GSTIN snapshots.</span>
|
||
</span>
|
||
</label>
|
||
|
||
<div class="lg:col-span-2">
|
||
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Save Tally Mapping</button>
|
||
</div>
|
||
</form>
|
||
</section>
|
||
{% endif %}
|
||
{% endif %}
|
||
|
||
{% if accounting and accounting.exists %}
|
||
<section class="rounded-2xl bg-white p-5 shadow-soft" id="full-accounting-export">
|
||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||
<div>
|
||
<h2 class="font-semibold text-slate-900">Mirror Tally to SQLite</h2>
|
||
<p class="mt-1 text-sm text-slate-500">Create or refresh the client Accounting Mirror from Tally using the high-speed read-only ODBC path. After this finishes, ERP analysis tools read the local SQLite .act data instead of repeatedly extracting from Tally.</p>
|
||
</div>
|
||
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700">Read-only export</span>
|
||
</div>
|
||
<div class="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900"><strong>Before export:</strong> open the mapped company in TallyPrime and set Tally's active period (Alt+F2) to cover the full period you want exported. The ODBC mirror reads Tally's active period; the ERP does not change Tally's period automatically.</div>
|
||
<div id="mirrorRegistryStatus" class="mt-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-700">
|
||
{% if selected_mirror_registration %}
|
||
<strong>Mirror available:</strong> FY {{ selected_mirror_registration.financial_year }} · Version {{ selected_mirror_registration.version_no }}{% if selected_mirror_registration.company_name %} · {{ selected_mirror_registration.company_name }}{% endif %}{% if selected_mirror_registration.last_synced_at_utc %} · last synced {{ selected_mirror_registration.last_synced_at_utc.strftime('%d-%m-%Y %H:%M') }}{% endif %}. Creating this FY again creates a new version and keeps the earlier version.
|
||
<div class="mt-3 grid gap-2 lg:grid-cols-2">
|
||
{% if selected_mirror_registration.mirror_local_path %}
|
||
<div class="rounded-lg border border-slate-200 bg-white p-2.5"><div class="text-[11px] font-semibold uppercase text-slate-500">Registered Mirror Path</div><div class="mt-1 break-all font-mono text-[11px] text-slate-700">{{ selected_mirror_registration.mirror_local_path }}</div></div>
|
||
{% endif %}
|
||
{% if selected_mirror_registration.tally_data_local_path %}
|
||
<div class="rounded-lg border border-slate-200 bg-white p-2.5"><div class="text-[11px] font-semibold uppercase text-slate-500">Tally Working Data Path</div><div class="mt-1 break-all font-mono text-[11px] text-slate-700">{{ selected_mirror_registration.tally_data_local_path }}</div></div>
|
||
{% endif %}
|
||
</div>
|
||
{% else %}
|
||
<strong>No registered mirror for FY {{ selected_financial_year }}.</strong> Creating the mirror will register it to this client and financial year for Accounting/Audit tools. Existing canonical mirrors discovered by the Local Agent are adopted automatically; you do not need to recreate them.
|
||
{% endif %}
|
||
</div>
|
||
<form method="post" action="/tools/tally/full-export" class="mt-5 grid gap-4 lg:grid-cols-5 lg:items-end" id="mirrorExportForm">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||
<input type="hidden" name="client_id" value="{{ selected_client.id }}" />
|
||
<input type="hidden" name="replace_existing" id="mirrorReplaceExisting" value="" />
|
||
<label class="text-sm font-medium text-slate-700">Target Financial Year
|
||
<select id="mirrorFinancialYear" name="financial_year" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
||
{% for fy in mirror_fy_options %}
|
||
<option value="{{ fy }}" {% if fy == selected_financial_year %}selected{% endif %}>FY {{ fy }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<span class="mt-1 block text-xs text-slate-500">Defaults to the ERP workspace FY (FY {{ workspace_financial_year }}). You can still choose another FY to create or refresh its separate mirror.</span>
|
||
</label>
|
||
<label class="text-sm font-medium text-slate-700">Mapped Tally Company
|
||
<select name="tally_guid" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
||
<option value="">Select mapped company</option>
|
||
{% for mapping in accounting.mappings or [] %}
|
||
{% if mapping.tally_guid and mapping.is_active %}
|
||
<option value="{{ mapping.tally_guid }}">{{ mapping.company_name }}{% if mapping.gstin %} · {{ mapping.gstin }}{% endif %}</option>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</select>
|
||
</label>
|
||
<label class="text-sm font-medium text-slate-700">From Date<input id="mirrorDateFrom" type="date" name="date_from" required value="{{ transaction_date_from }}" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm" /></label>
|
||
<label class="text-sm font-medium text-slate-700">To Date<input id="mirrorDateTo" type="date" name="date_to" required value="{{ transaction_date_to }}" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm" /></label>
|
||
<button id="fullExportButton" type="submit" class="rounded-xl bg-emerald-700 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-800">{% if selected_mirror_registration %}Create New Version{% else %}Mirror Tally to SQLite{% endif %}</button>
|
||
</form>
|
||
<div class="mt-3 flex flex-wrap items-center gap-3 text-sm">
|
||
<button type="button" id="mirrorPreviousFY" class="rounded-lg border border-slate-300 bg-white px-3 py-1.5 font-semibold text-slate-700">Use Previous FY</button>
|
||
<span class="text-slate-500">Select an FY here to create or refresh that year's separate local mirror in the client's FY storage hierarchy.</span>
|
||
</div>
|
||
<div id="fullExportProgress" class="mt-5 hidden rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||
<div class="flex items-center justify-between gap-3"><div><div id="fullExportStage" class="font-semibold text-slate-900">Waiting…</div><div id="fullExportMessage" class="mt-1 text-xs text-slate-500"></div></div><div id="fullExportPct" class="text-lg font-bold text-slate-900">0%</div></div>
|
||
<div class="mt-3 h-3 overflow-hidden rounded-full bg-slate-200"><div id="fullExportBar" class="h-full bg-emerald-600 transition-all duration-300" style="width:0%"></div></div>
|
||
<div class="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-5"><div class="rounded-lg bg-white p-3"><div class="text-xs uppercase text-slate-500">Ledgers</div><div id="fullExportLedgers" class="mt-1 text-lg font-semibold">0</div></div><div class="rounded-lg bg-white p-3"><div class="text-xs uppercase text-slate-500">Groups</div><div id="fullExportGroups" class="mt-1 text-lg font-semibold">0</div></div><div class="rounded-lg bg-white p-3"><div class="text-xs uppercase text-slate-500">Vouchers</div><div id="fullExportVouchers" class="mt-1 text-lg font-semibold">0</div></div><div class="rounded-lg bg-white p-3"><div class="text-xs uppercase text-slate-500">Voucher Lines</div><div id="fullExportLines" class="mt-1 text-lg font-semibold">0</div></div><div class="rounded-lg bg-white p-3"><div class="text-xs uppercase text-slate-500">Stock Movements</div><div id="fullExportInventory" class="mt-1 text-lg font-semibold">0</div></div></div>
|
||
<div id="fullExportMasterDiagnostics" class="mt-3 hidden rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs text-slate-600"></div>
|
||
<div id="fullExportPaths" class="mt-3 break-all text-xs text-slate-500"></div><div id="fullExportError" class="mt-3 hidden rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700"></div>
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
<details class="rounded-2xl border border-slate-200 bg-white shadow-soft">
|
||
<summary class="cursor-pointer list-none px-5 py-4">
|
||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<h2 class="font-semibold text-slate-900">Advanced / Legacy Tally Controls</h2>
|
||
<p class="mt-1 text-sm text-slate-500">Manual master sync, transaction sync, active mappings and loaded-company diagnostics are retained for compatibility and troubleshooting. Normal work only requires Mirror Tally to SQLite above.</p>
|
||
</div>
|
||
<span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600">Optional</span>
|
||
</div>
|
||
</summary>
|
||
<div class="space-y-6 border-t border-slate-100 p-5">
|
||
{% if accounting and accounting.exists and tally and tally.connected %}
|
||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||
<div>
|
||
<h2 class="font-semibold text-slate-900">Phase 3 · Accounting Master Sync</h2>
|
||
<p class="mt-1 text-sm text-slate-500">Select the company that is currently open/loaded in TallyPrime. The Local Agent verifies that the selected company is already mapped to this ERP client before reading masters.</p>
|
||
</div>
|
||
<span class="rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700">Read-only</span>
|
||
</div>
|
||
|
||
<form method="post" action="/tools/tally/sync-masters" class="mt-5 grid gap-4 lg:grid-cols-[1fr_auto] lg:items-end">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||
<input type="hidden" name="client_id" value="{{ selected_client.id }}" />
|
||
<label class="text-sm font-medium text-slate-700">
|
||
Currently Open in TallyPrime
|
||
<select name="tally_guid" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
||
<option value="">Select open Tally company</option>
|
||
{% for company in tally.companies %}
|
||
{% if company.guid %}
|
||
<option value="{{ company.guid }}">{{ company.name }}{% if company.gstin %} · {{ company.gstin }}{% endif %}</option>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</select>
|
||
<span class="mt-1 block text-xs font-normal text-slate-500">This dropdown is populated live from the companies currently loaded/open in the local TallyPrime instance.</span>
|
||
</label>
|
||
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Sync Accounting Masters</button>
|
||
</form>
|
||
|
||
<div class="mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||
{% set last_sync = accounting.latest_master_sync if accounting else none %}
|
||
<div class="rounded-xl border border-slate-200 p-3"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Latest Sync</div><div class="mt-1 text-sm font-medium text-slate-900">{{ last_sync.completed_at_utc if last_sync and last_sync.completed_at_utc else 'Not synced yet' }}</div></div>
|
||
<div class="rounded-xl border border-slate-200 p-3"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Company</div><div class="mt-1 text-sm font-medium text-slate-900">{{ last_sync.company_name if last_sync else '-' }}</div></div>
|
||
<div class="rounded-xl border border-slate-200 p-3"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Rows Stored</div><div class="mt-1 text-sm font-medium text-slate-900">{{ last_sync.rows_processed if last_sync else 0 }}</div></div>
|
||
<div class="rounded-xl border border-slate-200 p-3"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Status</div><div class="mt-1 text-sm font-medium text-slate-900">{{ last_sync.status|title if last_sync else '-' }}</div></div>
|
||
</div>
|
||
|
||
<div class="mt-4 rounded-xl bg-slate-50 px-4 py-3 text-xs text-slate-600">
|
||
Synced into the selected client's .act database: Groups, Ledgers, Voucher Types, Stock Groups, Stock Categories, Stock Items, Units, Cost Categories and Cost Centres. No voucher or transaction data is imported in Phase 3.
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% if accounting and accounting.exists and tally and tally.connected %}
|
||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||
<div>
|
||
<h2 class="font-semibold text-slate-900">Phase 4 · Accounting Transaction Sync</h2>
|
||
<p class="mt-1 text-sm text-slate-500">Read vouchers and their accounting allocations from the selected company currently open in TallyPrime. The selected company must already be mapped to this ERP client.</p>
|
||
</div>
|
||
<span class="rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700">Read-only</span>
|
||
</div>
|
||
|
||
<form method="post" action="/tools/tally/sync-transactions" class="mt-5 grid gap-4 lg:grid-cols-4 lg:items-end">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||
<input type="hidden" name="client_id" value="{{ selected_client.id }}" />
|
||
<label class="text-sm font-medium text-slate-700">
|
||
Currently Open in TallyPrime
|
||
<select name="tally_guid" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
||
<option value="">Select open Tally company</option>
|
||
{% for company in tally.companies %}
|
||
{% if company.guid %}
|
||
<option value="{{ company.guid }}">{{ company.name }}{% if company.gstin %} · {{ company.gstin }}{% endif %}</option>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</select>
|
||
<span class="mt-1 block text-xs font-normal text-slate-500">Live list from companies currently loaded/open in this computer's TallyPrime.</span>
|
||
</label>
|
||
<label class="text-sm font-medium text-slate-700">From Date
|
||
<input type="date" name="date_from" required value="{{ transaction_date_from }}" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm" />
|
||
</label>
|
||
<label class="text-sm font-medium text-slate-700">To Date
|
||
<input type="date" name="date_to" required value="{{ transaction_date_to }}" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm" />
|
||
</label>
|
||
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Sync Transactions</button>
|
||
</form>
|
||
|
||
{% set tx = accounting.latest_transaction_sync if accounting else none %}
|
||
<div class="mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||
<div class="rounded-xl border border-slate-200 p-3"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Latest Sync</div><div class="mt-1 text-sm font-medium text-slate-900">{{ tx.completed_at_utc if tx and tx.completed_at_utc else 'Not synced yet' }}</div></div>
|
||
<div class="rounded-xl border border-slate-200 p-3"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Period</div><div class="mt-1 text-sm font-medium text-slate-900">{% if tx and tx.details %}{{ tx.details.date_from or '-' }} → {{ tx.details.date_to or '-' }}{% else %}-{% endif %}</div></div>
|
||
<div class="rounded-xl border border-slate-200 p-3"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Vouchers</div><div class="mt-1 text-sm font-medium text-slate-900">{% if tx and tx.details and tx.details.counts %}{{ tx.details.counts.vouchers or 0 }}{% else %}0{% endif %}</div></div>
|
||
<div class="rounded-xl border border-slate-200 p-3"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Rows Stored</div><div class="mt-1 text-sm font-medium text-slate-900">{{ tx.rows_processed if tx else 0 }}</div></div>
|
||
</div>
|
||
|
||
<div class="mt-4 rounded-xl bg-slate-50 px-4 py-3 text-xs text-slate-600">
|
||
Stored locally in the client's .act database: vouchers, ledger entries, inventory entries, bill allocations, cost-centre allocations and bank allocations. Raw voucher XML is preserved. Re-syncing a date range replaces only that company's vouchers in the selected range. No Tally data is modified.
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% if accounting and accounting.exists %}
|
||
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
||
<div class="border-b border-slate-100 px-5 py-4">
|
||
<h2 class="font-semibold text-slate-900">Active Tally Mappings</h2>
|
||
<p class="text-sm text-slate-500">Mappings remain valid across Tally company renames because the GUID is the persistent key.</p>
|
||
</div>
|
||
{% if accounting.mappings %}
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||
<thead class="bg-slate-50">
|
||
<tr>
|
||
<th class="px-4 py-3 text-left font-semibold text-slate-600">ERP Scope</th>
|
||
<th class="px-4 py-3 text-left font-semibold text-slate-600">Tally Company</th>
|
||
<th class="px-4 py-3 text-left font-semibold text-slate-600">GUID</th>
|
||
<th class="px-4 py-3 text-left font-semibold text-slate-600">ERP GSTIN</th>
|
||
<th class="px-4 py-3 text-left font-semibold text-slate-600">Tally GSTIN</th>
|
||
<th class="px-4 py-3 text-left font-semibold text-slate-600">GSTIN Match</th>
|
||
<th class="px-4 py-3 text-left font-semibold text-slate-600">Loaded</th>
|
||
<th class="px-4 py-3 text-right font-semibold text-slate-600">Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-slate-100">
|
||
{% for mapping in accounting.mappings %}
|
||
<tr>
|
||
<td class="px-4 py-3">
|
||
{% if mapping.registration_id %}
|
||
<div class="font-medium text-slate-900">{{ mapping.registration_type_code or 'Registration' }} · {{ mapping.registration_number or mapping.registration_id }}</div>
|
||
<div class="text-xs text-slate-500">{{ mapping.registration_trade_name or mapping.registration_legal_name or selected_client.client_name }}</div>
|
||
{% else %}
|
||
<div class="font-medium text-slate-900">Client level</div>
|
||
<div class="text-xs text-slate-500">{{ selected_client.client_name }}</div>
|
||
{% endif %}
|
||
</td>
|
||
<td class="px-4 py-3 font-medium text-slate-900">{{ mapping.company_name }}</td>
|
||
<td class="px-4 py-3 font-mono text-xs text-slate-600">{{ mapping.tally_guid }}</td>
|
||
<td class="px-4 py-3 text-slate-600">
|
||
{% if mapping.erp_gstins %}
|
||
{% for gstin in mapping.erp_gstins %}<div class="font-mono text-xs">{{ gstin }}</div>{% endfor %}
|
||
{% else %}-{% endif %}
|
||
</td>
|
||
<td class="px-4 py-3 font-mono text-xs text-slate-600">{{ mapping.tally_gstin_display or '-' }}</td>
|
||
<td class="px-4 py-3">
|
||
{% if mapping.gstin_match_status == 'matched' %}
|
||
<span class="rounded-full bg-emerald-50 px-2 py-1 text-xs font-semibold text-emerald-700">Matched</span>
|
||
{% elif mapping.gstin_match_status == 'mismatch' %}
|
||
<span class="rounded-full bg-red-50 px-2 py-1 text-xs font-semibold text-red-700">Mismatch</span>
|
||
{% elif mapping.gstin_match_status == 'tally_missing' %}
|
||
<span class="rounded-full bg-amber-50 px-2 py-1 text-xs font-semibold text-amber-700">Tally GSTIN unavailable</span>
|
||
{% elif mapping.gstin_match_status == 'erp_missing' %}
|
||
<span class="rounded-full bg-amber-50 px-2 py-1 text-xs font-semibold text-amber-700">ERP GSTIN unavailable</span>
|
||
{% else %}
|
||
<span class="rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-600">Unavailable</span>
|
||
{% endif %}
|
||
</td>
|
||
<td class="px-4 py-3">
|
||
<span class="rounded-full px-2 py-1 text-xs font-semibold {% if mapping.currently_loaded %}bg-emerald-50 text-emerald-700{% else %}bg-slate-100 text-slate-600{% endif %}">
|
||
{{ 'Yes' if mapping.currently_loaded else 'No' }}
|
||
</span>
|
||
</td>
|
||
<td class="px-4 py-3 text-right">
|
||
<form method="post" action="/tools/tally/unmap-company" onsubmit="return confirm('Remove this Tally company mapping?');">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||
<input type="hidden" name="client_id" value="{{ selected_client.id }}" />
|
||
<input type="hidden" name="mapping_id" value="{{ mapping.id }}" />
|
||
<button type="submit" class="rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-semibold text-red-700 hover:bg-red-50">Unmap</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<div class="p-6 text-sm text-slate-500">No Tally company has been mapped for this client yet.</div>
|
||
{% endif %}
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% if tally %}
|
||
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
||
<div class="border-b border-slate-100 px-5 py-4">
|
||
<h2 class="font-semibold text-slate-900">Loaded Tally Companies</h2>
|
||
<p class="text-sm text-slate-500">Read-only company discovery from the local TallyPrime instance.</p>
|
||
</div>
|
||
{% if tally.companies %}
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||
<thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left font-semibold text-slate-600">Company</th><th class="px-4 py-3 text-left font-semibold text-slate-600">GUID</th><th class="px-4 py-3 text-left font-semibold text-slate-600">Tally GSTIN</th></tr></thead>
|
||
<tbody class="divide-y divide-slate-100">
|
||
{% for company in tally.companies %}
|
||
<tr>
|
||
<td class="px-4 py-3 font-medium text-slate-900">{{ company.name }}</td>
|
||
<td class="px-4 py-3 font-mono text-xs text-slate-600">{{ company.guid or '-' }}</td>
|
||
<td class="px-4 py-3 text-slate-600">{{ company.gstin or '-' }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<div class="p-6 text-sm text-slate-500">TallyPrime responded, but no loaded company was returned.</div>
|
||
{% endif %}
|
||
</section>
|
||
{% endif %}
|
||
|
||
</div>
|
||
</details>
|
||
{% endif %}
|
||
|
||
<div class="rounded-xl border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-800">
|
||
Normal workflow: map the company once, mirror Tally to SQLite, then run analysis from the client .act database. Approved write-back tools remain controlled separately.
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function(){
|
||
const clientId = {{ selected_client.id if selected_client else 'null' }};
|
||
if (!clientId) return;
|
||
const box=document.getElementById('fullExportProgress'); if(!box) return;
|
||
let pollTimer=null;
|
||
function setText(id,v){const e=document.getElementById(id);if(e)e.textContent=String(v??'');}
|
||
async function pollFullExport(){
|
||
try{
|
||
const fyEl=document.getElementById('mirrorFinancialYear'); const fyValue=fyEl?fyEl.value:''; const r=await fetch('/tools/tally/full-export/status?client_id='+encodeURIComponent(clientId)+'&financial_year='+encodeURIComponent(fyValue),{cache:'no-store'});
|
||
const d=await r.json(); if(!r.ok||!d.ok||!d.job)return;
|
||
const j=d.job||{}, p=Math.max(0,Math.min(100,Number(j.percent)||0)); box.classList.remove('hidden');
|
||
setText('fullExportStage',j.stage||j.status||'Working…'); setText('fullExportMessage',j.message||''); setText('fullExportPct',Math.round(p)+'%');
|
||
document.getElementById('fullExportBar').style.width=p+'%';
|
||
const rows=j.rows||((j.mirror||{}).counts)||{};
|
||
setText('fullExportLedgers',rows.ledger_master||0);
|
||
setText('fullExportGroups',rows.group_master||0);
|
||
setText('fullExportVouchers',rows.voucher||rows.voucher_header||0);
|
||
setText('fullExportLines',rows.voucher_ledger_entry||rows.voucher_ledger||0);
|
||
setText('fullExportInventory',rows.inventory_movement||rows.inventory||0);
|
||
const diag=j.import_diagnostics||{};
|
||
const ledgerDiag=diag.ledgers||{}, groupDiag=diag.groups||{};
|
||
const diagEl=document.getElementById('fullExportMasterDiagnostics');
|
||
if(diagEl && (ledgerDiag.extracted_rows!=null || groupDiag.extracted_rows!=null)){
|
||
const parts=[];
|
||
if(ledgerDiag.extracted_rows!=null) parts.push('Ledgers: '+Number(ledgerDiag.extracted_rows||0)+' extracted → '+Number(ledgerDiag.stored_rows||0)+' stored');
|
||
if(groupDiag.extracted_rows!=null) parts.push('Groups: '+Number(groupDiag.extracted_rows||0)+' extracted → '+Number(groupDiag.stored_rows||0)+' stored');
|
||
if(diag.flat_master_rows!=null) parts.push('Flat master rows: '+Number(diag.flat_master_rows||0));
|
||
diagEl.textContent=parts.join(' • ');
|
||
diagEl.classList.remove('hidden');
|
||
}
|
||
const paths=[]; if(j.mirror_db_path)paths.push('Mirror: '+j.mirror_db_path); if(j.accounting_db_path)paths.push('Client DB: '+j.accounting_db_path); setText('fullExportPaths',paths.join(' • '));
|
||
const er=document.getElementById('fullExportError'); if(j.error){er.textContent=j.error;er.classList.remove('hidden')}else{er.classList.add('hidden')}
|
||
const status=String(j.status||'').toLowerCase();
|
||
const btn=document.getElementById('fullExportButton'); if(btn)btn.disabled=['queued','running'].includes(status);
|
||
if(['completed','failed','cancelled','paused'].includes(status)){
|
||
if(pollTimer){clearInterval(pollTimer);pollTimer=null;}
|
||
}
|
||
}catch(e){}
|
||
}
|
||
pollTimer=setInterval(pollFullExport,1500);
|
||
pollFullExport();
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
const fy=document.getElementById("mirrorFinancialYear");
|
||
const from=document.getElementById("mirrorDateFrom");
|
||
const to=document.getElementById("mirrorDateTo");
|
||
const previous=document.getElementById("mirrorPreviousFY");
|
||
function applyFY(value){
|
||
const m=/^(\d{4})-(\d{2})$/.exec(value||"");
|
||
if(!m||!from||!to)return;
|
||
const y=parseInt(m[1],10);
|
||
from.value=String(y).padStart(4,"0")+"-04-01";
|
||
to.value=String(y+1).padStart(4,"0")+"-03-31";
|
||
}
|
||
if(fy)fy.addEventListener("change",function(){applyFY(fy.value);});
|
||
if(previous&&fy)previous.addEventListener("click",function(){
|
||
if(fy.selectedIndex+1<fy.options.length){
|
||
fy.selectedIndex+=1;
|
||
applyFY(fy.value);
|
||
}
|
||
});
|
||
|
||
const mirrorForm=document.getElementById("mirrorExportForm");
|
||
const replaceField=document.getElementById("mirrorReplaceExisting");
|
||
const registryStatus=document.getElementById("mirrorRegistryStatus");
|
||
let confirmingMirror=false;
|
||
async function refreshMirrorRegistrationStatus(){
|
||
if(!fy||!clientId)return;
|
||
try{
|
||
const r=await fetch('/tools/tally/mirror-registration/status?client_id='+encodeURIComponent(clientId)+'&financial_year='+encodeURIComponent(fy.value),{cache:'no-store'});
|
||
const d=await r.json();
|
||
if(!r.ok||!d.ok)return;
|
||
if(registryStatus){
|
||
registryStatus.innerHTML=d.exists
|
||
? '<strong>Mirror available:</strong> FY '+fy.value+' · Version '+(d.version_no||1)+(d.company_name?' · '+d.company_name:'')+(d.last_synced_at?' · last synced '+new Date(d.last_synced_at).toLocaleString():'')+'. Creating it again will create a new version and retain the current version.'
|
||
: '<strong>No registered mirror for FY '+fy.value+'.</strong> Creating the mirror will register it to this client and financial year for Accounting/Audit tools.';
|
||
const exportButton=document.getElementById('fullExportButton');
|
||
if(exportButton) exportButton.textContent=d.exists?'Create New Version':'Mirror Tally to SQLite';
|
||
}
|
||
}catch(e){}
|
||
}
|
||
if(fy)fy.addEventListener("change",refreshMirrorRegistrationStatus);
|
||
refreshMirrorRegistrationStatus();
|
||
|
||
if(mirrorForm){
|
||
mirrorForm.addEventListener("submit",async function(event){
|
||
if(confirmingMirror || (replaceField&&replaceField.value==='1'))return;
|
||
event.preventDefault();
|
||
try{
|
||
const r=await fetch('/tools/tally/mirror-registration/status?client_id='+encodeURIComponent(clientId)+'&financial_year='+encodeURIComponent(fy.value),{cache:'no-store'});
|
||
const d=await r.json();
|
||
if(!r.ok||!d.ok){ mirrorForm.submit(); return; }
|
||
if(d.exists){
|
||
const label=(d.client_name||'this client')+' - FY '+fy.value;
|
||
const ok=window.confirm('An Accounting Mirror already exists for '+label+'.\n\nCreate a new mirror version?\n\nThe current version will be preserved and remains active if the new export fails.');
|
||
if(!ok)return;
|
||
if(replaceField)replaceField.value='1';
|
||
}
|
||
confirmingMirror=true;
|
||
mirrorForm.submit();
|
||
}catch(e){
|
||
confirmingMirror=true;
|
||
mirrorForm.submit();
|
||
}
|
||
});
|
||
}
|
||
});
|
||
</script>
|
||
|
||
{% endblock %}
|