# app/modules/orders/models/invoice.py """ Invoice database models for the OMS. Provides models for: - StoreInvoiceSettings: Per-store invoice configuration (merchant details, VAT, numbering) - Invoice: Invoice records with snapshots of seller/buyer details """ import enum from sqlalchemy import ( Boolean, Column, DateTime, ForeignKey, Index, Integer, Numeric, String, Text, ) from sqlalchemy.dialects.sqlite import JSON from sqlalchemy.orm import relationship from app.core.database import Base from models.database.base import TimestampMixin class StoreInvoiceSettings(Base, TimestampMixin): """ Per-store invoice configuration. Stores merchant details, VAT number, invoice numbering preferences, and payment information for invoice generation. One-to-one relationship with Store. """ __tablename__ = "store_invoice_settings" id = Column(Integer, primary_key=True, index=True) store_id = Column( Integer, ForeignKey("stores.id"), unique=True, nullable=False, index=True ) # Legal merchant details for invoice header merchant_name = Column(String(255), nullable=False) # Legal name for invoices merchant_address = Column(String(255), nullable=True) # Street address merchant_city = Column(String(100), nullable=True) merchant_postal_code = Column(String(20), nullable=True) merchant_country = Column(String(2), nullable=False, default="LU") # ISO country code # VAT information vat_number = Column(String(50), nullable=True) # e.g., "LU12345678" is_vat_registered = Column(Boolean, default=True, nullable=False) # OSS (One-Stop-Shop) for EU VAT is_oss_registered = Column(Boolean, default=False, nullable=False) oss_registration_country = Column(String(2), nullable=True) # ISO country code # Invoice numbering invoice_prefix = Column(String(20), default="INV", nullable=False) invoice_next_number = Column(Integer, default=1, nullable=False) invoice_number_padding = Column(Integer, default=5, nullable=False) # e.g., INV00001 # Payment information payment_terms = Column(Text, nullable=True) # e.g., "Payment due within 30 days" bank_name = Column(String(255), nullable=True) bank_iban = Column(String(50), nullable=True) bank_bic = Column(String(20), nullable=True) # Invoice footer footer_text = Column(Text, nullable=True) # Custom footer text # Default VAT rate for Luxembourg invoices (17% standard) default_vat_rate = Column(Numeric(5, 2), default=17.00, nullable=False) # Relationships store = relationship("Store", back_populates="invoice_settings") def __repr__(self): return f"" def get_next_invoice_number(self) -> str: """Generate the next invoice number and increment counter.""" number = str(self.invoice_next_number).zfill(self.invoice_number_padding) return f"{self.invoice_prefix}{number}" class InvoiceStatus(str, enum.Enum): """Invoice status enumeration.""" DRAFT = "draft" ISSUED = "issued" PAID = "paid" CANCELLED = "cancelled" class VATRegime(str, enum.Enum): """VAT regime for invoice calculation.""" DOMESTIC = "domestic" # Same country as seller OSS = "oss" # EU cross-border with OSS registration REVERSE_CHARGE = "reverse_charge" # B2B with valid VAT number ORIGIN = "origin" # Cross-border without OSS (use origin VAT) EXEMPT = "exempt" # VAT exempt class Invoice(Base, TimestampMixin): """ Invoice record with snapshots of seller/buyer details. Stores complete invoice data including snapshots of seller and buyer details at time of creation for audit purposes. """ __tablename__ = "invoices" id = Column(Integer, primary_key=True, index=True) store_id = Column(Integer, ForeignKey("stores.id"), nullable=False, index=True) order_id = Column(Integer, ForeignKey("orders.id"), nullable=True, index=True) # Invoice identification invoice_number = Column(String(50), nullable=False) invoice_date = Column(DateTime(timezone=True), nullable=False) # Status status = Column(String(20), default=InvoiceStatus.DRAFT.value, nullable=False) # Seller details snapshot (captured at invoice creation) seller_details = Column(JSON, nullable=False) # Structure: { # "merchant_name": str, # "address": str, # "city": str, # "postal_code": str, # "country": str, # "vat_number": str | None # } # Buyer details snapshot (captured at invoice creation) buyer_details = Column(JSON, nullable=False) # Structure: { # "name": str, # "email": str, # "address": str, # "city": str, # "postal_code": str, # "country": str, # "vat_number": str | None (for B2B) # } # Line items snapshot line_items = Column(JSON, nullable=False) # Structure: [{ # "description": str, # "quantity": int, # "unit_price_cents": int, # "total_cents": int, # "sku": str | None, # "ean": str | None # }] # VAT information vat_regime = Column(String(20), default=VATRegime.DOMESTIC.value, nullable=False) destination_country = Column(String(2), nullable=True) # For OSS invoices vat_rate = Column(Numeric(5, 2), nullable=False) # e.g., 17.00 for 17% vat_rate_label = Column(String(50), nullable=True) # e.g., "Luxembourg Standard VAT" # Amounts (stored in cents for precision) currency = Column(String(3), default="EUR", nullable=False) subtotal_cents = Column(Integer, nullable=False) # Before VAT vat_amount_cents = Column(Integer, nullable=False) # VAT amount total_cents = Column(Integer, nullable=False) # After VAT # Payment information payment_terms = Column(Text, nullable=True) bank_details = Column(JSON, nullable=True) # IBAN, BIC snapshot footer_text = Column(Text, nullable=True) # PDF storage pdf_generated_at = Column(DateTime(timezone=True), nullable=True) pdf_path = Column(String(500), nullable=True) # Path to stored PDF # Notes notes = Column(Text, nullable=True) # Internal notes # Relationships store = relationship("Store", back_populates="invoices") order = relationship("Order", back_populates="invoices") __table_args__ = ( Index("idx_invoice_store_number", "store_id", "invoice_number", unique=True), Index("idx_invoice_store_date", "store_id", "invoice_date"), Index("idx_invoice_status", "store_id", "status"), ) def __repr__(self): return f"" @property def subtotal(self) -> float: """Get subtotal in EUR.""" return self.subtotal_cents / 100 @property def vat_amount(self) -> float: """Get VAT amount in EUR.""" return self.vat_amount_cents / 100 @property def total(self) -> float: """Get total in EUR.""" return self.total_cents / 100