207 lines
7.1 KiB
Python
207 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
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.historical_learning_models import AccountingHistoricalLearningRun
|
|
from app.modules.accounting.ledger_learning_service import available_tally_guids
|
|
from app.modules.accounting.sales_learning_models import AccountingSalesHistoricalRun
|
|
from app.modules.accounting.sales_learning_service import (
|
|
historical_sales_rows,
|
|
ingest_completed_sales_run,
|
|
learning_summary,
|
|
sales_mappings,
|
|
)
|
|
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
|
|
from app.modules.documents.agent_jobs import enqueue_agent_job
|
|
from app.modules.documents.models import ERPWorkstationAgent
|
|
|
|
router = APIRouter(
|
|
prefix="/tools/accounting/sales-learning",
|
|
tags=["accounting-sales-learning-ui"],
|
|
)
|
|
|
|
|
|
def _go(client_id=0, message="", error=""):
|
|
q = {}
|
|
if client_id:
|
|
q["client_id"] = client_id
|
|
if message:
|
|
q["message"] = message[:300]
|
|
if error:
|
|
q["error"] = error[:300]
|
|
return RedirectResponse(
|
|
"/tools/accounting/sales-learning" + ("?" + urlencode(q) if q else ""),
|
|
status_code=303,
|
|
)
|
|
|
|
|
|
def _workstations(db, tenant_id: int):
|
|
return list(db.execute(
|
|
select(ERPWorkstationAgent).where(
|
|
ERPWorkstationAgent.tenant_id == int(tenant_id),
|
|
ERPWorkstationAgent.is_active.is_(True),
|
|
ERPWorkstationAgent.tally_connected.is_(True),
|
|
).order_by(ERPWorkstationAgent.machine_name)
|
|
).scalars().all())
|
|
|
|
|
|
@router.get("")
|
|
def page(
|
|
request: Request,
|
|
client_id: int | None = None,
|
|
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(
|
|
(row for row in clients if client_id and int(row.id) == int(client_id)),
|
|
None,
|
|
)
|
|
|
|
mappings = []
|
|
evidence = []
|
|
summary = {"mapping_count": 0, "historical_rows": 0, "review_confirmed": 0, "high_confidence": 0}
|
|
guids = []
|
|
workstations = []
|
|
runs = []
|
|
|
|
if selected:
|
|
# Synchronize latest sales evidence run states opportunistically.
|
|
runs = list(db.execute(
|
|
select(AccountingSalesHistoricalRun).where(
|
|
AccountingSalesHistoricalRun.tenant_id == scope.tenant_id,
|
|
AccountingSalesHistoricalRun.client_id == selected.id,
|
|
).order_by(AccountingSalesHistoricalRun.id.desc()).limit(20)
|
|
).scalars().all())
|
|
for run in runs:
|
|
if run.status not in {"completed", "failed"}:
|
|
ingest_completed_sales_run(db, run)
|
|
|
|
mappings = sales_mappings(db, tenant_id=scope.tenant_id, client_id=selected.id)
|
|
evidence = historical_sales_rows(db, tenant_id=scope.tenant_id, client_id=selected.id)[:200]
|
|
summary = learning_summary(db, tenant_id=scope.tenant_id, client_id=selected.id)
|
|
guids = available_tally_guids(db, scope.tenant_id, selected.id)
|
|
workstations = _workstations(db, scope.tenant_id)
|
|
|
|
return templates.TemplateResponse(
|
|
"modules/accounting/templates/accounting/sales_learning.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": "Customer & Sales Ledger Intelligence",
|
|
"clients": clients,
|
|
"selected_client": selected,
|
|
"mappings": mappings,
|
|
"evidence": evidence,
|
|
"summary": summary,
|
|
"tally_guids": guids,
|
|
"workstations": workstations,
|
|
"runs": runs,
|
|
"message": message,
|
|
"error": error,
|
|
},
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/collect")
|
|
def collect(
|
|
request: Request,
|
|
client_id: int = Form(...),
|
|
workstation_id: int = Form(...),
|
|
tally_guid: str = Form(...),
|
|
date_from: str = Form(""),
|
|
date_to: 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 = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
return _go(error="Client is not visible.")
|
|
|
|
workstation = db.get(ERPWorkstationAgent, int(workstation_id))
|
|
if (
|
|
not workstation
|
|
or int(workstation.tenant_id) != int(scope.tenant_id)
|
|
or not workstation.is_active
|
|
or not workstation.tally_connected
|
|
):
|
|
raise ValueError("Selected workstation is not available with Tally connected.")
|
|
|
|
guid_options = dict(available_tally_guids(db, scope.tenant_id, client.id))
|
|
if tally_guid not in guid_options:
|
|
raise ValueError("Selected Tally company is not mapped to this client.")
|
|
|
|
if not date_from:
|
|
date_from = (date.today() - timedelta(days=730)).isoformat()
|
|
if not date_to:
|
|
date_to = date.today().isoformat()
|
|
|
|
payload = {
|
|
"tenant_id": scope.tenant_id,
|
|
"client_id": client.id,
|
|
"tally_guid": tally_guid,
|
|
"date_from": date_from,
|
|
"date_to": date_to,
|
|
"voucher_scope": "sales",
|
|
}
|
|
job = enqueue_agent_job(
|
|
db,
|
|
workstation_agent_id=workstation.id,
|
|
action="accounting_historical_evidence",
|
|
payload=payload,
|
|
priority=6,
|
|
max_attempts=2,
|
|
created_by_user_id=user.id,
|
|
)
|
|
|
|
run = AccountingSalesHistoricalRun(
|
|
tenant_id=scope.tenant_id,
|
|
client_id=client.id,
|
|
tally_guid=tally_guid,
|
|
company_name=guid_options.get(tally_guid, ""),
|
|
workstation_agent_id=workstation.id,
|
|
agent_job_id=job.id,
|
|
date_from=date_from,
|
|
date_to=date_to,
|
|
status="queued",
|
|
requested_by_user_id=user.id,
|
|
)
|
|
db.add(run)
|
|
db.commit()
|
|
|
|
return _go(
|
|
client.id,
|
|
message="Historical Tally Sales evidence collection queued.",
|
|
)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(client_id, error=str(exc))
|
|
finally:
|
|
db.close()
|