210 lines
6.3 KiB
Python
210 lines
6.3 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 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.sales_service import import_sales_file, list_sales, review_sales
|
|
from app.modules.accounting.sales_taxonomy import sales_income_natures
|
|
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/sales-income",
|
|
tags=["accounting-sales-income-ui"],
|
|
)
|
|
|
|
|
|
def _go(client_id=0, message="", error="", **filters):
|
|
query = {}
|
|
if client_id:
|
|
query["client_id"] = client_id
|
|
if message:
|
|
query["message"] = message[:300]
|
|
if error:
|
|
query["error"] = error[:300]
|
|
for key, value in filters.items():
|
|
if value not in (None, "", "all"):
|
|
query[key] = value
|
|
return RedirectResponse(
|
|
"/tools/accounting/sales-income" + ("?" + urlencode(query) if query else ""),
|
|
status_code=303,
|
|
)
|
|
|
|
|
|
@router.get("")
|
|
def page(
|
|
request: Request,
|
|
client_id: int | None = None,
|
|
status: str = "all",
|
|
customer: str = "",
|
|
period: str = "",
|
|
page: int = 1,
|
|
per_page: int = 25,
|
|
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(
|
|
(client for client in clients if client_id and int(client.id) == int(client_id)),
|
|
None,
|
|
)
|
|
|
|
rows = []
|
|
total = 0
|
|
pages = 1
|
|
if selected:
|
|
rows, total, page, pages = list_sales(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=selected.id,
|
|
status=status,
|
|
customer=customer,
|
|
period=period,
|
|
page=page,
|
|
per_page=per_page,
|
|
)
|
|
|
|
return templates.TemplateResponse(
|
|
"modules/accounting/templates/accounting/sales_income.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": "Sales & Income",
|
|
"clients": clients,
|
|
"selected_client": selected,
|
|
"rows": rows,
|
|
"total": total,
|
|
"page": page,
|
|
"pages": pages,
|
|
"per_page": per_page,
|
|
"status_filter": status,
|
|
"customer_filter": customer,
|
|
"period_filter": period,
|
|
"natures": sales_income_natures(db, scope.tenant_id),
|
|
"message": message,
|
|
"error": error,
|
|
},
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/import")
|
|
async def import_file(
|
|
request: Request,
|
|
client_id: int = Form(...),
|
|
source_type: str = Form("outward_import"),
|
|
csrf_token: str = Form(...),
|
|
file: UploadFile = File(...),
|
|
):
|
|
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.")
|
|
|
|
payload = await file.read()
|
|
if not payload:
|
|
raise ValueError("Uploaded Sales & Income file is empty.")
|
|
if len(payload) > 25 * 1024 * 1024:
|
|
raise ValueError("Sales & Income import file exceeds the 25 MB limit.")
|
|
|
|
result = import_sales_file(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=client.id,
|
|
filename=file.filename or "sales.xlsx",
|
|
payload=payload,
|
|
user_id=user.id,
|
|
source_type=(source_type or "outward_import").strip()[:30],
|
|
)
|
|
return _go(
|
|
client.id,
|
|
message=(
|
|
f"Imported {result['inserted']} sales/income row(s); "
|
|
f"{result['duplicates']} duplicate(s) skipped."
|
|
),
|
|
)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(client_id, error=str(exc))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/{row_id}/review")
|
|
def review(
|
|
request: Request,
|
|
row_id: int,
|
|
client_id: int = Form(...),
|
|
final_nature_id: int = Form(...),
|
|
final_ledger_name: str = Form(""),
|
|
review_note: str = Form(""),
|
|
status: str = Form("all"),
|
|
customer: str = Form(""),
|
|
period: str = Form(""),
|
|
page: int = Form(1),
|
|
per_page: int = Form(25),
|
|
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.")
|
|
|
|
review_sales(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=client.id,
|
|
row_id=row_id,
|
|
final_nature_id=final_nature_id,
|
|
final_ledger_name=final_ledger_name,
|
|
review_note=review_note,
|
|
user_id=user.id,
|
|
)
|
|
return _go(
|
|
client.id,
|
|
message="Sales / income accounting treatment reviewed.",
|
|
status=status,
|
|
customer=customer,
|
|
period=period,
|
|
page=page,
|
|
per_page=per_page,
|
|
)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(
|
|
client_id,
|
|
error=str(exc),
|
|
status=status,
|
|
customer=customer,
|
|
period=period,
|
|
page=page,
|
|
per_page=per_page,
|
|
)
|
|
finally:
|
|
db.close()
|