""" سرویس مدیریت اسناد حسابداری عمومی (General Accounting Documents) این سرویس برای مدیریت تمام اسناد حسابداری (عمومی و اتوماتیک) استفاده می‌شود. """ from __future__ import annotations from typing import Any, Dict, List, Optional from datetime import datetime, date import logging from sqlalchemy.orm import Session from adapters.db.repositories.document_repository import DocumentRepository from app.core.responses import ApiError logger = logging.getLogger(__name__) def list_documents( db: Session, business_id: int, query: Dict[str, Any], ) -> Dict[str, Any]: """ دریافت لیست اسناد حسابداری با فیلتر و صفحه‌بندی Args: db: جلسه دیتابیس business_id: شناسه کسب‌وکار query: فیلترها و تنظیمات: - document_type: نوع سند (expense, income, receipt, payment, transfer, manual, ...) - fiscal_year_id: شناسه سال مالی - from_date: از تاریخ - to_date: تا تاریخ - currency_id: شناسه ارز - is_proforma: پیش‌فاکتور یا قطعی - search: عبارت جستجو - sort_by: فیلد مرتب‌سازی - sort_desc: ترتیب نزولی - take: تعداد رکورد در هر صفحه - skip: تعداد رکورد صرف‌نظر شده Returns: دیکشنری حاوی items و pagination """ repo = DocumentRepository(db) # دریافت لیست اسناد documents, total = repo.list_documents_with_filters(business_id, query) # محاسبه pagination take = query.get("take", 50) skip = query.get("skip", 0) page = (skip // take) + 1 total_pages = (total + take - 1) // take return { "items": documents, "pagination": { "total": total, "page": page, "per_page": take, "total_pages": total_pages, "has_next": page < total_pages, "has_prev": page > 1, }, } def get_document(db: Session, document_id: int) -> Optional[Dict[str, Any]]: """ دریافت جزئیات کامل یک سند شامل سطرهای سند Args: db: جلسه دیتابیس document_id: شناسه سند Returns: دیکشنری حاوی اطلاعات کامل سند یا None """ repo = DocumentRepository(db) return repo.get_document_details(document_id) def delete_document(db: Session, document_id: int) -> bool: """ حذف یک سند حسابداری توجه: فقط اسناد عمومی (manual) قابل حذف هستند Args: db: جلسه دیتابیس document_id: شناسه سند Returns: True در صورت موفقیت، False در غیر این صورت Raises: ApiError: در صورت عدم وجود سند یا عدم امکان حذف """ repo = DocumentRepository(db) # بررسی وجود سند document = repo.get_document(document_id) if not document: raise ApiError( "DOCUMENT_NOT_FOUND", "Document not found", http_status=404 ) # بررسی نوع سند - فقط اسناد manual قابل حذف هستند if document.document_type != "manual": raise ApiError( "CANNOT_DELETE_AUTO_DOCUMENT", "Cannot delete automatically generated documents. Please delete from the original source.", http_status=400 ) # حذف سند success = repo.delete_document(document_id) if not success: raise ApiError( "DELETE_FAILED", "Failed to delete document", http_status=500 ) return True def delete_multiple_documents(db: Session, document_ids: List[int]) -> Dict[str, Any]: """ حذف گروهی اسناد حسابداری فقط اسناد عمومی (manual) حذف می‌شوند، اسناد اتوماتیک نادیده گرفته می‌شوند Args: db: جلسه دیتابیس document_ids: لیست شناسه‌های سند Returns: دیکشنری حاوی تعداد حذف شده و خطاها """ repo = DocumentRepository(db) deleted_count = 0 errors = [] skipped_auto = [] for doc_id in document_ids: try: document = repo.get_document(doc_id) if not document: errors.append({"id": doc_id, "error": "Document not found"}) continue # بررسی نوع سند if document.document_type != "manual": skipped_auto.append({ "id": doc_id, "type": document.document_type, "code": document.code }) continue # حذف سند if repo.delete_document(doc_id): deleted_count += 1 else: errors.append({"id": doc_id, "error": "Delete failed"}) except Exception as e: logger.error(f"Error deleting document {doc_id}: {str(e)}") errors.append({"id": doc_id, "error": str(e)}) return { "deleted_count": deleted_count, "total_requested": len(document_ids), "errors": errors, "skipped_auto_documents": skipped_auto, } def get_document_types_summary(db: Session, business_id: int) -> Dict[str, Any]: """ دریافت خلاصه آماری انواع اسناد Args: db: جلسه دیتابیس business_id: شناسه کسب‌وکار Returns: دیکشنری حاوی آمار هر نوع سند """ from adapters.db.models.document import Document from sqlalchemy import func results = ( db.query( Document.document_type, func.count(Document.id).label("count") ) .filter(Document.business_id == business_id) .group_by(Document.document_type) .all() ) summary = {} for row in results: summary[row.document_type] = row.count return summary def export_documents_excel( db: Session, business_id: int, filters: Dict[str, Any], ) -> bytes: """ خروجی Excel لیست اسناد Args: db: جلسه دیتابیس business_id: شناسه کسب‌وکار filters: فیلترها Returns: محتوای فایل Excel به صورت bytes """ try: import io from openpyxl import Workbook from openpyxl.styles import Font, Alignment, PatternFill repo = DocumentRepository(db) # دریافت تمام اسناد (بدون pagination) filters_copy = filters.copy() filters_copy["take"] = 10000 filters_copy["skip"] = 0 documents, _ = repo.list_documents_with_filters(business_id, filters_copy) # ایجاد Workbook wb = Workbook() ws = wb.active ws.title = "Documents" # تنظیم راست‌چین برای فارسی ws.sheet_view.rightToLeft = True # هدر headers = [ "شماره سند", "نوع سند", "تاریخ سند", "سال مالی", "ارز", "بدهکار", "بستانکار", "وضعیت", "توضیحات", "تاریخ ثبت", ] header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid") header_font = Font(bold=True, color="FFFFFF", size=12) for col_num, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col_num, value=header) cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal="center", vertical="center") # داده‌ها for row_num, doc in enumerate(documents, 2): ws.cell(row=row_num, column=1, value=doc.get("code")) ws.cell(row=row_num, column=2, value=doc.get("document_type")) ws.cell(row=row_num, column=3, value=str(doc.get("document_date"))) ws.cell(row=row_num, column=4, value=doc.get("fiscal_year_title")) ws.cell(row=row_num, column=5, value=doc.get("currency_code")) ws.cell(row=row_num, column=6, value=doc.get("total_debit", 0)) ws.cell(row=row_num, column=7, value=doc.get("total_credit", 0)) ws.cell(row=row_num, column=8, value="پیش‌فاکتور" if doc.get("is_proforma") else "قطعی") ws.cell(row=row_num, column=9, value=doc.get("description", "")) ws.cell(row=row_num, column=10, value=str(doc.get("created_at"))) # تنظیم عرض ستون‌ها ws.column_dimensions["A"].width = 15 ws.column_dimensions["B"].width = 15 ws.column_dimensions["C"].width = 15 ws.column_dimensions["D"].width = 20 ws.column_dimensions["E"].width = 10 ws.column_dimensions["F"].width = 15 ws.column_dimensions["G"].width = 15 ws.column_dimensions["H"].width = 12 ws.column_dimensions["I"].width = 30 ws.column_dimensions["J"].width = 20 # ذخیره در حافظه output = io.BytesIO() wb.save(output) output.seek(0) return output.read() except ImportError: raise ApiError( "OPENPYXL_NOT_INSTALLED", "openpyxl library is not installed", http_status=500 ) except Exception as e: logger.error(f"Error generating Excel: {str(e)}") raise ApiError( "EXCEL_GENERATION_FAILED", f"Failed to generate Excel file: {str(e)}", http_status=500 ) def export_documents_pdf( db: Session, business_id: int, filters: Dict[str, Any], ) -> bytes: """ خروجی PDF لیست اسناد Args: db: جلسه دیتابیس business_id: شناسه کسب‌وکار filters: فیلترها Returns: محتوای فایل PDF به صورت bytes """ # TODO: پیاده‌سازی export PDF # می‌توان از WeasyPrint یا ReportLab استفاده کرد raise ApiError( "NOT_IMPLEMENTED", "PDF export is not implemented yet", http_status=501 ) def create_manual_document( db: Session, business_id: int, fiscal_year_id: int, user_id: int, data: Dict[str, Any], ) -> Dict[str, Any]: """ ایجاد سند حسابداری دستی جدید Args: db: جلسه دیتابیس business_id: شناسه کسب‌وکار fiscal_year_id: شناسه سال مالی user_id: شناسه کاربر ایجادکننده data: اطلاعات سند و سطرها Returns: دیکشنری حاوی اطلاعات کامل سند ایجاد شده Raises: ApiError: در صورت بروز خطا """ repo = DocumentRepository(db) # اعتبارسنجی سطرهای سند lines_data = data.get("lines", []) is_valid, error_msg = repo.validate_document_balance(lines_data) if not is_valid: raise ApiError( "INVALID_DOCUMENT", error_msg, http_status=400 ) # تولید کد سند اگر وجود نداشت code = data.get("code") if not code: code = repo.generate_document_code(business_id, "manual") # تبدیل lines به فرمت مناسب repository lines_for_db = [] for line in lines_data: line_dict = { "account_id": line.get("account_id"), "person_id": line.get("person_id"), "product_id": line.get("product_id"), "bank_account_id": line.get("bank_account_id"), "cash_register_id": line.get("cash_register_id"), "petty_cash_id": line.get("petty_cash_id"), "check_id": line.get("check_id"), "quantity": line.get("quantity"), "debit": line.get("debit", 0), "credit": line.get("credit", 0), "description": line.get("description"), "extra_info": line.get("extra_info"), } lines_for_db.append(line_dict) # آماده‌سازی داده‌های سند document_data = { "code": code, "business_id": business_id, "fiscal_year_id": fiscal_year_id, "currency_id": data.get("currency_id"), "created_by_user_id": user_id, "document_date": data.get("document_date"), "document_type": "manual", "is_proforma": data.get("is_proforma", False), "description": data.get("description"), "extra_info": data.get("extra_info"), "lines": lines_for_db, } try: # ایجاد سند document = repo.create_document(document_data) # دریافت جزئیات کامل سند (با روابط) return repo.get_document_details(document.id) except Exception as e: logger.error(f"Error creating manual document: {str(e)}") db.rollback() raise ApiError( "CREATE_DOCUMENT_FAILED", f"Failed to create document: {str(e)}", http_status=500 ) def update_manual_document( db: Session, document_id: int, data: Dict[str, Any], ) -> Dict[str, Any]: """ ویرایش سند حسابداری دستی Args: db: جلسه دیتابیس document_id: شناسه سند data: اطلاعات جدید سند Returns: دیکشنری حاوی اطلاعات کامل سند ویرایش شده Raises: ApiError: در صورت بروز خطا """ repo = DocumentRepository(db) # بررسی وجود سند document = repo.get_document(document_id) if not document: raise ApiError( "DOCUMENT_NOT_FOUND", "Document not found", http_status=404 ) # بررسی اینکه فقط اسناد manual قابل ویرایش هستند if document.document_type != "manual": raise ApiError( "CANNOT_EDIT_AUTO_DOCUMENT", "Cannot edit automatically generated documents. Please edit from the original source.", http_status=400 ) # اگر سطرها ارسال شده، اعتبارسنجی کن if "lines" in data and data["lines"] is not None: lines_data = data["lines"] is_valid, error_msg = repo.validate_document_balance(lines_data) if not is_valid: raise ApiError( "INVALID_DOCUMENT", error_msg, http_status=400 ) # تبدیل lines به فرمت مناسب repository lines_for_db = [] for line in lines_data: line_dict = { "account_id": line.get("account_id"), "person_id": line.get("person_id"), "product_id": line.get("product_id"), "bank_account_id": line.get("bank_account_id"), "cash_register_id": line.get("cash_register_id"), "petty_cash_id": line.get("petty_cash_id"), "check_id": line.get("check_id"), "quantity": line.get("quantity"), "debit": line.get("debit", 0), "credit": line.get("credit", 0), "description": line.get("description"), "extra_info": line.get("extra_info"), } lines_for_db.append(line_dict) data["lines"] = lines_for_db try: # ویرایش سند updated_document = repo.update_document(document_id, data) if not updated_document: raise ApiError( "UPDATE_FAILED", "Failed to update document", http_status=500 ) # دریافت جزئیات کامل سند return repo.get_document_details(updated_document.id) except ApiError: raise except Exception as e: logger.error(f"Error updating manual document: {str(e)}") db.rollback() raise ApiError( "UPDATE_DOCUMENT_FAILED", f"Failed to update document: {str(e)}", http_status=500 )