Finance-Accounts/ar-aging-app/backend/app/db/models.py

275 lines
11 KiB
Python

"""SQLAlchemy ORM models for month-end closing sessions and their results."""
from __future__ import annotations
import datetime as dt
from sqlalchemy import (
Boolean, Column, Date, DateTime, Float, ForeignKey, Integer, String, Text, Index
)
from sqlalchemy.orm import relationship
from .database import Base
def _now() -> dt.datetime:
return dt.datetime.utcnow()
class Session(Base):
__tablename__ = "sessions"
id = Column(Integer, primary_key=True)
name = Column(String(255), nullable=False)
reporting_month = Column(String(32)) # "2026-01"
month_end_date = Column(Date)
reporting_currency = Column(String(16), default="USD")
clearing_lag_days = Column(Integer, default=2)
rounding_tolerance = Column(Float, default=0.01)
allowance_for_returns = Column(Float, default=0.0)
manual_adjustment = Column(Float, default=0.0)
manual_adjustment_note = Column(String(512), default="")
# How the opening AR balance is established: zero (default) | carry_forward | manual
opening_mode = Column(String(32), default="zero")
opening_source_session_id = Column(Integer)
status = Column(String(32), default="draft") # draft|processing|processed|error
progress_stage = Column(String(255), default="")
progress_pct = Column(Float, default=0.0)
progress_rows_done = Column(Integer, default=0)
progress_rows_total = Column(Integer, default=0)
eta_seconds = Column(Integer, default=0)
error = Column(Text, default="")
created_at = Column(DateTime, default=_now)
updated_at = Column(DateTime, default=_now, onupdate=_now)
files = relationship("SessionFile", back_populates="session", cascade="all, delete-orphan")
settlements = relationship("Settlement", back_populates="session", cascade="all, delete-orphan")
exceptions = relationship("Exception_", back_populates="session", cascade="all, delete-orphan")
fx_rates = relationship("FxRate", back_populates="session", cascade="all, delete-orphan")
reserves = relationship("Reserve", back_populates="session", cascade="all, delete-orphan")
class SessionFile(Base):
__tablename__ = "session_files"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
filename = Column(String(512))
stored_path = Column(String(1024))
size_bytes = Column(Integer)
sha256 = Column(String(64))
worksheets = Column(Text) # JSON list
data_sheet = Column(String(255))
imported_rows = Column(Integer, default=0)
min_date = Column(Date)
max_date = Column(Date)
currency = Column(String(16))
marketplace = Column(String(64))
status = Column(String(32), default="uploaded") # uploaded|parsed|invalid
message = Column(Text, default="")
session = relationship("Session", back_populates="files")
class Settlement(Base):
__tablename__ = "settlements"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
marketplace = Column(String(64))
account_type = Column(String(64))
settlement_id = Column(String(255))
order_total = Column(Float, default=0.0)
transfer_total = Column(Float, default=0.0)
transfer_amount = Column(Float) # boundary/received transfer amount if any
transfer_date = Column(Date)
transfer_received = Column(Boolean)
row_count = Column(Integer, default=0)
first_date = Column(Date)
last_date = Column(Date)
status = Column(String(32)) # paid|receivable
session = relationship("Session", back_populates="settlements")
Index("ix_settlement_session", Settlement.session_id, Settlement.marketplace, Settlement.account_type)
class Transaction(Base):
__tablename__ = "transactions"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
source_file = Column(String(512))
source_sheet = Column(String(255))
source_row = Column(Integer)
marketplace = Column(String(64))
settlement_id = Column(String(255))
order_id = Column(String(255))
sku = Column(String(255))
txn_type = Column(String(255)) # original (possibly localized) type
txn_type_en = Column(String(255)) # canonical English type
account_type = Column(String(64))
posted_date = Column(Date)
total = Column(Float, default=0.0)
currency = Column(String(16), default="USD")
settlement_status = Column(String(32)) # paid|receivable
receivable_flag = Column(Boolean, default=False)
storage_flag = Column(Boolean, default=False) # storage-fee detection drill-down
Index("ix_txn_session", Transaction.session_id)
class MappingRule(Base):
"""Admin-defined header alias: normalized source header -> canonical field.
Lets Finance map new/renamed Amazon headers without code changes."""
__tablename__ = "mapping_rules"
id = Column(Integer, primary_key=True)
normalized_header = Column(String(255), unique=True, nullable=False)
field = Column(String(128), nullable=False)
created_at = Column(DateTime, default=_now)
Index("ix_txn_settlement", Transaction.session_id, Transaction.settlement_id)
Index("ix_txn_type", Transaction.session_id, Transaction.txn_type)
Index("ix_txn_recv", Transaction.session_id, Transaction.receivable_flag)
class Exception_(Base):
__tablename__ = "exceptions"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
category = Column(String(128))
severity = Column(String(32)) # error|warning|info
detail = Column(Text)
source = Column(String(512), default="")
session = relationship("Session", back_populates="exceptions")
class FxRate(Base):
__tablename__ = "fx_rates"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
marketplace = Column(String(64))
currency = Column(String(16), default="USD")
rate = Column(Float, default=1.0)
source = Column(String(64), default="manual")
rate_date = Column(Date)
session = relationship("Session", back_populates="fx_rates")
class Reserve(Base):
__tablename__ = "reserves"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
marketplace = Column(String(64))
account_type = Column(String(64))
amount = Column(Float, default=0.0)
session = relationship("Session", back_populates="reserves")
class ReceivableResultRow(Base):
__tablename__ = "receivable_results"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
marketplace = Column(String(64))
account_type = Column(String(64)) # or "TOTAL"
additional_sales = Column(Float, default=0.0)
reserve = Column(Float, default=0.0)
receivable_local = Column(Float, default=0.0)
fx_rate = Column(Float, default=1.0)
receivable_usd = Column(Float, default=0.0)
currency = Column(String(16), default="USD")
class ReconciliationRow(Base):
__tablename__ = "reconciliation"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False, unique=True)
uploaded_total = Column(Float)
receivable_orders = Column(Float)
paid_orders = Column(Float)
transfers_total = Column(Float)
reserve_total = Column(Float)
manual_adjustments = Column(Float)
final_receivable_usd = Column(Float)
identity_difference = Column(Float)
status = Column(String(64))
notes = Column(Text, default="")
# AR roll-forward components
received_payouts = Column(Float, default=0.0) # Σ transfers received (negative)
all_payouts = Column(Float, default=0.0) # Σ all transfers (negative)
class FxRateDaily(Base):
"""Optional per-date FX override. Falls back to the marketplace's month rate."""
__tablename__ = "fx_rates_daily"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
marketplace = Column(String(64))
rate_date = Column(Date)
rate = Column(Float, default=1.0)
source = Column(String(64), default="manual")
Index("ix_fx_daily", FxRateDaily.session_id, FxRateDaily.marketplace, FxRateDaily.rate_date)
class MarketPayout(Base):
"""Per-marketplace disbursement totals for the AR roll-forward.
Transfers are attributed to the OWNER of their tagged settlement, not the file they
appear in — Amazon repeats account-level payouts across country reports (e.g. the
Belgium chain's transfers sit inside the Germany file).
"""
__tablename__ = "market_payouts"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
marketplace = Column(String(64))
received_payouts = Column(Float, default=0.0) # negative
all_payouts = Column(Float, default=0.0) # negative
Index("ix_market_payout_session", MarketPayout.session_id, MarketPayout.marketplace)
class OpeningBalance(Base):
__tablename__ = "opening_balances"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
marketplace = Column(String(64))
amount = Column(Float, default=0.0)
reason = Column(String(512), default="")
source = Column(String(64), default="manual") # manual | carried_forward
updated_at = Column(DateTime, default=_now, onupdate=_now)
Index("ix_opening_session", OpeningBalance.session_id, OpeningBalance.marketplace)
class FinanceControl(Base):
"""Finance's manual control-sheet amounts + sign-off, for the reconciliation section."""
__tablename__ = "finance_control"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False, unique=True)
gross_sales = Column(Float)
refunds = Column(Float)
net_revenue = Column(Float)
disbursements = Column(Float)
closing_receivable = Column(Float)
tolerance = Column(Float, default=1.0)
verified_by = Column(String(255), default="")
verified_at = Column(DateTime)
comment = Column(Text, default="")
class JournalEntry(Base):
__tablename__ = "journal_entries"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False, unique=True)
entry_no = Column(String(128), default="")
data = Column(Text, default="") # JSON: periods + lines + receivable
class ExportRecord(Base):
__tablename__ = "exports"
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
kind = Column(String(32), default="full") # full | summary
path = Column(String(1024))
sha256 = Column(String(64))
size_bytes = Column(Integer)
generated_at = Column(DateTime, default=_now)