262 lines
9.8 KiB
Python
262 lines
9.8 KiB
Python
from __future__ import annotations
|
|
|
|
from urllib.parse import urlencode
|
|
|
|
from fastapi import APIRouter, File, Form, Request, UploadFile
|
|
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.gstr2b_models import AccountingGSTR2BPurchase
|
|
from app.modules.accounting.gstr2b_service import purchases_for_client
|
|
from app.modules.accounting.ledger_learning_service import available_tally_guids
|
|
from app.modules.accounting.purchase_enrichment_models import (
|
|
AccountingPurchaseEnrichmentBatch,
|
|
AccountingPurchaseEnrichmentRecord,
|
|
)
|
|
from app.modules.accounting.purchase_enrichment_service import (
|
|
batches_for_client,
|
|
import_enrichment,
|
|
items_for_records,
|
|
manually_link_record,
|
|
records_for_batch,
|
|
rematch_batch,
|
|
unlink_record,
|
|
)
|
|
from app.modules.accounting.ui import _find_visible_client, _require_partner, _visible_clients
|
|
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
|
|
|
router = APIRouter(prefix="/tools/accounting/purchase-enrichment", tags=["accounting-purchase-enrichment-ui"])
|
|
|
|
|
|
def _redirect(client_id: int, *, batch_id: int | None = None, message: str = "", error: str = ""):
|
|
params = {"client_id": client_id}
|
|
if batch_id:
|
|
params["batch_id"] = batch_id
|
|
if message:
|
|
params["message"] = message[:220]
|
|
if error:
|
|
params["error"] = error[:220]
|
|
return RedirectResponse(
|
|
url="/tools/accounting/purchase-enrichment?" + urlencode(params),
|
|
status_code=303,
|
|
)
|
|
|
|
|
|
@router.get("")
|
|
def page(
|
|
request: Request,
|
|
client_id: int | None = None,
|
|
batch_id: int | None = None,
|
|
message: str = "",
|
|
error: str = "",
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.learning.view")
|
|
if response:
|
|
return response
|
|
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)
|
|
|
|
batches = []
|
|
selected_batch = None
|
|
records = []
|
|
items = {}
|
|
gstr2b_rows = []
|
|
gstr2b_by_id = {}
|
|
companies = []
|
|
|
|
if selected:
|
|
batches = batches_for_client(db, scope.tenant_id, selected.id)
|
|
selected_batch = next((b for b in batches if batch_id and b.id == batch_id), None)
|
|
if not selected_batch and batches:
|
|
selected_batch = batches[0]
|
|
if selected_batch:
|
|
records = records_for_batch(db, selected_batch.id)
|
|
items = items_for_records(db, [r.id for r in records])
|
|
gstr2b_rows = purchases_for_client(db, scope.tenant_id, selected.id, limit=500)
|
|
gstr2b_by_id = {r.id: r for r in gstr2b_rows}
|
|
companies = available_tally_guids(db, scope.tenant_id, selected.id)
|
|
|
|
return templates.TemplateResponse(
|
|
"modules/accounting/templates/accounting/purchase_enrichment.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": "E-Invoice / E-Way Bill Enrichment",
|
|
"clients": clients,
|
|
"selected_client": selected,
|
|
"batches": batches,
|
|
"selected_batch": selected_batch,
|
|
"records": records,
|
|
"items_by_record": items,
|
|
"gstr2b_rows": gstr2b_rows,
|
|
"gstr2b_by_id": gstr2b_by_id,
|
|
"tally_companies": companies,
|
|
"message": message,
|
|
"error": error,
|
|
},
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/upload")
|
|
async def upload(
|
|
request: Request,
|
|
client_id: int = Form(...),
|
|
source_type: str = Form(...),
|
|
source_period: str = Form(""),
|
|
tally_guid: str = Form(""),
|
|
upload: UploadFile = File(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.learning.manage")
|
|
if response:
|
|
return response
|
|
client, _, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
from app.core.http_responses import ui_access_denied
|
|
return ui_access_denied()
|
|
content = await upload.read()
|
|
batch, duplicate_file = import_enrichment(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=client.id,
|
|
tally_guid=tally_guid,
|
|
source_type=source_type,
|
|
source_period=source_period,
|
|
filename=upload.filename or "source.json",
|
|
content=content,
|
|
user_id=user.id,
|
|
)
|
|
if duplicate_file:
|
|
msg = f"This exact {source_type.replace('_', ' ')} file was already imported as batch #{batch.id}."
|
|
else:
|
|
msg = (
|
|
f"Imported {batch.records_imported} document(s): "
|
|
f"{batch.records_linked} linked, {batch.records_unmatched} unmatched, "
|
|
f"{batch.records_ambiguous} ambiguous, {batch.records_duplicate} duplicate(s)."
|
|
)
|
|
return _redirect(client.id, batch_id=batch.id, message=msg)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _redirect(client_id, error=str(exc))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/batch/{batch_id}/rematch")
|
|
def rematch(
|
|
request: Request,
|
|
batch_id: int,
|
|
client_id: int = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.learning.manage")
|
|
if response:
|
|
return response
|
|
client, _, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
from app.core.http_responses import ui_access_denied
|
|
return ui_access_denied()
|
|
batch = db.execute(select(AccountingPurchaseEnrichmentBatch).where(
|
|
AccountingPurchaseEnrichmentBatch.id == batch_id,
|
|
AccountingPurchaseEnrichmentBatch.tenant_id == scope.tenant_id,
|
|
AccountingPurchaseEnrichmentBatch.client_id == client.id,
|
|
)).scalar_one_or_none()
|
|
if not batch:
|
|
return _redirect(client_id, error="Enrichment batch was not found.")
|
|
count = rematch_batch(db, batch)
|
|
return _redirect(client_id, batch_id=batch.id, message=f"Re-matched {count} enrichment document(s).")
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _redirect(client_id, batch_id=batch_id, error=str(exc))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/record/{record_id}/link")
|
|
def manual_link(
|
|
request: Request,
|
|
record_id: int,
|
|
client_id: int = Form(...),
|
|
purchase_id: int = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.learning.manage")
|
|
if response:
|
|
return response
|
|
client, _, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
from app.core.http_responses import ui_access_denied
|
|
return ui_access_denied()
|
|
record = db.execute(select(AccountingPurchaseEnrichmentRecord).where(
|
|
AccountingPurchaseEnrichmentRecord.id == record_id,
|
|
AccountingPurchaseEnrichmentRecord.tenant_id == scope.tenant_id,
|
|
AccountingPurchaseEnrichmentRecord.client_id == client.id,
|
|
)).scalar_one_or_none()
|
|
purchase = db.execute(select(AccountingGSTR2BPurchase).where(
|
|
AccountingGSTR2BPurchase.id == purchase_id,
|
|
AccountingGSTR2BPurchase.tenant_id == scope.tenant_id,
|
|
AccountingGSTR2BPurchase.client_id == client.id,
|
|
)).scalar_one_or_none()
|
|
if not record or not purchase:
|
|
return _redirect(client_id, error="Record or GSTR-2B purchase was not found.")
|
|
manually_link_record(db, record, purchase)
|
|
return _redirect(client_id, batch_id=record.batch_id, message=f"Linked to GSTR-2B invoice {purchase.invoice_number}.")
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _redirect(client_id, error=str(exc))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/record/{record_id}/unlink")
|
|
def manual_unlink(
|
|
request: Request,
|
|
record_id: int,
|
|
client_id: int = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.learning.manage")
|
|
if response:
|
|
return response
|
|
client, _, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
from app.core.http_responses import ui_access_denied
|
|
return ui_access_denied()
|
|
record = db.execute(select(AccountingPurchaseEnrichmentRecord).where(
|
|
AccountingPurchaseEnrichmentRecord.id == record_id,
|
|
AccountingPurchaseEnrichmentRecord.tenant_id == scope.tenant_id,
|
|
AccountingPurchaseEnrichmentRecord.client_id == client.id,
|
|
)).scalar_one_or_none()
|
|
if not record:
|
|
return _redirect(client_id, error="Enrichment record was not found.")
|
|
batch_id = record.batch_id
|
|
unlink_record(db, record)
|
|
return _redirect(client_id, batch_id=batch_id, message="Enrichment source was unlinked.")
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _redirect(client_id, error=str(exc))
|
|
finally:
|
|
db.close()
|