hesabixArc/hesabixAPI/adapters/api/v1/kardex.py

286 lines
9.9 KiB
Python
Raw Normal View History

2025-11-03 15:54:44 +03:30
from typing import Any, Dict
from fastapi import APIRouter, Depends, Request, Body
from sqlalchemy.orm import Session
from adapters.db.session import get_db
from app.core.auth_dependency import get_current_user, AuthContext
from app.core.responses import success_response, format_datetime_fields
from app.core.permissions import require_business_access
from adapters.api.v1.schemas import QueryInfo
from app.services.kardex_service import list_kardex_lines
router = APIRouter(prefix="/kardex", tags=["kardex"])
@router.post(
"/businesses/{business_id}/lines",
summary="لیست کاردکس (خطوط اسناد)",
description="دریافت خطوط اسناد مرتبط با انتخاب‌های چندگانه موجودیت‌ها با فیلتر تاریخ",
)
@require_business_access("business_id")
async def list_kardex_lines_endpoint(
request: Request,
business_id: int,
query_info: QueryInfo,
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
):
# Compose query dict from QueryInfo and additional parameters from body
query_dict: Dict[str, Any] = {
"take": query_info.take,
"skip": query_info.skip,
"sort_by": query_info.sort_by or "document_date",
"sort_desc": query_info.sort_desc,
"search": query_info.search,
"search_fields": query_info.search_fields,
"filters": query_info.filters,
}
# Additional params from body (DataTable additionalParams)
try:
body_json = await request.json()
if isinstance(body_json, dict):
for key in (
"from_date",
"to_date",
"fiscal_year_id",
"person_ids",
"product_ids",
"bank_account_ids",
"cash_register_ids",
"petty_cash_ids",
"account_ids",
"check_ids",
"match_mode",
"result_scope",
):
if key in body_json and body_json.get(key) is not None:
query_dict[key] = body_json.get(key)
except Exception:
pass
result = list_kardex_lines(db, business_id, query_dict)
# Format date fields in response items (document_date)
try:
items = result.get("items", [])
for item in items:
# Use format_datetime_fields for consistency
item.update(format_datetime_fields({"document_date": item.get("document_date")}, request))
except Exception:
pass
return success_response(data=result, request=request, message="KARDEX_LINES")
@router.post(
"/businesses/{business_id}/lines/export/excel",
summary="خروجی Excel کاردکس",
description="خروجی اکسل از لیست خطوط کاردکس با فیلترهای اعمال‌شده",
)
@require_business_access("business_id")
async def export_kardex_excel_endpoint(
request: Request,
business_id: int,
body: Dict[str, Any] = Body(...),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
):
from fastapi.responses import Response
import datetime
try:
max_export_records = 10000
take_value = min(int(body.get("take", 1000)), max_export_records)
except Exception:
take_value = 1000
query_dict: Dict[str, Any] = {
"take": take_value,
"skip": int(body.get("skip", 0)),
"sort_by": body.get("sort_by") or "document_date",
"sort_desc": bool(body.get("sort_desc", True)),
"search": body.get("search"),
"search_fields": body.get("search_fields"),
"filters": body.get("filters"),
"from_date": body.get("from_date"),
"to_date": body.get("to_date"),
"person_ids": body.get("person_ids"),
"product_ids": body.get("product_ids"),
"bank_account_ids": body.get("bank_account_ids"),
"cash_register_ids": body.get("cash_register_ids"),
"petty_cash_ids": body.get("petty_cash_ids"),
"account_ids": body.get("account_ids"),
"check_ids": body.get("check_ids"),
"match_mode": body.get("match_mode") or "any",
"result_scope": body.get("result_scope") or "lines_matching",
"include_running_balance": bool(body.get("include_running_balance", False)),
}
result = list_kardex_lines(db, business_id, query_dict)
items = result.get("items", [])
items = [format_datetime_fields(it, request) for it in items]
# Build simple Excel using openpyxl
from openpyxl import Workbook
from io import BytesIO
wb = Workbook()
ws = wb.active
ws.title = "Kardex"
headers = [
"document_date", "document_code", "document_type", "description",
"debit", "credit", "quantity", "running_amount", "running_quantity",
]
ws.append(headers)
for it in items:
ws.append([
it.get("document_date"),
it.get("document_code"),
it.get("document_type"),
it.get("description"),
it.get("debit"),
it.get("credit"),
it.get("quantity"),
it.get("running_amount"),
it.get("running_quantity"),
])
buf = BytesIO()
wb.save(buf)
content = buf.getvalue()
filename = f"kardex_{business_id}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
return Response(
content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Content-Length": str(len(content)),
"Access-Control-Expose-Headers": "Content-Disposition",
},
)
@router.post(
"/businesses/{business_id}/lines/export/pdf",
summary="خروجی PDF کاردکس",
description="خروجی PDF از لیست خطوط کاردکس با فیلترهای اعمال‌شده",
)
@require_business_access("business_id")
async def export_kardex_pdf_endpoint(
request: Request,
business_id: int,
body: Dict[str, Any] = Body(...),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
):
from fastapi.responses import Response
import datetime
from weasyprint import HTML, CSS
from weasyprint.text.fonts import FontConfiguration
from html import escape
try:
max_export_records = 10000
take_value = min(int(body.get("take", 1000)), max_export_records)
except Exception:
take_value = 1000
query_dict: Dict[str, Any] = {
"take": take_value,
"skip": int(body.get("skip", 0)),
"sort_by": body.get("sort_by") or "document_date",
"sort_desc": bool(body.get("sort_desc", True)),
"search": body.get("search"),
"search_fields": body.get("search_fields"),
"filters": body.get("filters"),
"from_date": body.get("from_date"),
"to_date": body.get("to_date"),
"person_ids": body.get("person_ids"),
"product_ids": body.get("product_ids"),
"bank_account_ids": body.get("bank_account_ids"),
"cash_register_ids": body.get("cash_register_ids"),
"petty_cash_ids": body.get("petty_cash_ids"),
"account_ids": body.get("account_ids"),
"check_ids": body.get("check_ids"),
"match_mode": body.get("match_mode") or "any",
"result_scope": body.get("result_scope") or "lines_matching",
"include_running_balance": bool(body.get("include_running_balance", False)),
}
result = list_kardex_lines(db, business_id, query_dict)
items = result.get("items", [])
items = [format_datetime_fields(it, request) for it in items]
# Build simple HTML table
def cell(val: Any) -> str:
return escape(str(val)) if val is not None else ""
rows_html = "".join([
f"<tr>"
f"<td>{cell(it.get('document_date'))}</td>"
f"<td>{cell(it.get('document_code'))}</td>"
f"<td>{cell(it.get('document_type'))}</td>"
f"<td>{cell(it.get('description'))}</td>"
f"<td style='text-align:right'>{cell(it.get('debit'))}</td>"
f"<td style='text-align:right'>{cell(it.get('credit'))}</td>"
f"<td style='text-align:right'>{cell(it.get('quantity'))}</td>"
f"<td style='text-align:right'>{cell(it.get('running_amount'))}</td>"
f"<td style='text-align:right'>{cell(it.get('running_quantity'))}</td>"
f"</tr>"
for it in items
])
html = f"""
<html>
<head>
<meta charset='utf-8'/>
<style>
body {{ font-family: sans-serif; }}
table {{ width: 100%; border-collapse: collapse; }}
th, td {{ border: 1px solid #ddd; padding: 6px; font-size: 12px; }}
th {{ background: #f5f5f5; text-align: right; }}
</style>
</head>
<body>
<h3>گزارش کاردکس</h3>
<table>
<thead>
<tr>
<th>تاریخ سند</th>
<th>کد سند</th>
<th>نوع سند</th>
<th>شرح</th>
<th>بدهکار</th>
<th>بستانکار</th>
<th>تعداد</th>
<th>مانده مبلغ</th>
<th>مانده تعداد</th>
</tr>
</thead>
<tbody>
{rows_html}
</tbody>
</table>
</body>
</html>
"""
font_config = FontConfiguration()
pdf_bytes = HTML(string=html).write_pdf(stylesheets=[CSS(string="@page { size: A4 landscape; margin: 12mm; }")], font_config=font_config)
filename = f"kardex_{business_id}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Content-Length": str(len(pdf_bytes)),
"Access-Control-Expose-Headers": "Content-Disposition",
},
)