Move Tally accounting DB into FY client engagement folder
This commit is contained in:
@@ -94,7 +94,7 @@
|
|||||||
<div class="mt-4 overflow-x-auto"><table class="min-w-full text-sm"><thead class="bg-slate-50 text-left text-xs uppercase text-slate-500"><tr><th class="px-3 py-2">Ledger</th><th class="px-3 py-2 text-right">Rate</th><th class="px-3 py-2 text-right">Depreciation</th><th class="px-3 py-2 text-right">Closing WDV</th></tr></thead><tbody class="divide-y divide-slate-100">{% for row in depreciation_run.lines %}<tr><td class="px-3 py-2">{{ row.ledger_name }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.rate_percent) }}%</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.depreciation_amount) }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.closing_wdv) }}</td></tr>{% endfor %}</tbody></table></div>
|
<div class="mt-4 overflow-x-auto"><table class="min-w-full text-sm"><thead class="bg-slate-50 text-left text-xs uppercase text-slate-500"><tr><th class="px-3 py-2">Ledger</th><th class="px-3 py-2 text-right">Rate</th><th class="px-3 py-2 text-right">Depreciation</th><th class="px-3 py-2 text-right">Closing WDV</th></tr></thead><tbody class="divide-y divide-slate-100">{% for row in depreciation_run.lines %}<tr><td class="px-3 py-2">{{ row.ledger_name }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.rate_percent) }}%</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.depreciation_amount) }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.closing_wdv) }}</td></tr>{% endfor %}</tbody></table></div>
|
||||||
{% if depreciation_run.status == 'draft' %}
|
{% if depreciation_run.status == 'draft' %}
|
||||||
<form method="post" action="/tools/tally/depreciation/approve" class="mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4">
|
<form method="post" action="/tools/tally/depreciation/approve" class="mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}"><input type="hidden" name="run_id" value="{{ depreciation_run.id }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}"><input type="hidden" name="run_id" value="{{ depreciation_run.id }}"><input type="hidden" name="fy_start" value="{{ depreciation_run.fy_start }}">
|
||||||
<div class="font-semibold text-amber-900">Partner Approval Required</div>
|
<div class="font-semibold text-amber-900">Partner Approval Required</div>
|
||||||
<p class="mt-1 text-sm text-amber-800">Review the company, financial year, ledgers, rates and total before approving. Approval does not post anything to Tally.</p>
|
<p class="mt-1 text-sm text-amber-800">Review the company, financial year, ledgers, rates and total before approving. Approval does not post anything to Tally.</p>
|
||||||
<textarea name="approval_note" rows="2" class="mt-3 w-full rounded-xl border border-amber-300 px-3 py-2" placeholder="Approval note (optional)"></textarea>
|
<textarea name="approval_note" rows="2" class="mt-3 w-full rounded-xl border border-amber-300 px-3 py-2" placeholder="Approval note (optional)"></textarea>
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% elif depreciation_run.status == 'approved' %}
|
{% elif depreciation_run.status == 'approved' %}
|
||||||
<form method="post" action="/tools/tally/depreciation/post-to-tally" class="mt-5 rounded-xl border border-red-200 bg-red-50 p-4">
|
<form method="post" action="/tools/tally/depreciation/post-to-tally" class="mt-5 rounded-xl border border-red-200 bg-red-50 p-4">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}"><input type="hidden" name="run_id" value="{{ depreciation_run.id }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}"><input type="hidden" name="run_id" value="{{ depreciation_run.id }}"><input type="hidden" name="fy_start" value="{{ depreciation_run.fy_start }}">
|
||||||
<div class="font-semibold text-red-900">Controlled Tally Write-back</div>
|
<div class="font-semibold text-red-900">Controlled Tally Write-back</div>
|
||||||
<p class="mt-1 text-sm text-red-800">This creates a real Journal voucher in the mapped company currently open in TallyPrime. The Local Agent re-checks the Tally GUID immediately before posting. Successful drafts cannot be posted twice.</p>
|
<p class="mt-1 text-sm text-red-800">This creates a real Journal voucher in the mapped company currently open in TallyPrime. The Local Agent re-checks the Tally GUID immediately before posting. Successful drafts cannot be posted twice.</p>
|
||||||
<label class="mt-3 flex items-start gap-2 text-sm text-red-900"><input type="checkbox" name="confirm_posting" value="yes" class="mt-1"><span>Create this Journal voucher in TallyPrime.</span></label>
|
<label class="mt-3 flex items-start gap-2 text-sm text-red-900"><input type="checkbox" name="confirm_posting" value="yes" class="mt-1"><span>Create this Journal voucher in TallyPrime.</span></label>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from app.core.templating import templates
|
|||||||
from app.modules.clients.models import Client
|
from app.modules.clients.models import Client
|
||||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||||
from app.modules.core.rbac.permission_guard import require_permission
|
from app.modules.core.rbac.permission_guard import require_permission
|
||||||
from app.modules.documents.services import build_document_scope, get_active_storage_node_for_branch, client_folder_parts
|
from app.modules.documents.services import build_document_scope, get_active_storage_node_for_branch, client_folder_parts, sanitize_segment
|
||||||
from app.modules.accounting.agent_bridge import request_agent_command
|
from app.modules.accounting.agent_bridge import request_agent_command
|
||||||
from app.modules.registrations.models import ClientRegistration, RegistrationType
|
from app.modules.registrations.models import ClientRegistration, RegistrationType
|
||||||
|
|
||||||
@@ -56,14 +56,23 @@ def _find_visible_client(db, request: Request, user, client_id: int):
|
|||||||
return client, clients, scope
|
return client, clients, scope
|
||||||
|
|
||||||
|
|
||||||
def _accounting_storage_payload(client) -> dict:
|
def _financial_year_for_date(value: date | None = None) -> str:
|
||||||
"""Use the exact client folder naming policy already used by Permanent/Engagement storage."""
|
value = value or date.today()
|
||||||
|
start_year = value.year if value.month >= 4 else value.year - 1
|
||||||
|
return f"{start_year}-{str(start_year + 1)[-2:]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _accounting_storage_payload(client, financial_year: str | None = None) -> dict:
|
||||||
|
"""Use the same FY + client-folder policy as existing Engagement storage."""
|
||||||
|
fy_value = str(financial_year or _financial_year_for_date()).strip()
|
||||||
|
fy_folder = sanitize_segment(f"FY{fy_value}", "FY")
|
||||||
letter, client_folder = client_folder_parts(client, int(client.id))
|
letter, client_folder = client_folder_parts(client, int(client.id))
|
||||||
relative_dir = Path("Accounting") / "Clients" / letter / client_folder
|
relative_dir = Path(fy_folder) / "Clients" / letter / client_folder / "Accounting"
|
||||||
return {
|
return {
|
||||||
"client_id": int(client.id),
|
"client_id": int(client.id),
|
||||||
"client_name": str(client.client_name or "").strip(),
|
"client_name": str(client.client_name or "").strip(),
|
||||||
"client_code": str(getattr(client, "client_code", "") or "").strip(),
|
"client_code": str(getattr(client, "client_code", "") or "").strip(),
|
||||||
|
"financial_year": fy_value,
|
||||||
"accounting_relative_dir": relative_dir.as_posix(),
|
"accounting_relative_dir": relative_dir.as_posix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +471,7 @@ def sync_tally_transactions(
|
|||||||
node.node_code,
|
node.node_code,
|
||||||
"accounting_sync_transactions",
|
"accounting_sync_transactions",
|
||||||
{
|
{
|
||||||
**_accounting_storage_payload(client),
|
**_accounting_storage_payload(client, _financial_year_for_date(start)),
|
||||||
"tally_guid": str(tally_guid or "").strip(),
|
"tally_guid": str(tally_guid or "").strip(),
|
||||||
"date_from": start.isoformat(),
|
"date_from": start.isoformat(),
|
||||||
"date_to": end.isoformat(),
|
"date_to": end.isoformat(),
|
||||||
@@ -504,16 +513,16 @@ def depreciation_it_tool(
|
|||||||
live_result = None; preview = None; depreciation_run = None; command_error = error or ""
|
live_result = None; preview = None; depreciation_run = None; command_error = error or ""
|
||||||
if selected_client and node and online:
|
if selected_client and node and online:
|
||||||
try:
|
try:
|
||||||
status_response = request_agent_command(node.node_code,"phase6_status",_accounting_storage_payload(selected_client),timeout_seconds=20)
|
status_response = request_agent_command(node.node_code,"phase6_status",_accounting_storage_payload(selected_client, _financial_year_for_date(date.fromisoformat(start_text))),timeout_seconds=20)
|
||||||
if status_response.get("ok"): live_result=status_response.get("result") or {}
|
if status_response.get("ok"): live_result=status_response.get("result") or {}
|
||||||
else: command_error=str(status_response.get("error") or "Local Agent status failed.")
|
else: command_error=str(status_response.get("error") or "Local Agent status failed.")
|
||||||
chosen_guid=str(tally_guid or "").strip()
|
chosen_guid=str(tally_guid or "").strip()
|
||||||
if chosen_guid:
|
if chosen_guid:
|
||||||
preview_response=request_agent_command(node.node_code,"accounting_depreciation_preview",{**_accounting_storage_payload(selected_client),"tally_guid":chosen_guid,"fy_start":start_text,"fy_end":end_text},timeout_seconds=60)
|
preview_response=request_agent_command(node.node_code,"accounting_depreciation_preview",{**_accounting_storage_payload(selected_client, _financial_year_for_date(date.fromisoformat(start_text))),"tally_guid":chosen_guid,"fy_start":start_text,"fy_end":end_text},timeout_seconds=60)
|
||||||
if preview_response.get("ok"): preview=(preview_response.get("result") or {}).get("preview")
|
if preview_response.get("ok"): preview=(preview_response.get("result") or {}).get("preview")
|
||||||
else: command_error=str(preview_response.get("error") or "Depreciation preview failed.")
|
else: command_error=str(preview_response.get("error") or "Depreciation preview failed.")
|
||||||
if run_id:
|
if run_id:
|
||||||
run_response=request_agent_command(node.node_code,"accounting_get_it_depreciation_run",{**_accounting_storage_payload(selected_client),"run_id":int(run_id)},timeout_seconds=30)
|
run_response=request_agent_command(node.node_code,"accounting_get_it_depreciation_run",{**_accounting_storage_payload(selected_client, _financial_year_for_date(date.fromisoformat(start_text))),"run_id":int(run_id)},timeout_seconds=30)
|
||||||
if run_response.get("ok"): depreciation_run=(run_response.get("result") or {}).get("depreciation")
|
if run_response.get("ok"): depreciation_run=(run_response.get("result") or {}).get("depreciation")
|
||||||
except Exception as exc: command_error=str(exc)
|
except Exception as exc: command_error=str(exc)
|
||||||
base={"request":request,"current_user":user,"current_user_roles":get_user_roles(db,user.id),"current_user_permissions":get_user_permissions(db,user.id),"csrf_token":get_or_create_csrf_token(request)}
|
base={"request":request,"current_user":user,"current_user_roles":get_user_roles(db,user.id),"current_user_permissions":get_user_permissions(db,user.id),"csrf_token":get_or_create_csrf_token(request)}
|
||||||
@@ -545,7 +554,7 @@ async def calculate_it_depreciation(request: Request):
|
|||||||
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
|
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
|
||||||
if not node or not _node_online(node): return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&tally_guid={quote(tally_guid)}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&error={quote('ERP Local Agent is offline for the active branch.')}",status_code=303)
|
if not node or not _node_online(node): return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&tally_guid={quote(tally_guid)}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&error={quote('ERP Local Agent is offline for the active branch.')}",status_code=303)
|
||||||
try:
|
try:
|
||||||
result=request_agent_command(node.node_code,"accounting_calculate_it_depreciation",{**_accounting_storage_payload(client),"tally_guid":tally_guid,"fy_start":start.isoformat(),"fy_end":end.isoformat(),"assignments":assignments,"depreciation_expense_ledger":str(form.get("depreciation_expense_ledger") or ""),"depreciation_reserve_ledger":str(form.get("depreciation_reserve_ledger") or ""),"requested_by_user_id":int(user.id)},timeout_seconds=120)
|
result=request_agent_command(node.node_code,"accounting_calculate_it_depreciation",{**_accounting_storage_payload(client, _financial_year_for_date(start)),"tally_guid":tally_guid,"fy_start":start.isoformat(),"fy_end":end.isoformat(),"assignments":assignments,"depreciation_expense_ledger":str(form.get("depreciation_expense_ledger") or ""),"depreciation_reserve_ledger":str(form.get("depreciation_reserve_ledger") or ""),"requested_by_user_id":int(user.id)},timeout_seconds=120)
|
||||||
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Income-tax depreciation calculation failed."))
|
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Income-tax depreciation calculation failed."))
|
||||||
dep=(result.get("result") or {}).get("depreciation") or {}; rid=int(dep.get("run_id"))
|
dep=(result.get("result") or {}).get("depreciation") or {}; rid=int(dep.get("run_id"))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -565,8 +574,9 @@ async def approve_it_depreciation(request: Request):
|
|||||||
if response: return response
|
if response: return response
|
||||||
try:
|
try:
|
||||||
client_id = int(form.get("client_id")); run_id = int(form.get("run_id"))
|
client_id = int(form.get("client_id")); run_id = int(form.get("run_id"))
|
||||||
|
approval_fy_start = date.fromisoformat(str(form.get("fy_start") or ""))
|
||||||
except Exception:
|
except Exception:
|
||||||
return RedirectResponse(url="/tools/tally/depreciation?error=" + quote("Invalid depreciation draft."), status_code=303)
|
return RedirectResponse(url="/tools/tally/depreciation?error=" + quote("Invalid depreciation draft or financial year."), status_code=303)
|
||||||
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
||||||
if not client: return _denied()
|
if not client: return _denied()
|
||||||
if str(form.get("confirm_approval") or "") != "yes":
|
if str(form.get("confirm_approval") or "") != "yes":
|
||||||
@@ -577,7 +587,7 @@ async def approve_it_depreciation(request: Request):
|
|||||||
try:
|
try:
|
||||||
result = request_agent_command(
|
result = request_agent_command(
|
||||||
node.node_code, "accounting_approve_it_depreciation",
|
node.node_code, "accounting_approve_it_depreciation",
|
||||||
{**_accounting_storage_payload(client), "run_id": run_id, "approved_by_user_id": int(user.id), "approval_note": str(form.get("approval_note") or "")},
|
{**_accounting_storage_payload(client, _financial_year_for_date(approval_fy_start)), "run_id": run_id, "approved_by_user_id": int(user.id), "approval_note": str(form.get("approval_note") or "")},
|
||||||
timeout_seconds=30,
|
timeout_seconds=30,
|
||||||
)
|
)
|
||||||
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Approval failed."))
|
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Approval failed."))
|
||||||
@@ -600,8 +610,9 @@ async def post_it_depreciation_to_tally(request: Request):
|
|||||||
if response: return response
|
if response: return response
|
||||||
try:
|
try:
|
||||||
client_id = int(form.get("client_id")); run_id = int(form.get("run_id"))
|
client_id = int(form.get("client_id")); run_id = int(form.get("run_id"))
|
||||||
|
posting_fy_start = date.fromisoformat(str(form.get("fy_start") or ""))
|
||||||
except Exception:
|
except Exception:
|
||||||
return RedirectResponse(url="/tools/tally/depreciation?error=" + quote("Invalid depreciation draft."), status_code=303)
|
return RedirectResponse(url="/tools/tally/depreciation?error=" + quote("Invalid depreciation draft or financial year."), status_code=303)
|
||||||
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
||||||
if not client: return _denied()
|
if not client: return _denied()
|
||||||
if str(form.get("post_confirmation") or "").strip().upper() != "POST":
|
if str(form.get("post_confirmation") or "").strip().upper() != "POST":
|
||||||
@@ -614,7 +625,7 @@ async def post_it_depreciation_to_tally(request: Request):
|
|||||||
try:
|
try:
|
||||||
result = request_agent_command(
|
result = request_agent_command(
|
||||||
node.node_code, "accounting_post_it_depreciation",
|
node.node_code, "accounting_post_it_depreciation",
|
||||||
{**_accounting_storage_payload(client), "run_id": run_id, "posted_by_user_id": int(user.id)},
|
{**_accounting_storage_payload(client, _financial_year_for_date(posting_fy_start)), "run_id": run_id, "posted_by_user_id": int(user.id)},
|
||||||
timeout_seconds=120,
|
timeout_seconds=120,
|
||||||
)
|
)
|
||||||
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally write-back failed."))
|
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally write-back failed."))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import io
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
ERP_LOCAL_AGENT_VERSION = "1.8.1"
|
ERP_LOCAL_AGENT_VERSION = "1.8.2"
|
||||||
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.8.1
|
ERP Local Agent 1.8.2
|
||||||
|
|
||||||
Existing storage, WebSocket tunnel, Tally and client .act functionality are preserved.
|
Existing storage, WebSocket tunnel, Tally and client .act functionality are preserved.
|
||||||
|
|
||||||
@@ -42,3 +42,13 @@ Accounting storage policy 1.8.1:
|
|||||||
- Canonical layout: <STORAGE_ROOT>\Accounting\Clients\<Letter>\<ExistingClientFolder>\client_XXXXXXXX.act
|
- Canonical layout: <STORAGE_ROOT>\Accounting\Clients\<Letter>\<ExistingClientFolder>\client_XXXXXXXX.act
|
||||||
- Existing databases from <STORAGE_ROOT>\Accounting\client_XXXXXXXX or <ERP_Local_Agent>\data\accounting\client_XXXXXXXX are migrated by SQLite backup on first use.
|
- Existing databases from <STORAGE_ROOT>\Accounting\client_XXXXXXXX or <ERP_Local_Agent>\data\accounting\client_XXXXXXXX are migrated by SQLite backup on first use.
|
||||||
- Migration sources are retained as recovery copies; no existing .act file is deleted.
|
- Migration sources are retained as recovery copies; no existing .act file is deleted.
|
||||||
|
|
||||||
|
Accounting engagement-folder policy 1.8.2:
|
||||||
|
- Final canonical Accounting DB path follows the existing Engagement hierarchy:
|
||||||
|
<STORAGE_ROOT>\FY2026-27\Clients\<Letter>\<ExistingClientFolder>\Accounting\client_XXXXXXXX.act
|
||||||
|
- ERP uses the same sanitize_segment("FY"+financial_year) and client_folder_parts() convention as Engagement storage.
|
||||||
|
- Current Tally page/mapping/master sync use the current financial year.
|
||||||
|
- Transaction sync uses the financial year containing date_from.
|
||||||
|
- Depreciation/approval/write-back use the selected depreciation financial year.
|
||||||
|
- Existing .act databases from original flat, v1.8.0 Local-Agent-data, and v1.8.1 Accounting\Clients locations are migrated safely by SQLite backup and retained as recovery copies.
|
||||||
|
- The Accounting folder is the stable base for later Reports, Exports, Workpapers and Backups; this release does not change report-generation behavior.
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "1.8.1"
|
__version__ = "1.8.2"
|
||||||
AGENT_NAME = "ERP Local Agent"
|
AGENT_NAME = "ERP Local Agent"
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ class LocalAccountingStore:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, storage_root: Path):
|
def __init__(self, storage_root: Path):
|
||||||
# Accounting follows the same branch Storage Node root used by Permanent
|
# Accounting follows the same branch Storage Node and FY/client hierarchy used
|
||||||
# and Engagement storage. The ERP supplies the exact client-relative
|
# by Engagement storage. ERP supplies FY.../Clients/<letter>/<client>/Accounting
|
||||||
# directory built with its shared client_folder_parts() policy.
|
# using the existing sanitize_segment() + client_folder_parts() policy.
|
||||||
self.storage_root = Path(storage_root).resolve()
|
self.storage_root = Path(storage_root).resolve()
|
||||||
self.storage_root.mkdir(parents=True, exist_ok=True)
|
self.storage_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -89,10 +89,20 @@ class LocalAccountingStore:
|
|||||||
raise ValueError("Unsafe Accounting storage segment received from ERP.")
|
raise ValueError("Unsafe Accounting storage segment received from ERP.")
|
||||||
safe_parts.append(value)
|
safe_parts.append(value)
|
||||||
relative = Path(*safe_parts)
|
relative = Path(*safe_parts)
|
||||||
# Accounting paths are intentionally constrained to Accounting/Clients/...
|
# Canonical Accounting path mirrors Engagement storage:
|
||||||
parts_lower = [p.lower() for p in relative.parts]
|
# FY2026-27/Clients/A/CLIENT_FOLDER/Accounting
|
||||||
if len(parts_lower) < 4 or parts_lower[0] != "accounting" or parts_lower[1] != "clients":
|
parts = list(relative.parts)
|
||||||
raise ValueError("Accounting storage path does not follow the ERP Storage Node client policy.")
|
parts_lower = [p.lower() for p in parts]
|
||||||
|
if (
|
||||||
|
len(parts) != 5
|
||||||
|
or not parts_lower[0].startswith("fy")
|
||||||
|
or parts_lower[1] != "clients"
|
||||||
|
or len(parts[2]) != 1
|
||||||
|
or parts_lower[4] != "accounting"
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"Accounting storage path must follow FY<year>/Clients/<letter>/<client>/Accounting."
|
||||||
|
)
|
||||||
return relative
|
return relative
|
||||||
|
|
||||||
def bind_client_path(self, client_id: int, accounting_relative_dir: str) -> Path:
|
def bind_client_path(self, client_id: int, accounting_relative_dir: str) -> Path:
|
||||||
@@ -150,12 +160,17 @@ class LocalAccountingStore:
|
|||||||
# Temporary v1.8.0 Local-Agent data location.
|
# Temporary v1.8.0 Local-Agent data location.
|
||||||
candidates.append(self.legacy_agent_root / key / filename)
|
candidates.append(self.legacy_agent_root / key / filename)
|
||||||
|
|
||||||
# If a client was renamed, an earlier canonical client-folder path may exist.
|
# v1.8.1 interim Accounting/Clients/<letter>/<client> location.
|
||||||
accounting_clients = self.storage_root / "Accounting" / "Clients"
|
accounting_clients = self.storage_root / "Accounting" / "Clients"
|
||||||
if accounting_clients.exists():
|
if accounting_clients.exists():
|
||||||
for path in accounting_clients.glob(f"*/*/{filename}"):
|
for path in accounting_clients.glob(f"*/*/{filename}"):
|
||||||
candidates.append(path)
|
candidates.append(path)
|
||||||
|
|
||||||
|
# Earlier/future FY client Accounting locations (for client rename or FY
|
||||||
|
# resolver changes). The active target is excluded below.
|
||||||
|
for path in self.storage_root.glob(f"FY*/Clients/*/*/Accounting/{filename}"):
|
||||||
|
candidates.append(path)
|
||||||
|
|
||||||
unique = []
|
unique = []
|
||||||
seen = set()
|
seen = set()
|
||||||
for path in candidates:
|
for path in candidates:
|
||||||
@@ -201,7 +216,7 @@ class LocalAccountingStore:
|
|||||||
# Do not silently create a new Accounting DB in a non-policy location.
|
# Do not silently create a new Accounting DB in a non-policy location.
|
||||||
# Existing legacy DBs remain discoverable only after ERP supplies its
|
# Existing legacy DBs remain discoverable only after ERP supplies its
|
||||||
# canonical client path on the next status/initialize command.
|
# canonical client path on the next status/initialize command.
|
||||||
return self.storage_root / "Accounting" / "Clients" / "_UNBOUND" / self._client_key(client_id)
|
return self.storage_root / "_UNBOUND_ACCOUNTING" / self._client_key(client_id)
|
||||||
return self.storage_root / relative
|
return self.storage_root / relative
|
||||||
|
|
||||||
def db_path(self, client_id: int) -> Path:
|
def db_path(self, client_id: int) -> Path:
|
||||||
|
|||||||
Reference in New Issue
Block a user