709 lines
27 KiB
Python
709 lines
27 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, Optional
|
|
from datetime import datetime, date
|
|
from decimal import Decimal
|
|
import logging
|
|
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import and_, or_
|
|
|
|
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.currency import Currency
|
|
from adapters.db.models.fiscal_year import FiscalYear
|
|
from adapters.db.models.user import User
|
|
from adapters.db.models.bank_account import BankAccount
|
|
from adapters.db.models.cash_register import CashRegister
|
|
from adapters.db.models.petty_cash import PettyCash
|
|
from app.core.responses import ApiError
|
|
import jdatetime
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
DOCUMENT_TYPE_TRANSFER = "transfer"
|
|
|
|
|
|
def _parse_iso_date(dt: str | datetime | date) -> date:
|
|
if isinstance(dt, date):
|
|
return dt
|
|
if isinstance(dt, datetime):
|
|
return dt.date()
|
|
|
|
dt_str = str(dt).strip()
|
|
|
|
try:
|
|
dt_str_clean = dt_str.replace('Z', '+00:00')
|
|
parsed = datetime.fromisoformat(dt_str_clean)
|
|
return parsed.date()
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
if len(dt_str) == 10 and dt_str.count('-') == 2:
|
|
return datetime.strptime(dt_str, '%Y-%m-%d').date()
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
if len(dt_str) == 10 and dt_str.count('/') == 2:
|
|
parts = dt_str.split('/')
|
|
if len(parts) == 3:
|
|
year, month, day = parts
|
|
try:
|
|
year_int = int(year)
|
|
month_int = int(month)
|
|
day_int = int(day)
|
|
if year_int > 1500:
|
|
jalali_date = jdatetime.date(year_int, month_int, day_int)
|
|
gregorian_date = jalali_date.togregorian()
|
|
return gregorian_date
|
|
else:
|
|
return datetime.strptime(dt_str, '%Y/%m/%d').date()
|
|
except (ValueError, jdatetime.JalaliDateError):
|
|
return datetime.strptime(dt_str, '%Y/%m/%d').date()
|
|
except Exception:
|
|
pass
|
|
|
|
raise ApiError("INVALID_DATE", f"Invalid date format: {dt}", http_status=400)
|
|
|
|
|
|
def _get_current_fiscal_year(db: Session, business_id: int) -> FiscalYear:
|
|
fiscal_year = db.query(FiscalYear).filter(
|
|
and_(
|
|
FiscalYear.business_id == business_id,
|
|
FiscalYear.is_last == True,
|
|
)
|
|
).first()
|
|
if not fiscal_year:
|
|
raise ApiError("NO_FISCAL_YEAR", "No active fiscal year found for this business", http_status=400)
|
|
return fiscal_year
|
|
|
|
|
|
def _get_fixed_account_by_code(db: Session, account_code: str) -> Account:
|
|
account = db.query(Account).filter(
|
|
and_(
|
|
Account.business_id == None,
|
|
Account.code == account_code,
|
|
)
|
|
).first()
|
|
if not account:
|
|
raise ApiError("ACCOUNT_NOT_FOUND", f"Account with code {account_code} not found", http_status=500)
|
|
return account
|
|
|
|
|
|
def _account_code_for_type(account_type: str) -> str:
|
|
if account_type == "bank":
|
|
return "10203"
|
|
if account_type == "cash_register":
|
|
return "10202"
|
|
if account_type == "petty_cash":
|
|
return "10201"
|
|
raise ApiError("INVALID_ACCOUNT_TYPE", f"Invalid account type: {account_type}", http_status=400)
|
|
|
|
|
|
def _build_doc_code(prefix_base: str) -> str:
|
|
today = datetime.now().date()
|
|
prefix = f"{prefix_base}-{today.strftime('%Y%m%d')}"
|
|
return prefix
|
|
|
|
|
|
def create_transfer(
|
|
db: Session,
|
|
business_id: int,
|
|
user_id: int,
|
|
data: Dict[str, Any],
|
|
) -> Dict[str, Any]:
|
|
logger.info("=== شروع ایجاد سند انتقال ===")
|
|
|
|
document_date = _parse_iso_date(data.get("document_date", datetime.now()))
|
|
currency_id = data.get("currency_id")
|
|
if not currency_id:
|
|
raise ApiError("CURRENCY_REQUIRED", "currency_id is required", http_status=400)
|
|
currency = db.query(Currency).filter(Currency.id == int(currency_id)).first()
|
|
if not currency:
|
|
raise ApiError("CURRENCY_NOT_FOUND", "Currency not found", http_status=404)
|
|
|
|
fiscal_year = _get_current_fiscal_year(db, business_id)
|
|
|
|
source = data.get("source") or {}
|
|
destination = data.get("destination") or {}
|
|
amount = Decimal(str(data.get("amount", 0)))
|
|
commission = Decimal(str(data.get("commission", 0))) if data.get("commission") is not None else Decimal(0)
|
|
|
|
if amount <= 0:
|
|
raise ApiError("INVALID_AMOUNT", "amount must be greater than 0", http_status=400)
|
|
if commission < 0:
|
|
raise ApiError("INVALID_COMMISSION", "commission must be >= 0", http_status=400)
|
|
|
|
src_type = str(source.get("type") or "").strip()
|
|
dst_type = str(destination.get("type") or "").strip()
|
|
src_id = source.get("id")
|
|
dst_id = destination.get("id")
|
|
|
|
if src_type not in ("bank", "cash_register", "petty_cash"):
|
|
raise ApiError("INVALID_SOURCE", "source.type must be bank|cash_register|petty_cash", http_status=400)
|
|
if dst_type not in ("bank", "cash_register", "petty_cash"):
|
|
raise ApiError("INVALID_DESTINATION", "destination.type must be bank|cash_register|petty_cash", http_status=400)
|
|
if src_type == dst_type and src_id and dst_id and str(src_id) == str(dst_id):
|
|
raise ApiError("SAME_SOURCE_DESTINATION", "source and destination cannot be the same", http_status=400)
|
|
|
|
# Resolve accounts by fixed codes
|
|
src_account = _get_fixed_account_by_code(db, _account_code_for_type(src_type))
|
|
dst_account = _get_fixed_account_by_code(db, _account_code_for_type(dst_type))
|
|
|
|
# Generate document code TR-YYYYMMDD-NNNN
|
|
prefix = _build_doc_code("TR")
|
|
last_doc = db.query(Document).filter(
|
|
and_(
|
|
Document.business_id == business_id,
|
|
Document.code.like(f"{prefix}-%"),
|
|
)
|
|
).order_by(Document.code.desc()).first()
|
|
if last_doc:
|
|
try:
|
|
last_num = int(last_doc.code.split("-")[-1])
|
|
next_num = last_num + 1
|
|
except Exception:
|
|
next_num = 1
|
|
else:
|
|
next_num = 1
|
|
doc_code = f"{prefix}-{next_num:04d}"
|
|
|
|
# Resolve names for auto description if needed
|
|
def _resolve_name(tp: str, _id: Any) -> str | None:
|
|
try:
|
|
if tp == "bank" and _id is not None:
|
|
ba = db.query(BankAccount).filter(BankAccount.id == int(_id)).first()
|
|
return ba.name if ba else None
|
|
if tp == "cash_register" and _id is not None:
|
|
cr = db.query(CashRegister).filter(CashRegister.id == int(_id)).first()
|
|
return cr.name if cr else None
|
|
if tp == "petty_cash" and _id is not None:
|
|
pc = db.query(PettyCash).filter(PettyCash.id == int(_id)).first()
|
|
return pc.name if pc else None
|
|
except Exception:
|
|
return None
|
|
return None
|
|
|
|
auto_description = None
|
|
if not data.get("description"):
|
|
src_name = _resolve_name(src_type, src_id) or "مبدأ"
|
|
dst_name = _resolve_name(dst_type, dst_id) or "مقصد"
|
|
# human readable types
|
|
def _type_name(tp: str) -> str:
|
|
return "حساب بانکی" if tp == "bank" else ("صندوق" if tp == "cash_register" else "تنخواه")
|
|
auto_description = f"انتقال از {_type_name(src_type)} {src_name} به {_type_name(dst_type)} {dst_name}"
|
|
|
|
document = Document(
|
|
business_id=business_id,
|
|
fiscal_year_id=fiscal_year.id,
|
|
code=doc_code,
|
|
document_type=DOCUMENT_TYPE_TRANSFER,
|
|
document_date=document_date,
|
|
currency_id=int(currency_id),
|
|
created_by_user_id=user_id,
|
|
registered_at=datetime.utcnow(),
|
|
is_proforma=False,
|
|
description=data.get("description") or auto_description,
|
|
extra_info=data.get("extra_info"),
|
|
)
|
|
db.add(document)
|
|
db.flush()
|
|
|
|
# Destination line (Debit)
|
|
dest_kwargs: Dict[str, Any] = {}
|
|
if dst_type == "bank" and dst_id is not None:
|
|
try:
|
|
dest_kwargs["bank_account_id"] = int(dst_id)
|
|
except Exception:
|
|
pass
|
|
elif dst_type == "cash_register" and dst_id is not None:
|
|
dest_kwargs["cash_register_id"] = dst_id
|
|
elif dst_type == "petty_cash" and dst_id is not None:
|
|
dest_kwargs["petty_cash_id"] = dst_id
|
|
|
|
dest_line = DocumentLine(
|
|
document_id=document.id,
|
|
account_id=dst_account.id,
|
|
debit=amount,
|
|
credit=Decimal(0),
|
|
description=data.get("destination_description") or data.get("description"),
|
|
extra_info={
|
|
"side": "destination",
|
|
"destination_type": dst_type,
|
|
"destination_id": dst_id,
|
|
},
|
|
**dest_kwargs,
|
|
)
|
|
db.add(dest_line)
|
|
|
|
# Source line (Credit)
|
|
src_kwargs: Dict[str, Any] = {}
|
|
if src_type == "bank" and src_id is not None:
|
|
try:
|
|
src_kwargs["bank_account_id"] = int(src_id)
|
|
except Exception:
|
|
pass
|
|
elif src_type == "cash_register" and src_id is not None:
|
|
src_kwargs["cash_register_id"] = src_id
|
|
elif src_type == "petty_cash" and src_id is not None:
|
|
src_kwargs["petty_cash_id"] = src_id
|
|
|
|
src_line = DocumentLine(
|
|
document_id=document.id,
|
|
account_id=src_account.id,
|
|
debit=Decimal(0),
|
|
credit=amount,
|
|
description=data.get("source_description") or data.get("description"),
|
|
extra_info={
|
|
"side": "source",
|
|
"source_type": src_type,
|
|
"source_id": src_id,
|
|
},
|
|
**src_kwargs,
|
|
)
|
|
db.add(src_line)
|
|
|
|
if commission > 0:
|
|
# Debit commission expense 70902
|
|
commission_service_account = _get_fixed_account_by_code(db, "70902")
|
|
commission_expense_line = DocumentLine(
|
|
document_id=document.id,
|
|
account_id=commission_service_account.id,
|
|
debit=commission,
|
|
credit=Decimal(0),
|
|
description="کارمزد خدمات بانکی",
|
|
extra_info={
|
|
"side": "commission",
|
|
"is_commission_line": True,
|
|
},
|
|
**src_kwargs,
|
|
)
|
|
db.add(commission_expense_line)
|
|
|
|
# Credit commission to source account (increase credit of source)
|
|
commission_credit_line = DocumentLine(
|
|
document_id=document.id,
|
|
account_id=src_account.id,
|
|
debit=Decimal(0),
|
|
credit=commission,
|
|
description="کارمزد انتقال (ثبت در مبدأ)",
|
|
extra_info={
|
|
"side": "commission",
|
|
"is_commission_line": True,
|
|
"source_type": src_type,
|
|
"source_id": src_id,
|
|
},
|
|
**src_kwargs,
|
|
)
|
|
db.add(commission_credit_line)
|
|
|
|
db.commit()
|
|
db.refresh(document)
|
|
return transfer_document_to_dict(db, document)
|
|
|
|
|
|
def get_transfer(db: Session, document_id: int) -> Optional[Dict[str, Any]]:
|
|
document = db.query(Document).filter(Document.id == document_id).first()
|
|
if not document or document.document_type != DOCUMENT_TYPE_TRANSFER:
|
|
return None
|
|
return transfer_document_to_dict(db, document)
|
|
|
|
|
|
def list_transfers(db: Session, business_id: int, query: Dict[str, Any]) -> Dict[str, Any]:
|
|
q = db.query(Document).filter(
|
|
and_(
|
|
Document.business_id == business_id,
|
|
Document.document_type == DOCUMENT_TYPE_TRANSFER,
|
|
)
|
|
)
|
|
|
|
fiscal_year_id = query.get("fiscal_year_id")
|
|
if fiscal_year_id is not None:
|
|
try:
|
|
fiscal_year_id = int(fiscal_year_id)
|
|
except (TypeError, ValueError):
|
|
fiscal_year_id = None
|
|
if fiscal_year_id is None:
|
|
try:
|
|
fiscal_year = _get_current_fiscal_year(db, business_id)
|
|
fiscal_year_id = fiscal_year.id
|
|
except Exception:
|
|
fiscal_year_id = None
|
|
if fiscal_year_id is not None:
|
|
q = q.filter(Document.fiscal_year_id == fiscal_year_id)
|
|
|
|
from_date = query.get("from_date")
|
|
to_date = query.get("to_date")
|
|
if from_date:
|
|
try:
|
|
from_dt = _parse_iso_date(from_date)
|
|
q = q.filter(Document.document_date >= from_dt)
|
|
except Exception:
|
|
pass
|
|
if to_date:
|
|
try:
|
|
to_dt = _parse_iso_date(to_date)
|
|
q = q.filter(Document.document_date <= to_dt)
|
|
except Exception:
|
|
pass
|
|
|
|
# Apply advanced filters (e.g., DataTable date range filters)
|
|
filters = query.get("filters")
|
|
if filters and isinstance(filters, (list, tuple)):
|
|
for flt in filters:
|
|
try:
|
|
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 == 'document_date':
|
|
if isinstance(val, str) and val:
|
|
try:
|
|
dt = _parse_iso_date(val)
|
|
col = getattr(Document, prop)
|
|
if op == ">=":
|
|
q = q.filter(col >= dt)
|
|
elif op == "<=":
|
|
q = q.filter(col <= dt)
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
search = query.get("search")
|
|
if search:
|
|
q = q.filter(Document.code.ilike(f"%{search}%"))
|
|
|
|
sort_by = query.get("sort_by", "document_date")
|
|
sort_desc = bool(query.get("sort_desc", True))
|
|
if sort_by and isinstance(sort_by, str) and hasattr(Document, sort_by):
|
|
col = getattr(Document, sort_by)
|
|
q = q.order_by(col.desc() if sort_desc else col.asc())
|
|
else:
|
|
q = q.order_by(Document.document_date.desc())
|
|
|
|
skip = int(query.get("skip", 0))
|
|
take = int(query.get("take", 20))
|
|
|
|
total = q.count()
|
|
items = q.offset(skip).limit(take).all()
|
|
|
|
return {
|
|
"items": [transfer_document_to_dict(db, doc) for doc 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 delete_transfer(db: Session, document_id: int) -> bool:
|
|
document = db.query(Document).filter(Document.id == document_id).first()
|
|
if not document or document.document_type != DOCUMENT_TYPE_TRANSFER:
|
|
return False
|
|
try:
|
|
fiscal_year = db.query(FiscalYear).filter(FiscalYear.id == document.fiscal_year_id).first()
|
|
if fiscal_year is not None and getattr(fiscal_year, "is_last", False) is not True:
|
|
raise ApiError("FISCAL_YEAR_LOCKED", "سند متعلق به سال مالی جاری نیست و قابل حذف نمیباشد", http_status=409)
|
|
except ApiError:
|
|
raise
|
|
except Exception:
|
|
pass
|
|
|
|
db.delete(document)
|
|
db.commit()
|
|
return True
|
|
|
|
|
|
def update_transfer(
|
|
db: Session,
|
|
document_id: int,
|
|
user_id: int,
|
|
data: Dict[str, Any],
|
|
) -> Dict[str, Any]:
|
|
document = db.query(Document).filter(Document.id == document_id).first()
|
|
if document is None or document.document_type != DOCUMENT_TYPE_TRANSFER:
|
|
raise ApiError("DOCUMENT_NOT_FOUND", "Transfer document not found", http_status=404)
|
|
|
|
try:
|
|
fiscal_year = db.query(FiscalYear).filter(FiscalYear.id == document.fiscal_year_id).first()
|
|
if fiscal_year is not None and getattr(fiscal_year, "is_last", False) is not True:
|
|
raise ApiError("FISCAL_YEAR_LOCKED", "سند متعلق به سال مالی جاری نیست و قابل ویرایش نمیباشد", http_status=409)
|
|
except ApiError:
|
|
raise
|
|
except Exception:
|
|
pass
|
|
|
|
document_date = _parse_iso_date(data.get("document_date", document.document_date))
|
|
currency_id = data.get("currency_id", document.currency_id)
|
|
if not currency_id:
|
|
raise ApiError("CURRENCY_REQUIRED", "currency_id is required", http_status=400)
|
|
currency = db.query(Currency).filter(Currency.id == int(currency_id)).first()
|
|
if not currency:
|
|
raise ApiError("CURRENCY_NOT_FOUND", "Currency not found", http_status=404)
|
|
|
|
source = data.get("source") or {}
|
|
destination = data.get("destination") or {}
|
|
amount = Decimal(str(data.get("amount", 0)))
|
|
commission = Decimal(str(data.get("commission", 0))) if data.get("commission") is not None else Decimal(0)
|
|
|
|
if amount <= 0:
|
|
raise ApiError("INVALID_AMOUNT", "amount must be greater than 0", http_status=400)
|
|
if commission < 0:
|
|
raise ApiError("INVALID_COMMISSION", "commission must be >= 0", http_status=400)
|
|
|
|
src_type = str(source.get("type") or "").strip()
|
|
dst_type = str(destination.get("type") or "").strip()
|
|
src_id = source.get("id")
|
|
dst_id = destination.get("id")
|
|
|
|
if src_type not in ("bank", "cash_register", "petty_cash"):
|
|
raise ApiError("INVALID_SOURCE", "source.type must be bank|cash_register|petty_cash", http_status=400)
|
|
if dst_type not in ("bank", "cash_register", "petty_cash"):
|
|
raise ApiError("INVALID_DESTINATION", "destination.type must be bank|cash_register|petty_cash", http_status=400)
|
|
if src_type == dst_type and src_id and dst_id and str(src_id) == str(dst_id):
|
|
raise ApiError("SAME_SOURCE_DESTINATION", "source and destination cannot be the same", http_status=400)
|
|
|
|
# Update document fields
|
|
document.document_date = document_date
|
|
document.currency_id = int(currency_id)
|
|
if isinstance(data.get("extra_info"), dict) or data.get("extra_info") is None:
|
|
document.extra_info = data.get("extra_info")
|
|
if isinstance(data.get("description"), str) or data.get("description") is None:
|
|
if data.get("description"):
|
|
document.description = data.get("description")
|
|
else:
|
|
# regenerate auto description
|
|
def _resolve_name(tp: str, _id: Any) -> str | None:
|
|
try:
|
|
if tp == "bank" and _id is not None:
|
|
ba = db.query(BankAccount).filter(BankAccount.id == int(_id)).first()
|
|
return ba.name if ba else None
|
|
if tp == "cash_register" and _id is not None:
|
|
cr = db.query(CashRegister).filter(CashRegister.id == int(_id)).first()
|
|
return cr.name if cr else None
|
|
if tp == "petty_cash" and _id is not None:
|
|
pc = db.query(PettyCash).filter(PettyCash.id == int(_id)).first()
|
|
return pc.name if pc else None
|
|
except Exception:
|
|
return None
|
|
return None
|
|
def _type_name(tp: str) -> str:
|
|
return "حساب بانکی" if tp == "bank" else ("صندوق" if tp == "cash_register" else "تنخواه")
|
|
src_name = _resolve_name(src_type, src_id) or "مبدأ"
|
|
dst_name = _resolve_name(dst_type, dst_id) or "مقصد"
|
|
document.description = f"انتقال از {_type_name(src_type)} {src_name} به {_type_name(dst_type)} {dst_name}"
|
|
|
|
# Remove old lines and recreate
|
|
db.query(DocumentLine).filter(DocumentLine.document_id == document.id).delete(synchronize_session=False)
|
|
|
|
src_account = _get_fixed_account_by_code(db, _account_code_for_type(src_type))
|
|
dst_account = _get_fixed_account_by_code(db, _account_code_for_type(dst_type))
|
|
|
|
dest_kwargs: Dict[str, Any] = {}
|
|
if dst_type == "bank" and dst_id is not None:
|
|
try:
|
|
dest_kwargs["bank_account_id"] = int(dst_id)
|
|
except Exception:
|
|
pass
|
|
elif dst_type == "cash_register" and dst_id is not None:
|
|
dest_kwargs["cash_register_id"] = dst_id
|
|
elif dst_type == "petty_cash" and dst_id is not None:
|
|
dest_kwargs["petty_cash_id"] = dst_id
|
|
|
|
db.add(DocumentLine(
|
|
document_id=document.id,
|
|
account_id=dst_account.id,
|
|
debit=amount,
|
|
credit=Decimal(0),
|
|
description=data.get("destination_description") or data.get("description"),
|
|
extra_info={
|
|
"side": "destination",
|
|
"destination_type": dst_type,
|
|
"destination_id": dst_id,
|
|
},
|
|
**dest_kwargs,
|
|
))
|
|
|
|
src_kwargs: Dict[str, Any] = {}
|
|
if src_type == "bank" and src_id is not None:
|
|
try:
|
|
src_kwargs["bank_account_id"] = int(src_id)
|
|
except Exception:
|
|
pass
|
|
elif src_type == "cash_register" and src_id is not None:
|
|
src_kwargs["cash_register_id"] = src_id
|
|
elif src_type == "petty_cash" and src_id is not None:
|
|
src_kwargs["petty_cash_id"] = src_id
|
|
|
|
db.add(DocumentLine(
|
|
document_id=document.id,
|
|
account_id=src_account.id,
|
|
debit=Decimal(0),
|
|
credit=amount,
|
|
description=data.get("source_description") or data.get("description"),
|
|
extra_info={
|
|
"side": "source",
|
|
"source_type": src_type,
|
|
"source_id": src_id,
|
|
},
|
|
**src_kwargs,
|
|
))
|
|
|
|
if commission > 0:
|
|
commission_service_account = _get_fixed_account_by_code(db, "70902")
|
|
db.add(DocumentLine(
|
|
document_id=document.id,
|
|
account_id=commission_service_account.id,
|
|
debit=commission,
|
|
credit=Decimal(0),
|
|
description="کارمزد خدمات بانکی",
|
|
extra_info={
|
|
"side": "commission",
|
|
"is_commission_line": True,
|
|
},
|
|
**src_kwargs,
|
|
))
|
|
db.add(DocumentLine(
|
|
document_id=document.id,
|
|
account_id=src_account.id,
|
|
debit=Decimal(0),
|
|
credit=commission,
|
|
description="کارمزد انتقال (ثبت در مبدأ)",
|
|
extra_info={
|
|
"side": "commission",
|
|
"is_commission_line": True,
|
|
"source_type": src_type,
|
|
"source_id": src_id,
|
|
},
|
|
**src_kwargs,
|
|
))
|
|
|
|
db.commit()
|
|
db.refresh(document)
|
|
return transfer_document_to_dict(db, document)
|
|
|
|
|
|
def transfer_document_to_dict(db: Session, document: Document) -> Dict[str, Any]:
|
|
lines = db.query(DocumentLine).filter(DocumentLine.document_id == document.id).all()
|
|
|
|
account_lines = []
|
|
source_name = None
|
|
destination_name = None
|
|
source_type = None
|
|
destination_type = None
|
|
for line in lines:
|
|
account = db.query(Account).filter(Account.id == line.account_id).first()
|
|
if not account:
|
|
continue
|
|
|
|
line_dict: Dict[str, Any] = {
|
|
"id": line.id,
|
|
"account_id": line.account_id,
|
|
"bank_account_id": line.bank_account_id,
|
|
"cash_register_id": line.cash_register_id,
|
|
"petty_cash_id": line.petty_cash_id,
|
|
"quantity": float(line.quantity) if line.quantity else None,
|
|
"account_name": account.name,
|
|
"account_code": account.code,
|
|
"account_type": account.account_type,
|
|
"debit": float(line.debit),
|
|
"credit": float(line.credit),
|
|
"amount": float(line.debit if line.debit > 0 else line.credit),
|
|
"description": line.description,
|
|
"extra_info": line.extra_info,
|
|
}
|
|
|
|
if line.extra_info:
|
|
if "side" in line.extra_info:
|
|
line_dict["side"] = line.extra_info["side"]
|
|
if "source_type" in line.extra_info:
|
|
line_dict["source_type"] = line.extra_info["source_type"]
|
|
# Only assign source_type from source lines
|
|
if line_dict.get("side") == "source":
|
|
source_type = source_type or line.extra_info["source_type"]
|
|
if "destination_type" in line.extra_info:
|
|
line_dict["destination_type"] = line.extra_info["destination_type"]
|
|
# Only assign destination_type from destination lines
|
|
if line_dict.get("side") == "destination":
|
|
destination_type = destination_type or line.extra_info["destination_type"]
|
|
if "is_commission_line" in line.extra_info:
|
|
line_dict["is_commission_line"] = line.extra_info["is_commission_line"]
|
|
|
|
# capture source/destination names from linked entities
|
|
try:
|
|
if line_dict.get("side") == "source":
|
|
if line_dict.get("bank_account_id"):
|
|
ba = db.query(BankAccount).filter(BankAccount.id == int(line_dict["bank_account_id"])) .first()
|
|
source_name = ba.name if ba else source_name
|
|
elif line_dict.get("cash_register_id"):
|
|
cr = db.query(CashRegister).filter(CashRegister.id == int(line_dict["cash_register_id"])) .first()
|
|
source_name = cr.name if cr else source_name
|
|
elif line_dict.get("petty_cash_id"):
|
|
pc = db.query(PettyCash).filter(PettyCash.id == int(line_dict["petty_cash_id"])) .first()
|
|
source_name = pc.name if pc else source_name
|
|
elif line_dict.get("side") == "destination":
|
|
if line_dict.get("bank_account_id"):
|
|
ba = db.query(BankAccount).filter(BankAccount.id == int(line_dict["bank_account_id"])) .first()
|
|
destination_name = ba.name if ba else destination_name
|
|
elif line_dict.get("cash_register_id"):
|
|
cr = db.query(CashRegister).filter(CashRegister.id == int(line_dict["cash_register_id"])) .first()
|
|
destination_name = cr.name if cr else destination_name
|
|
elif line_dict.get("petty_cash_id"):
|
|
pc = db.query(PettyCash).filter(PettyCash.id == int(line_dict["petty_cash_id"])) .first()
|
|
destination_name = pc.name if pc else destination_name
|
|
except Exception:
|
|
pass
|
|
|
|
account_lines.append(line_dict)
|
|
|
|
# Compute total as sum of debits of non-commission lines (destination line amount)
|
|
total_amount = sum(l.get("debit", 0) for l in account_lines if not l.get("is_commission_line"))
|
|
|
|
created_by = db.query(User).filter(User.id == document.created_by_user_id).first()
|
|
created_by_name = f"{created_by.first_name} {created_by.last_name}".strip() if created_by else None
|
|
|
|
currency = db.query(Currency).filter(Currency.id == document.currency_id).first()
|
|
currency_code = currency.code if currency else None
|
|
|
|
return {
|
|
"id": document.id,
|
|
"code": document.code,
|
|
"business_id": document.business_id,
|
|
"document_type": document.document_type,
|
|
"document_type_name": "انتقال",
|
|
"document_date": document.document_date.isoformat(),
|
|
"registered_at": document.registered_at.isoformat(),
|
|
"currency_id": document.currency_id,
|
|
"currency_code": currency_code,
|
|
"created_by_user_id": document.created_by_user_id,
|
|
"created_by_name": created_by_name,
|
|
"is_proforma": document.is_proforma,
|
|
"description": document.description,
|
|
"source_type": source_type,
|
|
"source_name": source_name,
|
|
"destination_type": destination_type,
|
|
"destination_name": destination_name,
|
|
"extra_info": document.extra_info,
|
|
"person_lines": [],
|
|
"account_lines": account_lines,
|
|
"total_amount": float(total_amount),
|
|
"person_lines_count": 0,
|
|
"account_lines_count": len(account_lines),
|
|
"created_at": document.created_at.isoformat(),
|
|
"updated_at": document.updated_at.isoformat(),
|
|
}
|
|
|
|
|