hesabixArc/hesabixAPI/app/services/check_service.py
2025-11-04 01:51:23 +00:00

866 lines
34 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from typing import Any, Dict, List, Optional
from datetime import datetime, date
from decimal import Decimal
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, func
from adapters.db.models.check import Check, CheckType, CheckStatus, HolderType
from adapters.db.models.document import Document
from adapters.db.models.document_line import DocumentLine
from adapters.db.models.account import Account
from adapters.db.models.fiscal_year import FiscalYear
from adapters.db.models.currency import Currency
from adapters.db.models.person import Person
from app.core.responses import ApiError
def _parse_iso(dt: str) -> datetime:
try:
return datetime.fromisoformat(dt.replace('Z', '+00:00'))
except Exception:
raise ApiError("INVALID_DATE", f"Invalid date: {dt}", http_status=400)
def _parse_iso_date_only(dt: str | datetime | date) -> date:
if isinstance(dt, date) and not isinstance(dt, datetime):
return dt
if isinstance(dt, datetime):
return dt.date()
try:
return datetime.fromisoformat(str(dt)).date()
except Exception:
return datetime.utcnow().date()
def _get_fixed_account_by_code(db: Session, account_code: str) -> Account:
account = db.query(Account).filter(Account.code == str(account_code)).first()
if not account:
from app.core.responses import ApiError
raise ApiError("ACCOUNT_NOT_FOUND", f"Account with code {account_code} not found", http_status=404)
return account
def _get_business_fiscal_year(db: Session, business_id: int) -> FiscalYear:
from sqlalchemy import and_ # local import to avoid unused import if not used elsewhere
fy = db.query(FiscalYear).filter(
and_(FiscalYear.business_id == business_id, FiscalYear.is_closed == False) # noqa: E712
).order_by(FiscalYear.start_date.desc()).first()
if not fy:
from app.core.responses import ApiError
raise ApiError("FISCAL_YEAR_NOT_FOUND", "Active fiscal year not found", http_status=404)
return fy
def create_check(db: Session, business_id: int, user_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
ctype = str(data.get('type', '')).lower()
if ctype not in ("received", "transferred"):
raise ApiError("INVALID_CHECK_TYPE", "Invalid check type", http_status=400)
person_id = data.get('person_id')
if ctype == "received" and not person_id:
raise ApiError("PERSON_REQUIRED", "person_id is required for received checks", http_status=400)
issue_date = _parse_iso(str(data.get('issue_date')))
due_date = _parse_iso(str(data.get('due_date')))
if due_date < issue_date:
raise ApiError("INVALID_DATES", "due_date must be >= issue_date", http_status=400)
sayad = data.get('sayad_code')
if sayad is not None:
s = str(sayad).strip()
if s and (len(s) != 16 or not s.isdigit()):
raise ApiError("INVALID_SAYAD", "sayad_code must be 16 digits", http_status=400)
amount = data.get('amount')
try:
amount_val = float(amount)
except Exception:
raise ApiError("INVALID_AMOUNT", "amount must be a number", http_status=400)
if amount_val <= 0:
raise ApiError("INVALID_AMOUNT", "amount must be > 0", http_status=400)
check_number = str(data.get('check_number', '')).strip()
if not check_number:
raise ApiError("CHECK_NUMBER_REQUIRED", "check_number is required", http_status=400)
# یونیک بودن در سطح کسب‌وکار
exists = db.query(Check).filter(and_(Check.business_id == business_id, Check.check_number == check_number)).first()
if exists is not None:
raise ApiError("DUPLICATE_CHECK_NUMBER", "Duplicate check number in this business", http_status=400)
if sayad:
exists_sayad = db.query(Check).filter(and_(Check.business_id == business_id, Check.sayad_code == sayad)).first()
if exists_sayad is not None:
raise ApiError("DUPLICATE_SAYAD", "Duplicate sayad_code in this business", http_status=400)
obj = Check(
business_id=business_id,
type=CheckType[ctype.upper()],
person_id=int(person_id) if person_id else None,
issue_date=issue_date,
due_date=due_date,
check_number=check_number,
sayad_code=str(sayad).strip() if sayad else None,
bank_name=(str(data.get('bank_name')).strip() if data.get('bank_name') else None),
branch_name=(str(data.get('branch_name')).strip() if data.get('branch_name') else None),
amount=amount_val,
currency_id=int(data.get('currency_id')),
)
# تعیین وضعیت اولیه
if ctype == "received":
obj.status = CheckStatus.RECEIVED_ON_HAND
obj.current_holder_type = HolderType.BUSINESS
obj.current_holder_id = None
else:
obj.status = CheckStatus.TRANSFERRED_ISSUED
obj.current_holder_type = HolderType.PERSON if person_id else HolderType.BUSINESS
obj.current_holder_id = int(person_id) if person_id else None
db.add(obj)
db.commit()
db.refresh(obj)
# ایجاد سند حسابداری خودکار در صورت درخواست
created_document_id: Optional[int] = None
try:
if bool(data.get("auto_post")):
# آماده‌سازی داده‌های سند
document_date: date = _parse_iso_date_only(data.get("document_date") or issue_date)
fiscal_year = _get_business_fiscal_year(db, business_id)
# تعیین حساب‌ها و سطرها
amount_dec = Decimal(str(amount_val))
lines: List[Dict[str, Any]] = []
description = (str(data.get("document_description")).strip() or None) if data.get("document_description") is not None else None
if ctype == "received":
# بدهکار: اسناد دریافتنی 10403
acc_notes_recv = _get_fixed_account_by_code(db, "10403")
lines.append({
"account_id": acc_notes_recv.id,
"debit": amount_dec,
"credit": Decimal(0),
"description": description or "ثبت چک دریافتی",
"check_id": obj.id,
})
# بستانکار: حساب دریافتنی شخص 10401
acc_ar = _get_fixed_account_by_code(db, "10401")
lines.append({
"account_id": acc_ar.id,
"person_id": int(person_id) if person_id else None,
"debit": Decimal(0),
"credit": amount_dec,
"description": description or "ثبت چک دریافتی",
"check_id": obj.id,
})
else: # transferred
# بدهکار: حساب پرداختنی شخص 20201 (در صورت وجود شخص)
acc_ap = _get_fixed_account_by_code(db, "20201")
lines.append({
"account_id": acc_ap.id,
"person_id": int(person_id) if person_id else None,
"debit": amount_dec,
"credit": Decimal(0),
"description": description or "ثبت چک واگذار شده",
"check_id": obj.id,
})
# بستانکار: اسناد پرداختنی 20202
acc_notes_pay = _get_fixed_account_by_code(db, "20202")
lines.append({
"account_id": acc_notes_pay.id,
"debit": Decimal(0),
"credit": amount_dec,
"description": description or "ثبت چک واگذار شده",
"check_id": obj.id,
})
# ایجاد سند (اگر چک واگذار شخص ندارد، از ثبت سند صرف‌نظر می شود)
skip_autopost = (ctype == "transferred" and not person_id)
if not skip_autopost:
document = Document(
code=f"CHK-{document_date.strftime('%Y%m%d')}-{int(datetime.utcnow().timestamp())%100000}",
business_id=business_id,
fiscal_year_id=fiscal_year.id,
currency_id=int(data.get("currency_id")),
created_by_user_id=int(user_id),
document_date=document_date,
document_type="check",
is_proforma=False,
description=description,
extra_info={
"source": "check_create",
"check_id": obj.id,
"check_type": ctype,
},
)
db.add(document)
db.flush()
for line in lines:
db.add(DocumentLine(document_id=document.id, **line))
db.commit()
db.refresh(document)
created_document_id = document.id
except Exception:
# در صورت شکست ایجاد سند، تغییری در ایجاد چک نمی‌دهیم و خطا نمی‌ریزیم
# (می‌توان رفتار را سخت‌گیرانه کرد و رول‌بک نمود؛ فعلاً نرم)
db.rollback()
result = check_to_dict(db, obj)
if created_document_id:
result["document_id"] = created_document_id
return result
def get_check_by_id(db: Session, check_id: int) -> Optional[Dict[str, Any]]:
obj = db.query(Check).filter(Check.id == check_id).first()
return check_to_dict(db, obj) if obj else None
# =====================
# Action helpers
# =====================
def _create_document_for_check_action(
db: Session,
*,
business_id: int,
user_id: int,
currency_id: int,
document_date: date,
description: Optional[str],
lines: List[Dict[str, Any]],
extra_info: Dict[str, Any],
) -> int:
document = Document(
code=f"CHK-{document_date.strftime('%Y%m%d')}-{int(datetime.utcnow().timestamp())%100000}",
business_id=business_id,
fiscal_year_id=_get_business_fiscal_year(db, business_id).id,
currency_id=int(currency_id),
created_by_user_id=int(user_id),
document_date=document_date,
document_type="check",
is_proforma=False,
description=description,
extra_info=extra_info,
)
db.add(document)
db.flush()
for line in lines:
db.add(DocumentLine(document_id=document.id, **line))
db.commit()
db.refresh(document)
return document.id
def _ensure_account(db: Session, code: str) -> int:
return _get_fixed_account_by_code(db, code).id
def _parse_optional_date(d: Any, fallback: date) -> date:
return _parse_iso_date_only(d) if d else fallback
def _load_check_or_404(db: Session, check_id: int) -> Check:
obj = db.query(Check).filter(Check.id == check_id).first()
if not obj:
raise ApiError("CHECK_NOT_FOUND", "Check not found", http_status=404)
return obj
def endorse_check(db: Session, check_id: int, user_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
obj = _load_check_or_404(db, check_id)
if obj.type != CheckType.RECEIVED:
raise ApiError("INVALID_ACTION", "Only received checks can be endorsed", http_status=400)
if obj.status not in (CheckStatus.RECEIVED_ON_HAND, CheckStatus.RETURNED, CheckStatus.BOUNCED):
raise ApiError("INVALID_STATE", f"Cannot endorse from status {obj.status}", http_status=400)
target_person_id = int(data.get("target_person_id"))
document_date = _parse_optional_date(data.get("document_date"), obj.issue_date.date())
description = (data.get("description") or None)
lines: List[Dict[str, Any]] = []
amount_dec = Decimal(str(obj.amount))
# Dr 20201 (target person AP), Cr 10403
lines.append({
"account_id": _ensure_account(db, "20201"),
"person_id": target_person_id,
"debit": amount_dec,
"credit": Decimal(0),
"description": description or "واگذاری چک",
"check_id": obj.id,
})
lines.append({
"account_id": _ensure_account(db, "10403"),
"debit": Decimal(0),
"credit": amount_dec,
"description": description or "واگذاری چک",
"check_id": obj.id,
})
document_id = None
if bool(data.get("auto_post", True)):
document_id = _create_document_for_check_action(
db,
business_id=obj.business_id,
user_id=user_id,
currency_id=obj.currency_id,
document_date=document_date,
description=description,
lines=lines,
extra_info={"source": "check_action", "action": "endorse", "check_id": obj.id},
)
# Update state
obj.status = CheckStatus.ENDORSED
obj.status_at = datetime.utcnow()
obj.current_holder_type = HolderType.PERSON
obj.current_holder_id = target_person_id
obj.last_action_document_id = document_id
db.commit(); db.refresh(obj)
res = check_to_dict(db, obj)
if document_id:
res["document_id"] = document_id
return res
def clear_check(db: Session, check_id: int, user_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
obj = _load_check_or_404(db, check_id)
document_date = _parse_optional_date(data.get("document_date"), obj.due_date.date())
description = (data.get("description") or None)
amount_dec = Decimal(str(obj.amount))
lines: List[Dict[str, Any]] = []
if obj.type == CheckType.RECEIVED:
# Dr 10203 (bank), Cr 10403 یا 10404 بسته به وضعیت
credit_code = "10404" if obj.status == CheckStatus.DEPOSITED else "10403"
lines.append({
"account_id": _ensure_account(db, "10203"),
"bank_account_id": int(data.get("bank_account_id")),
"debit": amount_dec,
"credit": Decimal(0),
"description": description or "وصول چک",
"check_id": obj.id,
})
lines.append({
"account_id": _ensure_account(db, credit_code),
"debit": Decimal(0),
"credit": amount_dec,
"description": description or "وصول چک",
"check_id": obj.id,
})
else:
# transferred/pay: Dr 20202, Cr 10203
lines.append({
"account_id": _ensure_account(db, "20202"),
"debit": amount_dec,
"credit": Decimal(0),
"description": description or "پرداخت چک",
"check_id": obj.id,
})
lines.append({
"account_id": _ensure_account(db, "10203"),
"bank_account_id": int(data.get("bank_account_id")),
"debit": Decimal(0),
"credit": amount_dec,
"description": description or "پرداخت چک",
"check_id": obj.id,
})
document_id = None
if bool(data.get("auto_post", True)):
document_id = _create_document_for_check_action(
db,
business_id=obj.business_id,
user_id=user_id,
currency_id=obj.currency_id,
document_date=document_date,
description=description,
lines=lines,
extra_info={"source": "check_action", "action": "clear", "check_id": obj.id},
)
obj.status = CheckStatus.CLEARED
obj.status_at = datetime.utcnow()
obj.current_holder_type = HolderType.BANK
obj.current_holder_id = int(data.get("bank_account_id"))
obj.last_action_document_id = document_id
db.commit(); db.refresh(obj)
res = check_to_dict(db, obj)
if document_id:
res["document_id"] = document_id
return res
def pay_check(db: Session, check_id: int, user_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
# alias to clear_check for transferred
obj = _load_check_or_404(db, check_id)
if obj.type != CheckType.TRANSFERRED:
raise ApiError("INVALID_ACTION", "Only transferred checks can be paid", http_status=400)
return clear_check(db, check_id, user_id, data)
def return_check(db: Session, check_id: int, user_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
obj = _load_check_or_404(db, check_id)
document_date = _parse_optional_date(data.get("document_date"), obj.issue_date.date())
description = (data.get("description") or None)
amount_dec = Decimal(str(obj.amount))
lines: List[Dict[str, Any]] = []
if obj.type == CheckType.RECEIVED:
if not obj.person_id:
raise ApiError("PERSON_REQUIRED", "person_id is required on received check to return", http_status=400)
# Dr 10401(person), Cr 10403
lines.append({
"account_id": _ensure_account(db, "10401"),
"person_id": int(obj.person_id),
"debit": amount_dec,
"credit": Decimal(0),
"description": description or "عودت چک",
"check_id": obj.id,
})
lines.append({
"account_id": _ensure_account(db, "10403"),
"debit": Decimal(0),
"credit": amount_dec,
"description": description or "عودت چک",
"check_id": obj.id,
})
obj.current_holder_type = HolderType.PERSON
obj.current_holder_id = int(obj.person_id)
else:
# transferred: Dr 20202, Cr 20201(person)
if not obj.person_id:
raise ApiError("PERSON_REQUIRED", "person_id is required on transferred check to return", http_status=400)
lines.append({
"account_id": _ensure_account(db, "20202"),
"debit": amount_dec,
"credit": Decimal(0),
"description": description or "عودت چک",
"check_id": obj.id,
})
lines.append({
"account_id": _ensure_account(db, "20201"),
"person_id": int(obj.person_id),
"debit": Decimal(0),
"credit": amount_dec,
"description": description or "عودت چک",
"check_id": obj.id,
})
obj.current_holder_type = HolderType.BUSINESS
obj.current_holder_id = None
document_id = None
if bool(data.get("auto_post", True)):
document_id = _create_document_for_check_action(
db,
business_id=obj.business_id,
user_id=user_id,
currency_id=obj.currency_id,
document_date=document_date,
description=description,
lines=lines,
extra_info={"source": "check_action", "action": "return", "check_id": obj.id},
)
obj.status = CheckStatus.RETURNED
obj.status_at = datetime.utcnow()
obj.last_action_document_id = document_id
db.commit(); db.refresh(obj)
res = check_to_dict(db, obj)
if document_id:
res["document_id"] = document_id
return res
def bounce_check(db: Session, check_id: int, user_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
obj = _load_check_or_404(db, check_id)
document_date = _parse_optional_date(data.get("document_date"), obj.due_date.date())
description = (data.get("description") or None)
amount_dec = Decimal(str(obj.amount))
lines: List[Dict[str, Any]] = []
if obj.type == CheckType.RECEIVED:
# فقط از وضعیت های DEPOSITED یا CLEARED اجازه برگشت
if obj.status not in (CheckStatus.DEPOSITED, CheckStatus.CLEARED):
raise ApiError("INVALID_STATE", f"Cannot bounce from status {obj.status}", http_status=400)
bank_account_id = data.get("bank_account_id")
if obj.status == CheckStatus.DEPOSITED:
# Dr 10403, Cr 10404
lines.append({
"account_id": _ensure_account(db, "10403"),
"debit": amount_dec,
"credit": Decimal(0),
"description": description or "برگشت چک",
"check_id": obj.id,
})
lines.append({
"account_id": _ensure_account(db, "10404"),
"debit": Decimal(0),
"credit": amount_dec,
"description": description or "برگشت چک",
"check_id": obj.id,
})
else:
# CLEARED: Dr 10403, Cr 10203 (نیازمند bank_account_id)
if not bank_account_id:
raise ApiError("BANK_ACCOUNT_REQUIRED", "bank_account_id is required to bounce a cleared check", http_status=400)
lines.append({
"account_id": _ensure_account(db, "10403"),
"debit": amount_dec,
"credit": Decimal(0),
"description": description or "برگشت چک",
"check_id": obj.id,
})
lines.append({
"account_id": _ensure_account(db, "10203"),
"bank_account_id": int(bank_account_id),
"debit": Decimal(0),
"credit": amount_dec,
"description": description or "برگشت چک",
"check_id": obj.id,
})
else:
# transferred: Dr 20202, Cr 20201(person) (increase AP again)
if not obj.person_id:
raise ApiError("PERSON_REQUIRED", "person_id is required on transferred check to bounce", http_status=400)
lines.append({
"account_id": _ensure_account(db, "20202"),
"debit": amount_dec,
"credit": Decimal(0),
"description": description or "برگشت چک",
"check_id": obj.id,
})
lines.append({
"account_id": _ensure_account(db, "20201"),
"person_id": int(obj.person_id),
"debit": Decimal(0),
"credit": amount_dec,
"description": description or "برگشت چک",
"check_id": obj.id,
})
# Optional expense fee
expense_amount = data.get("expense_amount")
expense_account_id = data.get("expense_account_id")
bank_account_id = data.get("bank_account_id")
if expense_amount and expense_account_id and float(expense_amount) > 0:
lines.append({
"account_id": int(expense_account_id),
"debit": Decimal(str(expense_amount)),
"credit": Decimal(0),
"description": description or "هزینه برگشت چک",
"check_id": obj.id,
})
lines.append({
"account_id": _ensure_account(db, "10203"),
**({"bank_account_id": int(bank_account_id)} if bank_account_id else {}),
"debit": Decimal(0),
"credit": Decimal(str(expense_amount)),
"description": description or "هزینه برگشت چک",
"check_id": obj.id,
})
document_id = None
if bool(data.get("auto_post", True)):
document_id = _create_document_for_check_action(
db,
business_id=obj.business_id,
user_id=user_id,
currency_id=obj.currency_id,
document_date=document_date,
description=description,
lines=lines,
extra_info={"source": "check_action", "action": "bounce", "check_id": obj.id},
)
obj.status = CheckStatus.BOUNCED
obj.status_at = datetime.utcnow()
obj.current_holder_type = HolderType.BUSINESS
obj.current_holder_id = None
obj.last_action_document_id = document_id
db.commit(); db.refresh(obj)
res = check_to_dict(db, obj)
if document_id:
res["document_id"] = document_id
return res
def deposit_check(db: Session, check_id: int, user_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
obj = _load_check_or_404(db, check_id)
if obj.type != CheckType.RECEIVED:
raise ApiError("INVALID_ACTION", "Only received checks can be deposited", http_status=400)
document_date = _parse_optional_date(data.get("document_date"), obj.due_date.date())
description = (data.get("description") or None)
amount_dec = Decimal(str(obj.amount))
# Requires account 10404 to exist
in_collection = _get_fixed_account_by_code(db, "10404") # may raise 404
lines: List[Dict[str, Any]] = [
{
"account_id": in_collection.id,
"debit": amount_dec,
"credit": Decimal(0),
"description": description or "سپرده چک به بانک",
"check_id": obj.id,
},
{
"account_id": _ensure_account(db, "10403"),
"debit": Decimal(0),
"credit": amount_dec,
"description": description or "سپرده چک به بانک",
"check_id": obj.id,
},
]
document_id = None
if bool(data.get("auto_post", True)):
document_id = _create_document_for_check_action(
db,
business_id=obj.business_id,
user_id=user_id,
currency_id=obj.currency_id,
document_date=document_date,
description=description,
lines=lines,
extra_info={"source": "check_action", "action": "deposit", "check_id": obj.id},
)
obj.status = CheckStatus.DEPOSITED
obj.status_at = datetime.utcnow()
obj.current_holder_type = HolderType.BANK
obj.last_action_document_id = document_id
db.commit(); db.refresh(obj)
res = check_to_dict(db, obj)
if document_id:
res["document_id"] = document_id
return res
def update_check(db: Session, check_id: int, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
obj = db.query(Check).filter(Check.id == check_id).first()
if obj is None:
return None
if 'type' in data and data['type'] is not None:
ctype = str(data['type']).lower()
if ctype not in ("received", "transferred"):
raise ApiError("INVALID_CHECK_TYPE", "Invalid check type", http_status=400)
obj.type = CheckType[ctype.upper()]
if 'person_id' in data:
obj.person_id = int(data['person_id']) if data['person_id'] is not None else None
if 'issue_date' in data and data['issue_date'] is not None:
obj.issue_date = _parse_iso(str(data['issue_date']))
if 'due_date' in data and data['due_date'] is not None:
obj.due_date = _parse_iso(str(data['due_date']))
if obj.due_date < obj.issue_date:
raise ApiError("INVALID_DATES", "due_date must be >= issue_date", http_status=400)
if 'check_number' in data and data['check_number'] is not None:
new_num = str(data['check_number']).strip()
if not new_num:
raise ApiError("CHECK_NUMBER_REQUIRED", "check_number is required", http_status=400)
exists = db.query(Check).filter(and_(Check.business_id == obj.business_id, Check.check_number == new_num, Check.id != obj.id)).first()
if exists is not None:
raise ApiError("DUPLICATE_CHECK_NUMBER", "Duplicate check number in this business", http_status=400)
obj.check_number = new_num
if 'sayad_code' in data:
s = data['sayad_code']
if s is not None:
s = str(s).strip()
if s and (len(s) != 16 or not s.isdigit()):
raise ApiError("INVALID_SAYAD", "sayad_code must be 16 digits", http_status=400)
if s:
exists_sayad = db.query(Check).filter(and_(Check.business_id == obj.business_id, Check.sayad_code == s, Check.id != obj.id)).first()
if exists_sayad is not None:
raise ApiError("DUPLICATE_SAYAD", "Duplicate sayad_code in this business", http_status=400)
obj.sayad_code = s if s else None
for field in ["bank_name", "branch_name"]:
if field in data:
setattr(obj, field, (str(data[field]).strip() if data[field] is not None else None))
if 'amount' in data and data['amount'] is not None:
try:
amount_val = float(data['amount'])
except Exception:
raise ApiError("INVALID_AMOUNT", "amount must be a number", http_status=400)
if amount_val <= 0:
raise ApiError("INVALID_AMOUNT", "amount must be > 0", http_status=400)
obj.amount = amount_val
if 'currency_id' in data and data['currency_id'] is not None:
obj.currency_id = int(data['currency_id'])
db.commit()
db.refresh(obj)
return check_to_dict(db, obj)
def delete_check(db: Session, check_id: int) -> bool:
obj = db.query(Check).filter(Check.id == check_id).first()
if obj is None:
return False
db.delete(obj)
db.commit()
return True
def list_checks(db: Session, business_id: int, query: Dict[str, Any]) -> Dict[str, Any]:
q = db.query(Check).filter(Check.business_id == business_id)
# جستجو
if query.get("search") and query.get("search_fields"):
term = f"%{query['search']}%"
conditions = []
for f in query["search_fields"]:
if f == "check_number":
conditions.append(Check.check_number.ilike(term))
elif f == "sayad_code":
conditions.append(Check.sayad_code.ilike(term))
elif f == "bank_name":
conditions.append(Check.bank_name.ilike(term))
elif f == "branch_name":
conditions.append(Check.branch_name.ilike(term))
elif f == "person_name":
# join به persons
q = q.join(Person, Check.person_id == Person.id, isouter=True)
conditions.append(Person.alias_name.ilike(term))
if conditions:
from sqlalchemy import or_
q = q.filter(or_(*conditions))
# فیلترها
if query.get("filters"):
from app.core.calendar import CalendarConverter
for flt in query["filters"]:
prop = getattr(flt, 'property', None) if not isinstance(flt, dict) else flt.get('property')
op = getattr(flt, 'operator', None) if not isinstance(flt, dict) else flt.get('operator')
val = getattr(flt, 'value', None) if not isinstance(flt, dict) else flt.get('value')
if not prop or not op:
continue
if prop == 'type' and op == '=':
try:
enum_val = CheckType[str(val).upper()]
q = q.filter(Check.type == enum_val)
except Exception:
pass
elif prop == 'status':
try:
if op == '=' and isinstance(val, str) and val:
enum_val = CheckStatus[val]
q = q.filter(Check.status == enum_val)
elif op == 'in' and isinstance(val, list) and val:
enum_vals = []
for v in val:
try:
enum_vals.append(CheckStatus[str(v)])
except Exception:
pass
if enum_vals:
q = q.filter(Check.status.in_(enum_vals))
except Exception:
pass
elif prop == 'currency' and op == '=':
try:
q = q.filter(Check.currency_id == int(val))
except Exception:
pass
elif prop == 'person_id' and op == '=':
try:
q = q.filter(Check.person_id == int(val))
except Exception:
pass
elif prop in ('issue_date', 'due_date'):
# انتظار: فیلترهای بازه با اپراتورهای ">=" و "<=" از DataTable
try:
if isinstance(val, str) and val:
# ورودی تاریخ ممکن است بر اساس هدر تقویم باشد؛ در این لایه فرض بر ISO است (از فرانت ارسال می‌شود)
dt = _parse_iso(val)
col = getattr(Check, prop)
if op == ">=":
q = q.filter(col >= dt)
elif op == "<=":
q = q.filter(col <= dt)
except Exception:
pass
# additional params: person_id
person_param = query.get('person_id')
if person_param:
try:
q = q.filter(Check.person_id == int(person_param))
except Exception:
pass
# مرتب‌سازی
sort_by = query.get("sort_by") or "created_at"
sort_desc = bool(query.get("sort_desc", True))
col = getattr(Check, sort_by, Check.created_at)
q = q.order_by(col.desc() if sort_desc else col.asc())
# صفحه‌بندی
skip = int(query.get("skip", 0))
take = int(query.get("take", 20))
total = q.count()
items = q.offset(skip).limit(take).all()
return {
"items": [check_to_dict(db, i) for i in items],
"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": query,
}
def check_to_dict(db: Session, obj: Optional[Check]) -> Optional[Dict[str, Any]]:
if obj is None:
return None
person_name = None
if obj.person_id:
p = db.query(Person).filter(Person.id == obj.person_id).first()
person_name = getattr(p, 'alias_name', None)
currency_title = None
try:
c = db.query(Currency).filter(Currency.id == obj.currency_id).first()
currency_title = c.title or c.code if c else None
except Exception:
pass
return {
"id": obj.id,
"business_id": obj.business_id,
"type": obj.type.name.lower(),
"person_id": obj.person_id,
"person_name": person_name,
"issue_date": obj.issue_date.isoformat(),
"due_date": obj.due_date.isoformat(),
"check_number": obj.check_number,
"sayad_code": obj.sayad_code,
"bank_name": obj.bank_name,
"branch_name": obj.branch_name,
"amount": float(obj.amount),
"currency_id": obj.currency_id,
"currency": currency_title,
"status": (obj.status.name if obj.status else None),
"status_at": (obj.status_at.isoformat() if obj.status_at else None),
"current_holder_type": (obj.current_holder_type.name if obj.current_holder_type else None),
"current_holder_id": obj.current_holder_id,
"last_action_document_id": obj.last_action_document_id,
"created_at": obj.created_at.isoformat(),
"updated_at": obj.updated_at.isoformat(),
}