Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/adapters/api/v1/fiscal_years.py
2026-08-11 15:14:40 +03:30

736 lines
30 KiB
Python
Executable file

from typing import Dict, Any, Optional, List
from fastapi import APIRouter, Depends, Request, Body, Query
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field, validator
from datetime import date
from adapters.db.session import get_db
from app.core.auth_dependency import get_current_user, AuthContext
from app.core.permissions import require_business_access, require_business_permission_dep
from app.core.responses import success_response, ApiError, format_datetime_fields
from app.core.i18n import apply_format, get_request_translator
from app.core.cache import get_cache
from adapters.db.repositories.fiscal_year_repo import FiscalYearRepository
from app.services.year_end_closing_service import preview_year_end_closing, close_fiscal_year
from app.services.fiscal_year_rollback_service import (
preview_current_fiscal_year_rollback,
execute_current_fiscal_year_rollback,
)
router = APIRouter(prefix="/business", tags=["سال مالی", "حسابداری"])
def _date_ranges_overlap(a_start: date, a_end: date, b_start: date, b_end: date) -> bool:
"""بازه‌های شامل [start, end]؛ همپوشانی واقعی یا تماس مرزی یک‌روزه."""
return a_start <= b_end and a_end >= b_start
class ShareholderDistributionItem(BaseModel):
"""آیتم توزیع سود بین سهامدار"""
person_id: int = Field(..., description="شناسه سهامدار")
profit_amount: float = Field(..., description="مبلغ سود تخصیص یافته به این سهامدار")
class YearEndClosingRequest(BaseModel):
new_fiscal_year_title: str = Field(..., min_length=1, max_length=255, description="عنوان سال مالی جدید")
auto_create_opening_balance: bool = Field(default=True, description="ایجاد خودکار تراز افتتاحیه سال جدید")
# مالیات
tax_percentage: Optional[float] = Field(None, ge=0, le=100, description="درصد مالیات بر درآمد")
tax_amount: Optional[float] = Field(None, ge=0, description="مبلغ مالیات بر درآمد")
# تقسیم سود
profit_distribution_percentage: Optional[float] = Field(None, ge=0, le=100, description="درصد سود تقسیم شده")
profit_distribution_amount: Optional[float] = Field(None, ge=0, description="مبلغ سود تقسیم شده")
shareholder_profit_account_id: Optional[int] = Field(None, description="شناسه حساب برای ثبت سود سهامداران")
# سود انباشته سنواتی
retained_earnings_from_previous_years: Optional[float] = Field(None, ge=0, description="سود یا زیان انباشته سنواتی")
# تنظیمات
auto_issue_person_balance_document: bool = Field(default=False, description="صدور خودکار سند توازن اشخاص")
# تنظیمات سال مالی جدید
new_fiscal_year_start_date: Optional[date] = Field(None, description="تاریخ شروع سال مالی جدید (در صورت عدم ارسال، به صورت خودکار محاسبه می‌شود)")
new_fiscal_year_end_date: Optional[date] = Field(None, description="تاریخ پایان سال مالی جدید (در صورت عدم ارسال، به صورت خودکار محاسبه می‌شود)")
inventory_valuation_method: str = Field(default="FIFO", description="روش ارزیابی انبار: FIFO, LIFO, WeightedAverage")
# تقسیم سود بین سهامداران (اختیاری - اگر نباشد بر اساس درصد سهام محاسبه می‌شود)
shareholder_distributions: Optional[List[ShareholderDistributionItem]] = Field(None, description="لیست توزیع سود بین سهامداران")
# تقطیع سال مالی: پایان دورهٔ بستن و انتقال اسناد به سال جدید
closing_fiscal_end_date: Optional[date] = Field(
None,
description="تاریخ پایان دورهٔ بستن (شامل). در صورت ارسال و زودتر بودن از پایان تقویمی سال، دوره تقطیع می‌شود.",
)
document_relocation_basis: str = Field(
default="document_date",
description="معیار «بعد از پایان دوره» برای انتقال سند به سال جدید: document_date یا registered_at",
)
move_post_cutoff_documents_to_new_fiscal_year: bool = Field(
default=True,
description="اگر true باشد، اسناد بعد از پایان دورهٔ بستن به سال مالی جدید منتقل می‌شوند",
)
@validator('tax_percentage', 'tax_amount')
def validate_tax(cls, v, values):
"""بررسی اینکه فقط یکی از درصد یا مبلغ مالیات وارد شود"""
if 'tax_percentage' in values and 'tax_amount' in values:
if values.get('tax_percentage') is not None and values.get('tax_amount') is not None:
raise ValueError('فقط یکی از درصد یا مبلغ مالیات باید وارد شود')
return v
@validator('profit_distribution_percentage', 'profit_distribution_amount')
def validate_profit_distribution(cls, v, values):
"""بررسی اینکه فقط یکی از درصد یا مبلغ تقسیم سود وارد شود"""
if 'profit_distribution_percentage' in values and 'profit_distribution_amount' in values:
if values.get('profit_distribution_percentage') is not None and values.get('profit_distribution_amount') is not None:
raise ValueError('فقط یکی از درصد یا مبلغ تقسیم سود باید وارد شود')
return v
@validator('inventory_valuation_method')
def validate_inventory_method(cls, v):
"""بررسی اعتبار روش ارزیابی انبار"""
valid_methods = ['FIFO', 'LIFO', 'WeightedAverage']
if v not in valid_methods:
raise ValueError(f'روش ارزیابی انبار باید یکی از {valid_methods} باشد')
return v
@validator('document_relocation_basis')
def validate_document_relocation_basis(cls, v):
allowed = {'document_date', 'registered_at'}
vv = (v or 'document_date').strip().lower()
if vv not in allowed:
raise ValueError(f'document_relocation_basis باید یکی از {allowed} باشد')
return vv
@router.get("/{business_id}/fiscal-years")
@require_business_access("business_id")
def list_fiscal_years(
request: Request,
business_id: int,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
_: None = Depends(require_business_permission_dep("fiscal_years", "view")),
) -> Dict[str, Any]:
cache = get_cache()
cache_key = f"fiscal_years:{business_id}"
if cache.enabled:
cached = cache.get(cache_key)
if cached is not None:
return success_response(data=cached, request=request, message="FISCAL_YEARS_LIST_FETCHED")
repo = FiscalYearRepository(db)
items = repo.list_by_business(business_id)
data = [
{
"id": fy.id,
"title": fy.title,
"start_date": fy.start_date,
"end_date": fy.end_date,
"is_current": fy.is_last,
}
for fy in items
]
formatted = format_datetime_fields({"items": data}, request)
if cache.enabled:
cache.set(cache_key, formatted, ttl=120)
return success_response(data=formatted, request=request, message="FISCAL_YEARS_LIST_FETCHED")
@router.get("/{business_id}/fiscal-years/current")
@require_business_access("business_id")
def get_current_fiscal_year(
request: Request,
business_id: int,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
_: None = Depends(require_business_permission_dep("fiscal_years", "view")),
) -> Dict[str, Any]:
repo = FiscalYearRepository(db)
fy = repo.get_current_for_business(business_id)
if not fy:
return success_response(data=None, request=request, message="NO_CURRENT_FISCAL_YEAR")
data = {
"id": fy.id,
"title": fy.title,
"start_date": fy.start_date,
"end_date": fy.end_date,
"is_current": fy.is_last,
}
return success_response(data=format_datetime_fields(data, request), request=request, message="FISCAL_YEAR_CURRENT_FETCHED")
@router.get("/{business_id}/fiscal-years/{fiscal_year_id}/closing/preview")
@require_business_access("business_id")
def preview_year_end_closing_endpoint(
request: Request,
business_id: int,
fiscal_year_id: int,
closing_fiscal_end_date: Optional[date] = Query(
None,
description="تاریخ پایان دورهٔ بستن (شامل) برای پیش‌نمایش تقطیع؛ خالی = پایان تقویمی سال",
),
document_relocation_basis: str = Query(
"document_date",
description="document_date یا registered_at",
),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
_: None = Depends(require_business_permission_dep("fiscal_years", "view")),
) -> Dict[str, Any]:
"""پیش‌نمایش بستن سال مالی"""
try:
preview_data = preview_year_end_closing(
db,
business_id,
fiscal_year_id,
closing_fiscal_end_date=closing_fiscal_end_date,
document_relocation_basis=document_relocation_basis,
)
return success_response(
data=format_datetime_fields(preview_data, request),
request=request,
message="YEAR_END_CLOSING_PREVIEW_FETCHED"
)
except ApiError as e:
raise e
except Exception as e:
raise ApiError("PREVIEW_FAILED", f"خطا در پیش‌نمایش بستن سال مالی: {str(e)}", http_status=500)
@router.post("/{business_id}/fiscal-years/{fiscal_year_id}/close")
@require_business_access("business_id")
async def close_fiscal_year_endpoint(
request: Request,
business_id: int,
fiscal_year_id: int,
payload: YearEndClosingRequest = Body(...),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
_: None = Depends(require_business_permission_dep("fiscal_years", "close")),
) -> Dict[str, Any]:
"""بستن سال مالی"""
try:
result = await close_fiscal_year(
db=db,
business_id=business_id,
fiscal_year_id=fiscal_year_id,
user_id=ctx.get_user_id(),
new_fiscal_year_title=payload.new_fiscal_year_title,
auto_create_opening_balance=payload.auto_create_opening_balance,
# مالیات
tax_percentage=payload.tax_percentage,
tax_amount=payload.tax_amount,
# تقسیم سود
profit_distribution_percentage=payload.profit_distribution_percentage,
profit_distribution_amount=payload.profit_distribution_amount,
shareholder_profit_account_id=payload.shareholder_profit_account_id,
# سود انباشته سنواتی
retained_earnings_from_previous_years=payload.retained_earnings_from_previous_years,
# تنظیمات
auto_issue_person_balance_document=payload.auto_issue_person_balance_document,
# تنظیمات سال مالی جدید
new_fiscal_year_start_date=payload.new_fiscal_year_start_date,
new_fiscal_year_end_date=payload.new_fiscal_year_end_date,
inventory_valuation_method=payload.inventory_valuation_method,
# تقسیم سود بین سهامداران (تبدیل از Pydantic model به dict)
shareholder_distributions=[
{"person_id": item.person_id, "profit_amount": item.profit_amount}
for item in payload.shareholder_distributions
] if payload.shareholder_distributions else None,
closing_fiscal_end_date=payload.closing_fiscal_end_date,
document_relocation_basis=payload.document_relocation_basis,
move_post_cutoff_documents_to_new_fiscal_year=payload.move_post_cutoff_documents_to_new_fiscal_year,
)
cache = get_cache()
if cache.enabled:
cache.delete(f"fiscal_years:{business_id}")
return success_response(
data=format_datetime_fields(result, request),
request=request,
message="FISCAL_YEAR_CLOSED_SUCCESSFULLY"
)
except ApiError as e:
raise e
except Exception as e:
raise ApiError("CLOSING_FAILED", f"خطا در بستن سال مالی: {str(e)}", http_status=500)
class FiscalYearRollbackExecuteRequest(BaseModel):
confirmation_token: str = Field(..., min_length=10, description="توکن بازگشتی از پیش‌نمایش")
@router.get("/{business_id}/fiscal-years/current/rollback/preview")
@require_business_access("business_id")
def preview_fiscal_year_rollback(
request: Request,
business_id: int,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
_: None = Depends(require_business_permission_dep("fiscal_years", "rollback")),
) -> Dict[str, Any]:
"""پیش‌نمایش حذف سال مالی جاری و بازگشت به سال قبل"""
translator = get_request_translator(request)
try:
data = preview_current_fiscal_year_rollback(db, business_id, ctx.get_user_id(), translator)
return success_response(
data=format_datetime_fields(data, request),
request=request,
message="FISCAL_YEAR_ROLLBACK_PREVIEW",
)
except ApiError:
raise
except Exception as e:
template = translator.t(
"ROLLBACK_PREVIEW_FAILED",
default="Fiscal rollback preview failed. Check connectivity and try again. ({error})",
)
msg = apply_format(template, error=str(e))
raise ApiError("ROLLBACK_PREVIEW_FAILED", msg, http_status=500)
@router.post("/{business_id}/fiscal-years/current/rollback/execute")
@require_business_access("business_id")
def execute_fiscal_year_rollback(
request: Request,
business_id: int,
body: FiscalYearRollbackExecuteRequest = Body(...),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
_: None = Depends(require_business_permission_dep("fiscal_years", "rollback")),
) -> Dict[str, Any]:
"""اجرای حذف سال مالی جاری و بازگشت به سال قبل"""
translator = get_request_translator(request)
try:
data = execute_current_fiscal_year_rollback(
db,
business_id,
ctx.get_user_id(),
body.confirmation_token,
translator,
request=request,
)
return success_response(
data=format_datetime_fields(data, request),
request=request,
message="FISCAL_YEAR_ROLLBACK_DONE",
)
except ApiError:
raise
except Exception as e:
template = translator.t(
"ROLLBACK_EXECUTE_FAILED",
default="Fiscal rollback failed unexpectedly. If it persists, contact support. ({error})",
)
msg = apply_format(template, error=str(e))
raise ApiError("ROLLBACK_EXECUTE_FAILED", msg, http_status=500)
class FiscalYearUpdateRequest(BaseModel):
title: str = Field(..., min_length=1, max_length=255, description="عنوان سال مالی")
start_date: date = Field(..., description="تاریخ شروع سال مالی")
end_date: date = Field(..., description="تاریخ پایان سال مالی")
class FiscalYearCreateRequest(BaseModel):
title: str = Field(..., min_length=1, max_length=255, description="عنوان سال مالی")
start_date: date = Field(..., description="تاریخ شروع سال مالی")
end_date: date = Field(..., description="تاریخ پایان سال مالی")
set_as_current: bool = Field(
default=False,
description="اگر true باشد این سال جاری می‌شود؛ در غیر این صورت بدون تغییر is_last سال‌های موجود ایجاد می‌شود",
)
@router.post("/{business_id}/fiscal-years")
@require_business_access("business_id")
def create_fiscal_year_endpoint(
request: Request,
business_id: int,
payload: FiscalYearCreateRequest = Body(...),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
_: None = Depends(require_business_permission_dep("fiscal_years", "edit")),
) -> Dict[str, Any]:
"""ایجاد سال مالی جدید (برای مهاجرت چندساله بدون بستن حسابداری)."""
if payload.start_date >= payload.end_date:
raise ApiError("INVALID_DATE_RANGE", "تاریخ شروع باید قبل از تاریخ پایان باشد", http_status=400)
repo = FiscalYearRepository(db)
all_fiscal = repo.list_by_business(business_id)
for fy in all_fiscal:
if _date_ranges_overlap(payload.start_date, payload.end_date, fy.start_date, fy.end_date):
raise ApiError(
"FISCAL_YEAR_RANGE_OVERLAP",
f"بازهٔ انتخاب‌شده با سال مالی «{fy.title}» همپوشانی دارد.",
http_status=400,
details={
"conflicting_fiscal_year_id": fy.id,
"conflicting_start_date": fy.start_date.isoformat(),
"conflicting_end_date": fy.end_date.isoformat(),
},
)
is_last = bool(payload.set_as_current) or len(all_fiscal) == 0
if is_last and all_fiscal:
for fy in all_fiscal:
if fy.is_last:
fy.is_last = False
db.add(fy)
db.flush()
fiscal_year = repo.create_fiscal_year(
business_id=business_id,
title=payload.title,
start_date=payload.start_date,
end_date=payload.end_date,
is_last=is_last,
)
cache = get_cache()
if cache.enabled:
cache.delete(f"fiscal_years:{business_id}")
data = {
"id": fiscal_year.id,
"title": fiscal_year.title,
"start_date": fiscal_year.start_date,
"end_date": fiscal_year.end_date,
"is_current": fiscal_year.is_last,
}
return success_response(
data=format_datetime_fields(data, request),
request=request,
message="FISCAL_YEAR_CREATED_SUCCESSFULLY",
)
class MigrationEnsureYearsRequest(BaseModel):
years: List[FiscalYearCreateRequest] = Field(..., min_items=1)
current_start_date: Optional[date] = Field(
None,
description="اگر مشخص شود، سالی که start_date برابر این مقدار است جاری می‌شود",
)
# مسیرهای migration باید قبل از .../{fiscal_year_id}/set-current ثبت شوند؛
# وگرنه FastAPI مقدار "migration" را به‌عنوان fiscal_year_id می‌گیرد.
@router.post("/{business_id}/fiscal-years/migration/ensure")
@require_business_access("business_id")
def migration_ensure_fiscal_years(
request: Request,
business_id: int,
payload: MigrationEnsureYearsRequest = Body(...),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
_: None = Depends(require_business_permission_dep("fiscal_years", "edit")),
) -> Dict[str, Any]:
"""ایجاد/بازیابی دسته‌ای سال‌های مالی برای مهاجرت بدون بستن حسابداری."""
repo = FiscalYearRepository(db)
existing = repo.list_by_business(business_id)
by_start = {fy.start_date: fy for fy in existing}
created_count = 0
reused_count = 0
items_out: List[Dict[str, Any]] = []
for y in sorted(payload.years, key=lambda x: x.start_date):
if y.start_date >= y.end_date:
raise ApiError("INVALID_DATE_RANGE", "تاریخ شروع باید قبل از تاریخ پایان باشد", http_status=400)
found = by_start.get(y.start_date)
if found is not None:
reused_count += 1
items_out.append(
{
"id": found.id,
"title": found.title,
"start_date": found.start_date,
"end_date": found.end_date,
"is_current": found.is_last,
}
)
continue
for fy in existing:
if _date_ranges_overlap(y.start_date, y.end_date, fy.start_date, fy.end_date):
# سال پیش‌فرض کسب‌وکار جدید معمولاً با آخرین سال هلو همپوشانی دارد.
# اگر هنوز سندی روی آن نیست، همان رکورد را به بازهٔ درخواستی مهاجرت تغییر می‌دهیم.
from adapters.db.models.document import Document
has_docs = (
db.query(Document.id)
.filter(
Document.business_id == int(business_id),
Document.fiscal_year_id == int(fy.id),
)
.limit(1)
.first()
is not None
)
if has_docs:
raise ApiError(
"FISCAL_YEAR_RANGE_OVERLAP",
f"بازهٔ «{y.title}» با سال مالی «{fy.title}» همپوشانی دارد.",
http_status=400,
)
old_start = fy.start_date
fy.title = y.title
fy.start_date = y.start_date
fy.end_date = y.end_date
db.add(fy)
if old_start in by_start and by_start[old_start].id == fy.id:
del by_start[old_start]
by_start[y.start_date] = fy
reused_count += 1
items_out.append(
{
"id": fy.id,
"title": fy.title,
"start_date": fy.start_date,
"end_date": fy.end_date,
"is_current": fy.is_last,
}
)
found = fy
break
if found is not None:
continue
is_first = len(existing) == 0 and created_count == 0
fy_new = repo.create_fiscal_year(
business_id=business_id,
title=y.title,
start_date=y.start_date,
end_date=y.end_date,
is_last=is_first,
commit=False,
)
existing.append(fy_new)
by_start[y.start_date] = fy_new
created_count += 1
items_out.append(
{
"id": fy_new.id,
"title": fy_new.title,
"start_date": fy_new.start_date,
"end_date": fy_new.end_date,
"is_current": fy_new.is_last,
}
)
db.commit()
current = repo.get_current_for_business(business_id)
if payload.current_start_date is not None:
target = by_start.get(payload.current_start_date)
if target is None:
raise ApiError(
"FISCAL_YEAR_NOT_FOUND",
"سال مالی با start_date درخواستی یافت نشد",
http_status=404,
)
current = repo.set_current_for_business(business_id, int(target.id))
for row in items_out:
row["is_current"] = int(row["id"]) == int(current.id)
cache = get_cache()
if cache.enabled:
cache.delete(f"fiscal_years:{business_id}")
current_data = None
if current is not None:
current_data = {
"id": current.id,
"title": current.title,
"start_date": current.start_date,
"end_date": current.end_date,
"is_current": current.is_last,
}
return success_response(
data=format_datetime_fields(
{
"items": items_out,
"created_count": created_count,
"reused_count": reused_count,
"current": current_data,
},
request,
),
request=request,
message="FISCAL_YEARS_ENSURED_FOR_MIGRATION",
)
class MigrationSetCurrentRequest(BaseModel):
fiscal_year_id: int = Field(..., gt=0)
@router.post("/{business_id}/fiscal-years/migration/set-current")
@require_business_access("business_id")
def migration_set_current_fiscal_year(
request: Request,
business_id: int,
payload: MigrationSetCurrentRequest = Body(...),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
_: None = Depends(require_business_permission_dep("fiscal_years", "edit")),
) -> Dict[str, Any]:
"""تعویض نرم سال جاری برای مهاجرت (سازگار با کلاینت Holoo2Hesabix)."""
repo = FiscalYearRepository(db)
fiscal_year = repo.set_current_for_business(business_id, int(payload.fiscal_year_id))
cache = get_cache()
if cache.enabled:
cache.delete(f"fiscal_years:{business_id}")
current_data = {
"id": fiscal_year.id,
"title": fiscal_year.title,
"start_date": fiscal_year.start_date,
"end_date": fiscal_year.end_date,
"is_current": fiscal_year.is_last,
}
return success_response(
data=format_datetime_fields({"current": current_data}, request),
request=request,
message="FISCAL_YEAR_SET_CURRENT_SUCCESSFULLY",
)
@router.post("/{business_id}/fiscal-years/{fiscal_year_id}/set-current")
@require_business_access("business_id")
def set_current_fiscal_year_endpoint(
request: Request,
business_id: int,
fiscal_year_id: int,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
_: None = Depends(require_business_permission_dep("fiscal_years", "edit")),
) -> Dict[str, Any]:
"""
تعویض نرم سال مالی جاری (فقط is_last) بدون بستن سود و زیان.
مناسب مهاجرت تاریخچه سال‌به‌سال (Holoo و مشابه).
"""
repo = FiscalYearRepository(db)
fiscal_year = repo.set_current_for_business(business_id, fiscal_year_id)
cache = get_cache()
if cache.enabled:
cache.delete(f"fiscal_years:{business_id}")
data = {
"id": fiscal_year.id,
"title": fiscal_year.title,
"start_date": fiscal_year.start_date,
"end_date": fiscal_year.end_date,
"is_current": fiscal_year.is_last,
}
return success_response(
data=format_datetime_fields(data, request),
request=request,
message="FISCAL_YEAR_SET_CURRENT_SUCCESSFULLY",
)
@router.put("/{business_id}/fiscal-years/current")
@require_business_access("business_id")
def update_current_fiscal_year(
request: Request,
business_id: int,
payload: FiscalYearUpdateRequest = Body(...),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
_: None = Depends(require_business_permission_dep("fiscal_years", "edit")),
) -> Dict[str, Any]:
"""ویرایش یا ایجاد سال مالی جاری (اگر وجود نداشت، ایجاد می‌شود)"""
repo = FiscalYearRepository(db)
# بررسی اعتبار تاریخ‌ها
if payload.start_date >= payload.end_date:
raise ApiError("INVALID_DATE_RANGE", "تاریخ شروع باید قبل از تاریخ پایان باشد", http_status=400)
all_fiscal = repo.list_by_business(business_id)
exclude_id: Optional[int] = None
# دریافت سال مالی جاری
fiscal_year = repo.get_current_for_business(business_id)
if fiscal_year:
exclude_id = int(fiscal_year.id)
for fy in all_fiscal:
if exclude_id is not None and int(fy.id) == exclude_id:
continue
if _date_ranges_overlap(
payload.start_date,
payload.end_date,
fy.start_date,
fy.end_date,
):
raise ApiError(
"FISCAL_YEAR_RANGE_OVERLAP",
f"بازهٔ انتخاب‌شده با سال مالی «{fy.title}» همپوشانی دارد.",
http_status=400,
details={
"conflicting_fiscal_year_id": fy.id,
"conflicting_start_date": fy.start_date.isoformat(),
"conflicting_end_date": fy.end_date.isoformat(),
},
)
if fiscal_year:
# اگر سال مالی جاری وجود داشت: به‌روزرسانی می‌کنیم
# بررسی اینکه سال مالی متعلق به این کسب و کار است
if int(fiscal_year.business_id) != int(business_id):
raise ApiError("FISCAL_YEAR_NOT_FOUND", "سال مالی متعلق به این کسب‌وکار نیست", http_status=404)
# به‌روزرسانی فیلدها
fiscal_year.title = payload.title
fiscal_year.start_date = payload.start_date
fiscal_year.end_date = payload.end_date
db.commit()
db.refresh(fiscal_year)
message = "FISCAL_YEAR_UPDATED_SUCCESSFULLY"
else:
# اگر سال مالی جاری وجود نداشت: ایجاد می‌کنیم
fiscal_year = repo.create_fiscal_year(
business_id=business_id,
title=payload.title,
start_date=payload.start_date,
end_date=payload.end_date,
is_last=True, # سال مالی جدید به عنوان سال مالی جاری ایجاد می‌شود
)
message = "FISCAL_YEAR_CREATED_SUCCESSFULLY"
cache = get_cache()
if cache.enabled:
cache.delete(f"fiscal_years:{business_id}")
data = {
"id": fiscal_year.id,
"title": fiscal_year.title,
"start_date": fiscal_year.start_date,
"end_date": fiscal_year.end_date,
"is_current": fiscal_year.is_last,
}
return success_response(
data=format_datetime_fields(data, request),
request=request,
message=message
)