refactor: migrate modules from re-exports to canonical implementations
Move actual code implementations into module directories: - orders: 5 services, 4 models, order/invoice schemas - inventory: 3 services, 2 models, 30+ schemas - customers: 3 services, 2 models, customer schemas - messaging: 3 services, 2 models, message/notification schemas - monitoring: background_tasks_service - marketplace: 5+ services including letzshop submodule - dev_tools: code_quality_service, test_runner_service - billing: billing_service - contracts: definition.py Legacy files in app/services/, models/database/, models/schema/ now re-export from canonical module locations for backwards compatibility. Architecture validator passes with 0 errors. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,215 +1,27 @@
|
||||
# models/database/invoice.py
|
||||
"""
|
||||
Invoice database models for the OMS.
|
||||
LEGACY LOCATION - Re-exports from module for backwards compatibility.
|
||||
|
||||
Provides models for:
|
||||
- VendorInvoiceSettings: Per-vendor invoice configuration (company details, VAT, numbering)
|
||||
- Invoice: Invoice records with snapshots of seller/buyer details
|
||||
The canonical implementation is now in:
|
||||
app/modules/orders/models/invoice.py
|
||||
|
||||
This file exists to maintain backwards compatibility with code that
|
||||
imports from the old location. All new code should import directly
|
||||
from the module:
|
||||
|
||||
from app.modules.orders.models import Invoice, InvoiceStatus, VATRegime, VendorInvoiceSettings
|
||||
"""
|
||||
|
||||
import enum
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
Numeric,
|
||||
String,
|
||||
Text,
|
||||
from app.modules.orders.models.invoice import (
|
||||
Invoice,
|
||||
InvoiceStatus,
|
||||
VATRegime,
|
||||
VendorInvoiceSettings,
|
||||
)
|
||||
from sqlalchemy.dialects.sqlite import JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
|
||||
|
||||
class VendorInvoiceSettings(Base, TimestampMixin):
|
||||
"""
|
||||
Per-vendor invoice configuration.
|
||||
|
||||
Stores company details, VAT number, invoice numbering preferences,
|
||||
and payment information for invoice generation.
|
||||
|
||||
One-to-one relationship with Vendor.
|
||||
"""
|
||||
|
||||
__tablename__ = "vendor_invoice_settings"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(
|
||||
Integer, ForeignKey("vendors.id"), unique=True, nullable=False, index=True
|
||||
)
|
||||
|
||||
# Legal company details for invoice header
|
||||
company_name = Column(String(255), nullable=False) # Legal name for invoices
|
||||
company_address = Column(String(255), nullable=True) # Street address
|
||||
company_city = Column(String(100), nullable=True)
|
||||
company_postal_code = Column(String(20), nullable=True)
|
||||
company_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
|
||||
vendor = relationship("Vendor", back_populates="invoice_settings")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VendorInvoiceSettings(vendor_id={self.vendor_id}, company='{self.company_name}')>"
|
||||
|
||||
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)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.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: {
|
||||
# "company_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
|
||||
vendor = relationship("Vendor", back_populates="invoices")
|
||||
order = relationship("Order", back_populates="invoices")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_invoice_vendor_number", "vendor_id", "invoice_number", unique=True),
|
||||
Index("idx_invoice_vendor_date", "vendor_id", "invoice_date"),
|
||||
Index("idx_invoice_status", "vendor_id", "status"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Invoice(id={self.id}, number='{self.invoice_number}', status='{self.status}')>"
|
||||
|
||||
@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
|
||||
__all__ = [
|
||||
"Invoice",
|
||||
"InvoiceStatus",
|
||||
"VATRegime",
|
||||
"VendorInvoiceSettings",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user