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>
199 lines
8.9 KiB
Python
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"),
|
|
)
|