TorbatYar/backend/services/accounting/app/models/posting.py
Mortezakoohjani 12c8615615 Ship enterprise Accounting FE/API with CRUD parity and production wiring.
Adds accounting-service PATCH/archive, fiscal helpers, COA templates and setup status, plus SuperApp Accounting UI (DS, scoreboard, masters, vouchers, ledger, ops modules) with session refresh and HTTPS public API URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 15:26:43 +03:30

199 lines
8.9 KiB
Python

"""Phase 5.2 — Double-entry posting domain models."""
from __future__ import annotations
import uuid
from datetime import date, datetime
from decimal import Decimal
from sqlalchemy import (
Boolean,
Date,
DateTime,
ForeignKey,
Index,
Integer,
Numeric,
String,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
from app.models.base import TenantMixin, TimestampMixin, UUIDPrimaryKeyMixin
from app.models.types import (
GUID,
JournalStatus,
PostingStatus,
VoucherStatus,
)
class Voucher(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
__tablename__ = "vouchers"
fiscal_period_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
voucher_number: Mapped[str] = mapped_column(String(50), nullable=False)
voucher_date: Mapped[date] = mapped_column(Date, nullable=False)
status: Mapped[VoucherStatus] = mapped_column(
default=VoucherStatus.DRAFT, nullable=False
)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
reference_number: Mapped[str | None] = mapped_column(String(100), nullable=True)
source_module: Mapped[str | None] = mapped_column(String(50), nullable=True)
currency_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
total_debit: Mapped[Decimal] = mapped_column(
Numeric(18, 4), default=Decimal("0"), nullable=False
)
total_credit: Mapped[Decimal] = mapped_column(
Numeric(18, 4), default=Decimal("0"), nullable=False
)
posted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
posted_by: Mapped[str | None] = mapped_column(String(100), nullable=True)
reversed_voucher_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
created_by: Mapped[str | None] = mapped_column(String(100), nullable=True)
lines: Mapped[list["VoucherLine"]] = relationship(
back_populates="voucher", cascade="all, delete-orphan"
)
journal_entries: Mapped[list["JournalEntry"]] = relationship(back_populates="voucher")
__table_args__ = (
UniqueConstraint("tenant_id", "voucher_number", name="uq_voucher_tenant_number"),
Index("ix_vouchers_tenant_status", "tenant_id", "status"),
Index("ix_vouchers_fiscal_period", "tenant_id", "fiscal_period_id"),
)
class VoucherLine(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
__tablename__ = "voucher_lines"
voucher_id: Mapped[uuid.UUID] = mapped_column(
GUID(), ForeignKey("vouchers.id", ondelete="CASCADE"), nullable=False
)
line_number: Mapped[int] = mapped_column(Integer, nullable=False)
account_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
debit: Mapped[Decimal] = mapped_column(Numeric(18, 4), default=Decimal("0"), nullable=False)
credit: Mapped[Decimal] = mapped_column(Numeric(18, 4), default=Decimal("0"), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
cost_center_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
project_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
currency_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
exchange_rate: Mapped[Decimal | None] = mapped_column(Numeric(18, 8), nullable=True)
voucher: Mapped["Voucher"] = relationship(back_populates="lines")
__table_args__ = (
Index("ix_voucher_lines_voucher_id", "voucher_id"),
Index("ix_voucher_lines_account_id", "tenant_id", "account_id"),
)
class Journal(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
__tablename__ = "journals"
code: Mapped[str] = mapped_column(String(50), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
__table_args__ = (
UniqueConstraint("tenant_id", "code", name="uq_journal_tenant_code"),
)
class JournalEntry(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
"""Immutable after posting — only created via Posting Engine."""
__tablename__ = "journal_entries"
voucher_id: Mapped[uuid.UUID] = mapped_column(
GUID(), ForeignKey("vouchers.id", ondelete="RESTRICT"), nullable=False
)
journal_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
fiscal_period_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
entry_number: Mapped[str] = mapped_column(String(50), nullable=False)
entry_date: Mapped[date] = mapped_column(Date, nullable=False)
status: Mapped[JournalStatus] = mapped_column(
default=JournalStatus.DRAFT, nullable=False
)
account_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
debit: Mapped[Decimal] = mapped_column(Numeric(18, 4), default=Decimal("0"), nullable=False)
credit: Mapped[Decimal] = mapped_column(Numeric(18, 4), default=Decimal("0"), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
cost_center_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
project_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
posted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
posted_by: Mapped[str | None] = mapped_column(String(100), nullable=True)
voucher: Mapped["Voucher"] = relationship(back_populates="journal_entries")
__table_args__ = (
Index("ix_journal_entries_tenant_account", "tenant_id", "account_id"),
Index("ix_journal_entries_voucher", "voucher_id"),
Index("ix_journal_entries_period", "tenant_id", "fiscal_period_id"),
)
class PostingLog(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
__tablename__ = "posting_logs"
voucher_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
operation: Mapped[str] = mapped_column(String(50), nullable=False)
status: Mapped[PostingStatus] = mapped_column(nullable=False)
actor_user_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
source_module: Mapped[str | None] = mapped_column(String(50), nullable=True)
message: Mapped[str | None] = mapped_column(Text, nullable=True)
correlation_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
__table_args__ = (Index("ix_posting_logs_voucher", "tenant_id", "voucher_id"),)
class PostingError(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
__tablename__ = "posting_errors"
voucher_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
error_code: Mapped[str] = mapped_column(String(50), nullable=False)
error_message: Mapped[str] = mapped_column(Text, nullable=False)
field_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
__table_args__ = (Index("ix_posting_errors_voucher", "tenant_id", "voucher_id"),)
class PostingReference(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
__tablename__ = "posting_references"
voucher_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
source_module: Mapped[str] = mapped_column(String(50), nullable=False)
source_document_type: Mapped[str] = mapped_column(String(50), nullable=False)
source_document_id: Mapped[str] = mapped_column(String(100), nullable=False)
is_posted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
__table_args__ = (
UniqueConstraint(
"tenant_id", "source_module", "source_document_type", "source_document_id",
name="uq_posting_ref_source",
),
)
class AccountingAuditLog(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
__tablename__ = "accounting_audit_logs"
operation: Mapped[str] = mapped_column(String(50), nullable=False)
actor_user_id: Mapped[str] = mapped_column(String(100), nullable=False)
source_module: Mapped[str | None] = mapped_column(String(50), nullable=True)
voucher_number: Mapped[str | None] = mapped_column(String(50), nullable=True)
reference_number: Mapped[str | None] = mapped_column(String(100), nullable=True)
resource_type: Mapped[str] = mapped_column(String(50), nullable=False)
resource_id: Mapped[str] = mapped_column(String(100), nullable=False)
before_value: Mapped[str | None] = mapped_column(Text, nullable=True)
after_value: Mapped[str | None] = mapped_column(Text, nullable=True)
ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True)
correlation_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
__table_args__ = (
Index("ix_audit_logs_tenant_created", "tenant_id", "created_at"),
)