284 lines
11 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.core.errors import AccessDeniedException, ValidationException
from src.db.models import AppUser, UserRoleEnum, Vsp
from src.domain.schemas import VSPCreate, VSPUpdate
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 = {
"location_form",
"numbers",
"total_area",
"rent_contract_num",
"rent_end_date",
}
def __init__(self, db: AsyncSession):
self.db = db
self.vsp_repo = VSPRepository(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,
location_form: str | None = None,
numbers_min: int | None = None,
numbers_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,
location_form=location_form,
numbers_min=numbers_min,
numbers_max=numbers_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,
with_count: bool = False,
) -> list[Vsp]:
ssp_ids = await self._get_scoped_ssp_ids(user=user)
if ssp_id is not None:
if ssp_ids is None:
ssp_ids = [ssp_id]
elif ssp_id not in set(ssp_ids):
if with_count:
return 0, []
return []
else:
ssp_ids = [ssp_id]
return await self.vsp_repo.get_list(
ssp_ids=ssp_ids,
is_active=True,
close_date_is_null=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()
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,
location_form: str | None = None,
numbers_min: int | None = None,
numbers_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,
location_form=location_form,
numbers_min=numbers_min,
numbers_max=numbers_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",
"location_form",
"numbers",
"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.location_form or None,
item.numbers if item.numbers 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 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