Phase 1 OMS implementation: Invoicing: - Add Invoice and VendorInvoiceSettings database models - Full EU VAT support (27 countries, OSS, B2B reverse charge) - Invoice PDF generation with WeasyPrint + Jinja2 templates - Vendor invoice API endpoints for settings, creation, PDF download Subscription Tiers: - Add VendorSubscription model with 4 tiers (Essential/Professional/Business/Enterprise) - Tier limit enforcement for orders, products, team members - Feature gating based on subscription tier - Automatic trial subscription creation for new vendors - Integrate limit checks into order creation (direct and Letzshop sync) Marketing: - Update pricing documentation with 4-tier structure - Revise back-office positioning strategy - Update homepage with Veeqo-inspired Letzshop-focused messaging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
284 lines
7.3 KiB
Python
284 lines
7.3 KiB
Python
# models/schema/invoice.py
|
|
"""
|
|
Pydantic schemas for invoice operations.
|
|
|
|
Supports invoice settings management and invoice generation.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
# ============================================================================
|
|
# Invoice Settings Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class VendorInvoiceSettingsCreate(BaseModel):
|
|
"""Schema for creating vendor invoice settings."""
|
|
|
|
company_name: str = Field(..., min_length=1, max_length=255)
|
|
company_address: str | None = Field(None, max_length=255)
|
|
company_city: str | None = Field(None, max_length=100)
|
|
company_postal_code: str | None = Field(None, max_length=20)
|
|
company_country: str = Field(default="LU", min_length=2, max_length=2)
|
|
|
|
vat_number: str | None = Field(None, max_length=50)
|
|
is_vat_registered: bool = True
|
|
|
|
is_oss_registered: bool = False
|
|
oss_registration_country: str | None = Field(None, min_length=2, max_length=2)
|
|
|
|
invoice_prefix: str = Field(default="INV", max_length=20)
|
|
invoice_number_padding: int = Field(default=5, ge=1, le=10)
|
|
|
|
payment_terms: str | None = None
|
|
bank_name: str | None = Field(None, max_length=255)
|
|
bank_iban: str | None = Field(None, max_length=50)
|
|
bank_bic: str | None = Field(None, max_length=20)
|
|
|
|
footer_text: str | None = None
|
|
default_vat_rate: Decimal = Field(default=Decimal("17.00"), ge=0, le=100)
|
|
|
|
|
|
class VendorInvoiceSettingsUpdate(BaseModel):
|
|
"""Schema for updating vendor invoice settings."""
|
|
|
|
company_name: str | None = Field(None, min_length=1, max_length=255)
|
|
company_address: str | None = Field(None, max_length=255)
|
|
company_city: str | None = Field(None, max_length=100)
|
|
company_postal_code: str | None = Field(None, max_length=20)
|
|
company_country: str | None = Field(None, min_length=2, max_length=2)
|
|
|
|
vat_number: str | None = None
|
|
is_vat_registered: bool | None = None
|
|
|
|
is_oss_registered: bool | None = None
|
|
oss_registration_country: str | None = None
|
|
|
|
invoice_prefix: str | None = Field(None, max_length=20)
|
|
invoice_number_padding: int | None = Field(None, ge=1, le=10)
|
|
|
|
payment_terms: str | None = None
|
|
bank_name: str | None = Field(None, max_length=255)
|
|
bank_iban: str | None = Field(None, max_length=50)
|
|
bank_bic: str | None = Field(None, max_length=20)
|
|
|
|
footer_text: str | None = None
|
|
default_vat_rate: Decimal | None = Field(None, ge=0, le=100)
|
|
|
|
|
|
class VendorInvoiceSettingsResponse(BaseModel):
|
|
"""Schema for vendor invoice settings response."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
vendor_id: int
|
|
|
|
company_name: str
|
|
company_address: str | None
|
|
company_city: str | None
|
|
company_postal_code: str | None
|
|
company_country: str
|
|
|
|
vat_number: str | None
|
|
is_vat_registered: bool
|
|
|
|
is_oss_registered: bool
|
|
oss_registration_country: str | None
|
|
|
|
invoice_prefix: str
|
|
invoice_next_number: int
|
|
invoice_number_padding: int
|
|
|
|
payment_terms: str | None
|
|
bank_name: str | None
|
|
bank_iban: str | None
|
|
bank_bic: str | None
|
|
|
|
footer_text: str | None
|
|
default_vat_rate: Decimal
|
|
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
# ============================================================================
|
|
# Invoice Line Item Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class InvoiceLineItem(BaseModel):
|
|
"""Schema for invoice line item."""
|
|
|
|
description: str
|
|
quantity: int = Field(..., ge=1)
|
|
unit_price_cents: int
|
|
total_cents: int
|
|
sku: str | None = None
|
|
ean: str | None = None
|
|
|
|
|
|
class InvoiceLineItemResponse(BaseModel):
|
|
"""Schema for invoice line item in response."""
|
|
|
|
description: str
|
|
quantity: int
|
|
unit_price_cents: int
|
|
total_cents: int
|
|
sku: str | None = None
|
|
ean: str | None = None
|
|
|
|
@property
|
|
def unit_price(self) -> float:
|
|
return self.unit_price_cents / 100
|
|
|
|
@property
|
|
def total(self) -> float:
|
|
return self.total_cents / 100
|
|
|
|
|
|
# ============================================================================
|
|
# Invoice Address Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class InvoiceSellerDetails(BaseModel):
|
|
"""Seller details for invoice."""
|
|
|
|
company_name: str
|
|
address: str | None = None
|
|
city: str | None = None
|
|
postal_code: str | None = None
|
|
country: str
|
|
vat_number: str | None = None
|
|
|
|
|
|
class InvoiceBuyerDetails(BaseModel):
|
|
"""Buyer details for invoice."""
|
|
|
|
name: str
|
|
email: str | None = None
|
|
address: str | None = None
|
|
city: str | None = None
|
|
postal_code: str | None = None
|
|
country: str
|
|
vat_number: str | None = None # For B2B
|
|
|
|
|
|
# ============================================================================
|
|
# Invoice Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class InvoiceCreate(BaseModel):
|
|
"""Schema for creating an invoice from an order."""
|
|
|
|
order_id: int
|
|
notes: str | None = None
|
|
|
|
|
|
class InvoiceManualCreate(BaseModel):
|
|
"""Schema for creating a manual invoice (without order)."""
|
|
|
|
buyer_details: InvoiceBuyerDetails
|
|
line_items: list[InvoiceLineItem]
|
|
notes: str | None = None
|
|
payment_terms: str | None = None
|
|
|
|
|
|
class InvoiceResponse(BaseModel):
|
|
"""Schema for invoice response."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
vendor_id: int
|
|
order_id: int | None
|
|
|
|
invoice_number: str
|
|
invoice_date: datetime
|
|
status: str
|
|
|
|
seller_details: dict
|
|
buyer_details: dict
|
|
line_items: list[dict]
|
|
|
|
vat_regime: str
|
|
destination_country: str | None
|
|
vat_rate: Decimal
|
|
vat_rate_label: str | None
|
|
|
|
currency: str
|
|
subtotal_cents: int
|
|
vat_amount_cents: int
|
|
total_cents: int
|
|
|
|
payment_terms: str | None
|
|
bank_details: dict | None
|
|
footer_text: str | None
|
|
|
|
pdf_generated_at: datetime | None
|
|
pdf_path: str | None
|
|
|
|
notes: str | None
|
|
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
@property
|
|
def subtotal(self) -> float:
|
|
return self.subtotal_cents / 100
|
|
|
|
@property
|
|
def vat_amount(self) -> float:
|
|
return self.vat_amount_cents / 100
|
|
|
|
@property
|
|
def total(self) -> float:
|
|
return self.total_cents / 100
|
|
|
|
|
|
class InvoiceListResponse(BaseModel):
|
|
"""Schema for invoice list response (summary)."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
invoice_number: str
|
|
invoice_date: datetime
|
|
status: str
|
|
currency: str
|
|
total_cents: int
|
|
order_id: int | None
|
|
|
|
# Buyer name for display
|
|
buyer_name: str | None = None
|
|
|
|
@property
|
|
def total(self) -> float:
|
|
return self.total_cents / 100
|
|
|
|
|
|
class InvoiceStatusUpdate(BaseModel):
|
|
"""Schema for updating invoice status."""
|
|
|
|
status: str = Field(..., pattern="^(draft|issued|paid|cancelled)$")
|
|
|
|
|
|
# ============================================================================
|
|
# Paginated Response
|
|
# ============================================================================
|
|
|
|
|
|
class InvoiceListPaginatedResponse(BaseModel):
|
|
"""Paginated invoice list response."""
|
|
|
|
items: list[InvoiceListResponse]
|
|
total: int
|
|
page: int
|
|
per_page: int
|
|
pages: int
|