Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/services/petty_cash_service.py

356 lines
12 KiB
Python
Executable file

from __future__ import annotations
from typing import Any, Dict, List, Optional
from decimal import Decimal
from sqlalchemy.orm import Session
from sqlalchemy import and_, func
from adapters.db.models.petty_cash import PettyCash
from adapters.db.models.document import Document
from adapters.db.models.document_line import DocumentLine
from adapters.db.models.fiscal_year import FiscalYear
from adapters.db.repositories.petty_cash_repository import PettyCashRepository
from app.core.responses import ApiError
def create_petty_cash(db: Session, business_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
# validate required fields
name = (data.get("name") or "").strip()
if name == "":
raise ApiError("STRING_TOO_SHORT", "Name is required", http_status=400)
# code uniqueness in business if provided; else auto-generate numeric min 3 digits
code = data.get("code")
if code is not None and str(code).strip() != "":
if not str(code).isdigit():
raise ApiError("INVALID_PETTY_CASH_CODE", "Petty cash code must be numeric", http_status=400)
if len(str(code)) < 3:
raise ApiError("INVALID_PETTY_CASH_CODE", "Petty cash code must be at least 3 digits", http_status=400)
exists = db.query(PettyCash).filter(and_(PettyCash.business_id == business_id, PettyCash.code == str(code))).first()
if exists:
raise ApiError("DUPLICATE_PETTY_CASH_CODE", "Duplicate petty cash code", http_status=400)
else:
max_code = db.query(func.max(PettyCash.code)).filter(PettyCash.business_id == business_id).scalar()
try:
if max_code is not None and str(max_code).isdigit():
next_code_int = int(max_code) + 1
else:
next_code_int = 100
if next_code_int < 100:
next_code_int = 100
code = str(next_code_int)
except Exception:
code = "100"
repo = PettyCashRepository(db)
obj = repo.create(business_id, {
"name": name,
"code": code,
"description": data.get("description"),
"currency_id": int(data["currency_id"]),
"is_active": bool(data.get("is_active", True)),
"is_default": bool(data.get("is_default", False)),
})
# ensure single default
if obj.is_default:
repo.clear_default(business_id, except_id=obj.id)
db.commit()
db.refresh(obj)
return petty_cash_to_dict(obj)
def get_petty_cash_by_id(db: Session, id_: int) -> Optional[Dict[str, Any]]:
obj = db.query(PettyCash).filter(PettyCash.id == id_).first()
return petty_cash_to_dict(obj) if obj else None
def update_petty_cash(db: Session, id_: int, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
repo = PettyCashRepository(db)
obj = repo.get_by_id(id_)
if obj is None:
return None
# validate name if provided
if "name" in data:
name_val = (data.get("name") or "").strip()
if name_val == "":
raise ApiError("STRING_TOO_SHORT", "Name is required", http_status=400)
if "code" in data and data["code"] is not None and str(data["code"]).strip() != "":
if not str(data["code"]).isdigit():
raise ApiError("INVALID_PETTY_CASH_CODE", "Petty cash code must be numeric", http_status=400)
if len(str(data["code"])) < 3:
raise ApiError("INVALID_PETTY_CASH_CODE", "Petty cash code must be at least 3 digits", http_status=400)
exists = db.query(PettyCash).filter(and_(PettyCash.business_id == obj.business_id, PettyCash.code == str(data["code"]), PettyCash.id != obj.id)).first()
if exists:
raise ApiError("DUPLICATE_PETTY_CASH_CODE", "Duplicate petty cash code", http_status=400)
repo.update(obj, data)
if obj.is_default:
repo.clear_default(obj.business_id, except_id=obj.id)
db.commit()
db.refresh(obj)
return petty_cash_to_dict(obj)
def check_petty_cash_has_accounting_documents(db: Session, petty_cash_id: int) -> tuple[bool, list[str]]:
"""
بررسی وجود اسناد حسابداری مرتبط با تنخواه
Returns:
tuple: (has_documents, document_types)
- has_documents: True اگر سند مرتبطی وجود داشته باشد
- document_types: لیست انواع اسناد مرتبط
"""
from sqlalchemy import func
from adapters.db.models.document import Document
from adapters.db.models.document_line import DocumentLine
# بررسی وجود خطوط سند با petty_cash_id در اسناد قطعی (غیر پیش‌نویس)
document_lines_count = db.query(func.count(DocumentLine.id)).join(
Document, DocumentLine.document_id == Document.id
).filter(
DocumentLine.petty_cash_id == petty_cash_id,
Document.is_proforma == False
).scalar()
if document_lines_count and document_lines_count > 0:
# دریافت انواع اسناد مرتبط
document_types = db.query(Document.document_type).join(
DocumentLine, Document.id == DocumentLine.document_id
).filter(
DocumentLine.petty_cash_id == petty_cash_id,
Document.is_proforma == False
).distinct().all()
types_list = [doc_type[0] for doc_type in document_types if doc_type[0]]
# تبدیل انواع اسناد به نام‌های فارسی
type_names = []
type_mapping = {
"invoice_sales": "فاکتور فروش",
"invoice_sales_return": "برگشت از فروش",
"invoice_purchase": "فاکتور خرید",
"invoice_purchase_return": "برگشت از خرید",
"invoice_direct_consumption": "مصرف مستقیم",
"invoice_production": "تولید",
"invoice_waste": "ضایعات",
"receipt": "دریافت",
"payment": "پرداخت",
"expense": "هزینه",
"income": "درآمد",
"transfer": "انتقال",
"manual": "سند دستی",
"check": "چک",
}
for doc_type in types_list:
type_name = type_mapping.get(doc_type, doc_type)
if type_name not in type_names:
type_names.append(type_name)
return True, type_names
return False, []
def delete_petty_cash(db: Session, id_: int) -> tuple[bool, str | None]:
"""
حذف تنخواه
Returns:
tuple: (success, error_message)
- success: True اگر حذف موفق باشد
- error_message: پیام خطا در صورت عدم موفقیت
"""
obj = db.query(PettyCash).filter(PettyCash.id == id_).first()
if obj is None:
return False, "تنخواه یافت نشد"
# بررسی وجود اسناد حسابداری مرتبط
has_documents, document_types = check_petty_cash_has_accounting_documents(db, id_)
if has_documents:
types_str = "، ".join(document_types)
error_msg = f"امکان حذف این تنخواه وجود ندارد زیرا دارای اسناد حسابداری مرتبط است. انواع اسناد: {types_str}"
return False, error_msg
try:
db.delete(obj)
db.commit()
return True, None
except Exception as e:
db.rollback()
return False, f"خطا در حذف تنخواه: {str(e)}"
def bulk_delete_petty_cash(db: Session, business_id: int, ids: List[int]) -> Dict[str, Any]:
"""
حذف گروهی تنخواه‌ها
"""
if not ids:
return {"deleted": 0, "skipped": 0, "errors": []}
# بررسی وجود تنخواه‌ها و دسترسی به کسب‌وکار
petty_cash_list = db.query(PettyCash).filter(
PettyCash.id.in_(ids),
PettyCash.business_id == business_id
).all()
deleted_count = 0
skipped_count = 0
errors = []
for petty_cash in petty_cash_list:
try:
success, error_message = delete_petty_cash(db, petty_cash.id)
if success:
deleted_count += 1
else:
skipped_count += 1
if error_message:
errors.append(f"تنخواه {petty_cash.name or petty_cash.code or petty_cash.id}: {error_message}")
else:
errors.append(f"تنخواه {petty_cash.name or petty_cash.code or petty_cash.id}: امکان حذف وجود ندارد")
except Exception as e:
skipped_count += 1
errors.append(f"خطا در حذف تنخواه {petty_cash.name or petty_cash.code or petty_cash.id}: {str(e)}")
return {
"deleted": deleted_count,
"skipped": skipped_count,
"total_requested": len(ids),
"errors": errors
}
def _calculate_petty_cash_balance(
db: Session,
petty_cash_id: int,
business_id: int,
fiscal_year_id: Optional[int] = None,
) -> Decimal:
"""موجودی تنخواه به ارز همان تنخواه (با پشتیبانی مبلغ بومی بین‌ارزی)."""
from app.services.account_native_balance import line_native_signed_amount_for_account
pc = db.query(PettyCash).filter(PettyCash.id == int(petty_cash_id)).first()
pc_currency_id = int(pc.currency_id) if pc else None
query = db.query(DocumentLine, Document).join(
Document, DocumentLine.document_id == Document.id
).filter(
Document.business_id == business_id,
Document.is_proforma == False,
DocumentLine.petty_cash_id == petty_cash_id,
)
if fiscal_year_id:
query = query.filter(Document.fiscal_year_id == fiscal_year_id)
total = Decimal(0)
for line, doc in query.all():
total += line_native_signed_amount_for_account(
line, doc, account_currency_id=pc_currency_id
)
return total
def _calculate_petty_cash_balances_bulk(
db: Session,
petty_cash_ids: List[int],
business_id: int,
fiscal_year_id: Optional[int] = None,
) -> Dict[int, Decimal]:
"""موجودی چند تنخواه bulk با پشتیبانی مبلغ بومی."""
from app.services.account_native_balance import line_native_signed_amount_for_account
if not petty_cash_ids:
return {}
pcs = {
int(p.id): p
for p in db.query(PettyCash).filter(PettyCash.id.in_(petty_cash_ids)).all()
}
out: Dict[int, Decimal] = {int(i): Decimal(0) for i in petty_cash_ids}
query = db.query(DocumentLine, Document).join(
Document, DocumentLine.document_id == Document.id
).filter(
Document.business_id == business_id,
Document.is_proforma == False,
DocumentLine.petty_cash_id.in_(petty_cash_ids),
)
if fiscal_year_id:
query = query.filter(Document.fiscal_year_id == fiscal_year_id)
for line, doc in query.all():
pid = int(line.petty_cash_id)
pc = pcs.get(pid)
pc_currency_id = int(pc.currency_id) if pc else None
out[pid] += line_native_signed_amount_for_account(
line, doc, account_currency_id=pc_currency_id
)
return out
def list_petty_cash(db: Session, business_id: int, query: Dict[str, Any]) -> Dict[str, Any]:
# دریافت سال مالی از query (اختیاری)
fiscal_year_id = query.get("fiscal_year_id")
if fiscal_year_id is not None:
try:
fiscal_year_id = int(fiscal_year_id)
except (ValueError, TypeError):
fiscal_year_id = None
# اگر سال مالی مشخص نشده، از سال مالی جاری استفاده کن
if fiscal_year_id is None:
fiscal_year = db.query(FiscalYear).filter(
and_(
FiscalYear.business_id == business_id,
FiscalYear.is_last == True
)
).first()
if fiscal_year:
fiscal_year_id = fiscal_year.id
repo = PettyCashRepository(db)
res = repo.list(business_id, query)
# محاسبه موجودی برای همه تنخواه‌ها به صورت bulk
petty_cash_ids = [item.id for item in res["items"]]
balances = _calculate_petty_cash_balances_bulk(
db, petty_cash_ids, business_id, fiscal_year_id
)
# ساخت لیست با موجودی
result_items = []
for item in res["items"]:
item_dict = petty_cash_to_dict(item)
item_dict["balance"] = float(balances.get(item.id, Decimal(0)))
result_items.append(item_dict)
return {
"items": result_items,
"pagination": res["pagination"],
"query_info": res["query_info"],
}
def petty_cash_to_dict(obj: PettyCash, balance: Optional[float] = None) -> Dict[str, Any]:
result = {
"id": obj.id,
"business_id": obj.business_id,
"name": obj.name,
"code": obj.code,
"description": obj.description,
"currency_id": obj.currency_id,
"is_active": bool(obj.is_active),
"is_default": bool(obj.is_default),
"created_at": obj.created_at.isoformat(),
"updated_at": obj.updated_at.isoformat(),
}
if balance is not None:
result["balance"] = balance
return result