Andrey Epifancev 8d7d014391 fix: nullable jsonb пишет SQL NULL вместо jsonb null (I-1)
Все 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>
2026-10-04 01:49:26 +04:00

60 lines
3.4 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.

"""Часть 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)