from typing import Any, Dict, List 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, ApiError from app.core.permissions import require_business_management_dep, require_business_access from adapters.api.v1.schemas import QueryInfo from app.services.cash_register_service import ( create_cash_register, update_cash_register, delete_cash_register, get_cash_register_by_id, list_cash_registers, bulk_delete_cash_registers, ) router = APIRouter(prefix="/cash-registers", tags=["cash-registers"]) @router.post( "/businesses/{business_id}/cash-registers", summary="لیست صندوق‌ها", description="دریافت لیست صندوق‌های یک کسب و کار با امکان جستجو و فیلتر", ) @require_business_access("business_id") async def list_cash_registers_endpoint( request: Request, business_id: int, query_info: QueryInfo, db: Session = Depends(get_db), ctx: AuthContext = Depends(get_current_user), ): query_dict: Dict[str, Any] = { "take": query_info.take, "skip": query_info.skip, "sort_by": query_info.sort_by, "sort_desc": query_info.sort_desc, "search": query_info.search, "search_fields": query_info.search_fields, "filters": query_info.filters, } result = list_cash_registers(db, business_id, query_dict) result["items"] = [format_datetime_fields(item, request) for item in result.get("items", [])] return success_response(data=result, request=request, message="CASH_REGISTERS_LIST_FETCHED") @router.post( "/businesses/{business_id}/cash-registers/create", summary="ایجاد صندوق جدید", description="ایجاد صندوق برای یک کسب‌وکار مشخص", ) @require_business_access("business_id") async def create_cash_register_endpoint( request: Request, business_id: int, body: Dict[str, Any] = Body(...), db: Session = Depends(get_db), ctx: AuthContext = Depends(get_current_user), _: None = Depends(require_business_management_dep), ): payload: Dict[str, Any] = dict(body or {}) created = create_cash_register(db, business_id, payload) return success_response(data=format_datetime_fields(created, request), request=request, message="CASH_REGISTER_CREATED") @router.get( "/cash-registers/{cash_id}", summary="جزئیات صندوق", description="دریافت جزئیات صندوق بر اساس شناسه", ) async def get_cash_register_endpoint( request: Request, cash_id: int, db: Session = Depends(get_db), ctx: AuthContext = Depends(get_current_user), _: None = Depends(require_business_management_dep), ): result = get_cash_register_by_id(db, cash_id) if not result: raise ApiError("CASH_REGISTER_NOT_FOUND", "Cash register not found", http_status=404) try: biz_id = int(result.get("business_id")) except Exception: biz_id = None if biz_id is not None and not ctx.can_access_business(biz_id): raise ApiError("FORBIDDEN", "Access denied", http_status=403) return success_response(data=format_datetime_fields(result, request), request=request, message="CASH_REGISTER_DETAILS") @router.put( "/cash-registers/{cash_id}", summary="ویرایش صندوق", description="ویرایش اطلاعات صندوق", ) async def update_cash_register_endpoint( request: Request, cash_id: int, body: Dict[str, Any] = Body(...), db: Session = Depends(get_db), ctx: AuthContext = Depends(get_current_user), _: None = Depends(require_business_management_dep), ): payload: Dict[str, Any] = dict(body or {}) result = update_cash_register(db, cash_id, payload) if result is None: raise ApiError("CASH_REGISTER_NOT_FOUND", "Cash register not found", http_status=404) try: biz_id = int(result.get("business_id")) except Exception: biz_id = None if biz_id is not None and not ctx.can_access_business(biz_id): raise ApiError("FORBIDDEN", "Access denied", http_status=403) return success_response(data=format_datetime_fields(result, request), request=request, message="CASH_REGISTER_UPDATED") @router.delete( "/cash-registers/{cash_id}", summary="حذف صندوق", description="حذف یک صندوق", ) async def delete_cash_register_endpoint( request: Request, cash_id: int, db: Session = Depends(get_db), ctx: AuthContext = Depends(get_current_user), _: None = Depends(require_business_management_dep), ): result = get_cash_register_by_id(db, cash_id) if result: try: biz_id = int(result.get("business_id")) except Exception: biz_id = None if biz_id is not None and not ctx.can_access_business(biz_id): raise ApiError("FORBIDDEN", "Access denied", http_status=403) ok = delete_cash_register(db, cash_id) if not ok: raise ApiError("CASH_REGISTER_NOT_FOUND", "Cash register not found", http_status=404) return success_response(data=None, request=request, message="CASH_REGISTER_DELETED") @router.post( "/businesses/{business_id}/cash-registers/bulk-delete", summary="حذف گروهی صندوق‌ها", description="حذف چندین صندوق بر اساس شناسه‌ها", ) @require_business_access("business_id") async def bulk_delete_cash_registers_endpoint( request: Request, business_id: int, body: Dict[str, Any] = Body(...), db: Session = Depends(get_db), ctx: AuthContext = Depends(get_current_user), _: None = Depends(require_business_management_dep), ): ids = body.get("ids") if not isinstance(ids, list): ids = [] try: ids = [int(x) for x in ids if isinstance(x, (int, str)) and str(x).isdigit()] except Exception: ids = [] if not ids: return success_response({"deleted": 0, "skipped": 0, "total_requested": 0}, request, message="NO_VALID_IDS_FOR_DELETE") result = bulk_delete_cash_registers(db, business_id, ids) return success_response(result, request, message="CASH_REGISTERS_BULK_DELETE_DONE") @router.post( "/businesses/{business_id}/cash-registers/export/excel", summary="خروجی Excel لیست صندوق‌ها", description="خروجی Excel لیست صندوق‌ها با قابلیت فیلتر و مرتب‌سازی", ) @require_business_access("business_id") async def export_cash_registers_excel( business_id: int, request: Request, body: Dict[str, Any] = Body(...), ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), ): import io from fastapi.responses import Response from openpyxl import Workbook from openpyxl.styles import Font, Alignment, PatternFill, Border, Side from app.core.i18n import negotiate_locale query_dict = { "take": int(body.get("take", 1000)), "skip": int(body.get("skip", 0)), "sort_by": body.get("sort_by"), "sort_desc": bool(body.get("sort_desc", False)), "search": body.get("search"), "search_fields": body.get("search_fields"), "filters": body.get("filters"), } result = list_cash_registers(db, business_id, query_dict) items: List[Dict[str, Any]] = result.get("items", []) items = [format_datetime_fields(item, request) for item in items] # Map currency_id -> currency title for display try: from adapters.db.models.currency import Currency currency_ids = set() for it in items: cid = it.get("currency_id") try: if cid is not None: currency_ids.add(int(cid)) except Exception: pass currency_map: Dict[int, str] = {} if currency_ids: rows = db.query(Currency).filter(Currency.id.in_(list(currency_ids))).all() currency_map = {c.id: (c.title or c.code or str(c.id)) for c in rows} for it in items: cid = it.get("currency_id") it["currency"] = currency_map.get(cid, cid) except Exception: # In case of any issue, fallback without blocking export for it in items: if "currency" not in it and "currency_id" in it: it["currency"] = it.get("currency_id") headers: List[str] = [ "code", "name", "currency", "is_active", "is_default", "payment_switch_number", "payment_terminal_number", "merchant_id", "description", ] wb = Workbook() ws = wb.active ws.title = "CashRegisters" locale = negotiate_locale(request.headers.get("Accept-Language")) if locale == 'fa': try: ws.sheet_view.rightToLeft = True except Exception: pass header_font = Font(bold=True, color="FFFFFF") header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid") header_alignment = Alignment(horizontal="center", vertical="center") border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin')) for col_idx, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col_idx, value=header) cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = border for row_idx, item in enumerate(items, 2): for col_idx, key in enumerate(headers, 1): value = item.get(key, "") if key in ("is_active", "is_default"): try: truthy = bool(value) if isinstance(value, bool) else str(value).strip().lower() in ("true", "1", "yes", "y", "t") except Exception: truthy = bool(value) value = "✓" if truthy else "✗" if isinstance(value, list): value = ", ".join(str(v) for v in value) cell = ws.cell(row=row_idx, column=col_idx, value=value) cell.border = border if locale == 'fa': cell.alignment = Alignment(horizontal="right") for column in ws.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except Exception: pass ws.column_dimensions[column_letter].width = min(max_length + 2, 50) buffer = io.BytesIO() wb.save(buffer) buffer.seek(0) content = buffer.getvalue() return Response( content=content, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={ "Content-Disposition": "attachment; filename=cash_registers.xlsx", "Content-Length": str(len(content)), "Access-Control-Expose-Headers": "Content-Disposition", }, ) @router.post( "/businesses/{business_id}/cash-registers/export/pdf", summary="خروجی PDF لیست صندوق‌ها", description="خروجی PDF لیست صندوق‌ها با قابلیت فیلتر و مرتب‌سازی", ) @require_business_access("business_id") async def export_cash_registers_pdf( business_id: int, request: Request, body: Dict[str, Any] = Body(...), ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), ): from fastapi.responses import Response from weasyprint import HTML from weasyprint.text.fonts import FontConfiguration from app.core.i18n import negotiate_locale query_dict = { "take": int(body.get("take", 1000)), "skip": int(body.get("skip", 0)), "sort_by": body.get("sort_by"), "sort_desc": bool(body.get("sort_desc", False)), "search": body.get("search"), "search_fields": body.get("search_fields"), "filters": body.get("filters"), } result = list_cash_registers(db, business_id, query_dict) items: List[Dict[str, Any]] = result.get("items", []) items = [format_datetime_fields(item, request) for item in items] # Map currency_id -> currency title for display try: from adapters.db.models.currency import Currency currency_ids = set() for it in items: cid = it.get("currency_id") try: if cid is not None: currency_ids.add(int(cid)) except Exception: pass currency_map: Dict[int, str] = {} if currency_ids: rows = db.query(Currency).filter(Currency.id.in_(list(currency_ids))).all() currency_map = {c.id: (c.title or c.code or str(c.id)) for c in rows} for it in items: cid = it.get("currency_id") it["currency"] = currency_map.get(cid, cid) except Exception: for it in items: if "currency" not in it and "currency_id" in it: it["currency"] = it.get("currency_id") selected_only = bool(body.get('selected_only', False)) selected_indices = body.get('selected_indices') if selected_only and selected_indices is not None: indices = None if isinstance(selected_indices, str): import json try: indices = json.loads(selected_indices) except (json.JSONDecodeError, TypeError): indices = None elif isinstance(selected_indices, list): indices = selected_indices if isinstance(indices, list): items = [items[i] for i in indices if isinstance(i, int) and 0 <= i < len(items)] headers: List[str] = [] keys: List[str] = [] export_columns = body.get('export_columns') if export_columns: for col in export_columns: key = col.get('key') label = col.get('label', key) if key: # Replace currency_id key with currency to show human-readable value if str(key) == 'currency_id': keys.append('currency') else: keys.append(str(key)) headers.append(str(label)) else: keys = [ "code", "name", "currency", "is_active", "is_default", "payment_switch_number", "payment_terminal_number", "merchant_id", "description", ] headers = keys 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 = negotiate_locale(request.headers.get("Accept-Language")) is_fa = (locale == 'fa') html_lang = 'fa' if is_fa else 'en' html_dir = 'rtl' if is_fa else 'ltr' try: from app.core.calendar import CalendarConverter calendar_header = request.headers.get("X-Calendar-Type", "jalali").lower() formatted_now = CalendarConverter.format_datetime( __import__("datetime").datetime.now(), "jalali" if calendar_header in ["jalali", "persian", "shamsi"] else "gregorian", ) now_str = formatted_now.get('formatted', formatted_now.get('date_time', '')) except Exception: from datetime import datetime now_str = datetime.now().strftime('%Y/%m/%d %H:%M') def esc(v: Any) -> str: try: return str(v).replace('&', '&').replace('<', '<').replace('>', '>') except Exception: return str(v) rows_html: List[str] = [] for item in items: tds = [] for key in keys: value = item.get(key) if value is None: value = "" elif isinstance(value, list): value = ", ".join(str(v) for v in value) elif key in ("is_active", "is_default"): try: truthy = bool(value) if isinstance(value, bool) else str(value).strip().lower() in ("true", "1", "yes", "y", "t") except Exception: truthy = bool(value) value = "✓" if truthy else "✗" tds.append(f"{esc(value)}") rows_html.append(f"{''.join(tds)}") headers_html = ''.join(f"{esc(h)}" for h in headers) table_html = f"""
{esc('گزارش صندوق‌ها' if is_fa else 'Cash Registers Report')}
{esc('نام کسب‌وکار' if is_fa else 'Business Name')}: {esc(business_name)} | {esc('تاریخ گزارش' if is_fa else 'Report Date')}: {esc(now_str)}
{headers_html}{''.join(rows_html)}
""" font_config = FontConfiguration() pdf_bytes = HTML(string=table_html).write_pdf(font_config=font_config) return Response( content=pdf_bytes, media_type="application/pdf", headers={ "Content-Disposition": "attachment; filename=cash_registers.pdf", "Content-Length": str(len(pdf_bytes)), "Access-Control-Expose-Headers": "Content-Disposition", }, )