105 lines
5.6 KiB
Python
105 lines
5.6 KiB
Python
"""Уточнение ОСВ в транзакции запроса с сохранением первички и аудитом."""
|
||
|
||
import uuid
|
||
|
||
from sqlalchemy import delete, select
|
||
from sqlalchemy.exc import IntegrityError
|
||
|
||
from src.common.clock import utc_now
|
||
from src.common.exceptions import ConflictError, NotFoundError
|
||
from src.domain.osv_header import HEADER_FIELDS, corrected_header
|
||
from src.domain.policy import IN_WORK_STATUSES
|
||
from src.models.consolidation import ConsolidationDocument
|
||
from src.models.enums import AuditAction, AuditSection, ConsolidationStatus, DocumentStatus, DocumentType, FileStatus
|
||
from src.models.files import Document
|
||
from src.repositories import files as files_repo
|
||
from src.schemas.documents import DocumentOut, OsvHeaderPatch
|
||
from src.services import consolidation_documents
|
||
from src.services.documents import DocumentService
|
||
|
||
|
||
class OsvHeaderService(DocumentService):
|
||
"""Разделяет видимость, карточку и транзакцию с обычным сервисом документов."""
|
||
|
||
async def _twin(self, document: Document, account: object) -> uuid.UUID | None:
|
||
return await self._session.scalar(
|
||
select(Document.id).where(
|
||
Document.stored_file_id == document.stored_file_id,
|
||
Document.document_type == DocumentType.OSV,
|
||
Document.osv_account_code == account,
|
||
Document.id != document.id,
|
||
)
|
||
)
|
||
|
||
async def patch(self, document_id: uuid.UUID, body: OsvHeaderPatch) -> DocumentOut:
|
||
visible = await self._visible(document_id)
|
||
file = await files_repo.lock_file(self._session, visible.stored_file_id)
|
||
document = await files_repo.lock_document(self._session, document_id)
|
||
if file is None or document is None:
|
||
raise NotFoundError("документ не найден")
|
||
if document.document_type is not DocumentType.OSV:
|
||
raise ConflictError("DOCUMENT_NOT_OSV", "Документ не является ОСВ")
|
||
if file.status is FileStatus.SUPERSEDED:
|
||
raise ConflictError("FILE_SUPERSEDED", "Файл заменён новой версией")
|
||
current = {key: getattr(document, key) for key in HEADER_FIELDS}
|
||
result = corrected_header(current, body.model_dump(exclude_unset=True))
|
||
twin = await self._twin(document, result["osv_account_code"])
|
||
if twin is not None:
|
||
raise ConflictError("DUPLICATE_ACCOUNT", "Счёт уже есть в файле", document_id=str(twin))
|
||
versions = await consolidation_documents.bound_versions(self._session, documents=[document])
|
||
blocked = [
|
||
str(vid) for vid, status in versions if status not in IN_WORK_STATUSES | {ConsolidationStatus.CANCELLED}
|
||
]
|
||
if blocked:
|
||
raise ConflictError("VERSION_NOT_EDITABLE", "Документ используется в закрытой версии", version_ids=blocked)
|
||
changes = {key: value for key, value in result.items() if value != current[key]}
|
||
if not changes:
|
||
return await self._card(document, file)
|
||
before = {key: current[key] for key in changes}
|
||
after = dict(changes)
|
||
try:
|
||
async with self._session.begin_nested():
|
||
if document.original_values is None:
|
||
document.original_values = current
|
||
for key, value in changes.items():
|
||
setattr(document, key, value)
|
||
document.is_modified = result != document.original_values
|
||
document.modified_by = self._audit.user_id
|
||
document.modified_at = document.updated_at = utc_now()
|
||
if document.status is DocumentStatus.CONFIRMED:
|
||
before.update(
|
||
status=document.status.value,
|
||
confirmed_by=str(document.confirmed_by) if document.confirmed_by else None,
|
||
confirmed_at=document.confirmed_at.isoformat() if document.confirmed_at else None,
|
||
)
|
||
document.status = DocumentStatus.NEEDS_CHECK
|
||
document.confirmed_by = document.confirmed_at = None
|
||
after.update(status=document.status.value, confirmed_by=None, confirmed_at=None)
|
||
working = [vid for vid, status in versions if status in IN_WORK_STATUSES]
|
||
await self._session.execute(
|
||
delete(ConsolidationDocument).where(
|
||
ConsolidationDocument.document_id == document.id,
|
||
ConsolidationDocument.consolidation_version_id.in_(working),
|
||
)
|
||
)
|
||
after["unbound_version_ids"] = [str(vid) for vid in working]
|
||
await self._session.flush()
|
||
except IntegrityError:
|
||
# SAVEPOINT rollback expires the changed ORM object; reload asynchronously before accessing fields.
|
||
document = await files_repo.lock_document(self._session, document_id)
|
||
if document is None:
|
||
raise
|
||
twin = await self._twin(document, result["osv_account_code"])
|
||
if twin is None:
|
||
raise
|
||
raise ConflictError("DUPLICATE_ACCOUNT", "Счёт уже есть в файле", document_id=str(twin)) from None
|
||
await self._audit.record(
|
||
action=AuditAction.RECOGNIZED_DATA_EDIT,
|
||
section=AuditSection.RECOGNITION,
|
||
object_type="document",
|
||
object_id=document.id,
|
||
before_value=before,
|
||
after_value=after,
|
||
)
|
||
return await self._card(document, file)
|