Prepare ERP source for Gitea deployment

This commit is contained in:
A R R R Associates
2026-06-20 15:01:44 +05:30
commit 5c75eb6bd9
450 changed files with 67698 additions and 0 deletions
+469
View File
@@ -0,0 +1,469 @@
from __future__ import annotations
from urllib.parse import quote
from fastapi import APIRouter, File, Form, Request, UploadFile
from fastapi.responses import RedirectResponse, StreamingResponse
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.security.session_auth import get_current_user
from app.core.templating import templates
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.notice_cases.service import (
CASE_DOCUMENT_TYPES,
CASE_PRIORITIES,
CASE_STATUSES,
CASE_TYPES,
DEPARTMENTS,
case_dashboard_summary,
EVENT_TYPES,
HEARING_STATUSES,
ORDER_TYPES,
add_event,
add_hearing,
add_order,
can_delete_notice_case_documents,
can_manage_notice_cases,
can_upload_notice_case_documents,
case_document_absolute_path,
create_case,
get_case,
list_assignable_users,
list_cases,
list_clients_for_case,
list_engagements_for_client,
save_case_document,
update_case,
user_can_access_case,
)
from app.modules.notice_cases.models import NoticeCaseDocument
from app.modules.core.tenancy.models import FinancialYear
from app.modules.core.tenancy.year_control import redirect_if_financial_year_locked, is_row_financial_year_locked
router = APIRouter(prefix="/notice-cases", tags=["notice-cases-ui"])
def _base_ctx(request: Request, user, db, **ctx):
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),
"departments": DEPARTMENTS,
"case_types": CASE_TYPES,
"case_statuses": CASE_STATUSES,
"case_priorities": CASE_PRIORITIES,
"event_types": EVENT_TYPES,
"hearing_statuses": HEARING_STATUSES,
"order_types": ORDER_TYPES,
"case_document_types": CASE_DOCUMENT_TYPES,
}
base.update(ctx)
return base
def _render(request: Request, template_name: str, db, user, **ctx):
return templates.TemplateResponse(template_name, _base_ctx(request, user, db, **ctx))
def _redirect_denied():
return RedirectResponse(url="/system-settings", status_code=303)
def _require_user(request: Request, db, permission: str):
user = get_current_user(request, db=db)
if not user:
return None, RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, permission)
except Exception:
return user, _redirect_denied()
return user, None
def _active_tenant_id(request: Request, user) -> int:
return int(request.session.get("active_tenant_id") or request.session.get("selected_tenant_id") or request.session.get("tenant_id") or user.tenant_id)
def _active_branch_id(request: Request, user, db) -> int | None:
value = request.session.get("active_branch_id")
perms = set(get_user_permissions(db, user.id))
if value in (None, "", 0, "0"):
if "notice_cases.cross_branch" in perms or "notice_cases.cross_tenant" in perms:
return None
return int(getattr(user, "branch_id", 0) or 0) or None
return int(value)
def _active_financial_year(request: Request) -> str | None:
value = request.session.get("active_financial_year") or getattr(request.state, "year_code", None)
value = (value or "").strip()
return value or None
def _active_assessment_year(db, tenant_id: int, financial_year: str | None) -> str | None:
fy_code = (financial_year or "").strip()
if not fy_code:
return None
row = db.execute(
select(FinancialYear).where(
FinancialYear.tenant_id == tenant_id,
FinancialYear.year_code == fy_code,
)
).scalar_one_or_none()
return row.assessment_year if row else None
def _financial_year_options(db, tenant_id: int):
return list(
db.execute(
select(FinancialYear)
.where(FinancialYear.tenant_id == tenant_id)
.order_by(FinancialYear.start_date.desc(), FinancialYear.year_code.desc())
).scalars()
)
def _selected_financial_year(request: Request, query_financial_year: str | None) -> str | None:
value = (query_financial_year or "").strip()
if value.lower() == "all":
return None
return value or _active_financial_year(request)
def _case_access_allowed(request: Request, db, user, case) -> bool:
return user_can_access_case(
db,
user,
case,
active_tenant_id=_active_tenant_id(request, user),
active_branch_id=_active_branch_id(request, user, db),
active_financial_year=_active_financial_year(request),
active_assessment_year=_active_assessment_year(db, _active_tenant_id(request, user), _active_financial_year(request)),
)
def _form_data(**kwargs):
return {k: v for k, v in kwargs.items()}
@router.get("")
def cases_list(
request: Request,
q: str = "",
department: str = "",
status: str = "",
financial_year: str = "",
assessment_year: str = "",
include_archived: bool = False,
):
db = CommonSessionLocal()
try:
user, response = _require_user(request, db, "notice_cases.view")
if response:
return response
tenant_id = _active_tenant_id(request, user)
branch_id = _active_branch_id(request, user, db)
selected_fy = _selected_financial_year(request, financial_year)
selected_ay = (assessment_year or "").strip() or None
if selected_fy:
selected_ay = None
rows = list_cases(
db,
tenant_id=tenant_id,
branch_id=branch_id,
q=q,
department=department,
status=status,
include_archived=include_archived,
financial_year=selected_fy,
assessment_year=selected_ay,
)
return _render(
request,
"modules/notice_cases/templates/notice_cases/list.html",
db,
user,
title="Notices & Cases",
rows=rows,
summary=case_dashboard_summary(rows),
q=q,
department=department,
status=status,
financial_year=selected_fy or "all",
assessment_year=selected_ay or "",
active_financial_year=_active_financial_year(request),
financial_year_options=_financial_year_options(db, tenant_id),
include_archived=include_archived,
can_manage=can_manage_notice_cases(db, user),
)
finally:
db.close()
@router.get("/new")
def case_create_page(request: Request, client_id: int | None = None):
db = CommonSessionLocal()
try:
user, response = _require_user(request, db, "notice_cases.create")
if response:
return response
tenant_id = _active_tenant_id(request, user)
branch_id = _active_branch_id(request, user, db)
active_fy = _active_financial_year(request)
active_ay = _active_assessment_year(db, tenant_id, active_fy)
clients = list_clients_for_case(db, tenant_id=tenant_id, branch_id=branch_id)
engagements = list_engagements_for_client(db, tenant_id=tenant_id, client_id=client_id, financial_year=active_fy) if client_id else []
users = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id)
return _render(request, "modules/notice_cases/templates/notice_cases/form.html", db, user, title="Create Notice / Case", mode="create", case=None, clients=clients, engagements=engagements, assignable_users=users, selected_client_id=client_id, form_error="", active_financial_year=active_fy, active_assessment_year=active_ay)
finally:
db.close()
@router.post("/new")
def case_create_submit(
request: Request,
client_id: int = Form(...),
engagement_id: str = Form(""),
department: str = Form("GST"),
case_type: str = Form("Notice"),
title: str = Form(""),
reference_no: str = Form(""),
din_ack_no: str = Form(""),
notice_date: str = Form(""),
due_date: str = Form(""),
financial_year: str = Form(""),
assessment_year: str = Form(""),
period_label: str = Form(""),
status: str = Form("open"),
priority: str = Form("normal"),
assigned_partner_user_id: str = Form(""),
assigned_manager_user_id: str = Form(""),
assigned_staff_user_id: str = Form(""),
issue_summary: str = Form(""),
remarks: str = Form(""),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user, response = _require_user(request, db, "notice_cases.create")
if response:
return response
tenant_id = _active_tenant_id(request, user)
branch_id = _active_branch_id(request, user, db)
active_fy = _active_financial_year(request)
active_ay = _active_assessment_year(db, tenant_id, active_fy)
locked_response = redirect_if_financial_year_locked(db, tenant_id=tenant_id, year_code=active_fy, redirect_url=f"/notice-cases?financial_year={active_fy or ''}")
if locked_response:
return locked_response
data = locals().copy(); data.pop("request", None); data.pop("db", None); data.pop("user", None); data.pop("response", None); data.pop("csrf_token", None); data.pop("tenant_id", None); data.pop("branch_id", None)
data["active_financial_year"] = active_fy
data["active_assessment_year"] = active_ay
try:
row = create_case(db, tenant_id=tenant_id, branch_id=branch_id, user=user, data=data)
db.commit()
return RedirectResponse(url=f"/notice-cases/{row.id}?created=1", status_code=303)
except Exception as exc:
db.rollback()
clients = list_clients_for_case(db, tenant_id=tenant_id, branch_id=branch_id)
engagements = list_engagements_for_client(db, tenant_id=tenant_id, client_id=client_id, financial_year=active_fy) if client_id else []
users = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id)
return _render(request, "modules/notice_cases/templates/notice_cases/form.html", db, user, title="Create Notice / Case", mode="create", case=data, clients=clients, engagements=engagements, assignable_users=users, selected_client_id=client_id, form_error=str(exc), active_financial_year=active_fy, active_assessment_year=active_ay)
finally:
db.close()
@router.get("/{case_id}")
def case_detail(request: Request, case_id: int):
db = CommonSessionLocal()
try:
user, response = _require_user(request, db, "notice_cases.view")
if response:
return response
case = get_case(db, case_id)
if not case or not _case_access_allowed(request, db, user, case):
return _redirect_denied()
return _render(request, "modules/notice_cases/templates/notice_cases/detail.html", db, user, title=case.case_code, case=case, can_manage=can_manage_notice_cases(db, user), can_upload=can_upload_notice_case_documents(db, user), can_delete_documents=can_delete_notice_case_documents(db, user))
finally:
db.close()
@router.get("/{case_id}/edit")
def case_edit_page(request: Request, case_id: int):
db = CommonSessionLocal()
try:
user, response = _require_user(request, db, "notice_cases.edit")
if response:
return response
case = get_case(db, case_id)
if not case or not _case_access_allowed(request, db, user, case):
return _redirect_denied()
clients = [case.client]
active_fy = _active_financial_year(request)
active_ay = _active_assessment_year(db, case.tenant_id, active_fy)
engagements = list_engagements_for_client(db, tenant_id=case.tenant_id, client_id=case.client_id, financial_year=active_fy)
users = list_assignable_users(db, tenant_id=case.tenant_id, branch_id=case.branch_id)
return _render(request, "modules/notice_cases/templates/notice_cases/form.html", db, user, title="Edit Notice / Case", mode="edit", case=case, clients=clients, engagements=engagements, assignable_users=users, selected_client_id=case.client_id, form_error="", active_financial_year=active_fy, active_assessment_year=active_ay)
finally:
db.close()
@router.post("/{case_id}/edit")
def case_edit_submit(request: Request, case_id: int, department: str = Form("GST"), case_type: str = Form("Notice"), title: str = Form(""), reference_no: str = Form(""), din_ack_no: str = Form(""), notice_date: str = Form(""), due_date: str = Form(""), financial_year: str = Form(""), assessment_year: str = Form(""), period_label: str = Form(""), status: str = Form("open"), priority: str = Form("normal"), assigned_partner_user_id: str = Form(""), assigned_manager_user_id: str = Form(""), assigned_staff_user_id: str = Form(""), issue_summary: str = Form(""), remarks: str = Form(""), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user, response = _require_user(request, db, "notice_cases.edit")
if response:
return response
case = get_case(db, case_id)
if not case or not _case_access_allowed(request, db, user, case):
return _redirect_denied()
if is_row_financial_year_locked(db, case):
return RedirectResponse(url=f"/notice-cases/{case.id}?year_locked=1", status_code=303)
active_fy = _active_financial_year(request)
active_ay = _active_assessment_year(db, case.tenant_id, active_fy)
data = locals().copy(); [data.pop(k, None) for k in ["request","db","user","response","case","csrf_token","case_id"]]
data["active_financial_year"] = active_fy
data["active_assessment_year"] = active_ay
try:
update_case(db, case=case, user=user, data=data)
db.commit()
return RedirectResponse(url=f"/notice-cases/{case.id}?updated=1", status_code=303)
except Exception as exc:
db.rollback()
clients = [case.client]
engagements = list_engagements_for_client(db, tenant_id=case.tenant_id, client_id=case.client_id, financial_year=active_fy)
users = list_assignable_users(db, tenant_id=case.tenant_id, branch_id=case.branch_id)
return _render(request, "modules/notice_cases/templates/notice_cases/form.html", db, user, title="Edit Notice / Case", mode="edit", case=case, clients=clients, engagements=engagements, assignable_users=users, selected_client_id=case.client_id, form_error=str(exc), active_financial_year=active_fy, active_assessment_year=active_ay)
finally:
db.close()
@router.post("/{case_id}/events")
def case_add_event(request: Request, case_id: int, event_type: str = Form("Internal Note"), event_date: str = Form(""), description: str = Form(""), next_due_date: str = Form(""), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user, response = _require_user(request, db, "notice_cases.events.manage")
if response:
return response
case = get_case(db, case_id)
if not case or not _case_access_allowed(request, db, user, case):
return _redirect_denied()
if is_row_financial_year_locked(db, case):
return RedirectResponse(url=f"/notice-cases/{case.id}?year_locked=1", status_code=303)
add_event(db, case=case, user=user, data=locals())
db.commit()
return RedirectResponse(url=f"/notice-cases/{case_id}?event_added=1", status_code=303)
finally:
db.close()
@router.post("/{case_id}/hearings")
def case_add_hearing(request: Request, case_id: int, hearing_date: str = Form(""), hearing_time: str = Form(""), venue_or_mode: str = Form(""), officer_name: str = Form(""), agenda: str = Form(""), outcome: str = Form(""), status: str = Form("scheduled"), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user, response = _require_user(request, db, "notice_cases.hearings.manage")
if response:
return response
case = get_case(db, case_id)
if not case or not _case_access_allowed(request, db, user, case):
return _redirect_denied()
if is_row_financial_year_locked(db, case):
return RedirectResponse(url=f"/notice-cases/{case.id}?year_locked=1", status_code=303)
add_hearing(db, case=case, user=user, data=locals())
db.commit()
return RedirectResponse(url=f"/notice-cases/{case_id}?hearing_added=1", status_code=303)
finally:
db.close()
@router.post("/{case_id}/orders")
def case_add_order(request: Request, case_id: int, order_type: str = Form("Other"), order_no: str = Form(""), order_date: str = Form(""), demand_amount: str = Form("0"), tax_amount: str = Form("0"), interest_amount: str = Form("0"), penalty_amount: str = Form("0"), summary: str = Form(""), appeal_due_date: str = Form(""), appeal_filed: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user, response = _require_user(request, db, "notice_cases.orders.manage")
if response:
return response
case = get_case(db, case_id)
if not case or not _case_access_allowed(request, db, user, case):
return _redirect_denied()
if is_row_financial_year_locked(db, case):
return RedirectResponse(url=f"/notice-cases/{case.id}?year_locked=1", status_code=303)
add_order(db, case=case, user=user, data=locals())
db.commit()
return RedirectResponse(url=f"/notice-cases/{case_id}?order_added=1", status_code=303)
finally:
db.close()
@router.post("/{case_id}/documents/upload")
def case_document_upload(request: Request, case_id: int, title: str = Form(""), document_type: str = Form("OTHER"), description: str = Form(""), event_id: str = Form(""), file: UploadFile = File(...), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user, response = _require_user(request, db, "notice_cases.documents.upload")
if response:
return response
case = get_case(db, case_id)
if not case or not _case_access_allowed(request, db, user, case):
return _redirect_denied()
if is_row_financial_year_locked(db, case):
return RedirectResponse(url=f"/notice-cases/{case.id}?year_locked=1", status_code=303)
if not file or not file.filename:
return RedirectResponse(url=f"/notice-cases/{case_id}?error=missing_file", status_code=303)
save_case_document(db, case=case, upload_file=file, user=user, title=title, document_type=document_type, description=description, event_id=int(event_id) if event_id else None)
db.commit()
return RedirectResponse(url=f"/notice-cases/{case_id}?uploaded=1", status_code=303)
finally:
db.close()
@router.get("/documents/{document_id}/download")
def case_document_download(request: Request, document_id: int):
db = CommonSessionLocal()
try:
user, response = _require_user(request, db, "notice_cases.documents.download")
if response:
return response
doc = db.execute(select(NoticeCaseDocument).where(NoticeCaseDocument.id == document_id, NoticeCaseDocument.is_deleted.is_(False))).scalar_one_or_none()
case = get_case(db, doc.case_id) if doc else None
if not doc or not case or not _case_access_allowed(request, db, user, case):
return _redirect_denied()
path = case_document_absolute_path(doc)
if not path.exists():
return RedirectResponse(url=f"/notice-cases/{doc.case_id}?error=file_missing", status_code=303)
quoted = quote(doc.original_filename)
headers = {"Content-Disposition": f"attachment; filename*=UTF-8''{quoted}"}
return StreamingResponse(path.open("rb"), media_type=doc.content_type or "application/octet-stream", headers=headers)
finally:
db.close()
@router.post("/documents/{document_id}/delete")
def case_document_delete(request: Request, document_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user, response = _require_user(request, db, "notice_cases.documents.delete")
if response:
return response
doc = db.execute(select(NoticeCaseDocument).where(NoticeCaseDocument.id == document_id, NoticeCaseDocument.is_deleted.is_(False))).scalar_one_or_none()
case = get_case(db, doc.case_id) if doc else None
if not doc or not case or not _case_access_allowed(request, db, user, case):
return _redirect_denied()
doc.is_deleted = True
doc.deleted_at_utc = __import__('datetime').datetime.now(__import__('datetime').timezone.utc)
doc.deleted_by_user_id = user.id
db.commit()
return RedirectResponse(url=f"/notice-cases/{doc.case_id}?deleted=1", status_code=303)
finally:
db.close()