feat: add VAT tax rate, cost, and Letzshop feed settings
Product Model: - Add tax_rate_percent (NOT NULL, default 17) for Luxembourg VAT - Add cost_cents for profit calculation - Add profit calculation properties: net_price, vat_amount, profit, margin - Rename supplier_cost_cents to cost_cents MarketplaceProduct Model: - Add tax_rate_percent (NOT NULL, default 17) Vendor Model (Letzshop feed settings): - letzshop_default_tax_rate: Default VAT for new products (0, 3, 8, 14, 17) - letzshop_boost_sort: Product sort priority (0.0-10.0) - letzshop_delivery_method: nationwide, package_delivery, self_collect - letzshop_preorder_days: Pre-order shipping delay VAT Strategy: - Store prices as gross (VAT-inclusive) for B2C - Calculate net from gross when needed for profit - Luxembourg VAT rates: 0%, 3%, 8%, 14%, 17% 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -308,6 +308,97 @@ def test_price_precision():
|
||||
assert back_to_euros == price
|
||||
```
|
||||
|
||||
## VAT Handling
|
||||
|
||||
### Storage Strategy: Gross Prices (VAT-Inclusive)
|
||||
|
||||
The platform stores all prices as **gross** (VAT-inclusive) for B2C simplicity:
|
||||
|
||||
- Customers see the final price immediately (EU legal requirement)
|
||||
- No calculation needed for display
|
||||
- Profit is calculated by extracting net from gross
|
||||
|
||||
### Luxembourg VAT Rates
|
||||
|
||||
| Rate | Percentage | Applies To |
|
||||
|------|------------|------------|
|
||||
| Standard | **17%** | Most products (electronics, furniture, cosmetics) |
|
||||
| Intermediate | **14%** | Wines, printed materials, heating oils |
|
||||
| Reduced | **8%** | Utilities, hairdressing, small repairs |
|
||||
| Super-reduced | **3%** | Food, books, children's clothing, medicine |
|
||||
| Zero | **0%** | Exports, certain financial services |
|
||||
|
||||
### Database Fields
|
||||
|
||||
```python
|
||||
class Product(Base):
|
||||
# Tax rate (0, 3, 8, 14, or 17 for Luxembourg)
|
||||
tax_rate_percent = Column(Integer, default=17, nullable=False)
|
||||
|
||||
# Cost for profit calculation (what vendor pays to acquire)
|
||||
cost_cents = Column(Integer, nullable=True)
|
||||
```
|
||||
|
||||
### Profit Calculation
|
||||
|
||||
```python
|
||||
class Product(Base):
|
||||
@property
|
||||
def net_price_cents(self) -> int:
|
||||
"""Calculate net price (excluding VAT) from gross price.
|
||||
|
||||
Formula: Net = Gross * 100 / (100 + rate)
|
||||
Example: €119 gross at 17% VAT = €119 * 100 / 117 = €101.71 net
|
||||
"""
|
||||
return int(self.effective_price_cents * 100 / (100 + self.tax_rate_percent))
|
||||
|
||||
@property
|
||||
def vat_amount_cents(self) -> int:
|
||||
"""VAT = Gross - Net"""
|
||||
return self.effective_price_cents - self.net_price_cents
|
||||
|
||||
@property
|
||||
def profit_cents(self) -> int:
|
||||
"""Profit = Net Revenue - Cost"""
|
||||
return self.net_price_cents - self.cost_cents
|
||||
|
||||
@property
|
||||
def profit_margin_percent(self) -> float:
|
||||
"""Margin% = (Profit / Net) * 100"""
|
||||
return round((self.profit_cents / self.net_price_cents) * 100, 2)
|
||||
```
|
||||
|
||||
### Example Calculation
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Gross Price | €119.00 (11900 cents) |
|
||||
| Tax Rate | 17% |
|
||||
| Net Price | €101.71 (10171 cents) |
|
||||
| VAT Amount | €17.29 (1729 cents) |
|
||||
| Cost | €60.00 (6000 cents) |
|
||||
| **Profit** | €41.71 (4171 cents) |
|
||||
| **Margin** | 41.0% |
|
||||
|
||||
### Vendor Letzshop Settings
|
||||
|
||||
Vendors have default settings for the Letzshop feed:
|
||||
|
||||
```python
|
||||
class Vendor(Base):
|
||||
# Default VAT rate for new products
|
||||
letzshop_default_tax_rate = Column(Integer, default=17)
|
||||
|
||||
# Product sort priority (0.0-10.0, higher = displayed first)
|
||||
letzshop_boost_sort = Column(String(10), default="5.0")
|
||||
|
||||
# Delivery method: 'nationwide', 'package_delivery', 'self_collect'
|
||||
letzshop_delivery_method = Column(String(100), default="package_delivery")
|
||||
|
||||
# Pre-order days before shipping (default 1 day)
|
||||
letzshop_preorder_days = Column(Integer, default=1)
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Layer | Format | Example |
|
||||
@@ -318,4 +409,7 @@ def test_price_precision():
|
||||
| API request/response | Float euros | `105.91` |
|
||||
| Frontend display | Formatted string | `"105,91 €"` |
|
||||
|
||||
**Golden Rule:** All arithmetic happens with integers. Conversion to/from euros only at system boundaries (API, display).
|
||||
**Golden Rules:**
|
||||
1. All arithmetic happens with integers. Conversion to/from euros only at system boundaries.
|
||||
2. Prices are stored as gross (VAT-inclusive). Net is calculated when needed.
|
||||
3. Tax rate is stored per product, with vendor defaults for new products.
|
||||
|
||||
Reference in New Issue
Block a user