Integrate read-only accounting mirror analytics runtime
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
__version__ = "1.22.23"
|
||||
__version__ = "1.23.0"
|
||||
AGENT_NAME = "ERP Local Agent"
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
|
||||
MIRROR_SCHEMA_VERSION = "3.0.1"
|
||||
DEFAULT_DSN = "TallyODBC64_9000"
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def _text(value: Any) -> str:
|
||||
return str(value or "").replace("\x04", "").strip()
|
||||
|
||||
|
||||
def _float(value: Any) -> float:
|
||||
try:
|
||||
return float(value or 0)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MirrorPaths:
|
||||
accounting_db: Path
|
||||
mirror_db: Path
|
||||
work_root: Path
|
||||
extractor_exe: Path
|
||||
tdl_file: Path
|
||||
loader_file: Path
|
||||
|
||||
|
||||
class AccountingMirrorEngine:
|
||||
"""Read-only Tally -> local SQLite accounting mirror.
|
||||
|
||||
The existing client .act database is deliberately left untouched.
|
||||
The mirror lives beside it as client_XXXXXXXX_mirror.act so existing
|
||||
accounting, cash-payment, depreciation, storage and review features
|
||||
continue to use their current database and business logic unchanged.
|
||||
"""
|
||||
|
||||
def __init__(self, store, logger):
|
||||
self.store = store
|
||||
self.logger = logger
|
||||
self.runtime_root = Path(__file__).resolve().parent / "mirror_runtime"
|
||||
self.agent_data_root = Path(__file__).resolve().parents[1] / "data" / "accounting_mirror"
|
||||
self.agent_data_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def paths(self, client_id: int) -> MirrorPaths:
|
||||
accounting_db = self.store.db_path(int(client_id))
|
||||
key = accounting_db.stem
|
||||
mirror_db = accounting_db.with_name(f"{key}_mirror.act")
|
||||
runtime_dir = self.agent_data_root / "runtime"
|
||||
runtime_dir.mkdir(parents=True, exist_ok=True)
|
||||
return MirrorPaths(
|
||||
accounting_db=accounting_db,
|
||||
mirror_db=mirror_db,
|
||||
work_root=accounting_db.parent / ".mirror_sync",
|
||||
extractor_exe=runtime_dir / "TallyAccountingMirrorV301.exe",
|
||||
tdl_file=self.runtime_root / "ERP_Accounting_Mirror_v3_0_1.tdl",
|
||||
loader_file=self.runtime_root / "build_act_v3_0_1.py",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _find_csc() -> Path:
|
||||
windir = Path(os.environ.get("WINDIR") or r"C:\Windows")
|
||||
candidates = [
|
||||
windir / "Microsoft.NET/Framework64/v4.0.30319/csc.exe",
|
||||
windir / "Microsoft.NET/Framework/v4.0.30319/csc.exe",
|
||||
]
|
||||
for candidate in candidates:
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
raise RuntimeError(
|
||||
"Microsoft .NET Framework C# compiler (csc.exe) was not found. "
|
||||
"Windows .NET Framework 4.x is required for the Tally ODBC mirror reader."
|
||||
)
|
||||
|
||||
def _ensure_extractor(self, paths: MirrorPaths) -> None:
|
||||
source = self.runtime_root / "TallyAccountingMirrorV301.cs"
|
||||
if not source.is_file():
|
||||
raise RuntimeError(f"Accounting Mirror C# source is missing: {source}")
|
||||
rebuild = not paths.extractor_exe.is_file()
|
||||
if not rebuild:
|
||||
try:
|
||||
rebuild = source.stat().st_mtime > paths.extractor_exe.stat().st_mtime
|
||||
except OSError:
|
||||
rebuild = True
|
||||
if not rebuild:
|
||||
return
|
||||
csc = self._find_csc()
|
||||
paths.extractor_exe.parent.mkdir(parents=True, exist_ok=True)
|
||||
cmd = [
|
||||
str(csc),
|
||||
"/nologo",
|
||||
"/target:exe",
|
||||
f"/out:{paths.extractor_exe}",
|
||||
"/reference:System.Data.dll",
|
||||
str(source),
|
||||
]
|
||||
cp = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
if cp.returncode != 0 or not paths.extractor_exe.is_file():
|
||||
detail = (cp.stdout or "") + "\n" + (cp.stderr or "")
|
||||
raise RuntimeError("Accounting Mirror extractor build failed: " + detail.strip())
|
||||
|
||||
@staticmethod
|
||||
def _database_summary(db_path: Path) -> dict[str, Any]:
|
||||
if not db_path.is_file():
|
||||
return {"ready": False}
|
||||
db = sqlite3.connect(db_path, timeout=30)
|
||||
db.row_factory = sqlite3.Row
|
||||
try:
|
||||
sync = db.execute(
|
||||
"SELECT * FROM sync_state ORDER BY last_sync_at DESC LIMIT 1"
|
||||
).fetchone()
|
||||
company = db.execute(
|
||||
"SELECT * FROM company_master ORDER BY synced_at DESC LIMIT 1"
|
||||
).fetchone()
|
||||
counts = {}
|
||||
for table in (
|
||||
"ledger_master",
|
||||
"stock_item_master",
|
||||
"stock_item_hsn_detail",
|
||||
"stock_item_gst_detail",
|
||||
"voucher",
|
||||
"voucher_ledger_entry",
|
||||
"inventory_movement",
|
||||
):
|
||||
try:
|
||||
counts[table] = int(db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0])
|
||||
except sqlite3.Error:
|
||||
counts[table] = 0
|
||||
unbalanced = 0
|
||||
try:
|
||||
unbalanced = int(
|
||||
db.execute(
|
||||
"SELECT COUNT(*) FROM voucher_balance_check "
|
||||
"WHERE ABS(COALESCE(difference,0)) > 0.01"
|
||||
).fetchone()[0]
|
||||
)
|
||||
except sqlite3.Error:
|
||||
pass
|
||||
return {
|
||||
"ready": True,
|
||||
"path": str(db_path),
|
||||
"size_bytes": db_path.stat().st_size,
|
||||
"company": dict(company) if company else {},
|
||||
"sync": dict(sync) if sync else {},
|
||||
"counts": counts,
|
||||
"unbalanced_vouchers": unbalanced,
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def status(self, client_id: int) -> dict[str, Any]:
|
||||
paths = self.paths(client_id)
|
||||
return {
|
||||
"mirror_schema_version": MIRROR_SCHEMA_VERSION,
|
||||
"client_id": int(client_id),
|
||||
"tdl_path": str(paths.tdl_file),
|
||||
"tdl_exists": paths.tdl_file.is_file(),
|
||||
"extractor_ready": paths.extractor_exe.is_file(),
|
||||
"accounting_db_path": str(paths.accounting_db),
|
||||
"mirror_db_path": str(paths.mirror_db),
|
||||
"mirror": self._database_summary(paths.mirror_db),
|
||||
}
|
||||
|
||||
def sync(
|
||||
self,
|
||||
*,
|
||||
client_id: int,
|
||||
expected_company_name: str = "",
|
||||
expected_company_guid: str = "",
|
||||
dsn: str = DEFAULT_DSN,
|
||||
timeout_seconds: int = 300,
|
||||
) -> dict[str, Any]:
|
||||
paths = self.paths(client_id)
|
||||
if not paths.accounting_db.parent.exists():
|
||||
paths.accounting_db.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not paths.tdl_file.is_file() or not paths.loader_file.is_file():
|
||||
raise RuntimeError("Accounting Mirror runtime resources are incomplete.")
|
||||
|
||||
self._ensure_extractor(paths)
|
||||
|
||||
shutil.rmtree(paths.work_root, ignore_errors=True)
|
||||
paths.work_root.mkdir(parents=True, exist_ok=True)
|
||||
output_dir = paths.work_root / "mirror_output_v3_0_1"
|
||||
|
||||
cmd = [
|
||||
str(paths.extractor_exe),
|
||||
"--dsn",
|
||||
str(dsn or DEFAULT_DSN),
|
||||
"--out",
|
||||
str(output_dir),
|
||||
]
|
||||
self.logger.info(
|
||||
"Accounting Mirror extraction starting client_id=%s expected_company=%s",
|
||||
client_id,
|
||||
expected_company_name,
|
||||
)
|
||||
try:
|
||||
cp = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=max(30, int(timeout_seconds)),
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise RuntimeError(
|
||||
f"Accounting Mirror ODBC extraction timed out after {timeout_seconds} seconds."
|
||||
) from exc
|
||||
|
||||
extraction_log = ((cp.stdout or "") + "\n" + (cp.stderr or "")).strip()
|
||||
(paths.work_root / "extractor.log").write_text(extraction_log, encoding="utf-8")
|
||||
if cp.returncode != 0:
|
||||
raise RuntimeError("Accounting Mirror ODBC extraction failed. " + extraction_log[-4000:])
|
||||
|
||||
loader_copy = paths.work_root / "build_act_v3_0_1.py"
|
||||
shutil.copy2(paths.loader_file, loader_copy)
|
||||
loader = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(loader_copy),
|
||||
"--source",
|
||||
output_dir.name,
|
||||
],
|
||||
cwd=str(paths.work_root),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=max(60, int(timeout_seconds)),
|
||||
)
|
||||
loader_log = ((loader.stdout or "") + "\n" + (loader.stderr or "")).strip()
|
||||
(paths.work_root / "loader.log").write_text(loader_log, encoding="utf-8")
|
||||
if loader.returncode != 0:
|
||||
raise RuntimeError("Accounting Mirror SQLite build failed. " + loader_log[-4000:])
|
||||
|
||||
candidate = output_dir / "accounting_mirror_v3_0_1.act"
|
||||
if not candidate.is_file():
|
||||
raise RuntimeError("Accounting Mirror SQLite database was not produced.")
|
||||
|
||||
candidate_summary = self._database_summary(candidate)
|
||||
company = candidate_summary.get("company") or {}
|
||||
actual_name = _text(company.get("company_name"))
|
||||
actual_guid = _text(company.get("company_guid"))
|
||||
exp_name = _text(expected_company_name)
|
||||
exp_guid = _text(expected_company_guid)
|
||||
|
||||
if exp_guid and actual_guid and exp_guid != actual_guid:
|
||||
raise RuntimeError(
|
||||
f"Mirror company GUID mismatch. ERP expected {exp_guid}, Tally returned {actual_guid}."
|
||||
)
|
||||
if exp_name and actual_name and exp_name.casefold() != actual_name.casefold():
|
||||
raise RuntimeError(
|
||||
f"Mirror company mismatch. ERP expected '{exp_name}', Tally returned '{actual_name}'."
|
||||
)
|
||||
|
||||
paths.mirror_db.parent.mkdir(parents=True, exist_ok=True)
|
||||
temp_target = paths.mirror_db.with_suffix(".act.new")
|
||||
if temp_target.exists():
|
||||
temp_target.unlink()
|
||||
|
||||
src = sqlite3.connect(candidate, timeout=60)
|
||||
dst = sqlite3.connect(temp_target, timeout=60)
|
||||
try:
|
||||
src.backup(dst)
|
||||
dst.commit()
|
||||
finally:
|
||||
dst.close()
|
||||
src.close()
|
||||
temp_target.replace(paths.mirror_db)
|
||||
|
||||
summary = self._database_summary(paths.mirror_db)
|
||||
self.logger.info(
|
||||
"Accounting Mirror sync completed client_id=%s company=%s vouchers=%s",
|
||||
client_id,
|
||||
actual_name,
|
||||
(summary.get("counts") or {}).get("voucher"),
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"synced_at_utc": _utc_now(),
|
||||
"extractor_log_tail": extraction_log[-2000:],
|
||||
"loader_log_tail": loader_log[-2000:],
|
||||
**self.status(client_id),
|
||||
}
|
||||
|
||||
def _connect(self, client_id: int) -> sqlite3.Connection:
|
||||
path = self.paths(client_id).mirror_db
|
||||
if not path.is_file():
|
||||
raise RuntimeError(
|
||||
"Accounting Mirror is not available for this client/FY. Run Mirror Sync first."
|
||||
)
|
||||
db = sqlite3.connect(path, timeout=30)
|
||||
db.row_factory = sqlite3.Row
|
||||
db.execute("PRAGMA query_only=ON")
|
||||
return db
|
||||
|
||||
@staticmethod
|
||||
def _rows(db: sqlite3.Connection, sql: str, params=()) -> list[dict[str, Any]]:
|
||||
return [dict(row) for row in db.execute(sql, params).fetchall()]
|
||||
|
||||
def query(self, client_id: int, query_name: str, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
filters = filters or {}
|
||||
name = _text(query_name).lower()
|
||||
limit = max(1, min(5000, int(filters.get("limit") or 500)))
|
||||
from_date = _text(filters.get("from_date"))
|
||||
to_date = _text(filters.get("to_date"))
|
||||
db = self._connect(client_id)
|
||||
try:
|
||||
if name == "summary":
|
||||
return self._database_summary(self.paths(client_id).mirror_db)
|
||||
|
||||
if name == "daybook":
|
||||
where = ["1=1"]
|
||||
params: list[Any] = []
|
||||
if from_date:
|
||||
where.append("date >= ?"); params.append(from_date)
|
||||
if to_date:
|
||||
where.append("date <= ?"); params.append(to_date)
|
||||
rows = self._rows(
|
||||
db,
|
||||
"SELECT * FROM daybook_flat WHERE " + " AND ".join(where)
|
||||
+ " ORDER BY date,voucher_type,voucher_number LIMIT ?",
|
||||
(*params, limit),
|
||||
)
|
||||
return {"rows": rows, "count": len(rows)}
|
||||
|
||||
if name == "trial_balance":
|
||||
rows = self._rows(
|
||||
db,
|
||||
"""
|
||||
SELECT
|
||||
lm.ledger_guid,
|
||||
lm.ledger_name,
|
||||
lm.parent_group,
|
||||
COALESCE(lm.opening_balance,0) AS opening_balance,
|
||||
ROUND(COALESCE(SUM(CASE WHEN vle.dr_cr='DR' THEN vle.amount ELSE 0 END),0),2) AS debit,
|
||||
ROUND(COALESCE(SUM(CASE WHEN vle.dr_cr='CR' THEN vle.amount ELSE 0 END),0),2) AS credit,
|
||||
ROUND(COALESCE(lm.opening_balance,0)
|
||||
+ COALESCE(SUM(CASE WHEN vle.dr_cr='CR' THEN vle.amount ELSE 0 END),0)
|
||||
- COALESCE(SUM(CASE WHEN vle.dr_cr='DR' THEN vle.amount ELSE 0 END),0),2) AS closing_balance
|
||||
FROM ledger_master lm
|
||||
LEFT JOIN voucher_ledger_entry vle
|
||||
ON vle.ledger_name = lm.ledger_name
|
||||
GROUP BY lm.ledger_guid,lm.ledger_name,lm.parent_group,lm.opening_balance
|
||||
ORDER BY lm.parent_group,lm.ledger_name
|
||||
LIMIT ?
|
||||
""",
|
||||
(limit,),
|
||||
)
|
||||
return {"rows": rows, "count": len(rows)}
|
||||
|
||||
if name == "ledger_transactions":
|
||||
ledger = _text(filters.get("ledger_name"))
|
||||
if not ledger:
|
||||
raise ValueError("ledger_name is required.")
|
||||
where = ["ledger_name = ?"]
|
||||
params: list[Any] = [ledger]
|
||||
if from_date:
|
||||
where.append("voucher_date >= ?"); params.append(from_date)
|
||||
if to_date:
|
||||
where.append("voucher_date <= ?"); params.append(to_date)
|
||||
rows = self._rows(
|
||||
db,
|
||||
"""
|
||||
SELECT voucher_date,voucher_type,voucher_number,party_ledger,
|
||||
voucher_guid,dr_cr,amount,raw_amount,alter_id
|
||||
FROM voucher_ledger_entry
|
||||
WHERE """ + " AND ".join(where) +
|
||||
" ORDER BY voucher_date,voucher_number,line_no LIMIT ?",
|
||||
(*params, limit),
|
||||
)
|
||||
return {"ledger_name": ledger, "rows": rows, "count": len(rows)}
|
||||
|
||||
if name == "inventory":
|
||||
item = _text(filters.get("stock_item_name"))
|
||||
where = ["1=1"]
|
||||
params: list[Any] = []
|
||||
if item:
|
||||
where.append("stock_item_name = ?"); params.append(item)
|
||||
if from_date:
|
||||
where.append("voucher_date >= ?"); params.append(from_date)
|
||||
if to_date:
|
||||
where.append("voucher_date <= ?"); params.append(to_date)
|
||||
rows = self._rows(
|
||||
db,
|
||||
"""
|
||||
SELECT voucher_date,voucher_type,voucher_number,party_ledger,
|
||||
stock_item_name,hsn,gst_rate,cgst_rate,sgst_rate,igst_rate,cess_rate,
|
||||
direction,signed_qty,uom,rate,signed_value,voucher_guid
|
||||
FROM inventory_movement
|
||||
WHERE """ + " AND ".join(where) +
|
||||
" ORDER BY voucher_date,voucher_number,line_no LIMIT ?",
|
||||
(*params, limit),
|
||||
)
|
||||
return {"rows": rows, "count": len(rows)}
|
||||
|
||||
if name == "stock_items":
|
||||
rows = self._rows(
|
||||
db,
|
||||
"""
|
||||
SELECT product_name,parent_group,base_uom,opening_qty,opening_value,
|
||||
current_hsn,current_gst_rate,gst_type_of_supply,gst_applicable,stock_item_guid
|
||||
FROM stock_item_master ORDER BY product_name LIMIT ?
|
||||
""",
|
||||
(limit,),
|
||||
)
|
||||
return {"rows": rows, "count": len(rows)}
|
||||
|
||||
if name == "hsn_history":
|
||||
item = _text(filters.get("stock_item_name"))
|
||||
if not item:
|
||||
raise ValueError("stock_item_name is required.")
|
||||
rows = self._rows(
|
||||
db,
|
||||
"""
|
||||
SELECT stock_item_name,history_position,applicable_from,hsn_source,
|
||||
hsn_code,hsn_value,effective_hsn,description
|
||||
FROM stock_item_hsn_detail
|
||||
WHERE stock_item_name = ? COLLATE NOCASE
|
||||
ORDER BY applicable_from,history_position
|
||||
""",
|
||||
(item,),
|
||||
)
|
||||
return {"rows": rows, "count": len(rows)}
|
||||
|
||||
if name == "gst_history":
|
||||
item = _text(filters.get("stock_item_name"))
|
||||
if not item:
|
||||
raise ValueError("stock_item_name is required.")
|
||||
rows = self._rows(
|
||||
db,
|
||||
"""
|
||||
SELECT stock_item_name,history_position,applicable_from,gst_source,taxability,
|
||||
supply_type,reporting_uom,state_name,cgst_rate,sgst_rate,igst_rate,
|
||||
cess_rate,total_gst_rate
|
||||
FROM stock_item_gst_detail
|
||||
WHERE stock_item_name = ? COLLATE NOCASE
|
||||
ORDER BY applicable_from,history_position
|
||||
""",
|
||||
(item,),
|
||||
)
|
||||
return {"rows": rows, "count": len(rows)}
|
||||
|
||||
if name == "cash_transactions":
|
||||
ledger = _text(filters.get("ledger_name")) or "Cash"
|
||||
where = ["ledger_name = ?"]
|
||||
params: list[Any] = [ledger]
|
||||
if from_date:
|
||||
where.append("voucher_date >= ?"); params.append(from_date)
|
||||
if to_date:
|
||||
where.append("voucher_date <= ?"); params.append(to_date)
|
||||
rows = self._rows(
|
||||
db,
|
||||
"""
|
||||
SELECT voucher_date,voucher_type,voucher_number,party_ledger,
|
||||
ledger_name,dr_cr,amount,voucher_guid
|
||||
FROM voucher_ledger_entry
|
||||
WHERE """ + " AND ".join(where) +
|
||||
" ORDER BY voucher_date,voucher_number,line_no LIMIT ?",
|
||||
(*params, limit),
|
||||
)
|
||||
return {"rows": rows, "count": len(rows)}
|
||||
|
||||
if name == "voucher":
|
||||
guid = _text(filters.get("voucher_guid"))
|
||||
if not guid:
|
||||
raise ValueError("voucher_guid is required.")
|
||||
voucher = db.execute("SELECT * FROM voucher WHERE voucher_guid=?", (guid,)).fetchone()
|
||||
ledger_rows = self._rows(
|
||||
db,
|
||||
"SELECT * FROM voucher_ledger_entry WHERE voucher_guid=? ORDER BY line_no",
|
||||
(guid,),
|
||||
)
|
||||
inventory_rows = self._rows(
|
||||
db,
|
||||
"SELECT * FROM inventory_movement WHERE voucher_guid=? ORDER BY line_no",
|
||||
(guid,),
|
||||
)
|
||||
return {
|
||||
"voucher": dict(voucher) if voucher else None,
|
||||
"ledger_entries": ledger_rows,
|
||||
"inventory_entries": inventory_rows,
|
||||
}
|
||||
|
||||
if name == "exceptions":
|
||||
rows = self._rows(
|
||||
db,
|
||||
"""
|
||||
SELECT * FROM voucher_balance_check
|
||||
WHERE ABS(COALESCE(difference,0)) > 0.01
|
||||
ORDER BY ABS(difference) DESC LIMIT ?
|
||||
""",
|
||||
(limit,),
|
||||
)
|
||||
return {"unbalanced_vouchers": rows, "count": len(rows)}
|
||||
|
||||
raise ValueError(
|
||||
"Unsupported mirror query. Supported: summary, daybook, trial_balance, "
|
||||
"ledger_transactions, inventory, stock_items, hsn_history, gst_history, "
|
||||
"cash_transactions, voucher, exceptions."
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
@@ -13,6 +13,7 @@ from . import __version__
|
||||
from .accounting_store import LocalAccountingStore
|
||||
from .tally import TallyLiveConnector
|
||||
from .native_voucher_engine import NativeVoucherEngine
|
||||
from .accounting_mirror import AccountingMirrorEngine
|
||||
|
||||
|
||||
_CASH_TALLY_EXTRACTION_LOCK = threading.Lock()
|
||||
@@ -66,6 +67,7 @@ class AgentCommandProcessor:
|
||||
self.logger = logger
|
||||
self.store = LocalAccountingStore(config.storage_root)
|
||||
self.tally = TallyLiveConnector()
|
||||
self.mirror = AccountingMirrorEngine(self.store, logger)
|
||||
|
||||
def process(self, command: dict[str, Any]) -> dict[str, Any]:
|
||||
command_id = str(command.get("command_id") or "").strip()
|
||||
@@ -144,6 +146,12 @@ class AgentCommandProcessor:
|
||||
result = self._approve_it_depreciation(payload)
|
||||
elif action == "accounting_post_it_depreciation":
|
||||
result = self._post_it_depreciation(payload)
|
||||
elif action == "accounting_mirror_status":
|
||||
result = self._accounting_mirror_status(payload)
|
||||
elif action == "accounting_mirror_sync":
|
||||
result = self._accounting_mirror_sync(payload)
|
||||
elif action == "accounting_mirror_query":
|
||||
result = self._accounting_mirror_query(payload)
|
||||
else:
|
||||
raise ValueError(f"Unsupported local-agent command: {action}")
|
||||
ok = True
|
||||
@@ -2791,3 +2799,51 @@ class AgentCommandProcessor:
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
|
||||
def _accounting_mirror_status(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
client_id = int(payload.get("client_id") or 0)
|
||||
if client_id <= 0:
|
||||
raise ValueError("client_id is required.")
|
||||
return self.mirror.status(client_id)
|
||||
|
||||
def _accounting_mirror_sync(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
client_id = int(payload.get("client_id") or 0)
|
||||
if client_id <= 0:
|
||||
raise ValueError("client_id is required.")
|
||||
|
||||
expected_company_name = str(
|
||||
payload.get("company_name")
|
||||
or payload.get("tally_company_name")
|
||||
or payload.get("expected_company_name")
|
||||
or ""
|
||||
).strip()
|
||||
expected_company_guid = str(
|
||||
payload.get("company_guid")
|
||||
or payload.get("tally_guid")
|
||||
or payload.get("expected_company_guid")
|
||||
or ""
|
||||
).strip()
|
||||
|
||||
# The extractor reads the active Tally company/period. The produced
|
||||
# SQLite mirror is verified against the ERP-selected company before
|
||||
# the current mirror database is replaced.
|
||||
return self.mirror.sync(
|
||||
client_id=client_id,
|
||||
expected_company_name=expected_company_name,
|
||||
expected_company_guid=expected_company_guid,
|
||||
dsn=str(payload.get("dsn") or "TallyODBC64_9000").strip(),
|
||||
timeout_seconds=int(payload.get("timeout_seconds") or 300),
|
||||
)
|
||||
|
||||
def _accounting_mirror_query(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
client_id = int(payload.get("client_id") or 0)
|
||||
if client_id <= 0:
|
||||
raise ValueError("client_id is required.")
|
||||
query_name = str(payload.get("query") or payload.get("query_name") or "").strip()
|
||||
if not query_name:
|
||||
raise ValueError("query is required.")
|
||||
filters = payload.get("filters") or {}
|
||||
if not isinstance(filters, dict):
|
||||
raise ValueError("filters must be an object.")
|
||||
return self.mirror.query(client_id, query_name, filters)
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
;; ============================================================
|
||||
;; ERP Accounting Mirror - Unified GST v2.3
|
||||
;; SINGLE TDL / READ ONLY / NO XML / NO TALLY WRITES
|
||||
;; ============================================================
|
||||
|
||||
;; ================================================================
|
||||
;; ERP Accounting Mirror v2
|
||||
;; READ-ONLY Tally ODBC collections.
|
||||
;; Uses the active Tally company and active Tally period.
|
||||
;; ================================================================
|
||||
|
||||
[Collection : ERPAMCompanyInfo]
|
||||
Type : Company
|
||||
Fetch : Name, GUID, AlterID, StartingFrom, BooksFrom
|
||||
Is ODBC Table : Yes
|
||||
|
||||
[Collection : ERPAMLedgerMaster]
|
||||
Type : Ledger
|
||||
Fetch : Name, Parent, GUID, AlterID, OpeningBalance, ClosingBalance, IsRevenue, PartyGSTIN, GSTRegistrationType
|
||||
Is ODBC Table : Yes
|
||||
|
||||
[Collection : ERPAMStockMaster]
|
||||
Type : StockItem
|
||||
Fetch : Name, Parent, GUID, AlterID, BaseUnits, OpeningBalance, OpeningValue, GSTApplicable, GSTRepUoM, GSTTypeofSupply, GSTItemHSNCodeEx
|
||||
Is ODBC Table : Yes
|
||||
|
||||
[Collection : ERPAMVoucherSource]
|
||||
Type : Voucher
|
||||
Fetch : GUID, AlterID, MasterID, Date, VoucherTypeName, VoucherNumber, PartyLedgerName, Narration, Reference, Amount, IsCancelled, IsOptional
|
||||
|
||||
[Collection : ERPAMVoucherHeader]
|
||||
Source Collection : ERPAMVoucherSource
|
||||
Compute : VoucherGUID : $GUID
|
||||
Compute : VoucherAlterID : $AlterID
|
||||
Compute : VoucherMasterID : $MasterID
|
||||
Compute : VoucherDate : $Date
|
||||
Compute : VoucherType : $VoucherTypeName
|
||||
Compute : VoucherNumber : $VoucherNumber
|
||||
Compute : PartyLedger : $PartyLedgerName
|
||||
Compute : VoucherNarration : $Narration
|
||||
Compute : VoucherReference : $Reference
|
||||
Compute : VoucherAmount : $Amount
|
||||
Compute : IsCancelled : $IsCancelled
|
||||
Compute : IsOptional : $IsOptional
|
||||
Is ODBC Table : Yes
|
||||
|
||||
[Collection : ERPAMVoucherLedger]
|
||||
Source Collection : ERPAMVoucherSource
|
||||
Walk : All Ledger Entries
|
||||
By : VoucherGUID : $$Owner:$GUID
|
||||
By : VoucherAlterID : $$Owner:$AlterID
|
||||
By : VoucherMasterID : $$Owner:$MasterID
|
||||
By : VoucherDate : $$Owner:$Date
|
||||
By : VoucherType : $$Owner:$VoucherTypeName
|
||||
By : VoucherNumber : $$Owner:$VoucherNumber
|
||||
By : PartyLedger : $$Owner:$PartyLedgerName
|
||||
By : LedgerName : $LedgerName
|
||||
By : IsDeemedPositive : $IsDeemedPositive
|
||||
Aggr Compute : LedgerAmount : Sum : $Amount
|
||||
Is ODBC Table : Yes
|
||||
|
||||
[Collection : ERPAMInventoryMovement]
|
||||
Source Collection : ERPAMVoucherSource
|
||||
Walk : Inventory Entries
|
||||
By : VoucherGUID : $$Owner:$GUID
|
||||
By : VoucherAlterID : $$Owner:$AlterID
|
||||
By : VoucherMasterID : $$Owner:$MasterID
|
||||
By : VoucherDate : $$Owner:$Date
|
||||
By : VoucherType : $$Owner:$VoucherTypeName
|
||||
By : VoucherNumber : $$Owner:$VoucherNumber
|
||||
By : PartyLedger : $$Owner:$PartyLedgerName
|
||||
By : StockItemName : $StockItemName
|
||||
By : HSNCode : $GSTItemHSNCodeEx
|
||||
By : BilledQty : $BilledQty
|
||||
By : ActualQty : $ActualQty
|
||||
By : Rate : $Rate
|
||||
By : IsDeemedPositive : $IsDeemedPositive
|
||||
Aggr Compute : InventoryAmount : Sum : $Amount
|
||||
Is ODBC Table : Yes
|
||||
|
||||
;; ================================================================
|
||||
;; PROVEN STOCK ITEM HSN DIRECT EVALUATION
|
||||
;; Only HSNDetails[1..5] are used because those exact positions
|
||||
;; were already tested successfully.
|
||||
;; ================================================================
|
||||
|
||||
[Collection : ERPAMStockHSNExport]
|
||||
Type : StockItem
|
||||
Fetch : Name, GUID, AlterID, Parent, BaseUnits, GSTApplicable, GSTTypeofSupply
|
||||
|
||||
Compute : HSNSource1 : $().HSNDetails[1].SrcOfHSNDetails
|
||||
Compute : HSNApplicableFrom1 : $().HSNDetails[1].ApplicableFrom
|
||||
Compute : HSNCode1 : $().HSNDetails[1].HSNCode
|
||||
Compute : HSNValue1 : $().HSNDetails[1].HSN
|
||||
Compute : HSNDescription1 : $().HSNDetails[1].Description
|
||||
|
||||
Compute : HSNSource2 : $().HSNDetails[2].SrcOfHSNDetails
|
||||
Compute : HSNApplicableFrom2 : $().HSNDetails[2].ApplicableFrom
|
||||
Compute : HSNCode2 : $().HSNDetails[2].HSNCode
|
||||
Compute : HSNValue2 : $().HSNDetails[2].HSN
|
||||
Compute : HSNDescription2 : $().HSNDetails[2].Description
|
||||
|
||||
Compute : HSNSource3 : $().HSNDetails[3].SrcOfHSNDetails
|
||||
Compute : HSNApplicableFrom3 : $().HSNDetails[3].ApplicableFrom
|
||||
Compute : HSNCode3 : $().HSNDetails[3].HSNCode
|
||||
Compute : HSNValue3 : $().HSNDetails[3].HSN
|
||||
Compute : HSNDescription3 : $().HSNDetails[3].Description
|
||||
|
||||
Compute : HSNSource4 : $().HSNDetails[4].SrcOfHSNDetails
|
||||
Compute : HSNApplicableFrom4 : $().HSNDetails[4].ApplicableFrom
|
||||
Compute : HSNCode4 : $().HSNDetails[4].HSNCode
|
||||
Compute : HSNValue4 : $().HSNDetails[4].HSN
|
||||
Compute : HSNDescription4 : $().HSNDetails[4].Description
|
||||
|
||||
Compute : HSNSource5 : $().HSNDetails[5].SrcOfHSNDetails
|
||||
Compute : HSNApplicableFrom5 : $().HSNDetails[5].ApplicableFrom
|
||||
Compute : HSNCode5 : $().HSNDetails[5].HSNCode
|
||||
Compute : HSNValue5 : $().HSNDetails[5].HSN
|
||||
Compute : HSNDescription5 : $().HSNDetails[5].Description
|
||||
|
||||
Is ODBC Table : Yes
|
||||
|
||||
|
||||
;; ================================================================
|
||||
;; PROVEN STOCK ITEM GST RATE DIRECT EVALUATION
|
||||
;; Exact tested path:
|
||||
;; GSTDetails[1] -> StateWiseDetails[1] -> RateDetails[1..4]
|
||||
;; ================================================================
|
||||
|
||||
[Collection : ERPAMStockGSTExport]
|
||||
Type : StockItem
|
||||
Fetch : Name, GUID, AlterID, Parent, BaseUnits, GSTApplicable, GSTTypeofSupply, GSTRepUoM
|
||||
|
||||
Compute : GSTApplicableFrom : $().GSTDetails[1].ApplicableFrom
|
||||
Compute : GSTSource : $().GSTDetails[1].SrcOfGSTDetails
|
||||
Compute : GSTTaxability : $().GSTDetails[1].Taxability
|
||||
Compute : GSTSupplyType : $().GSTDetails[1].SupplyType
|
||||
Compute : GSTReportingUOM : $().GSTDetails[1].ReportingUOM
|
||||
Compute : GSTStateName : $().GSTDetails[1].StateWiseDetails[1].StateName
|
||||
|
||||
Compute : GSTRateDutyHead1 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[1].GSTRateDutyHead
|
||||
Compute : GSTRate1 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[1].GSTRate
|
||||
Compute : GSTValuationType1 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[1].GSTRateValuationType
|
||||
Compute : GSTRatePerUnit1 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[1].GSTRatePerUnit
|
||||
|
||||
Compute : GSTRateDutyHead2 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[2].GSTRateDutyHead
|
||||
Compute : GSTRate2 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[2].GSTRate
|
||||
Compute : GSTValuationType2 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[2].GSTRateValuationType
|
||||
Compute : GSTRatePerUnit2 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[2].GSTRatePerUnit
|
||||
|
||||
Compute : GSTRateDutyHead3 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[3].GSTRateDutyHead
|
||||
Compute : GSTRate3 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[3].GSTRate
|
||||
Compute : GSTValuationType3 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[3].GSTRateValuationType
|
||||
Compute : GSTRatePerUnit3 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[3].GSTRatePerUnit
|
||||
|
||||
Compute : GSTRateDutyHead4 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[4].GSTRateDutyHead
|
||||
Compute : GSTRate4 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[4].GSTRate
|
||||
Compute : GSTValuationType4 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[4].GSTRateValuationType
|
||||
Compute : GSTRatePerUnit4 : $().GSTDetails[1].StateWiseDetails[1].RateDetails[4].GSTRatePerUnit
|
||||
|
||||
Is ODBC Table : Yes
|
||||
|
||||
;; ============================================================
|
||||
;; END
|
||||
;; ============================================================
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Odbc;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
class TallyAccountingMirrorV301
|
||||
{
|
||||
static string Safe(object v) {
|
||||
if (v == null || v == DBNull.Value) return "";
|
||||
return Convert.ToString(v, CultureInfo.InvariantCulture) ?? "";
|
||||
}
|
||||
|
||||
static string Csv(string v) {
|
||||
v = v ?? "";
|
||||
if (v.Contains("\"")) v = v.Replace("\"","\"\"");
|
||||
if (v.Contains(",") || v.Contains("\"") || v.Contains("\r") || v.Contains("\n"))
|
||||
return "\"" + v + "\"";
|
||||
return v;
|
||||
}
|
||||
|
||||
static void Extract(OdbcConnection cn, string label, string sql, string path,
|
||||
out int rows, out double sec)
|
||||
{
|
||||
rows = 0;
|
||||
var sw = Stopwatch.StartNew();
|
||||
Console.WriteLine(label + " - BEFORE QUERY");
|
||||
Console.Out.Flush();
|
||||
|
||||
using (var cmd = cn.CreateCommand()) {
|
||||
cmd.CommandText = sql;
|
||||
cmd.CommandTimeout = 120;
|
||||
using (var r = cmd.ExecuteReader()) {
|
||||
sec = sw.Elapsed.TotalSeconds;
|
||||
Console.WriteLine(label + " - QUERY RETURNED in " + sec.ToString("0.000") + " sec");
|
||||
|
||||
var headers = new List<string>();
|
||||
for (int i=0; i<r.FieldCount; i++) headers.Add(r.GetName(i));
|
||||
|
||||
using (var w = new StreamWriter(path,false,new UTF8Encoding(true))) {
|
||||
w.WriteLine(String.Join(",",headers.ConvertAll(Csv)));
|
||||
while (r.Read()) {
|
||||
rows++;
|
||||
var vals = new List<string>();
|
||||
for (int i=0; i<r.FieldCount; i++) vals.Add(Safe(r.GetValue(i)));
|
||||
w.WriteLine(String.Join(",",vals.ConvertAll(Csv)));
|
||||
if (rows <= 2)
|
||||
Console.WriteLine(label+" ROW "+rows+" | "+String.Join(" | ",vals));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine(label+" - ROWS READ: "+rows);
|
||||
Console.WriteLine(label+" - CSV: "+Path.GetFullPath(path));
|
||||
Console.Out.Flush();
|
||||
}
|
||||
|
||||
static int Main(string[] args)
|
||||
{
|
||||
string dsn="TallyODBC64_9000", outDir="mirror_output_v3_0_1";
|
||||
for(int i=0;i<args.Length;i++) {
|
||||
if(args[i]=="--dsn" && i+1<args.Length) dsn=args[++i];
|
||||
else if(args[i]=="--out" && i+1<args.Length) outDir=args[++i];
|
||||
}
|
||||
Directory.CreateDirectory(outDir);
|
||||
|
||||
try {
|
||||
var total=Stopwatch.StartNew();
|
||||
Console.WriteLine("ERP ACCOUNTING MIRROR v3.0.1 - ERP INTEGRATION CANDIDATE");
|
||||
Console.WriteLine("READ ONLY | ODBC | NO XML | NO TALLY WRITES");
|
||||
Console.WriteLine("BEFORE CONNECT");
|
||||
|
||||
using(var cn=new OdbcConnection("DSN="+dsn+";")) {
|
||||
cn.Open();
|
||||
Console.WriteLine("CONNECTED");
|
||||
|
||||
int c,l,s,tax,v,vl,im;
|
||||
double sc,sl,ss,stax,sv,svl,sim;
|
||||
|
||||
Extract(cn,"COMPANY",
|
||||
"SELECT $Name,$GUID,$AlterID,$StartingFrom,$BooksFrom FROM ERPAMCompanyInfo",
|
||||
Path.Combine(outDir,"company.csv"),out c,out sc);
|
||||
|
||||
Extract(cn,"LEDGER MASTER",
|
||||
"SELECT $GUID,$AlterID,$Name,$Parent,$OpeningBalance,$ClosingBalance,$IsRevenue,$PartyGSTIN,$GSTRegistrationType FROM ERPAMLedgerMaster",
|
||||
Path.Combine(outDir,"ledger_master.csv"),out l,out sl);
|
||||
|
||||
Extract(cn,"STOCK MASTER",
|
||||
"SELECT $GUID,$AlterID,$Name,$Parent,$BaseUnits,$OpeningBalance,$OpeningValue,$GSTApplicable,$GSTRepUoM,$GSTTypeofSupply,$GSTItemHSNCodeEx FROM ERPAMStockMaster",
|
||||
Path.Combine(outDir,"stock_master.csv"),out s,out ss);
|
||||
|
||||
Extract(cn,"STOCK HSN MASTER",
|
||||
"SELECT $Name,$GUID,$AlterID,$Parent,$BaseUnits,$GSTApplicable,$GSTTypeofSupply,"+
|
||||
"$HSNSource1,$HSNApplicableFrom1,$HSNCode1,$HSNValue1,$HSNDescription1,"+
|
||||
"$HSNSource2,$HSNApplicableFrom2,$HSNCode2,$HSNValue2,$HSNDescription2,"+
|
||||
"$HSNSource3,$HSNApplicableFrom3,$HSNCode3,$HSNValue3,$HSNDescription3,"+
|
||||
"$HSNSource4,$HSNApplicableFrom4,$HSNCode4,$HSNValue4,$HSNDescription4,"+
|
||||
"$HSNSource5,$HSNApplicableFrom5,$HSNCode5,$HSNValue5,$HSNDescription5 "+
|
||||
"FROM ERPAMStockHSNExport",
|
||||
Path.Combine(outDir,"stock_hsn_master.csv"),out tax,out stax);
|
||||
|
||||
int gstRows; double gstSec;
|
||||
Extract(cn,"STOCK GST MASTER",
|
||||
"SELECT $Name,$GUID,$AlterID,$Parent,$BaseUnits,$GSTApplicable,$GSTTypeofSupply,$GSTRepUoM,"+
|
||||
"$GSTApplicableFrom,$GSTSource,$GSTTaxability,$GSTSupplyType,$GSTReportingUOM,$GSTStateName,"+
|
||||
"$GSTRateDutyHead1,$GSTRate1,$GSTValuationType1,$GSTRatePerUnit1,"+
|
||||
"$GSTRateDutyHead2,$GSTRate2,$GSTValuationType2,$GSTRatePerUnit2,"+
|
||||
"$GSTRateDutyHead3,$GSTRate3,$GSTValuationType3,$GSTRatePerUnit3,"+
|
||||
"$GSTRateDutyHead4,$GSTRate4,$GSTValuationType4,$GSTRatePerUnit4 "+
|
||||
"FROM ERPAMStockGSTExport",
|
||||
Path.Combine(outDir,"stock_gst_master.csv"),out gstRows,out gstSec);
|
||||
|
||||
Extract(cn,"VOUCHER HEADER",
|
||||
"SELECT $VoucherGUID,$VoucherAlterID,$VoucherMasterID,$VoucherDate,$VoucherType,$VoucherNumber,$PartyLedger,$VoucherNarration,$VoucherReference,$VoucherAmount,$IsCancelled,$IsOptional FROM ERPAMVoucherHeader",
|
||||
Path.Combine(outDir,"voucher_header.csv"),out v,out sv);
|
||||
|
||||
Extract(cn,"VOUCHER LEDGER",
|
||||
"SELECT $VoucherGUID,$VoucherAlterID,$VoucherMasterID,$VoucherDate,$VoucherType,$VoucherNumber,$PartyLedger,$LedgerName,$IsDeemedPositive,$LedgerAmount FROM ERPAMVoucherLedger",
|
||||
Path.Combine(outDir,"voucher_ledger.csv"),out vl,out svl);
|
||||
|
||||
Extract(cn,"INVENTORY",
|
||||
"SELECT $VoucherGUID,$VoucherAlterID,$VoucherMasterID,$VoucherDate,$VoucherType,$VoucherNumber,$PartyLedger,$StockItemName,$HSNCode,$BilledQty,$ActualQty,$Rate,$IsDeemedPositive,$InventoryAmount FROM ERPAMInventoryMovement",
|
||||
Path.Combine(outDir,"inventory_movement.csv"),out im,out sim);
|
||||
|
||||
Console.WriteLine("SUMMARY | Company="+c+
|
||||
" | Ledger="+l+
|
||||
" | Stock="+s+
|
||||
" | StockHSN="+tax+" | StockGST="+gstRows+
|
||||
" | Voucher="+v+
|
||||
" | VoucherLedger="+vl+
|
||||
" | Inventory="+im+
|
||||
" | TotalSeconds="+total.Elapsed.TotalSeconds.ToString("0.000"));
|
||||
}
|
||||
|
||||
Console.WriteLine("SUCCESS");
|
||||
return 0;
|
||||
}
|
||||
catch(Exception ex) {
|
||||
Console.WriteLine("ERROR TYPE: "+ex.GetType().FullName);
|
||||
Console.WriteLine("ERROR: "+ex.Message);
|
||||
if(ex.InnerException!=null) Console.WriteLine("INNER: "+ex.InnerException.Message);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
+612
@@ -0,0 +1,612 @@
|
||||
from pathlib import Path
|
||||
import argparse, csv, hashlib, re, sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
def ck(k): return (k or "").strip().lstrip("$").strip()
|
||||
def norm(row): return {ck(k):(v or "") for k,v in row.items()}
|
||||
def t(v): return (v or "").replace("\x04","").strip()
|
||||
|
||||
def num(v):
|
||||
s=t(v).replace(",","")
|
||||
m=re.search(r"[-+]?\d+(?:\.\d+)?",s)
|
||||
return float(m.group(0)) if m else None
|
||||
|
||||
def integer(v):
|
||||
x=num(v)
|
||||
return int(x) if x is not None else None
|
||||
|
||||
def yes(v):
|
||||
return 1 if t(v).lower() in {"yes","true","1"} else 0
|
||||
|
||||
def date_iso(v):
|
||||
s=t(v)
|
||||
if not s: return None
|
||||
for fmt in ("%Y%m%d","%m/%d/%Y %H:%M:%S","%d-%b-%y","%d-%b-%Y",
|
||||
"%d/%m/%Y","%m/%d/%Y","%Y-%m-%d"):
|
||||
try:
|
||||
return datetime.strptime(s,fmt).date().isoformat()
|
||||
except ValueError:
|
||||
pass
|
||||
return s
|
||||
|
||||
def unit_from_qty(v):
|
||||
s=t(v)
|
||||
m=re.search(r"[-+]?\d+(?:\.\d+)?\s*(.+)$",s)
|
||||
return m.group(1).strip() if m else ""
|
||||
|
||||
def csv_rows(src, name):
|
||||
p=src/name
|
||||
if not p.exists():
|
||||
raise SystemExit(f"Missing {p}. Run run_v3_0.ps1 first.")
|
||||
with p.open("r",encoding="utf-8-sig",newline="") as f:
|
||||
r=csv.DictReader(f)
|
||||
print(f"{name} headers:",[ck(x) for x in (r.fieldnames or [])])
|
||||
for row in r:
|
||||
yield norm(row)
|
||||
|
||||
def company_instance_id(guid, name, starting_from, books_from):
|
||||
raw="|".join([t(guid),t(name),t(starting_from),t(books_from)]).encode("utf-8")
|
||||
return hashlib.sha256(raw).hexdigest()[:24]
|
||||
|
||||
def applicable(rows, target_date):
|
||||
"""Latest effective row whose applicable_from <= target_date.
|
||||
If target date is absent, use latest dated row."""
|
||||
if not rows:
|
||||
return None
|
||||
dated=[r for r in rows if r.get("applicable_from")]
|
||||
if target_date:
|
||||
eligible=[r for r in dated if r["applicable_from"] <= target_date]
|
||||
if eligible:
|
||||
return max(eligible,key=lambda x:x["applicable_from"])
|
||||
if dated:
|
||||
return max(dated,key=lambda x:x["applicable_from"])
|
||||
return rows[-1]
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser()
|
||||
ap.add_argument("--source", default="mirror_output_v3_0_1")
|
||||
args=ap.parse_args()
|
||||
|
||||
src=ROOT/args.source
|
||||
db=src/"accounting_mirror_v3_0_1.act"
|
||||
src.mkdir(parents=True,exist_ok=True)
|
||||
|
||||
con=sqlite3.connect(db)
|
||||
con.execute("PRAGMA journal_mode=WAL")
|
||||
con.execute("PRAGMA synchronous=NORMAL")
|
||||
con.execute("PRAGMA foreign_keys=ON")
|
||||
|
||||
con.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS company_master(
|
||||
company_instance_id TEXT PRIMARY KEY,
|
||||
company_guid TEXT,
|
||||
company_name TEXT NOT NULL,
|
||||
alter_id INTEGER,
|
||||
starting_from TEXT,
|
||||
books_from TEXT,
|
||||
synced_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ledger_master(
|
||||
company_instance_id TEXT NOT NULL,
|
||||
company_guid TEXT,
|
||||
ledger_guid TEXT NOT NULL,
|
||||
alter_id INTEGER,
|
||||
ledger_name TEXT NOT NULL,
|
||||
parent_group TEXT,
|
||||
opening_balance REAL,
|
||||
closing_balance REAL,
|
||||
is_revenue INTEGER NOT NULL DEFAULT 0,
|
||||
party_gstin TEXT,
|
||||
gst_registration_type TEXT,
|
||||
synced_at TEXT NOT NULL,
|
||||
PRIMARY KEY(company_instance_id,ledger_guid)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_item_master(
|
||||
company_instance_id TEXT NOT NULL,
|
||||
company_guid TEXT,
|
||||
stock_item_guid TEXT NOT NULL,
|
||||
alter_id INTEGER,
|
||||
product_name TEXT NOT NULL,
|
||||
parent_group TEXT,
|
||||
base_uom TEXT,
|
||||
opening_qty_text TEXT,
|
||||
opening_qty REAL,
|
||||
opening_value REAL,
|
||||
gst_reporting_uom TEXT,
|
||||
gst_type_of_supply TEXT,
|
||||
gst_applicable TEXT,
|
||||
current_hsn TEXT,
|
||||
current_gst_rate REAL,
|
||||
synced_at TEXT NOT NULL,
|
||||
PRIMARY KEY(company_instance_id,stock_item_guid)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_item_hsn_detail(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
company_instance_id TEXT NOT NULL,
|
||||
company_guid TEXT,
|
||||
stock_item_guid TEXT NOT NULL,
|
||||
stock_item_name TEXT NOT NULL,
|
||||
history_position INTEGER NOT NULL,
|
||||
applicable_from TEXT,
|
||||
hsn_source TEXT,
|
||||
hsn_code TEXT,
|
||||
hsn_value TEXT,
|
||||
effective_hsn TEXT,
|
||||
description TEXT,
|
||||
synced_at TEXT NOT NULL,
|
||||
UNIQUE(company_instance_id,stock_item_guid,history_position)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_item_gst_detail(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
company_instance_id TEXT NOT NULL,
|
||||
company_guid TEXT,
|
||||
stock_item_guid TEXT NOT NULL,
|
||||
stock_item_name TEXT NOT NULL,
|
||||
history_position INTEGER NOT NULL,
|
||||
applicable_from TEXT,
|
||||
gst_source TEXT,
|
||||
taxability TEXT,
|
||||
supply_type TEXT,
|
||||
reporting_uom TEXT,
|
||||
state_name TEXT,
|
||||
cgst_rate REAL,
|
||||
sgst_rate REAL,
|
||||
igst_rate REAL,
|
||||
cess_rate REAL,
|
||||
total_gst_rate REAL,
|
||||
synced_at TEXT NOT NULL,
|
||||
UNIQUE(company_instance_id,stock_item_guid,history_position)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_item_gst_rate_detail(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
company_instance_id TEXT NOT NULL,
|
||||
company_guid TEXT,
|
||||
stock_item_guid TEXT NOT NULL,
|
||||
stock_item_name TEXT NOT NULL,
|
||||
gst_history_position INTEGER NOT NULL,
|
||||
rate_position INTEGER NOT NULL,
|
||||
applicable_from TEXT,
|
||||
state_name TEXT,
|
||||
duty_head TEXT,
|
||||
gst_rate REAL,
|
||||
valuation_type TEXT,
|
||||
rate_per_unit REAL,
|
||||
synced_at TEXT NOT NULL,
|
||||
UNIQUE(company_instance_id,stock_item_guid,gst_history_position,rate_position)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS voucher(
|
||||
company_instance_id TEXT NOT NULL,
|
||||
company_guid TEXT,
|
||||
voucher_guid TEXT NOT NULL,
|
||||
alter_id INTEGER,
|
||||
master_id INTEGER,
|
||||
voucher_date TEXT,
|
||||
voucher_type TEXT,
|
||||
voucher_number TEXT,
|
||||
party_ledger TEXT,
|
||||
narration TEXT,
|
||||
reference TEXT,
|
||||
voucher_amount REAL,
|
||||
is_cancelled INTEGER NOT NULL DEFAULT 0,
|
||||
is_optional INTEGER NOT NULL DEFAULT 0,
|
||||
synced_at TEXT NOT NULL,
|
||||
PRIMARY KEY(company_instance_id,voucher_guid)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS voucher_ledger_entry(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
company_instance_id TEXT NOT NULL,
|
||||
company_guid TEXT,
|
||||
voucher_guid TEXT NOT NULL,
|
||||
line_no INTEGER NOT NULL,
|
||||
alter_id INTEGER,
|
||||
master_id INTEGER,
|
||||
voucher_date TEXT,
|
||||
voucher_type TEXT,
|
||||
voucher_number TEXT,
|
||||
party_ledger TEXT,
|
||||
ledger_name TEXT NOT NULL,
|
||||
dr_cr TEXT NOT NULL,
|
||||
amount REAL,
|
||||
raw_amount REAL,
|
||||
raw_is_deemed_positive TEXT,
|
||||
UNIQUE(company_instance_id,voucher_guid,line_no)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inventory_movement(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
company_instance_id TEXT NOT NULL,
|
||||
company_guid TEXT,
|
||||
voucher_guid TEXT NOT NULL,
|
||||
line_no INTEGER NOT NULL,
|
||||
alter_id INTEGER,
|
||||
master_id INTEGER,
|
||||
voucher_date TEXT,
|
||||
voucher_type TEXT,
|
||||
voucher_number TEXT,
|
||||
party_ledger TEXT,
|
||||
stock_item_name TEXT NOT NULL,
|
||||
stock_item_guid TEXT,
|
||||
hsn TEXT,
|
||||
gst_rate REAL,
|
||||
cgst_rate REAL,
|
||||
sgst_rate REAL,
|
||||
igst_rate REAL,
|
||||
cess_rate REAL,
|
||||
taxability TEXT,
|
||||
billed_qty_text TEXT,
|
||||
billed_qty REAL,
|
||||
actual_qty_text TEXT,
|
||||
actual_qty REAL,
|
||||
uom TEXT,
|
||||
rate_text TEXT,
|
||||
rate REAL,
|
||||
value REAL,
|
||||
direction TEXT,
|
||||
signed_qty REAL,
|
||||
signed_value REAL,
|
||||
raw_is_deemed_positive TEXT,
|
||||
UNIQUE(company_instance_id,voucher_guid,line_no)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS opening_stock(
|
||||
company_instance_id TEXT NOT NULL,
|
||||
company_guid TEXT,
|
||||
stock_item_guid TEXT NOT NULL,
|
||||
stock_item_name TEXT NOT NULL,
|
||||
hsn TEXT,
|
||||
gst_rate REAL,
|
||||
uom TEXT,
|
||||
opening_qty REAL,
|
||||
opening_value REAL,
|
||||
source_type TEXT NOT NULL DEFAULT 'OPENING_BALANCE',
|
||||
PRIMARY KEY(company_instance_id,stock_item_guid)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sync_state(
|
||||
company_instance_id TEXT PRIMARY KEY,
|
||||
company_guid TEXT,
|
||||
company_name TEXT,
|
||||
period_from TEXT,
|
||||
period_to TEXT,
|
||||
max_voucher_alter_id INTEGER,
|
||||
max_ledger_alter_id INTEGER,
|
||||
max_stock_alter_id INTEGER,
|
||||
voucher_count INTEGER,
|
||||
voucher_ledger_count INTEGER,
|
||||
inventory_count INTEGER,
|
||||
hsn_history_count INTEGER,
|
||||
gst_history_count INTEGER,
|
||||
last_full_sync_at TEXT,
|
||||
last_sync_at TEXT,
|
||||
sync_mode TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS adjustment_queue(
|
||||
adjustment_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
company_instance_id TEXT NOT NULL,
|
||||
company_guid TEXT,
|
||||
source_voucher_guid TEXT,
|
||||
adjustment_type TEXT NOT NULL,
|
||||
proposed_voucher_type TEXT NOT NULL,
|
||||
voucher_date TEXT NOT NULL,
|
||||
narration TEXT,
|
||||
reason TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
created_by TEXT,
|
||||
approved_by TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
approved_at TEXT,
|
||||
posted_at TEXT,
|
||||
tally_response TEXT,
|
||||
posted_voucher_guid TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS adjustment_line(
|
||||
line_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
adjustment_id INTEGER NOT NULL,
|
||||
line_no INTEGER NOT NULL,
|
||||
ledger_name TEXT NOT NULL,
|
||||
dr_cr TEXT NOT NULL CHECK(dr_cr IN('DR','CR')),
|
||||
amount REAL NOT NULL,
|
||||
FOREIGN KEY(adjustment_id) REFERENCES adjustment_queue(adjustment_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_voucher_date ON voucher(company_instance_id,voucher_date);
|
||||
CREATE INDEX IF NOT EXISTS ix_voucher_alter ON voucher(company_instance_id,alter_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_vle_ledger ON voucher_ledger_entry(company_instance_id,ledger_name);
|
||||
CREATE INDEX IF NOT EXISTS ix_inv_item ON inventory_movement(company_instance_id,stock_item_name);
|
||||
CREATE INDEX IF NOT EXISTS ix_inv_date ON inventory_movement(company_instance_id,voucher_date);
|
||||
CREATE INDEX IF NOT EXISTS ix_hsn_effective ON stock_item_hsn_detail(company_instance_id,stock_item_guid,applicable_from);
|
||||
CREATE INDEX IF NOT EXISTS ix_gst_effective ON stock_item_gst_detail(company_instance_id,stock_item_guid,applicable_from);
|
||||
""")
|
||||
|
||||
now=datetime.now().isoformat(timespec="seconds")
|
||||
company_rows=list(csv_rows(src,"company.csv"))
|
||||
if not company_rows:
|
||||
raise SystemExit("No company returned by ODBC.")
|
||||
c=company_rows[0]
|
||||
guid=t(c.get("GUID")) or "ACTIVE_COMPANY"
|
||||
name=t(c.get("Name")) or "Active Tally Company"
|
||||
starting=date_iso(c.get("StartingFrom"))
|
||||
books=date_iso(c.get("BooksFrom"))
|
||||
instance=company_instance_id(guid,name,starting,books)
|
||||
|
||||
con.execute("""INSERT OR REPLACE INTO company_master
|
||||
VALUES(?,?,?,?,?,?,?)""",
|
||||
(instance,guid,name,integer(c.get("AlterID")),starting,books,now))
|
||||
|
||||
for table in ("voucher_ledger_entry","inventory_movement","voucher","opening_stock",
|
||||
"ledger_master","stock_item_master","stock_item_hsn_detail",
|
||||
"stock_item_gst_detail","stock_item_gst_rate_detail"):
|
||||
con.execute(f"DELETE FROM {table} WHERE company_instance_id=?",(instance,))
|
||||
|
||||
ledger_count=0; max_ledger_alter=0
|
||||
for r in csv_rows(src,"ledger_master.csv"):
|
||||
lg=t(r.get("GUID"))
|
||||
if not lg: continue
|
||||
aid=integer(r.get("AlterID")) or 0
|
||||
max_ledger_alter=max(max_ledger_alter,aid)
|
||||
con.execute("""INSERT OR REPLACE INTO ledger_master
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(instance,guid,lg,aid,t(r.get("Name")),t(r.get("Parent")),
|
||||
num(r.get("OpeningBalance")),num(r.get("ClosingBalance")),yes(r.get("IsRevenue")),
|
||||
t(r.get("PartyGSTIN")),t(r.get("GSTRegistrationType")),now))
|
||||
ledger_count+=1
|
||||
|
||||
# Read stock tax data first so effective histories are available when stock and
|
||||
# inventory rows are inserted.
|
||||
tax_by_name={}
|
||||
hsn_by_guid={}
|
||||
gst_by_guid={}
|
||||
hsn_count=gst_count=0
|
||||
|
||||
# HSN history - use only the indexed positions already proven in Tally.
|
||||
for r in csv_rows(src,"stock_hsn_master.csv"):
|
||||
sg=t(r.get("GUID"))
|
||||
sn=t(r.get("Name"))
|
||||
if not sg or not sn: continue
|
||||
tax_by_name.setdefault(sn.casefold(),{"guid":sg})
|
||||
hrows=[]
|
||||
for i in range(1,6):
|
||||
d=date_iso(r.get(f"HSNApplicableFrom{i}"))
|
||||
code=t(r.get(f"HSNCode{i}"))
|
||||
val=t(r.get(f"HSNValue{i}"))
|
||||
source=t(r.get(f"HSNSource{i}"))
|
||||
desc=t(r.get(f"HSNDescription{i}"))
|
||||
if not any((d,code,val,source,desc)): continue
|
||||
eff=val or code
|
||||
row={"history_position":i,"applicable_from":d,"hsn_source":source,
|
||||
"hsn_code":code,"hsn_value":val,"effective_hsn":eff,"description":desc}
|
||||
hrows.append(row)
|
||||
con.execute("""INSERT INTO stock_item_hsn_detail
|
||||
(company_instance_id,company_guid,stock_item_guid,stock_item_name,history_position,
|
||||
applicable_from,hsn_source,hsn_code,hsn_value,effective_hsn,description,synced_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(instance,guid,sg,sn,i,d,source,code,val,eff,desc,now))
|
||||
hsn_count+=1
|
||||
hsn_by_guid[sg]=hrows
|
||||
|
||||
# GST rate master - exact GSTDetails[1] path already proven in Tally.
|
||||
for r in csv_rows(src,"stock_gst_master.csv"):
|
||||
sg=t(r.get("GUID"))
|
||||
sn=t(r.get("Name"))
|
||||
if not sg or not sn: continue
|
||||
tax_by_name.setdefault(sn.casefold(),{"guid":sg})
|
||||
d=date_iso(r.get("GSTApplicableFrom"))
|
||||
source=t(r.get("GSTSource"))
|
||||
tax=t(r.get("GSTTaxability"))
|
||||
supply=t(r.get("GSTSupplyType"))
|
||||
rep=t(r.get("GSTReportingUOM"))
|
||||
state=t(r.get("GSTStateName"))
|
||||
rates={}
|
||||
rate_rows=[]
|
||||
for rp in range(1,5):
|
||||
duty=t(r.get(f"GSTRateDutyHead{rp}"))
|
||||
rate=num(r.get(f"GSTRate{rp}"))
|
||||
valuation=t(r.get(f"GSTValuationType{rp}"))
|
||||
perunit=num(r.get(f"GSTRatePerUnit{rp}"))
|
||||
if not any((duty, rate is not None, valuation, perunit is not None)):
|
||||
continue
|
||||
rate_rows.append((rp,duty,rate,valuation,perunit))
|
||||
dk=duty.replace("\x04","").strip().upper()
|
||||
if dk=="CGST": rates["cgst"]=rate
|
||||
elif "SGST" in dk or "UTGST" in dk: rates["sgst"]=rate
|
||||
elif dk=="IGST": rates["igst"]=rate
|
||||
elif "CESS" in dk: rates["cess"]=rate
|
||||
con.execute("""INSERT INTO stock_item_gst_rate_detail
|
||||
(company_instance_id,company_guid,stock_item_guid,stock_item_name,
|
||||
gst_history_position,rate_position,applicable_from,state_name,duty_head,
|
||||
gst_rate,valuation_type,rate_per_unit,synced_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(instance,guid,sg,sn,1,rp,d,state,duty,rate,valuation,perunit,now))
|
||||
|
||||
if any((d,source,tax,supply,rep,state,rate_rows)):
|
||||
cg=rates.get("cgst"); ss=rates.get("sgst"); ig=rates.get("igst"); cess=rates.get("cess")
|
||||
total=ig if ig is not None else ((cg or 0)+(ss or 0) if cg is not None or ss is not None else None)
|
||||
grow={"history_position":1,"applicable_from":d,"gst_source":source,
|
||||
"taxability":tax,"supply_type":supply,"reporting_uom":rep,"state_name":state,
|
||||
"cgst_rate":cg,"sgst_rate":ss,"igst_rate":ig,"cess_rate":cess,"total_gst_rate":total}
|
||||
gst_by_guid[sg]=[grow]
|
||||
con.execute("""INSERT INTO stock_item_gst_detail
|
||||
(company_instance_id,company_guid,stock_item_guid,stock_item_name,history_position,
|
||||
applicable_from,gst_source,taxability,supply_type,reporting_uom,state_name,
|
||||
cgst_rate,sgst_rate,igst_rate,cess_rate,total_gst_rate,synced_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(instance,guid,sg,sn,1,d,source,tax,supply,rep,state,cg,ss,ig,cess,total,now))
|
||||
gst_count+=1
|
||||
else:
|
||||
gst_by_guid[sg]=[]
|
||||
|
||||
stock_by_name={}; stock_count=0; max_stock_alter=0
|
||||
for r in csv_rows(src,"stock_master.csv"):
|
||||
sg=t(r.get("GUID"))
|
||||
if not sg: continue
|
||||
aid=integer(r.get("AlterID")) or 0
|
||||
max_stock_alter=max(max_stock_alter,aid)
|
||||
sn=t(r.get("Name"))
|
||||
base=t(r.get("BaseUnits"))
|
||||
rep=t(r.get("GSTRepUoM"))
|
||||
taxrow=tax_by_name.get(sn.casefold(),{})
|
||||
hcur=applicable(hsn_by_guid.get(sg,[]), None)
|
||||
gcur=applicable(gst_by_guid.get(sg,[]), None)
|
||||
current_hsn=(hcur or {}).get("effective_hsn") or t(r.get("GSTItemHSNCodeEx"))
|
||||
current_rate=(gcur or {}).get("total_gst_rate")
|
||||
con.execute("""INSERT OR REPLACE INTO stock_item_master
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(instance,guid,sg,aid,sn,t(r.get("Parent")),base,t(r.get("OpeningBalance")),
|
||||
num(r.get("OpeningBalance")),num(r.get("OpeningValue")),rep,t(r.get("GSTTypeofSupply")),
|
||||
t(r.get("GSTApplicable")),current_hsn,current_rate,now))
|
||||
con.execute("""INSERT OR REPLACE INTO opening_stock
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?)""",
|
||||
(instance,guid,sg,sn,current_hsn,current_rate,base,num(r.get("OpeningBalance")),
|
||||
num(r.get("OpeningValue")),"OPENING_BALANCE"))
|
||||
stock_by_name[sn.casefold()]={"guid":sg,"uom":base}
|
||||
stock_count+=1
|
||||
|
||||
voucher_count=0; max_voucher_alter=0; voucher_dates=[]
|
||||
for r in csv_rows(src,"voucher_header.csv"):
|
||||
vg=t(r.get("VoucherGUID"))
|
||||
if not vg: continue
|
||||
aid=integer(r.get("VoucherAlterID")) or 0
|
||||
max_voucher_alter=max(max_voucher_alter,aid)
|
||||
d=date_iso(r.get("VoucherDate"))
|
||||
if d: voucher_dates.append(d)
|
||||
con.execute("""INSERT OR REPLACE INTO voucher
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(instance,guid,vg,aid,integer(r.get("VoucherMasterID")),d,t(r.get("VoucherType")),
|
||||
t(r.get("VoucherNumber")),t(r.get("PartyLedger")),t(r.get("VoucherNarration")),
|
||||
t(r.get("VoucherReference")),num(r.get("VoucherAmount")),yes(r.get("IsCancelled")),
|
||||
yes(r.get("IsOptional")),now))
|
||||
voucher_count+=1
|
||||
|
||||
line_seq={}; vle_count=0
|
||||
for r in csv_rows(src,"voucher_ledger.csv"):
|
||||
vg=t(r.get("VoucherGUID"))
|
||||
if not vg: continue
|
||||
line_seq[vg]=line_seq.get(vg,0)+1
|
||||
raw=num(r.get("LedgerAmount"))
|
||||
deemed=t(r.get("IsDeemedPositive"))
|
||||
if raw is not None and raw<0: drcr="DR"
|
||||
elif raw is not None and raw>0: drcr="CR"
|
||||
else: drcr="DR" if deemed.lower() in {"yes","true","1"} else "CR"
|
||||
con.execute("""INSERT INTO voucher_ledger_entry
|
||||
(company_instance_id,company_guid,voucher_guid,line_no,alter_id,master_id,voucher_date,
|
||||
voucher_type,voucher_number,party_ledger,ledger_name,dr_cr,amount,raw_amount,raw_is_deemed_positive)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(instance,guid,vg,line_seq[vg],integer(r.get("VoucherAlterID")),integer(r.get("VoucherMasterID")),
|
||||
date_iso(r.get("VoucherDate")),t(r.get("VoucherType")),t(r.get("VoucherNumber")),
|
||||
t(r.get("PartyLedger")),t(r.get("LedgerName")),drcr,abs(raw) if raw is not None else None,
|
||||
raw,deemed))
|
||||
vle_count+=1
|
||||
|
||||
inv_seq={}; inv_count=0
|
||||
for r in csv_rows(src,"inventory_movement.csv"):
|
||||
vg=t(r.get("VoucherGUID"))
|
||||
if not vg: continue
|
||||
inv_seq[vg]=inv_seq.get(vg,0)+1
|
||||
sn=t(r.get("StockItemName"))
|
||||
master=stock_by_name.get(sn.casefold(),{})
|
||||
sg=master.get("guid")
|
||||
d=date_iso(r.get("VoucherDate"))
|
||||
hrow=applicable(hsn_by_guid.get(sg,[]), d)
|
||||
grow=applicable(gst_by_guid.get(sg,[]), d)
|
||||
hsn=(hrow or {}).get("effective_hsn") or t(r.get("HSNCode"))
|
||||
gst=(grow or {}).get("total_gst_rate")
|
||||
qty=num(r.get("BilledQty")); aq=num(r.get("ActualQty"))
|
||||
value=num(r.get("InventoryAmount")); rate=num(r.get("Rate"))
|
||||
uom=master.get("uom","") or unit_from_qty(r.get("BilledQty"))
|
||||
vt=t(r.get("VoucherType")).casefold()
|
||||
if "sales" in vt: direction,sgn="OUTWARD",-1.0
|
||||
elif "purchase" in vt: direction,sgn="INWARD",1.0
|
||||
else:
|
||||
sgn=-1.0 if (qty is not None and qty<0) else 1.0
|
||||
direction="OUTWARD" if sgn<0 else "INWARD"
|
||||
|
||||
con.execute("""INSERT INTO inventory_movement
|
||||
(company_instance_id,company_guid,voucher_guid,line_no,alter_id,master_id,voucher_date,
|
||||
voucher_type,voucher_number,party_ledger,stock_item_name,stock_item_guid,hsn,gst_rate,
|
||||
cgst_rate,sgst_rate,igst_rate,cess_rate,taxability,billed_qty_text,billed_qty,
|
||||
actual_qty_text,actual_qty,uom,rate_text,rate,value,direction,signed_qty,signed_value,
|
||||
raw_is_deemed_positive)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(instance,guid,vg,inv_seq[vg],integer(r.get("VoucherAlterID")),integer(r.get("VoucherMasterID")),
|
||||
d,t(r.get("VoucherType")),t(r.get("VoucherNumber")),t(r.get("PartyLedger")),sn,sg,hsn,gst,
|
||||
(grow or {}).get("cgst_rate"),(grow or {}).get("sgst_rate"),(grow or {}).get("igst_rate"),
|
||||
(grow or {}).get("cess_rate"),(grow or {}).get("taxability"),t(r.get("BilledQty")),
|
||||
abs(qty) if qty is not None else None,t(r.get("ActualQty")),
|
||||
abs(aq) if aq is not None else None,uom,t(r.get("Rate")),rate,
|
||||
abs(value) if value is not None else None,direction,
|
||||
abs(qty)*sgn if qty is not None else None,
|
||||
abs(value)*sgn if value is not None else None,t(r.get("IsDeemedPositive"))))
|
||||
inv_count+=1
|
||||
|
||||
period_from=min(voucher_dates) if voucher_dates else None
|
||||
period_to=max(voucher_dates) if voucher_dates else None
|
||||
con.execute("""INSERT OR REPLACE INTO sync_state VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(instance,guid,name,period_from,period_to,max_voucher_alter,max_ledger_alter,max_stock_alter,
|
||||
voucher_count,vle_count,inv_count,hsn_count,gst_count,now,now,"FULL_SNAPSHOT"))
|
||||
|
||||
con.executescript("""
|
||||
DROP VIEW IF EXISTS daybook_flat;
|
||||
CREATE VIEW daybook_flat AS
|
||||
SELECT v.company_instance_id,v.company_guid,v.voucher_guid,v.voucher_date AS date,
|
||||
v.voucher_number,v.voucher_type,
|
||||
GROUP_CONCAT(CASE WHEN l.dr_cr='DR' THEN l.ledger_name END,' | ') AS debit_ledgers,
|
||||
GROUP_CONCAT(CASE WHEN l.dr_cr='CR' THEN l.ledger_name END,' | ') AS credit_ledgers,
|
||||
GROUP_CONCAT(CASE WHEN l.dr_cr='DR' THEN printf('%.2f',l.amount) END,' | ') AS debit_amounts,
|
||||
GROUP_CONCAT(CASE WHEN l.dr_cr='CR' THEN printf('%.2f',l.amount) END,' | ') AS credit_amounts,
|
||||
v.narration,v.reference,v.is_cancelled,v.is_optional,v.alter_id
|
||||
FROM voucher v
|
||||
LEFT JOIN voucher_ledger_entry l
|
||||
ON l.company_instance_id=v.company_instance_id AND l.voucher_guid=v.voucher_guid
|
||||
GROUP BY v.company_instance_id,v.voucher_guid;
|
||||
|
||||
DROP VIEW IF EXISTS inventory_relational;
|
||||
CREATE VIEW inventory_relational AS
|
||||
SELECT company_instance_id,company_guid,voucher_guid,voucher_date AS date,
|
||||
voucher_number AS accounting_voucher_number,voucher_type,
|
||||
stock_item_name AS product_name,hsn,gst_rate,cgst_rate,sgst_rate,igst_rate,cess_rate,
|
||||
taxability,direction,signed_qty AS qty,uom,rate,signed_value AS value,alter_id
|
||||
FROM inventory_movement;
|
||||
|
||||
DROP VIEW IF EXISTS voucher_balance_check;
|
||||
CREATE VIEW voucher_balance_check AS
|
||||
SELECT company_instance_id,voucher_guid,
|
||||
ROUND(SUM(CASE WHEN dr_cr='DR' THEN amount ELSE 0 END),2) AS debit_total,
|
||||
ROUND(SUM(CASE WHEN dr_cr='CR' THEN amount ELSE 0 END),2) AS credit_total,
|
||||
ROUND(SUM(CASE WHEN dr_cr='DR' THEN amount ELSE -amount END),2) AS difference
|
||||
FROM voucher_ledger_entry
|
||||
GROUP BY company_instance_id,voucher_guid;
|
||||
""")
|
||||
|
||||
con.commit()
|
||||
bad=con.execute("""SELECT COUNT(*) FROM voucher_balance_check
|
||||
WHERE company_instance_id=? AND ABS(COALESCE(difference,0))>0.01""",(instance,)).fetchone()[0]
|
||||
|
||||
print("\nACCOUNTING MIRROR v3.0 BUILT")
|
||||
print("DB :",db)
|
||||
print("Company :",name)
|
||||
print("Company instance :",instance)
|
||||
print("Company GUID :",guid)
|
||||
print("Period :",period_from,"to",period_to)
|
||||
print("Ledger master :",ledger_count)
|
||||
print("Stock master :",stock_count)
|
||||
print("HSN history :",hsn_count)
|
||||
print("GST history :",gst_count)
|
||||
print("Vouchers :",voucher_count)
|
||||
print("Voucher ledgers :",vle_count)
|
||||
print("Inventory :",inv_count)
|
||||
print("Unbalanced :",bad)
|
||||
con.close()
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user