356 lines
11 KiB
Python
356 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from urllib.parse import urlencode
|
|
|
|
from fastapi import APIRouter, Form, Request
|
|
from fastapi.responses import RedirectResponse
|
|
from sqlalchemy import select
|
|
|
|
from app.core.db.common import CommonSessionLocal
|
|
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
|
|
from app.core.templating import templates
|
|
from app.modules.accounting.chart_models import AccountingChartLedger
|
|
from app.modules.accounting.chart_service import ROLE_LABELS
|
|
from app.modules.accounting.ui import _find_visible_client, _require_partner, _visible_clients
|
|
from app.modules.accounting.voucher_engine_service import (
|
|
BUSINESS_NATURES,
|
|
VOUCHER_TYPES,
|
|
approve_draft,
|
|
chart_ledgers,
|
|
create_draft,
|
|
get_draft,
|
|
list_drafts,
|
|
queue_post,
|
|
queue_preflight,
|
|
review_draft,
|
|
sync_drafts,
|
|
visible_workstations,
|
|
void_draft,
|
|
)
|
|
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
|
|
|
router = APIRouter(prefix="/tools/accounting/voucher-engine", tags=["accounting-native-voucher-engine-ui"])
|
|
|
|
|
|
def _go(client_id=0, message="", error="", **extra):
|
|
params = {}
|
|
if client_id:
|
|
params["client_id"] = client_id
|
|
if message:
|
|
params["message"] = message[:350]
|
|
if error:
|
|
params["error"] = error[:350]
|
|
for key, value in extra.items():
|
|
if value not in ("", None):
|
|
params[key] = value
|
|
return RedirectResponse(
|
|
"/tools/accounting/voucher-engine" + ("?" + urlencode(params) if params else ""),
|
|
status_code=303,
|
|
)
|
|
|
|
|
|
def _company_options(db, tenant_id, client_id):
|
|
rows = list(
|
|
db.execute(
|
|
select(AccountingChartLedger.tally_guid, AccountingChartLedger.company_name)
|
|
.where(
|
|
AccountingChartLedger.tenant_id == int(tenant_id),
|
|
AccountingChartLedger.client_id == int(client_id),
|
|
)
|
|
.distinct()
|
|
.order_by(AccountingChartLedger.company_name)
|
|
).all()
|
|
)
|
|
return [{"guid": guid, "name": name or guid} for guid, name in rows if guid]
|
|
|
|
|
|
def _company_name(options, tally_guid):
|
|
return next((row["name"] for row in options if row["guid"] == tally_guid), "")
|
|
|
|
|
|
@router.get("")
|
|
def page(
|
|
request: Request,
|
|
client_id: int | None = None,
|
|
tally_guid: str = "",
|
|
status: str = "",
|
|
page: int = 1,
|
|
per_page: int = 20,
|
|
message: str = "",
|
|
error: str = "",
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, denied = _require_partner(request, db, "accounting.learning.view")
|
|
if denied:
|
|
return denied
|
|
|
|
clients, scope = _visible_clients(db, request, user)
|
|
selected = next((c for c in clients if client_id and int(c.id) == int(client_id)), None)
|
|
|
|
company_options = []
|
|
ledgers = []
|
|
drafts = []
|
|
total = 0
|
|
pages = 1
|
|
workstations = []
|
|
|
|
if selected:
|
|
company_options = _company_options(db, scope.tenant_id, selected.id)
|
|
if not tally_guid and len(company_options) == 1:
|
|
tally_guid = company_options[0]["guid"]
|
|
if tally_guid and tally_guid not in {row["guid"] for row in company_options}:
|
|
tally_guid = ""
|
|
|
|
if tally_guid:
|
|
ledgers = chart_ledgers(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=selected.id,
|
|
tally_guid=tally_guid,
|
|
)
|
|
|
|
drafts, total, page, pages = list_drafts(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=selected.id,
|
|
status=status,
|
|
page=page,
|
|
per_page=per_page,
|
|
)
|
|
sync_drafts(db, drafts)
|
|
drafts, total, page, pages = list_drafts(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=selected.id,
|
|
status=status,
|
|
page=page,
|
|
per_page=per_page,
|
|
)
|
|
workstations = visible_workstations(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
branch_id=scope.branch_id,
|
|
)
|
|
|
|
return templates.TemplateResponse(
|
|
"modules/accounting/templates/accounting/voucher_engine.html",
|
|
{
|
|
"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),
|
|
"title": "Accounting Voucher Engine",
|
|
"clients": clients,
|
|
"selected_client": selected,
|
|
"selected_tally_guid": tally_guid,
|
|
"company_options": company_options,
|
|
"ledgers": ledgers,
|
|
"role_labels": ROLE_LABELS,
|
|
"voucher_types": VOUCHER_TYPES,
|
|
"business_natures": BUSINESS_NATURES,
|
|
"drafts": drafts,
|
|
"total": total,
|
|
"page": page,
|
|
"pages": pages,
|
|
"per_page": per_page,
|
|
"status_filter": status,
|
|
"workstations": workstations,
|
|
"message": message,
|
|
"error": error,
|
|
},
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/draft")
|
|
def create(
|
|
request: Request,
|
|
client_id: int = Form(...),
|
|
tally_guid: str = Form(...),
|
|
voucher_type: str = Form(...),
|
|
business_nature: str = Form(...),
|
|
voucher_date: str = Form(...),
|
|
reference: str = Form(""),
|
|
narration: str = Form(""),
|
|
source_type: str = Form("manual"),
|
|
ledger_name: list[str] = Form(...),
|
|
dr_cr: list[str] = Form(...),
|
|
amount: list[float] = Form(...),
|
|
line_note: list[str] = Form([]),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, denied = _require_partner(request, db, "accounting.learning.manage")
|
|
if denied:
|
|
return denied
|
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
return _go(error="Client is not visible.")
|
|
|
|
options = _company_options(db, scope.tenant_id, client.id)
|
|
company_name = _company_name(options, tally_guid)
|
|
if not company_name:
|
|
raise ValueError("Chart of Accounts for the selected Tally company is not available.")
|
|
|
|
draft = create_draft(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=client.id,
|
|
tally_guid=tally_guid,
|
|
company_name=company_name,
|
|
voucher_type=voucher_type,
|
|
business_nature=business_nature,
|
|
voucher_date=voucher_date,
|
|
reference=reference,
|
|
narration=narration,
|
|
source_type=source_type,
|
|
ledger_names=ledger_name,
|
|
dr_cr_values=dr_cr,
|
|
amounts=amount,
|
|
notes=line_note,
|
|
user_id=user.id,
|
|
)
|
|
return _go(client.id, tally_guid=tally_guid, message=f"{draft.voucher_type} voucher draft #{draft.id} created.")
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(client_id, tally_guid=tally_guid, error=str(exc))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def _draft_for_action(db, request, user, client_id, draft_id):
|
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
raise ValueError("Client is not visible.")
|
|
draft = get_draft(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=client.id,
|
|
draft_id=draft_id,
|
|
)
|
|
if not draft:
|
|
raise ValueError("Voucher draft was not found.")
|
|
return client, scope, draft
|
|
|
|
|
|
@router.post("/draft/{draft_id}/review")
|
|
def review(
|
|
request: Request,
|
|
draft_id: int,
|
|
client_id: int = Form(...),
|
|
note: str = Form(""),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, denied = _require_partner(request, db, "accounting.learning.manage")
|
|
if denied:
|
|
return denied
|
|
client, scope, draft = _draft_for_action(db, request, user, client_id, draft_id)
|
|
review_draft(db, draft=draft, note=note, user_id=user.id)
|
|
return _go(client.id, message=f"Voucher draft #{draft.id} reviewed.")
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(client_id, error=str(exc))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/draft/{draft_id}/approve")
|
|
def approve(
|
|
request: Request,
|
|
draft_id: int,
|
|
client_id: int = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, denied = _require_partner(request, db, "accounting.learning.manage")
|
|
if denied:
|
|
return denied
|
|
client, scope, draft = _draft_for_action(db, request, user, client_id, draft_id)
|
|
approve_draft(db, draft=draft, user_id=user.id)
|
|
return _go(client.id, message=f"Voucher draft #{draft.id} approved for Tally preflight.")
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(client_id, error=str(exc))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/draft/{draft_id}/preflight")
|
|
def preflight(
|
|
request: Request,
|
|
draft_id: int,
|
|
client_id: int = Form(...),
|
|
workstation_id: int = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, denied = _require_partner(request, db, "accounting.learning.manage")
|
|
if denied:
|
|
return denied
|
|
client, scope, draft = _draft_for_action(db, request, user, client_id, draft_id)
|
|
queue_preflight(db, draft=draft, workstation_id=workstation_id, user_id=user.id)
|
|
return _go(client.id, message=f"Voucher draft #{draft.id} Tally preflight queued.")
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(client_id, error=str(exc))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/draft/{draft_id}/post")
|
|
def post(
|
|
request: Request,
|
|
draft_id: int,
|
|
client_id: int = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, denied = _require_partner(request, db, "accounting.learning.manage")
|
|
if denied:
|
|
return denied
|
|
client, scope, draft = _draft_for_action(db, request, user, client_id, draft_id)
|
|
queue_post(db, draft=draft, user_id=user.id)
|
|
return _go(client.id, message=f"Voucher draft #{draft.id} controlled Tally posting queued.")
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(client_id, error=str(exc))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/draft/{draft_id}/void")
|
|
def void(
|
|
request: Request,
|
|
draft_id: int,
|
|
client_id: int = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, denied = _require_partner(request, db, "accounting.learning.manage")
|
|
if denied:
|
|
return denied
|
|
client, scope, draft = _draft_for_action(db, request, user, client_id, draft_id)
|
|
void_draft(db, draft=draft)
|
|
return _go(client.id, message=f"Voucher draft #{draft.id} voided.")
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(client_id, error=str(exc))
|
|
finally:
|
|
db.close()
|