hesabixArc/hesabixAPI/adapters/api/v1/inventory_transfers.py
2025-11-09 05:16:37 +00:00

255 lines
9.4 KiB
Python

from __future__ import annotations
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.permissions import require_business_access
from app.core.responses import success_response, ApiError, format_datetime_fields
from adapters.api.v1.schemas import QueryInfo
from adapters.db.models.document import Document
from sqlalchemy import and_
from app.services.inventory_transfer_service import create_inventory_transfer, DOCUMENT_TYPE_INVENTORY_TRANSFER
router = APIRouter(prefix="/inventory-transfers", tags=["inventory_transfers"])
@router.post("/business/{business_id}")
@require_business_access("business_id")
def create_inventory_transfer_endpoint(
request: Request,
business_id: int,
payload: Dict[str, Any] = Body(...),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
if not ctx.has_business_permission("inventory", "write"):
raise ApiError("FORBIDDEN", "Missing business permission: inventory.write", http_status=403)
result = create_inventory_transfer(db, business_id, ctx.user_id, payload)
return success_response(data=format_datetime_fields(result["data"], request), request=request, message=result.get("message"))
@router.post("/business/{business_id}/query")
@require_business_access("business_id")
def query_inventory_transfers_endpoint(
request: Request,
business_id: int,
payload: QueryInfo,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
if not ctx.can_read_section("inventory"):
raise ApiError("FORBIDDEN", "Missing business permission: inventory.read", http_status=403)
take = max(1, payload.take)
skip = max(0, payload.skip)
q = db.query(Document).filter(
and_(
Document.business_id == business_id,
Document.document_type == DOCUMENT_TYPE_INVENTORY_TRANSFER,
)
)
total = q.count()
rows = q.order_by(Document.document_date.desc(), Document.id.desc()).offset(skip).limit(take).all()
items = [{
"id": d.id,
"code": d.code,
"document_date": d.document_date,
"currency_id": d.currency_id,
"description": d.description,
} for d in rows]
return success_response(data={
"items": format_datetime_fields(items, request),
"pagination": {
"total": total,
"page": (skip // take) + 1,
"per_page": take,
"total_pages": (total + take - 1) // take,
"has_next": skip + take < total,
"has_prev": skip > 0,
},
"query_info": payload.model_dump(),
}, request=request)
@router.post("/business/{business_id}/export/excel",
summary="خروجی Excel لیست انتقال موجودی",
description="خروجی اکسل از لیست اسناد انتقال موجودی بین انبارها",
)
@require_business_access("business_id")
def export_inventory_transfers_excel(
request: Request,
business_id: int,
body: Dict[str, Any] = Body(default={}),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
):
if not ctx.can_read_section("inventory"):
raise ApiError("FORBIDDEN", "Missing business permission: inventory.read", http_status=403)
from fastapi.responses import Response
from openpyxl import Workbook
from io import BytesIO
import datetime
take = min(int(body.get("take", 1000)), 10000)
skip = int(body.get("skip", 0))
q = db.query(Document).filter(and_(Document.business_id == business_id, Document.document_type == DOCUMENT_TYPE_INVENTORY_TRANSFER))
rows = q.order_by(Document.document_date.desc(), Document.id.desc()).offset(skip).limit(take).all()
wb = Workbook()
ws = wb.active
ws.title = "InventoryTransfers"
ws.append(["code", "document_date", "description"])
for d in rows:
ws.append([d.code, d.document_date.isoformat(), d.description])
buf = BytesIO()
wb.save(buf)
content = buf.getvalue()
filename = f"inventory_transfers_{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("/business/{business_id}/export/pdf",
summary="خروجی PDF لیست انتقال موجودی",
description="خروجی PDF از لیست اسناد انتقال موجودی بین انبارها",
)
@require_business_access("business_id")
def export_inventory_transfers_pdf(
request: Request,
business_id: int,
body: Dict[str, Any] = Body(default={}),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
):
if not ctx.can_read_section("inventory"):
raise ApiError("FORBIDDEN", "Missing business permission: inventory.read", http_status=403)
from fastapi.responses import Response
from weasyprint import HTML, CSS
from weasyprint.text.fonts import FontConfiguration
from html import escape
import datetime
take = min(int(body.get("take", 1000)), 10000)
skip = int(body.get("skip", 0))
q = db.query(Document).filter(and_(Document.business_id == business_id, Document.document_type == DOCUMENT_TYPE_INVENTORY_TRANSFER))
rows = q.order_by(Document.document_date.desc(), Document.id.desc()).offset(skip).limit(take).all()
def cell(v: Any) -> str:
return escape(v.isoformat()) if hasattr(v, 'isoformat') else escape(str(v) if v is not None else "")
rows_html = "".join([
f"<tr>"
f"<td>{cell(d.code)}</td>"
f"<td>{cell(d.document_date)}</td>"
f"<td>{cell(d.description)}</td>"
f"</tr>" for d in rows
])
# تلاش برای رندر با قالب سفارشی (inventory_transfers/list)
resolved_html = None
try:
from app.services.report_template_service import ReportTemplateService
explicit_template_id = None
try:
if body.get("template_id") is not None:
explicit_template_id = int(body.get("template_id"))
except Exception:
explicit_template_id = None
# نام کسب‌وکار
business_name = ""
try:
from adapters.db.models.business import Business
biz = db.query(Business).filter(Business.id == business_id).first()
if biz is not None:
business_name = biz.name or ""
except Exception:
business_name = ""
# Locale
from app.core.i18n import negotiate_locale
locale = negotiate_locale(request.headers.get("Accept-Language"))
is_fa = (locale == 'fa')
headers = ["کد سند", "تاریخ سند", "شرح"]
keys = ["code", "document_date", "description"]
headers_html = "<th>کد سند</th><th>تاریخ سند</th><th>شرح</th>"
template_context = {
"title_text": "لیست انتقال‌ها" if is_fa else "Transfers List",
"business_name": business_name,
"generated_at": datetime.datetime.now().strftime('%Y/%m/%d %H:%M'),
"is_fa": is_fa,
"headers": headers,
"keys": keys,
"items": [ {"code": d.code, "document_date": d.document_date, "description": d.description} for d in rows ],
"table_headers_html": headers_html,
"table_rows_html": rows_html,
}
resolved_html = ReportTemplateService.try_render_resolved(
db=db,
business_id=business_id,
module_key="inventory_transfers",
subtype="list",
context=template_context,
explicit_template_id=explicit_template_id,
)
except Exception:
resolved_html = None
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>
</tr>
</thead>
<tbody>
{rows_html}
</tbody>
</table>
</body>
</html>
"""
final_html = resolved_html or html
font_config = FontConfiguration()
pdf_bytes = HTML(string=final_html).write_pdf(stylesheets=[CSS(string="@page { size: A4 portrait; margin: 12mm; }")], font_config=font_config)
filename = f"inventory_transfers_{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",
},
)