arm-core/api/src/services/osv_header.py

105 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Уточнение ОСВ в транзакции запроса с сохранением первички и аудитом."""
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)