2026-07-08 12:50:21 +03:00

307 lines
12 KiB
Python

import asyncio
import io
from datetime import date, datetime
from typing import Optional
from openpyxl import Workbook
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from src.repository.budget_form_repository import BudgetFormRepository
from src.core.errors import AccessDeniedException, ValidationException
from src.db.models import AppUser, UserRoleEnum, Vsp
from src.domain.schemas import VSPCreate, VSPUpdate
from src.repository.org_unit_repository import OrgUnitRepository
from src.repository.user_repository import UserRepository
from src.repository.vsp_repository import VSPRepository
_last_info_close_sync_run: Optional[date] = None
_info_close_sync_lock = asyncio.Lock()
class VSPService:
EXECUTOR_EDITABLE_FIELDS = {
"placement_type",
"staff_count",
"total_area",
"rent_contract_num",
"rent_end_date",
}
def __init__(self, db: AsyncSession):
self.db = db
self.vsp_repo = VSPRepository(db)
self.form_repo = BudgetFormRepository(db)
self.org_unit_repo = OrgUnitRepository(db)
self.user_repo = UserRepository(db)
async def get(self, vsp_id: int, user: AppUser, load_org_unit: bool = False) -> Optional[Vsp]:
vsp = await self.vsp_repo.get(vsp_id=vsp_id, load_org_unit=load_org_unit)
if not vsp:
return None
if user.role_id == UserRoleEnum.ADMIN:
return vsp
ssp_ids = await self._get_scoped_ssp_ids(user=user)
if not ssp_ids or vsp.branch_id not in set(ssp_ids):
raise AccessDeniedException()
return vsp
async def get_list(
self,
user: AppUser,
registration_number: str | None = None,
address: str | None = None,
ssp_id: int | None = None,
ssp_ids: list[int] | None = None,
open_date_start: date | None = None,
open_date_end: date | None = None,
placement_type: str | None = None,
staff_count_min: int | None = None,
staff_count_max: int | None = None,
is_active: bool | None = None,
load_org_unit: bool = False,
with_count: bool = False,
) -> list[Vsp] | tuple[int, list[Vsp]]:
await self._sync_closed_records()
scoped_ssp_ids = await self._get_scoped_ssp_ids(user=user)
requested_ssp_ids = list(dict.fromkeys(ssp_ids or []))
if ssp_id is not None and ssp_id not in requested_ssp_ids:
requested_ssp_ids.append(ssp_id)
effective_ssp_ids = scoped_ssp_ids
if requested_ssp_ids:
if scoped_ssp_ids is None:
effective_ssp_ids = requested_ssp_ids
else:
scoped_set = set(scoped_ssp_ids)
effective_ssp_ids = [item for item in requested_ssp_ids if item in scoped_set]
if not effective_ssp_ids:
if with_count:
return 0, []
return []
return await self.vsp_repo.get_list(
registration_number=registration_number,
address=address,
ssp_ids=effective_ssp_ids,
open_date_start=open_date_start,
open_date_end=open_date_end,
placement_type=placement_type,
staff_count_min=staff_count_min,
staff_count_max=staff_count_max,
is_active=is_active,
load_org_unit=load_org_unit,
with_count=with_count,
)
async def get_dropdown(
self,
user: AppUser,
ssp_id: int | None = None,
form_id: int | None = None,
with_count: bool = False,
) -> list[Vsp]:
ssp_ids = await self._get_scoped_ssp_ids(user=user)
checking_ssps = set()
if ssp_id is not None:
checking_ssps.add(ssp_id)
if form_id is not None:
form = await self.form_repo.get(budget_form_id=form_id)
if form is not None:
checking_ssps.add(form.org_unit_id)
if checking_ssps:
if ssp_ids is None:
ssp_ids = list(checking_ssps)
elif not checking_ssps & set(ssp_ids):
if with_count:
return 0, []
return []
else:
ssp_ids = list(checking_ssps & set(ssp_ids))
await self._sync_closed_records()
return await self.vsp_repo.get_list(
ssp_ids=ssp_ids,
is_active=True,
with_count=with_count,
)
async def create(self, payload: VSPCreate, user: AppUser, load_org_unit: bool = False) -> Vsp:
if not self._can_modify(user):
raise AccessDeniedException()
await self._ensure_active_branch(payload.branch_id)
try:
created = await self.vsp_repo.create(payload=payload, created_by=user.id, load_org_unit=load_org_unit)
if created.closed_at is not None and created.closed_at <= date.today():
created = await self.vsp_repo.update(
vsp=created,
data={"is_active": False},
updated_by=user.id,
load_org_unit=load_org_unit,
)
return created
except IntegrityError as exc:
message = str(getattr(exc, "orig", exc) or exc).strip()
if "vsp_reg_number_unique" in message:
raise ValidationException(description="Указанный номер уже существует")
if "chk_closed_after_opened" in message:
raise ValidationException(description="Дата закрытия не может быть меньше даты открытия")
raise ValidationException(description="Некорректные данные для записи ВСП") from exc
async def export_xlsx(
self,
user: AppUser,
registration_number: str | None = None,
address: str | None = None,
ssp_ids: list[int] | None = None,
open_date_start: date | None = None,
open_date_end: date | None = None,
placement_type: str | None = None,
staff_count_min: int | None = None,
staff_count_max: int | None = None,
) -> tuple[io.BytesIO, str]:
data = await self.get_list(
user=user,
registration_number=registration_number,
address=address,
ssp_ids=ssp_ids,
open_date_start=open_date_start,
open_date_end=open_date_end,
placement_type=placement_type,
staff_count_min=staff_count_min,
staff_count_max=staff_count_max,
is_active=None,
load_org_unit=True,
)
workbook = Workbook()
sheet = workbook.active
sheet.title = "INFO"
sheet.append(
[
"id",
"ssp_id",
"system_code",
"regional_branch",
"vsp_type",
"registration_number",
"address",
"approved_format",
"open_date",
"close_date",
"notes",
"placement_type",
"staff_count",
"area",
"rent_contract_num",
"rent_end_date",
"is_active",
"is_deleted",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
)
for item in data:
sheet.append(
[
item.id,
item.branch_id,
item.system_code or None,
item.org_unit.title or None,
item.vsp_type or None,
item.reg_number,
item.address,
item.format or None,
item.opened_at.isoformat() if item.opened_at else None,
item.closed_at.isoformat() if item.closed_at else None,
item.notes or None,
item.placement_type or None,
item.staff_count if item.staff_count is not None else None,
float(item.total_area) if item.total_area is not None else None,
item.rent_contract_num or None,
item.rent_end_date.isoformat() if item.rent_end_date else None,
item.is_active,
item.is_deleted,
item.created_at.isoformat() if item.created_at else None,
item.updated_at.isoformat() if item.updated_at else None,
item.created_by,
item.updated_by if item.updated_by is not None else None,
]
)
stream = io.BytesIO()
workbook.save(stream)
workbook.close()
stream.seek(0)
file_name = f"info_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
return stream, file_name
async def update(self, vsp_id: int, payload: VSPUpdate, user: AppUser, load_org_unit: bool = False) -> Vsp | None:
vsp = await self.vsp_repo.get(vsp_id=vsp_id, is_active=None)
if not vsp:
return None
data = payload.model_dump(exclude_unset=True)
if not data:
return vsp
if "branch_id" in data:
await self._ensure_active_branch(data["branch_id"])
if user.role_id != UserRoleEnum.ADMIN:
ssp_ids = await self._get_scoped_ssp_ids(user=user)
if not ssp_ids or vsp.branch_id not in set(ssp_ids):
raise AccessDeniedException()
if not set(data.keys()).issubset(self.EXECUTOR_EDITABLE_FIELDS):
raise AccessDeniedException()
effective_close_date = data.get("close_date", vsp.closed_at)
if effective_close_date is not None and effective_close_date <= date.today():
data["is_active"] = False
try:
return await self.vsp_repo.update(
vsp=vsp, data=data, updated_by=user.id, load_org_unit=load_org_unit,
)
except IntegrityError as exc:
message = str(getattr(exc, "orig", exc) or exc).strip()
if "vsp_reg_number_unique" in message:
raise ValidationException(description="Указанный номер уже существует")
if "chk_closed_after_opened" in message:
raise ValidationException(description="Дата закрытия не может быть меньше даты открытия")
raise ValidationException(description="Некорректные данные для записи ВСП") from exc
async def logical_delete(self, vsp_id: int, user: AppUser) -> bool:
if not self._can_modify(user):
raise AccessDeniedException()
return await self.vsp_repo.logical_delete(vsp_id=vsp_id, updated_by=user.id)
def _can_modify(self, user: AppUser) -> bool:
return user.role_id == UserRoleEnum.ADMIN
async def _get_scoped_ssp_ids(self, user: AppUser) -> list[int] | None:
if user.role_id == UserRoleEnum.ADMIN:
return None
return list(set(await self.user_repo.get_many_ssp_ids(user_id=user.id)))
async def _sync_closed_records(self) -> None:
global _last_info_close_sync_run
today = date.today()
if _last_info_close_sync_run is not None and _last_info_close_sync_run >= today:
return
async with _info_close_sync_lock:
if _last_info_close_sync_run is not None and _last_info_close_sync_run >= today:
return
await self.vsp_repo.deactivate_by_close_date(as_of=today)
_last_info_close_sync_run = today
async def _ensure_active_branch(self, branch_id: int | None) -> None:
if branch_id is None:
return
org_unit = await self.org_unit_repo.get(org_unit_id=branch_id)
if org_unit is None or not org_unit.is_active:
raise ValidationException(description="Указан неактивный или отсутствующий ССП/РФ")