Все nullable jsonb-колонки ORM (audit_event.before_value/after_value, ex_snapshot_item.payload, recognition_job.group_schema/metrics, osv_entry.original_values) объявлены через nullable_jsonb() = JSONB(none_as_null=True): Python None сохраняется как SQL NULL, IS NULL находит строку. DDL не меняется; сверка с DBML и миграции зелёные. Обходы для не-объекта в metrics (CASE по jsonb_typeof в публикаторе, merge_metrics в inbox) оставлены: строки до исправления и сырой SQL могут содержать jsonb null. Тесты: SQL NULL после AuditService.record без before/after, metrics фабрики None -> SQL NULL, повтор публикации и слияние метрик для SQL NULL и jsonb null, перечень колонок с флагом. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
60 lines
3.4 KiB
Python
60 lines
3.4 KiB
Python
"""Часть 05 DBML v5.13 «ОСВ»: строка оборотно-сальдовой ведомости (ТЗ 6.2, 6.5).
|
||
|
||
Первичка не мутируется: сырые значения хранятся как пришли, правка аналитика — рядом
|
||
(`is_modified`, `original_values`, `modified_by/at` + audit_event(RECOGNIZED_DATA_EDIT)).
|
||
"""
|
||
|
||
import datetime
|
||
import decimal
|
||
import uuid
|
||
from typing import Any
|
||
|
||
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, Numeric, SmallInteger, String, text
|
||
from sqlalchemy.dialects.postgresql import UUID
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from .base import Base, UUIDPrimaryMixin, nullable_jsonb
|
||
from .enums import OsvDataType, OsvGroupType, pg_enum
|
||
|
||
|
||
class OsvEntry(UUIDPrimaryMixin, Base):
|
||
"""Строка ОСВ; суммы — рубли, `numeric(18,2)`."""
|
||
|
||
__tablename__ = "osv_entry"
|
||
__table_args__ = (
|
||
Index("uq_osv_entry_document_id_row_number", "document_id", "row_number", unique=True),
|
||
Index("ix_osv_entry_owner_account_counterparty", "owner_legal_entity_id", "account_id", "counterparty_id"),
|
||
)
|
||
|
||
recognition_job_id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True), ForeignKey("recognition_job.id"), nullable=False
|
||
)
|
||
document_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("document.id"), nullable=False)
|
||
owner_legal_entity_id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True), ForeignKey("legal_entity.id"), nullable=False
|
||
)
|
||
parent_entry_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("osv_entry.id"))
|
||
group_type: Mapped[OsvGroupType | None] = mapped_column(pg_enum("osv_group_type"))
|
||
group_name: Mapped[str | None] = mapped_column(String)
|
||
row_number: Mapped[int | None] = mapped_column(Integer)
|
||
level: Mapped[int | None] = mapped_column(SmallInteger)
|
||
row_value: Mapped[str | None] = mapped_column(String)
|
||
data_type: Mapped[OsvDataType | None] = mapped_column(pg_enum("osv_data_type"))
|
||
account_code: Mapped[str | None] = mapped_column(String)
|
||
account_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("chart_of_account.id"))
|
||
account_root: Mapped[str | None] = mapped_column(String)
|
||
counterparty_name: Mapped[str | None] = mapped_column(String)
|
||
counterparty_inn: Mapped[str | None] = mapped_column(String)
|
||
counterparty_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("legal_entity.id"))
|
||
opening_debit: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2))
|
||
opening_credit: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2))
|
||
turnover_debit: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2))
|
||
turnover_credit: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2))
|
||
closing_debit: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2))
|
||
closing_credit: Mapped[decimal.Decimal | None] = mapped_column(Numeric(18, 2))
|
||
is_modified: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("false"))
|
||
original_values: Mapped[Any | None] = mapped_column(nullable_jsonb())
|
||
modified_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"))
|
||
modified_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
|
||
is_excisable: Mapped[bool | None] = mapped_column(Boolean)
|