537 lines
30 KiB
Python
537 lines
30 KiB
Python
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
from typing import Iterator, Sequence
|
|
|
|
ACT_SCHEMA_VERSION = 6
|
|
|
|
|
|
class AccountingActStoreError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def _utc_now_iso() -> str:
|
|
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
|
|
|
|
def _safe_client_id(client_id: int) -> int:
|
|
value = int(client_id)
|
|
if value <= 0:
|
|
raise AccountingActStoreError("client_id must be a positive integer")
|
|
return value
|
|
|
|
|
|
class AccountingActStore:
|
|
"""Client-scoped SQLite storage for ERP accounting data.
|
|
|
|
Phase 1 provides accounting storage and read-only Tally discovery.
|
|
Phase 2 adds durable client/registration -> Tally company mapping keyed by
|
|
Tally GUID. Phase 3 adds read-only Tally accounting master snapshot tables.
|
|
Phase 4 adds read-only voucher/transaction tables. Phase 5 adds draft-only
|
|
Income-tax depreciation rules and calculation tables. Tally write-back remains
|
|
out of scope.
|
|
"""
|
|
|
|
def __init__(self, root: str | Path) -> None:
|
|
self.root = Path(root).expanduser().resolve()
|
|
|
|
def client_dir(self, client_id: int) -> Path:
|
|
cid = _safe_client_id(client_id)
|
|
return self.root / "Accounting" / f"client_{cid:08d}"
|
|
|
|
def db_path(self, client_id: int) -> Path:
|
|
cid = _safe_client_id(client_id)
|
|
return self.client_dir(cid) / f"client_{cid:08d}.act"
|
|
|
|
@contextmanager
|
|
def connect(self, client_id: int) -> Iterator[sqlite3.Connection]:
|
|
path = self.db_path(client_id)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
connection = sqlite3.connect(path, timeout=30)
|
|
connection.row_factory = sqlite3.Row
|
|
try:
|
|
connection.execute("PRAGMA foreign_keys = ON")
|
|
connection.execute("PRAGMA journal_mode = WAL")
|
|
connection.execute("PRAGMA synchronous = NORMAL")
|
|
yield connection
|
|
connection.commit()
|
|
except Exception:
|
|
connection.rollback()
|
|
raise
|
|
finally:
|
|
connection.close()
|
|
|
|
def initialize(
|
|
self,
|
|
client_id: int,
|
|
*,
|
|
tenant_id: int | None = None,
|
|
client_name: str = "",
|
|
created_by_user_id: int | None = None,
|
|
) -> Path:
|
|
path = self.db_path(client_id)
|
|
with self.connect(client_id) as db:
|
|
db.executescript(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS act_meta (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at_utc TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tally_companies (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
tally_guid TEXT NOT NULL DEFAULT '',
|
|
company_name TEXT NOT NULL,
|
|
gstin TEXT NOT NULL DEFAULT '',
|
|
first_seen_at_utc TEXT NOT NULL,
|
|
last_seen_at_utc TEXT NOT NULL,
|
|
is_currently_loaded INTEGER NOT NULL DEFAULT 1,
|
|
UNIQUE(tally_guid, company_name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tally_company_mapping (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
client_id INTEGER NOT NULL,
|
|
registration_id INTEGER,
|
|
registration_type_code TEXT NOT NULL DEFAULT '',
|
|
registration_number TEXT NOT NULL DEFAULT '',
|
|
registration_legal_name TEXT NOT NULL DEFAULT '',
|
|
registration_trade_name TEXT NOT NULL DEFAULT '',
|
|
business_unit_id INTEGER,
|
|
client_branch_id INTEGER,
|
|
tally_guid TEXT NOT NULL,
|
|
company_name TEXT NOT NULL,
|
|
gstin TEXT NOT NULL DEFAULT '',
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
mapped_at_utc TEXT NOT NULL,
|
|
mapped_by_user_id INTEGER,
|
|
updated_at_utc TEXT NOT NULL,
|
|
unmapped_at_utc TEXT,
|
|
unmapped_by_user_id INTEGER
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tally_connection_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
checked_at_utc TEXT NOT NULL,
|
|
connected INTEGER NOT NULL,
|
|
tally_url TEXT NOT NULL DEFAULT '',
|
|
company_count INTEGER NOT NULL DEFAULT 0,
|
|
error_message TEXT,
|
|
payload_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tally_sync_runs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
sync_type TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
started_at_utc TEXT NOT NULL,
|
|
completed_at_utc TEXT,
|
|
requested_by_user_id INTEGER,
|
|
records_received INTEGER NOT NULL DEFAULT 0,
|
|
error_message TEXT,
|
|
details_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tally_groups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL, master_guid TEXT NOT NULL DEFAULT '', name TEXT NOT NULL, parent TEXT NOT NULL DEFAULT '', synced_at_utc TEXT NOT NULL, payload_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_ledgers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL, master_guid TEXT NOT NULL DEFAULT '', name TEXT NOT NULL, parent TEXT NOT NULL DEFAULT '', opening_balance REAL NOT NULL DEFAULT 0, closing_balance REAL NOT NULL DEFAULT 0, is_billwise_on TEXT NOT NULL DEFAULT '', gst_applicable TEXT NOT NULL DEFAULT '', synced_at_utc TEXT NOT NULL, payload_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_voucher_types (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL, master_guid TEXT NOT NULL DEFAULT '', name TEXT NOT NULL, parent TEXT NOT NULL DEFAULT '', numbering_method TEXT NOT NULL DEFAULT '', synced_at_utc TEXT NOT NULL, payload_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_stock_groups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL, master_guid TEXT NOT NULL DEFAULT '', name TEXT NOT NULL, parent TEXT NOT NULL DEFAULT '', synced_at_utc TEXT NOT NULL, payload_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_stock_categories (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL, master_guid TEXT NOT NULL DEFAULT '', name TEXT NOT NULL, parent TEXT NOT NULL DEFAULT '', synced_at_utc TEXT NOT NULL, payload_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_stock_items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL, master_guid TEXT NOT NULL DEFAULT '', name TEXT NOT NULL, parent TEXT NOT NULL DEFAULT '', category TEXT NOT NULL DEFAULT '', base_units TEXT NOT NULL DEFAULT '', hsn_code TEXT NOT NULL DEFAULT '', synced_at_utc TEXT NOT NULL, payload_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_units (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL, master_guid TEXT NOT NULL DEFAULT '', name TEXT NOT NULL, original_name TEXT NOT NULL DEFAULT '', synced_at_utc TEXT NOT NULL, payload_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_cost_categories (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL, master_guid TEXT NOT NULL DEFAULT '', name TEXT NOT NULL, synced_at_utc TEXT NOT NULL, payload_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_cost_centres (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL, master_guid TEXT NOT NULL DEFAULT '', name TEXT NOT NULL, parent TEXT NOT NULL DEFAULT '', category TEXT NOT NULL DEFAULT '', synced_at_utc TEXT NOT NULL, payload_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tally_vouchers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL,
|
|
voucher_guid TEXT NOT NULL DEFAULT '', master_id TEXT NOT NULL DEFAULT '', alter_id TEXT NOT NULL DEFAULT '', voucher_key TEXT NOT NULL DEFAULT '',
|
|
voucher_date TEXT NOT NULL DEFAULT '', effective_date TEXT NOT NULL DEFAULT '', voucher_type_name TEXT NOT NULL DEFAULT '', voucher_number TEXT NOT NULL DEFAULT '',
|
|
reference TEXT NOT NULL DEFAULT '', narration TEXT NOT NULL DEFAULT '', party_ledger_name TEXT NOT NULL DEFAULT '', is_cancelled TEXT NOT NULL DEFAULT '',
|
|
is_optional TEXT NOT NULL DEFAULT '', is_post_dated TEXT NOT NULL DEFAULT '', persisted_view TEXT NOT NULL DEFAULT '', synced_at_utc TEXT NOT NULL, raw_xml TEXT NOT NULL DEFAULT ''
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_voucher_ledger_entries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, voucher_id INTEGER NOT NULL, line_no INTEGER NOT NULL, ledger_name TEXT NOT NULL DEFAULT '', amount REAL NOT NULL DEFAULT 0,
|
|
is_deemed_positive TEXT NOT NULL DEFAULT '', ledger_from_item TEXT NOT NULL DEFAULT '', remove_zero_entries TEXT NOT NULL DEFAULT '', payload_json TEXT NOT NULL DEFAULT '{}',
|
|
FOREIGN KEY(voucher_id) REFERENCES tally_vouchers(id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_voucher_inventory_entries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, voucher_id INTEGER NOT NULL, line_no INTEGER NOT NULL, stock_item_name TEXT NOT NULL DEFAULT '', is_deemed_positive TEXT NOT NULL DEFAULT '',
|
|
actual_qty TEXT NOT NULL DEFAULT '', billed_qty TEXT NOT NULL DEFAULT '', rate TEXT NOT NULL DEFAULT '', amount REAL NOT NULL DEFAULT 0, godown_name TEXT NOT NULL DEFAULT '',
|
|
batch_name TEXT NOT NULL DEFAULT '', tracking_number TEXT NOT NULL DEFAULT '', order_number TEXT NOT NULL DEFAULT '', payload_json TEXT NOT NULL DEFAULT '{}',
|
|
FOREIGN KEY(voucher_id) REFERENCES tally_vouchers(id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_bill_allocations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, ledger_entry_id INTEGER NOT NULL, line_no INTEGER NOT NULL, name TEXT NOT NULL DEFAULT '', bill_type TEXT NOT NULL DEFAULT '',
|
|
amount REAL NOT NULL DEFAULT 0, bill_credit_period TEXT NOT NULL DEFAULT '', payload_json TEXT NOT NULL DEFAULT '{}',
|
|
FOREIGN KEY(ledger_entry_id) REFERENCES tally_voucher_ledger_entries(id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_cost_centre_allocations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, ledger_entry_id INTEGER NOT NULL, line_no INTEGER NOT NULL, category TEXT NOT NULL DEFAULT '', cost_centre TEXT NOT NULL DEFAULT '',
|
|
amount REAL NOT NULL DEFAULT 0, payload_json TEXT NOT NULL DEFAULT '{}', FOREIGN KEY(ledger_entry_id) REFERENCES tally_voucher_ledger_entries(id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tally_bank_allocations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, ledger_entry_id INTEGER NOT NULL, line_no INTEGER NOT NULL, transaction_type TEXT NOT NULL DEFAULT '', instrument_number TEXT NOT NULL DEFAULT '',
|
|
instrument_date TEXT NOT NULL DEFAULT '', bank_name TEXT NOT NULL DEFAULT '', payment_favouring TEXT NOT NULL DEFAULT '', unique_reference_number TEXT NOT NULL DEFAULT '',
|
|
amount REAL NOT NULL DEFAULT 0, payload_json TEXT NOT NULL DEFAULT '{}', FOREIGN KEY(ledger_entry_id) REFERENCES tally_voucher_ledger_entries(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_vouchers_company_date ON tally_vouchers(tally_guid, voucher_date);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_voucher_ledger_entries_voucher ON tally_voucher_ledger_entries(voucher_id, line_no);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_bank_allocations_ledger_entry ON tally_bank_allocations(ledger_entry_id, line_no);
|
|
|
|
CREATE TABLE IF NOT EXISTS it_depreciation_rate_rules (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, ledger_key TEXT NOT NULL, ledger_guid TEXT NOT NULL DEFAULT '',
|
|
ledger_name TEXT NOT NULL, rate_percent REAL NOT NULL DEFAULT 0, updated_by_user_id INTEGER, updated_at_utc TEXT NOT NULL,
|
|
UNIQUE(tally_guid, ledger_key)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS it_depreciation_runs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL, mapping_id INTEGER,
|
|
fy_start TEXT NOT NULL, fy_end TEXT NOT NULL, depreciation_expense_ledger TEXT NOT NULL DEFAULT '', depreciation_reserve_ledger TEXT NOT NULL DEFAULT '',
|
|
requested_by_user_id INTEGER, status TEXT NOT NULL DEFAULT 'draft', created_at_utc TEXT NOT NULL, total_depreciation REAL NOT NULL DEFAULT 0, details_json TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
CREATE TABLE IF NOT EXISTS it_depreciation_run_lines (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, run_id INTEGER NOT NULL, line_no INTEGER NOT NULL, ledger_key TEXT NOT NULL, ledger_guid TEXT NOT NULL DEFAULT '',
|
|
ledger_name TEXT NOT NULL, parent_group TEXT NOT NULL DEFAULT '', opening_wdv REAL NOT NULL DEFAULT 0, additions_full_rate REAL NOT NULL DEFAULT 0,
|
|
additions_half_rate REAL NOT NULL DEFAULT 0, reductions REAL NOT NULL DEFAULT 0, rate_percent REAL NOT NULL DEFAULT 0, depreciation_amount REAL NOT NULL DEFAULT 0,
|
|
closing_wdv REAL NOT NULL DEFAULT 0, payload_json TEXT NOT NULL DEFAULT '{}', FOREIGN KEY(run_id) REFERENCES it_depreciation_runs(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS ix_it_dep_rate_rules_company ON it_depreciation_rate_rules(tally_guid, ledger_name);
|
|
CREATE INDEX IF NOT EXISTS ix_it_dep_runs_company_fy ON it_depreciation_runs(tally_guid, fy_start, fy_end, id);
|
|
CREATE INDEX IF NOT EXISTS ix_it_dep_lines_run ON it_depreciation_run_lines(run_id, line_no);
|
|
|
|
CREATE TABLE IF NOT EXISTS tally_writeback_attempts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, depreciation_run_id INTEGER NOT NULL,
|
|
tally_guid TEXT NOT NULL, company_name TEXT NOT NULL, voucher_type TEXT NOT NULL DEFAULT 'Journal',
|
|
voucher_date TEXT NOT NULL, reference TEXT NOT NULL DEFAULT '', debit_ledger TEXT NOT NULL,
|
|
credit_ledger TEXT NOT NULL, amount REAL NOT NULL DEFAULT 0, requested_by_user_id INTEGER,
|
|
started_at_utc TEXT NOT NULL, completed_at_utc TEXT, status TEXT NOT NULL,
|
|
tally_created INTEGER NOT NULL DEFAULT 0, tally_altered INTEGER NOT NULL DEFAULT 0,
|
|
tally_errors INTEGER NOT NULL DEFAULT 0, tally_last_voucher_id TEXT NOT NULL DEFAULT '',
|
|
request_xml TEXT NOT NULL DEFAULT '', response_xml TEXT NOT NULL DEFAULT '', error_message TEXT,
|
|
FOREIGN KEY(depreciation_run_id) REFERENCES it_depreciation_runs(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_writeback_run ON tally_writeback_attempts(depreciation_run_id, id);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_tally_groups_company ON tally_groups(tally_guid, name);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_ledgers_company ON tally_ledgers(tally_guid, name);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_voucher_types_company ON tally_voucher_types(tally_guid, name);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_stock_groups_company ON tally_stock_groups(tally_guid, name);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_stock_categories_company ON tally_stock_categories(tally_guid, name);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_stock_items_company ON tally_stock_items(tally_guid, name);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_units_company ON tally_units(tally_guid, name);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_cost_categories_company ON tally_cost_categories(tally_guid, name);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_cost_centres_company ON tally_cost_centres(tally_guid, name);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_tally_companies_last_seen
|
|
ON tally_companies(last_seen_at_utc);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_company_mapping_client_active
|
|
ON tally_company_mapping(client_id, is_active);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_company_mapping_guid
|
|
ON tally_company_mapping(tally_guid, is_active);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_connection_history_checked
|
|
ON tally_connection_history(checked_at_utc);
|
|
CREATE INDEX IF NOT EXISTS ix_tally_sync_runs_started
|
|
ON tally_sync_runs(started_at_utc);
|
|
"""
|
|
)
|
|
self._ensure_mapping_columns(db)
|
|
dep_columns = {row["name"] for row in db.execute("PRAGMA table_info(it_depreciation_runs)").fetchall()}
|
|
for name, ddl in {
|
|
"approved_by_user_id": "INTEGER", "approved_at_utc": "TEXT", "approval_note": "TEXT NOT NULL DEFAULT ''",
|
|
"posted_by_user_id": "INTEGER", "posted_at_utc": "TEXT", "posted_voucher_type": "TEXT NOT NULL DEFAULT ''",
|
|
"posted_voucher_number": "TEXT NOT NULL DEFAULT ''", "tally_posting_status": "TEXT NOT NULL DEFAULT ''",
|
|
"tally_posting_error": "TEXT NOT NULL DEFAULT ''",
|
|
}.items():
|
|
if name not in dep_columns:
|
|
db.execute(f"ALTER TABLE it_depreciation_runs ADD COLUMN {name} {ddl}")
|
|
now = _utc_now_iso()
|
|
metadata = {
|
|
"schema_version": str(ACT_SCHEMA_VERSION),
|
|
"client_id": str(_safe_client_id(client_id)),
|
|
"tenant_id": "" if tenant_id is None else str(int(tenant_id)),
|
|
"client_name": str(client_name or ""),
|
|
"created_by_user_id": "" if created_by_user_id is None else str(int(created_by_user_id)),
|
|
"storage_kind": "ERP_ACCOUNTING_ACT_SQLITE",
|
|
}
|
|
for key, value in metadata.items():
|
|
db.execute(
|
|
"""
|
|
INSERT INTO act_meta(key, value, updated_at_utc)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET
|
|
value=excluded.value,
|
|
updated_at_utc=excluded.updated_at_utc
|
|
""",
|
|
(key, value, now),
|
|
)
|
|
return path
|
|
|
|
@staticmethod
|
|
def _ensure_mapping_columns(db: sqlite3.Connection) -> None:
|
|
columns = {row["name"] for row in db.execute("PRAGMA table_info(tally_company_mapping)").fetchall()}
|
|
additions = {
|
|
"registration_type_code": "TEXT NOT NULL DEFAULT ''",
|
|
"registration_number": "TEXT NOT NULL DEFAULT ''",
|
|
"registration_legal_name": "TEXT NOT NULL DEFAULT ''",
|
|
"registration_trade_name": "TEXT NOT NULL DEFAULT ''",
|
|
"business_unit_id": "INTEGER",
|
|
"client_branch_id": "INTEGER",
|
|
"updated_at_utc": "TEXT",
|
|
"unmapped_at_utc": "TEXT",
|
|
"unmapped_by_user_id": "INTEGER",
|
|
}
|
|
for name, ddl in additions.items():
|
|
if name not in columns:
|
|
db.execute(f"ALTER TABLE tally_company_mapping ADD COLUMN {name} {ddl}")
|
|
|
|
def record_tally_status(self, client_id: int, status: dict) -> None:
|
|
self.initialize(client_id)
|
|
now = _utc_now_iso()
|
|
companies: Sequence[dict] = status.get("companies") or []
|
|
with self.connect(client_id) as db:
|
|
db.execute("UPDATE tally_companies SET is_currently_loaded = 0")
|
|
for company in companies:
|
|
name = str(company.get("name") or "").strip()
|
|
if not name:
|
|
continue
|
|
guid = str(company.get("guid") or "").strip()
|
|
gstin = str(company.get("gstin") or "").strip().upper()
|
|
existing = db.execute(
|
|
"""
|
|
SELECT id FROM tally_companies
|
|
WHERE tally_guid = ? AND company_name = ?
|
|
LIMIT 1
|
|
""",
|
|
(guid, name),
|
|
).fetchone()
|
|
if existing:
|
|
db.execute(
|
|
"""
|
|
UPDATE tally_companies
|
|
SET gstin=?, last_seen_at_utc=?, is_currently_loaded=1
|
|
WHERE id=?
|
|
""",
|
|
(gstin, now, int(existing["id"])),
|
|
)
|
|
else:
|
|
db.execute(
|
|
"""
|
|
INSERT INTO tally_companies(
|
|
tally_guid, company_name, gstin,
|
|
first_seen_at_utc, last_seen_at_utc, is_currently_loaded
|
|
) VALUES (?, ?, ?, ?, ?, 1)
|
|
""",
|
|
(guid, name, gstin, now, now),
|
|
)
|
|
|
|
db.execute(
|
|
"""
|
|
INSERT INTO tally_connection_history(
|
|
checked_at_utc, connected, tally_url, company_count,
|
|
error_message, payload_json
|
|
) VALUES (?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
now,
|
|
1 if status.get("connected") else 0,
|
|
str(status.get("url") or ""),
|
|
int(status.get("company_count") or 0),
|
|
str(status.get("error") or "") or None,
|
|
json.dumps(status, ensure_ascii=False, separators=(",", ":")),
|
|
),
|
|
)
|
|
|
|
def map_company(
|
|
self,
|
|
client_id: int,
|
|
*,
|
|
tally_guid: str,
|
|
company_name: str,
|
|
gstin: str = "",
|
|
registration: dict | None = None,
|
|
mapped_by_user_id: int | None = None,
|
|
) -> dict:
|
|
self.initialize(client_id)
|
|
guid = str(tally_guid or "").strip()
|
|
name = str(company_name or "").strip()
|
|
if not guid:
|
|
raise AccountingActStoreError("Tally company GUID is required for permanent mapping.")
|
|
if not name:
|
|
raise AccountingActStoreError("Tally company name is required.")
|
|
|
|
registration = registration or {}
|
|
registration_id = registration.get("id")
|
|
registration_id = int(registration_id) if registration_id not in (None, "") else None
|
|
now = _utc_now_iso()
|
|
|
|
with self.connect(client_id) as db:
|
|
if registration_id is None:
|
|
db.execute(
|
|
"""
|
|
UPDATE tally_company_mapping
|
|
SET is_active=0, updated_at_utc=?, unmapped_at_utc=?
|
|
WHERE client_id=? AND registration_id IS NULL AND is_active=1
|
|
""",
|
|
(now, now, int(client_id)),
|
|
)
|
|
else:
|
|
db.execute(
|
|
"""
|
|
UPDATE tally_company_mapping
|
|
SET is_active=0, updated_at_utc=?, unmapped_at_utc=?
|
|
WHERE client_id=? AND registration_id=? AND is_active=1
|
|
""",
|
|
(now, now, int(client_id), registration_id),
|
|
)
|
|
|
|
db.execute(
|
|
"""
|
|
UPDATE tally_company_mapping
|
|
SET is_active=0, updated_at_utc=?, unmapped_at_utc=?
|
|
WHERE client_id=? AND tally_guid=? AND is_active=1
|
|
""",
|
|
(now, now, int(client_id), guid),
|
|
)
|
|
|
|
cursor = db.execute(
|
|
"""
|
|
INSERT INTO tally_company_mapping(
|
|
client_id, registration_id, registration_type_code,
|
|
registration_number, registration_legal_name,
|
|
registration_trade_name, business_unit_id, client_branch_id,
|
|
tally_guid, company_name, gstin, is_active,
|
|
mapped_at_utc, mapped_by_user_id, updated_at_utc
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
|
|
""",
|
|
(
|
|
int(client_id),
|
|
registration_id,
|
|
str(registration.get("registration_type_code") or ""),
|
|
str(registration.get("registration_number") or ""),
|
|
str(registration.get("legal_name") or ""),
|
|
str(registration.get("trade_name") or ""),
|
|
registration.get("business_unit_id"),
|
|
registration.get("client_branch_id"),
|
|
guid,
|
|
name,
|
|
str(gstin or "").strip().upper(),
|
|
now,
|
|
mapped_by_user_id,
|
|
now,
|
|
),
|
|
)
|
|
mapping_id = int(cursor.lastrowid)
|
|
return self.get_mapping(client_id, mapping_id)
|
|
|
|
def unmap_company(self, client_id: int, mapping_id: int, *, unmapped_by_user_id: int | None = None) -> dict:
|
|
self.initialize(client_id)
|
|
now = _utc_now_iso()
|
|
with self.connect(client_id) as db:
|
|
row = db.execute(
|
|
"SELECT id FROM tally_company_mapping WHERE id=? AND client_id=? AND is_active=1",
|
|
(int(mapping_id), int(client_id)),
|
|
).fetchone()
|
|
if not row:
|
|
raise AccountingActStoreError("Active Tally company mapping was not found.")
|
|
db.execute(
|
|
"""
|
|
UPDATE tally_company_mapping
|
|
SET is_active=0, updated_at_utc=?, unmapped_at_utc=?, unmapped_by_user_id=?
|
|
WHERE id=? AND client_id=?
|
|
""",
|
|
(now, now, unmapped_by_user_id, int(mapping_id), int(client_id)),
|
|
)
|
|
return {"mapping_id": int(mapping_id), "unmapped": True}
|
|
|
|
def get_mapping(self, client_id: int, mapping_id: int) -> dict:
|
|
with self.connect(client_id) as db:
|
|
row = db.execute(
|
|
"""
|
|
SELECT *
|
|
FROM tally_company_mapping
|
|
WHERE id=? AND client_id=?
|
|
LIMIT 1
|
|
""",
|
|
(int(mapping_id), int(client_id)),
|
|
).fetchone()
|
|
if not row:
|
|
raise AccountingActStoreError("Tally company mapping was not found.")
|
|
return dict(row)
|
|
|
|
def snapshot(self, client_id: int) -> dict:
|
|
self.initialize(client_id)
|
|
with self.connect(client_id) as db:
|
|
meta_rows = db.execute("SELECT key, value FROM act_meta ORDER BY key").fetchall()
|
|
companies = db.execute(
|
|
"""
|
|
SELECT tally_guid AS guid, company_name AS name, gstin,
|
|
first_seen_at_utc, last_seen_at_utc, is_currently_loaded
|
|
FROM tally_companies
|
|
ORDER BY is_currently_loaded DESC, company_name COLLATE NOCASE
|
|
"""
|
|
).fetchall()
|
|
mappings = db.execute(
|
|
"""
|
|
SELECT id, client_id, registration_id, registration_type_code,
|
|
registration_number, registration_legal_name,
|
|
registration_trade_name, business_unit_id, client_branch_id,
|
|
tally_guid, company_name, gstin, is_active,
|
|
mapped_at_utc, mapped_by_user_id, updated_at_utc,
|
|
unmapped_at_utc, unmapped_by_user_id
|
|
FROM tally_company_mapping
|
|
WHERE is_active=1
|
|
ORDER BY CASE WHEN registration_id IS NULL THEN 0 ELSE 1 END,
|
|
registration_type_code, registration_number, id
|
|
"""
|
|
).fetchall()
|
|
latest = db.execute(
|
|
"""
|
|
SELECT checked_at_utc, connected, tally_url, company_count, error_message
|
|
FROM tally_connection_history
|
|
ORDER BY id DESC LIMIT 1
|
|
"""
|
|
).fetchone()
|
|
loaded_guids = {str(row["guid"] or "") for row in companies if int(row["is_currently_loaded"] or 0)}
|
|
mapping_rows = []
|
|
for row in mappings:
|
|
item = dict(row)
|
|
item["currently_loaded"] = bool(item.get("tally_guid") and item["tally_guid"] in loaded_guids)
|
|
mapping_rows.append(item)
|
|
return {
|
|
"db_path": str(self.db_path(client_id)),
|
|
"metadata": {row["key"]: row["value"] for row in meta_rows},
|
|
"companies": [dict(row) for row in companies],
|
|
"mappings": mapping_rows,
|
|
"latest_connection": dict(latest) if latest else None,
|
|
}
|
|
|
|
|
|
def default_accounting_root() -> Path:
|
|
env = os.getenv("AUDIT_ACCOUNTING_STORAGE_ROOT", "").strip()
|
|
if env:
|
|
return Path(env).expanduser().resolve()
|
|
return Path.cwd() / "data" / "accounting"
|