294 lines
11 KiB
Python
294 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.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.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.form_repo = BudgetFormRepository(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,
|
|
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))
|
|
|
|
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
|