Add Phase 14 sales and income foundation
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user