Refine Tally depreciation storage rates and additions
This commit is contained in:
@@ -36,7 +36,11 @@
|
|||||||
{% if preview %}
|
{% if preview %}
|
||||||
{% set coverage = preview.transaction_coverage %}
|
{% set coverage = preview.transaction_coverage %}
|
||||||
<div class="rounded-xl border {{ 'border-emerald-200 bg-emerald-50 text-emerald-800' if coverage.complete else 'border-amber-200 bg-amber-50 text-amber-900' }} px-4 py-3 text-sm">
|
<div class="rounded-xl border {{ 'border-emerald-200 bg-emerald-50 text-emerald-800' if coverage.complete else 'border-amber-200 bg-amber-50 text-amber-900' }} px-4 py-3 text-sm">
|
||||||
{% if coverage.complete %}Phase 4 transaction data completely covers {{ preview.fy_start }} to {{ preview.fy_end }}.{% else %}Phase 4 transaction data does not fully cover this financial year. Synchronize the missing period before calculation.{% endif %}
|
{% if coverage.complete %}
|
||||||
|
Phase 4 transaction data covers the required period {{ coverage.required_from }} to {{ coverage.required_to }}{% if coverage.provisional %}. This is a provisional current-FY calculation; future dates are not required yet{% endif %}.
|
||||||
|
{% else %}
|
||||||
|
Phase 4 transaction data does not yet cover {{ coverage.required_from }} to {{ coverage.required_to }}. You may save a draft, but approval/write-back will remain blocked until the missing period is synchronized.
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="/tools/tally/depreciation/calculate" class="space-y-5">
|
<form method="post" action="/tools/tally/depreciation/calculate" class="space-y-5">
|
||||||
@@ -50,11 +54,29 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
||||||
<div class="border-b border-slate-200 px-5 py-4"><h2 class="font-semibold text-slate-900">Fixed Asset Ledgers</h2><p class="mt-1 text-sm text-slate-500">Rate is selected by the reviewer. Transaction date is used only as a review proxy for the 180-day put-to-use test; verify where actual put-to-use differs.</p></div>
|
<div class="border-b border-slate-200 px-5 py-4">
|
||||||
<div class="overflow-x-auto"><table class="min-w-full text-sm"><thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500"><tr><th class="px-4 py-3">Ledger</th><th class="px-4 py-3 text-right">Opening WDV</th><th class="px-4 py-3 text-right">Additions ≥180 days</th><th class="px-4 py-3 text-right">Additions <180 days</th><th class="px-4 py-3 text-right">Reductions</th><th class="px-4 py-3">Rate</th></tr></thead><tbody class="divide-y divide-slate-100">
|
<h2 class="font-semibold text-slate-900">Fixed Asset Ledgers</h2>
|
||||||
{% for row in preview.fixed_asset_ledgers %}<tr><td class="px-4 py-3"><div class="font-medium text-slate-900">{{ row.ledger_name }}</div><div class="text-xs text-slate-500">{{ row.parent_group }}</div></td><td class="px-4 py-3 text-right">{{ '%.2f'|format(row.opening_wdv) }}</td><td class="px-4 py-3 text-right">{{ '%.2f'|format(row.additions_full_rate) }}</td><td class="px-4 py-3 text-right">{{ '%.2f'|format(row.additions_half_rate) }}</td><td class="px-4 py-3 text-right">{{ '%.2f'|format(row.reductions) }}</td><td class="px-4 py-3"><select name="rate__{{ row.ledger_key }}" class="rounded-lg border border-slate-300 px-2 py-1.5"><option value="0" {% if row.rate_percent == 0 %}selected{% endif %}>0%</option>{% for rate in [5,10,15,20,25,30,40,45,60,80,100] %}<option value="{{ rate }}" {% if row.rate_percent == rate %}selected{% endif %}>{{ rate }}%</option>{% endfor %}</select></td></tr>{% else %}<tr><td colspan="6" class="px-4 py-8 text-center text-slate-500">No ledgers under Fixed Assets were found in the synchronized master snapshot.</td></tr>{% endfor %}
|
<p class="mt-1 text-sm text-slate-500">Zero-opening ledgers with no current-period addition are omitted. Effective date is preferred for the 180-day test; voucher date is used when effective date is unavailable.</p>
|
||||||
|
<div class="mt-4 flex flex-wrap items-end gap-3 rounded-xl border border-slate-200 bg-slate-50 p-3">
|
||||||
|
<label class="flex items-center gap-2 text-sm font-medium text-slate-700"><input id="select-all-assets" type="checkbox" class="rounded border-slate-300"> Select all displayed ledgers</label>
|
||||||
|
<div><label class="block text-xs font-semibold uppercase tracking-wide text-slate-500">Bulk rate</label><select id="bulk-rate" class="mt-1 rounded-lg border border-slate-300 px-3 py-2 text-sm"><option value="">Select rate</option><option value="0">0%</option>{% for rate in [5,10,15,20,25,30,40,45,60,80,100] %}<option value="{{ rate }}">{{ rate }}%</option>{% endfor %}</select></div>
|
||||||
|
<button type="button" id="apply-bulk-rate" class="rounded-lg border border-brand-300 bg-white px-3 py-2 text-sm font-semibold text-brand-700 hover:bg-brand-50">Apply rate to selected</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto"><table class="min-w-full text-sm"><thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500"><tr><th class="px-4 py-3 w-10">Select</th><th class="px-4 py-3">Ledger</th><th class="px-4 py-3 text-right">Opening WDV</th><th class="px-4 py-3 text-right">Additions ≥180 days</th><th class="px-4 py-3 text-right">Additions <180 days</th><th class="px-4 py-3 text-right">Reductions</th><th class="px-4 py-3">Rate</th></tr></thead><tbody class="divide-y divide-slate-100">
|
||||||
|
{% for row in preview.fixed_asset_ledgers %}
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-3"><input type="checkbox" class="asset-select rounded border-slate-300" data-ledger-key="{{ row.ledger_key }}"></td>
|
||||||
|
<td class="px-4 py-3"><div class="font-medium text-slate-900">{{ row.ledger_name }}</div><div class="text-xs text-slate-500">{{ row.parent_group }}</div></td>
|
||||||
|
<td class="px-4 py-3 text-right">{{ '%.2f'|format(row.opening_wdv) }}</td>
|
||||||
|
<td class="px-4 py-3 text-right">{{ '%.2f'|format(row.additions_full_rate) }}</td>
|
||||||
|
<td class="px-4 py-3 text-right">{{ '%.2f'|format(row.additions_half_rate) }}</td>
|
||||||
|
<td class="px-4 py-3 text-right">{{ '%.2f'|format(row.reductions) }}</td>
|
||||||
|
<td class="px-4 py-3"><select id="rate-{{ loop.index0 }}" data-ledger-key="{{ row.ledger_key }}" name="rate__{{ row.ledger_key }}" class="asset-rate rounded-lg border border-slate-300 px-2 py-1.5"><option value="0" {% if row.rate_percent == 0 %}selected{% endif %}>0%</option>{% for rate in [5,10,15,20,25,30,40,45,60,80,100] %}<option value="{{ rate }}" {% if row.rate_percent == rate %}selected{% endif %}>{{ rate }}%</option>{% endfor %}</select></td>
|
||||||
|
</tr>
|
||||||
|
{% else %}<tr><td colspan="7" class="px-4 py-8 text-center text-slate-500">No active Fixed Asset ledgers were found. Zero-opening ledgers without additions are intentionally omitted.</td></tr>{% endfor %}
|
||||||
</tbody></table></div>
|
</tbody></table></div>
|
||||||
<div class="flex justify-end border-t border-slate-200 px-5 py-4"><button type="submit" {% if not coverage.complete or not preview.fixed_asset_ledgers %}disabled{% endif %} class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50">Calculate & Save Draft</button></div>
|
<div class="flex justify-end border-t border-slate-200 px-5 py-4"><button type="submit" {% if not preview.fixed_asset_ledgers %}disabled{% endif %} class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50">Calculate & Save Draft</button></div>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -100,4 +122,30 @@
|
|||||||
|
|
||||||
<div class="rounded-xl border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-800">Phase 6 enables only controlled depreciation Journal write-back. All master and transaction synchronization remain read-only; no other Tally create/alter/delete operation is enabled.</div>
|
<div class="rounded-xl border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-800">Phase 6 enables only controlled depreciation Journal write-back. All master and transaction synchronization remain read-only; no other Tally create/alter/delete operation is enabled.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const selectAll = document.getElementById('select-all-assets');
|
||||||
|
const bulkRate = document.getElementById('bulk-rate');
|
||||||
|
const applyButton = document.getElementById('apply-bulk-rate');
|
||||||
|
const selections = () => Array.from(document.querySelectorAll('.asset-select'));
|
||||||
|
if (selectAll) {
|
||||||
|
selectAll.addEventListener('change', () => {
|
||||||
|
selections().forEach(cb => { cb.checked = selectAll.checked; });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (applyButton) {
|
||||||
|
applyButton.addEventListener('click', () => {
|
||||||
|
const value = bulkRate ? bulkRate.value : '';
|
||||||
|
if (value === '') return;
|
||||||
|
selections().filter(cb => cb.checked).forEach(cb => {
|
||||||
|
const key = cb.dataset.ledgerKey;
|
||||||
|
const rate = Array.from(document.querySelectorAll('.asset-rate')).find(el => el.dataset.ledgerKey === key);
|
||||||
|
if (rate) rate.value = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import io
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
ERP_LOCAL_AGENT_VERSION = "1.7.1"
|
ERP_LOCAL_AGENT_VERSION = "1.8.0"
|
||||||
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
||||||
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
||||||
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
|
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
ERP Local Agent 1.7.1
|
ERP Local Agent 1.8.0
|
||||||
|
|
||||||
Existing storage, WebSocket tunnel, Tally and client .act functionality are preserved.
|
Existing storage, WebSocket tunnel, Tally and client .act functionality are preserved.
|
||||||
|
|
||||||
@@ -27,4 +27,11 @@ Phase 5 adds draft-only Income-tax depreciation calculations from synchronized .
|
|||||||
|
|
||||||
Phase 6: approved Income-tax depreciation Journal write-back is available only after explicit Partner approval and POST confirmation. All other Tally operations remain read-only.
|
Phase 6: approved Income-tax depreciation Journal write-back is available only after explicit Partner approval and POST confirmation. All other Tally operations remain read-only.
|
||||||
|
|
||||||
Hotfix 1.7.1: accepts phase6_status as a status alias; no accounting behavior changed.
|
Depreciation refinement 1.8.0:
|
||||||
|
- Accounting .act files default to <Local Agent>\data\accounting, separate from document STORAGE_ROOT.
|
||||||
|
- Existing legacy <STORAGE_ROOT>\Accounting client databases are copied automatically on first use and left in place as recovery copies.
|
||||||
|
- Current-FY transaction coverage is required only through today, not through a future FY-end date.
|
||||||
|
- Draft calculation is available with incomplete coverage; approval/write-back remains blocked until required coverage is complete.
|
||||||
|
- Empty Fixed Asset ledgers are omitted unless they have current-period additions.
|
||||||
|
- Bulk checkbox + rate assignment is available.
|
||||||
|
- Nested Tally accounting allocations and case-insensitive ledger matching improve fixed-asset additions detection.
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "1.7.1"
|
__version__ = "1.8.0"
|
||||||
AGENT_NAME = "ERP Local Agent"
|
AGENT_NAME = "ERP Local Agent"
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import date, datetime, timedelta, timezone
|
from datetime import date, datetime, timedelta, timezone
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
@@ -37,9 +39,32 @@ class LocalAccountingStore:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, storage_root: Path):
|
def __init__(self, storage_root: Path):
|
||||||
self.root = Path(storage_root).resolve() / "Accounting"
|
# Accounting databases are application data, not document-storage payloads.
|
||||||
|
# Keep them under the Local Agent data directory by default. An explicit
|
||||||
|
# ACCOUNTING_ROOT may override this without changing document STORAGE_ROOT.
|
||||||
|
configured = str(os.getenv("ACCOUNTING_ROOT", "") or "").strip()
|
||||||
|
install_root = Path(__file__).resolve().parents[1]
|
||||||
|
self.root = Path(configured).expanduser().resolve() if configured else (install_root / "data" / "accounting").resolve()
|
||||||
|
self.legacy_root = Path(storage_root).resolve() / "Accounting"
|
||||||
self.root.mkdir(parents=True, exist_ok=True)
|
self.root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _migrate_legacy_client_if_needed(self, client_id: int) -> None:
|
||||||
|
key = self._client_key(client_id)
|
||||||
|
new_dir = self.root / key
|
||||||
|
new_db = new_dir / f"{key}.act"
|
||||||
|
old_dir = self.legacy_root / key
|
||||||
|
old_db = old_dir / f"{key}.act"
|
||||||
|
if new_db.exists() or not old_db.exists():
|
||||||
|
return
|
||||||
|
new_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Copy, do not delete, the legacy database. The old copy remains a recovery
|
||||||
|
# fallback while all new reads/writes switch to Local Agent data/accounting.
|
||||||
|
shutil.copy2(old_db, new_db)
|
||||||
|
for suffix in ("-wal", "-shm"):
|
||||||
|
src = Path(str(old_db) + suffix)
|
||||||
|
if src.exists():
|
||||||
|
shutil.copy2(src, Path(str(new_db) + suffix))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _client_key(client_id: int) -> str:
|
def _client_key(client_id: int) -> str:
|
||||||
return f"client_{int(client_id):08d}"
|
return f"client_{int(client_id):08d}"
|
||||||
@@ -48,13 +73,16 @@ class LocalAccountingStore:
|
|||||||
return self.root / self._client_key(client_id)
|
return self.root / self._client_key(client_id)
|
||||||
|
|
||||||
def db_path(self, client_id: int) -> Path:
|
def db_path(self, client_id: int) -> Path:
|
||||||
|
self._migrate_legacy_client_if_needed(client_id)
|
||||||
key = self._client_key(client_id)
|
key = self._client_key(client_id)
|
||||||
return self.client_dir(client_id) / f"{key}.act"
|
return self.client_dir(client_id) / f"{key}.act"
|
||||||
|
|
||||||
def exists(self, client_id: int) -> bool:
|
def exists(self, client_id: int) -> bool:
|
||||||
|
self._migrate_legacy_client_if_needed(client_id)
|
||||||
return self.db_path(client_id).is_file()
|
return self.db_path(client_id).is_file()
|
||||||
|
|
||||||
def connect(self, client_id: int):
|
def connect(self, client_id: int):
|
||||||
|
self._migrate_legacy_client_if_needed(client_id)
|
||||||
path = self.db_path(client_id)
|
path = self.db_path(client_id)
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
db = sqlite3.connect(path, timeout=60)
|
db = sqlite3.connect(path, timeout=60)
|
||||||
@@ -633,6 +661,8 @@ class LocalAccountingStore:
|
|||||||
return fixed
|
return fixed
|
||||||
|
|
||||||
def _transaction_coverage(self, db: sqlite3.Connection, tally_guid: str, fy_start: date, fy_end: date) -> dict:
|
def _transaction_coverage(self, db: sqlite3.Connection, tally_guid: str, fy_start: date, fy_end: date) -> dict:
|
||||||
|
today = date.today()
|
||||||
|
required_end = min(fy_end, today)
|
||||||
rows = db.execute(
|
rows = db.execute(
|
||||||
"SELECT details_json FROM tally_sync_runs WHERE sync_type='transactions' AND tally_guid=? AND status='completed' ORDER BY id",
|
"SELECT details_json FROM tally_sync_runs WHERE sync_type='transactions' AND tally_guid=? AND status='completed' ORDER BY id",
|
||||||
(tally_guid,),
|
(tally_guid,),
|
||||||
@@ -642,8 +672,8 @@ class LocalAccountingStore:
|
|||||||
try:
|
try:
|
||||||
d = json.loads(row["details_json"] or "{}")
|
d = json.loads(row["details_json"] or "{}")
|
||||||
a = date.fromisoformat(str(d.get("date_from") or "")); b = date.fromisoformat(str(d.get("date_to") or ""))
|
a = date.fromisoformat(str(d.get("date_from") or "")); b = date.fromisoformat(str(d.get("date_to") or ""))
|
||||||
if b >= fy_start and a <= fy_end:
|
if b >= fy_start and a <= required_end:
|
||||||
ranges.append((max(a, fy_start), min(b, fy_end)))
|
ranges.append((max(a, fy_start), min(b, required_end)))
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
ranges.sort()
|
ranges.sort()
|
||||||
@@ -651,8 +681,23 @@ class LocalAccountingStore:
|
|||||||
for a,b in ranges:
|
for a,b in ranges:
|
||||||
if not merged or a > merged[-1][1] + timedelta(days=1): merged.append([a,b])
|
if not merged or a > merged[-1][1] + timedelta(days=1): merged.append([a,b])
|
||||||
elif b > merged[-1][1]: merged[-1][1] = b
|
elif b > merged[-1][1]: merged[-1][1] = b
|
||||||
complete = bool(merged and merged[0][0] <= fy_start and merged[-1][1] >= fy_end and all(merged[i][1] + timedelta(days=1) >= merged[i+1][0] for i in range(len(merged)-1)))
|
complete = bool(
|
||||||
return {"complete": complete, "ranges": [{"from":a.isoformat(),"to":b.isoformat()} for a,b in merged]}
|
required_end < fy_start
|
||||||
|
or (
|
||||||
|
merged
|
||||||
|
and merged[0][0] <= fy_start
|
||||||
|
and merged[-1][1] >= required_end
|
||||||
|
and all(merged[i][1] + timedelta(days=1) >= merged[i+1][0] for i in range(len(merged)-1))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"complete": bool(complete),
|
||||||
|
"ranges": [{"from":a.isoformat(),"to":b.isoformat()} for a,b in merged],
|
||||||
|
"required_from": fy_start.isoformat(),
|
||||||
|
"required_to": required_end.isoformat(),
|
||||||
|
"financial_year_end": fy_end.isoformat(),
|
||||||
|
"provisional": fy_end > today,
|
||||||
|
}
|
||||||
|
|
||||||
def depreciation_preview(self, client_id: int, *, tally_guid: str, fy_start: str, fy_end: str) -> dict:
|
def depreciation_preview(self, client_id: int, *, tally_guid: str, fy_start: str, fy_end: str) -> dict:
|
||||||
if not self.exists(client_id):
|
if not self.exists(client_id):
|
||||||
@@ -676,26 +721,42 @@ class LocalAccountingStore:
|
|||||||
for row in ledgers:
|
for row in ledgers:
|
||||||
if str(row["parent"] or "").strip().casefold() not in fixed_fold: continue
|
if str(row["parent"] or "").strip().casefold() not in fixed_fold: continue
|
||||||
item=dict(row); key=self._ledger_key(item); name=str(row["name"] or "").strip()
|
item=dict(row); key=self._ledger_key(item); name=str(row["name"] or "").strip()
|
||||||
|
movement_end = min(end, date.today())
|
||||||
entries=db.execute(
|
entries=db.execute(
|
||||||
"""SELECT v.voucher_date, e.amount, e.is_deemed_positive
|
"""SELECT COALESCE(NULLIF(v.effective_date,''), v.voucher_date) AS movement_date,
|
||||||
|
e.amount, e.is_deemed_positive
|
||||||
FROM tally_voucher_ledger_entries e JOIN tally_vouchers v ON v.id=e.voucher_id
|
FROM tally_voucher_ledger_entries e JOIN tally_vouchers v ON v.id=e.voucher_id
|
||||||
WHERE v.tally_guid=? AND e.ledger_name=? AND v.voucher_date>=? AND v.voucher_date<=?
|
WHERE v.tally_guid=?
|
||||||
|
AND lower(trim(COALESCE(e.ledger_name,'')))=lower(trim(?))
|
||||||
|
AND COALESCE(NULLIF(v.effective_date,''), v.voucher_date)>=?
|
||||||
|
AND COALESCE(NULLIF(v.effective_date,''), v.voucher_date)<=?
|
||||||
AND lower(COALESCE(v.is_cancelled,'')) NOT IN ('yes','true','1')
|
AND lower(COALESCE(v.is_cancelled,'')) NOT IN ('yes','true','1')
|
||||||
AND lower(COALESCE(v.is_optional,'')) NOT IN ('yes','true','1')""",
|
AND lower(COALESCE(v.is_optional,'')) NOT IN ('yes','true','1')""",
|
||||||
(tally_guid,name,start.isoformat(),end.isoformat()),
|
(tally_guid,name,start.isoformat(),movement_end.isoformat()),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
full=half=reductions=0.0
|
full=half=reductions=0.0
|
||||||
for e in entries:
|
for e in entries:
|
||||||
amt=abs(float(e["amount"] or 0))
|
amt=abs(float(e["amount"] or 0))
|
||||||
if self._is_debit_entry(e):
|
if self._is_debit_entry(e):
|
||||||
if self._half_rate_addition(str(e["voucher_date"] or ""), end): half += amt
|
if self._half_rate_addition(str(e["movement_date"] or ""), end): half += amt
|
||||||
else: full += amt
|
else: full += amt
|
||||||
else: reductions += amt
|
else: reductions += amt
|
||||||
|
opening_wdv = abs(float(row["opening_balance"] or 0))
|
||||||
|
tally_closing = abs(float(row["closing_balance"] or 0))
|
||||||
|
additions_total = round(full + half, 2)
|
||||||
|
|
||||||
|
# Do not clutter the depreciation schedule with empty Fixed Asset
|
||||||
|
# ledgers. A zero-opening ledger remains visible only when a current-
|
||||||
|
# period addition has actually been identified.
|
||||||
|
if opening_wdv == 0 and additions_total == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
result.append({
|
result.append({
|
||||||
"ledger_key":key,"ledger_guid":str(row["master_guid"] or ""),"ledger_name":name,"parent_group":str(row["parent"] or ""),
|
"ledger_key":key,"ledger_guid":str(row["master_guid"] or ""),"ledger_name":name,"parent_group":str(row["parent"] or ""),
|
||||||
"opening_wdv":abs(float(row["opening_balance"] or 0)),"tally_closing_balance":abs(float(row["closing_balance"] or 0)),
|
"opening_wdv":opening_wdv,"tally_closing_balance":tally_closing,
|
||||||
"additions_full_rate":round(full,2),"additions_half_rate":round(half,2),"reductions":round(reductions,2),
|
"additions_full_rate":round(full,2),"additions_half_rate":round(half,2),"reductions":round(reductions,2),
|
||||||
"rate_percent":rules.get(key,0.0),
|
"rate_percent":rules.get(key,0.0),
|
||||||
|
"movement_entries":len(entries),
|
||||||
})
|
})
|
||||||
all_ledgers=[str(r["name"] or "").strip() for r in ledgers if str(r["name"] or "").strip()]
|
all_ledgers=[str(r["name"] or "").strip() for r in ledgers if str(r["name"] or "").strip()]
|
||||||
latest=db.execute("SELECT id, fy_start, fy_end, total_depreciation, depreciation_expense_ledger, depreciation_reserve_ledger, created_at_utc FROM it_depreciation_runs WHERE tally_guid=? ORDER BY id DESC LIMIT 1",(tally_guid,)).fetchone()
|
latest=db.execute("SELECT id, fy_start, fy_end, total_depreciation, depreciation_expense_ledger, depreciation_reserve_ledger, created_at_utc FROM it_depreciation_runs WHERE tally_guid=? ORDER BY id DESC LIMIT 1",(tally_guid,)).fetchone()
|
||||||
@@ -703,8 +764,9 @@ class LocalAccountingStore:
|
|||||||
|
|
||||||
def calculate_it_depreciation(self, client_id: int, *, tally_guid: str, fy_start: str, fy_end: str, assignments: list[dict], depreciation_expense_ledger: str = "", depreciation_reserve_ledger: str = "", requested_by_user_id: int | None = None) -> dict:
|
def calculate_it_depreciation(self, client_id: int, *, tally_guid: str, fy_start: str, fy_end: str, assignments: list[dict], depreciation_expense_ledger: str = "", depreciation_reserve_ledger: str = "", requested_by_user_id: int | None = None) -> dict:
|
||||||
preview=self.depreciation_preview(client_id,tally_guid=tally_guid,fy_start=fy_start,fy_end=fy_end)
|
preview=self.depreciation_preview(client_id,tally_guid=tally_guid,fy_start=fy_start,fy_end=fy_end)
|
||||||
if not preview["transaction_coverage"].get("complete"):
|
# A draft may be calculated even when transaction coverage is incomplete so
|
||||||
raise ValueError("Phase 4 transaction data does not completely cover the selected financial year. Synchronize the full financial year before calculating depreciation.")
|
# the reviewer can work progressively. Approval/write-back remains blocked
|
||||||
|
# until Phase 4 covers the required period.
|
||||||
by_key={str(x.get("ledger_key")):x for x in preview["fixed_asset_ledgers"]}
|
by_key={str(x.get("ledger_key")):x for x in preview["fixed_asset_ledgers"]}
|
||||||
assignment_map={str(x.get("ledger_key") or ""): float(x.get("rate_percent") or 0) for x in assignments}
|
assignment_map={str(x.get("ledger_key") or ""): float(x.get("rate_percent") or 0) for x in assignments}
|
||||||
unknown=[k for k in assignment_map if k not in by_key]
|
unknown=[k for k in assignment_map if k not in by_key]
|
||||||
@@ -727,7 +789,7 @@ class LocalAccountingStore:
|
|||||||
db.execute("""INSERT INTO it_depreciation_rate_rules(tally_guid,ledger_key,ledger_guid,ledger_name,rate_percent,updated_by_user_id,updated_at_utc)
|
db.execute("""INSERT INTO it_depreciation_rate_rules(tally_guid,ledger_key,ledger_guid,ledger_name,rate_percent,updated_by_user_id,updated_at_utc)
|
||||||
VALUES(?,?,?,?,?,?,?) ON CONFLICT(tally_guid,ledger_key) DO UPDATE SET ledger_guid=excluded.ledger_guid,ledger_name=excluded.ledger_name,rate_percent=excluded.rate_percent,updated_by_user_id=excluded.updated_by_user_id,updated_at_utc=excluded.updated_at_utc""",
|
VALUES(?,?,?,?,?,?,?) ON CONFLICT(tally_guid,ledger_key) DO UPDATE SET ledger_guid=excluded.ledger_guid,ledger_name=excluded.ledger_name,rate_percent=excluded.rate_percent,updated_by_user_id=excluded.updated_by_user_id,updated_at_utc=excluded.updated_at_utc""",
|
||||||
(tally_guid,row["ledger_key"],row["ledger_guid"],row["ledger_name"],row["rate_percent"],requested_by_user_id,now))
|
(tally_guid,row["ledger_key"],row["ledger_guid"],row["ledger_name"],row["rate_percent"],requested_by_user_id,now))
|
||||||
details={"calculation_basis":"Ledger-level draft using synchronized opening balance and voucher movements; transaction date is used as the review proxy for put-to-use date for the 180-day test.","transaction_coverage":preview["transaction_coverage"],"no_tally_writeback":True}
|
details={"calculation_basis":"Ledger-level draft using synchronized opening balance and voucher movements; effective date is preferred, otherwise voucher date is used as the review proxy for the 180-day put-to-use test.","transaction_coverage":preview["transaction_coverage"],"provisional":bool(preview["transaction_coverage"].get("provisional")),"no_tally_writeback":True}
|
||||||
cur=db.execute("""INSERT INTO it_depreciation_runs(tally_guid,company_name,mapping_id,fy_start,fy_end,depreciation_expense_ledger,depreciation_reserve_ledger,requested_by_user_id,status,created_at_utc,total_depreciation,details_json)
|
cur=db.execute("""INSERT INTO it_depreciation_runs(tally_guid,company_name,mapping_id,fy_start,fy_end,depreciation_expense_ledger,depreciation_reserve_ledger,requested_by_user_id,status,created_at_utc,total_depreciation,details_json)
|
||||||
VALUES(?,?,?,?,?,?,?,?, 'draft',?,?,?)""",
|
VALUES(?,?,?,?,?,?,?,?, 'draft',?,?,?)""",
|
||||||
(tally_guid,str(mapping.get("company_name") or ""),mapping.get("id"),str(fy_start),str(fy_end),str(depreciation_expense_ledger or ""),str(depreciation_reserve_ledger or ""),requested_by_user_id,now,round(total,2),json.dumps(details,ensure_ascii=False,separators=(",",":"))))
|
(tally_guid,str(mapping.get("company_name") or ""),mapping.get("id"),str(fy_start),str(fy_end),str(depreciation_expense_ledger or ""),str(depreciation_reserve_ledger or ""),requested_by_user_id,now,round(total,2),json.dumps(details,ensure_ascii=False,separators=(",",":"))))
|
||||||
@@ -762,6 +824,14 @@ class LocalAccountingStore:
|
|||||||
raise ValueError(f"Depreciation draft cannot be approved from status '{status}'.")
|
raise ValueError(f"Depreciation draft cannot be approved from status '{status}'.")
|
||||||
if float(run["total_depreciation"] or 0) <= 0:
|
if float(run["total_depreciation"] or 0) <= 0:
|
||||||
raise ValueError("Depreciation amount must be greater than zero before approval.")
|
raise ValueError("Depreciation amount must be greater than zero before approval.")
|
||||||
|
try:
|
||||||
|
run_details = json.loads(run["details_json"] or "{}")
|
||||||
|
except Exception:
|
||||||
|
run_details = {}
|
||||||
|
coverage = run_details.get("transaction_coverage") or {}
|
||||||
|
if not coverage.get("complete"):
|
||||||
|
required_to = str(coverage.get("required_to") or "the required period")
|
||||||
|
raise ValueError(f"Phase 4 transactions must be synchronized through {required_to} before this draft can be approved.")
|
||||||
if not str(run["depreciation_expense_ledger"] or "").strip() or not str(run["depreciation_reserve_ledger"] or "").strip():
|
if not str(run["depreciation_expense_ledger"] or "").strip() or not str(run["depreciation_reserve_ledger"] or "").strip():
|
||||||
raise ValueError("Select both the depreciation expense ledger and the depreciation reserve/credit ledger before approval.")
|
raise ValueError("Select both the depreciation expense ledger and the depreciation reserve/credit ledger before approval.")
|
||||||
db.execute(
|
db.execute(
|
||||||
|
|||||||
@@ -382,7 +382,20 @@ class TallyLiveConnector:
|
|||||||
ledger_entries: list[dict] = []
|
ledger_entries: list[dict] = []
|
||||||
inventory_entries: list[dict] = []
|
inventory_entries: list[dict] = []
|
||||||
|
|
||||||
for entry in _direct_children(voucher, ledger_tags):
|
# Tally can emit ledger allocations directly under VOUCHER or nested
|
||||||
|
# inside invoice/inventory structures. Read both forms once.
|
||||||
|
ledger_nodes = []
|
||||||
|
seen_ledger_nodes = set()
|
||||||
|
for entry in voucher.iter():
|
||||||
|
if entry is voucher or _tag(entry) not in (ledger_tags | {"ACCOUNTINGALLOCATIONS.LIST"}):
|
||||||
|
continue
|
||||||
|
marker = id(entry)
|
||||||
|
if marker in seen_ledger_nodes:
|
||||||
|
continue
|
||||||
|
seen_ledger_nodes.add(marker)
|
||||||
|
ledger_nodes.append(entry)
|
||||||
|
|
||||||
|
for entry in ledger_nodes:
|
||||||
bills = []
|
bills = []
|
||||||
banks = []
|
banks = []
|
||||||
cost_centres = []
|
cost_centres = []
|
||||||
|
|||||||
Reference in New Issue
Block a user