feat: integer cents money handling, order page fixes, and vendor filter persistence
Money Handling Architecture: - Store all monetary values as integer cents (€105.91 = 10591) - Add app/utils/money.py with Money class and conversion helpers - Add static/shared/js/money.js for frontend formatting - Update all database models to use _cents columns (Product, Order, etc.) - Update CSV processor to convert prices to cents on import - Add Alembic migration for Float to Integer conversion - Create .architecture-rules/money.yaml with 7 validation rules - Add docs/architecture/money-handling.md documentation Order Details Page Fixes: - Fix customer name showing 'undefined undefined' - use flat field names - Fix vendor info empty - add vendor_name/vendor_code to OrderDetailResponse - Fix shipping address using wrong nested object structure - Enrich order detail API response with vendor info Vendor Filter Persistence Fixes: - Fix orders.js: restoreSavedVendor now sets selectedVendor and filters - Fix orders.js: init() only loads orders if no saved vendor to restore - Fix marketplace-letzshop.js: restoreSavedVendor calls selectVendor() - Fix marketplace-letzshop.js: clearVendorSelection clears TomSelect dropdown - Align vendor selector placeholder text between pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
273
scripts/create_dummy_letzshop_order.py
Executable file
273
scripts/create_dummy_letzshop_order.py
Executable file
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create a dummy Letzshop order for testing purposes.
|
||||
|
||||
This script creates a realistic Letzshop order in the database without
|
||||
calling the actual Letzshop API. Useful for testing the order UI and workflow.
|
||||
|
||||
Usage:
|
||||
python scripts/create_dummy_letzshop_order.py --vendor-id 1
|
||||
python scripts/create_dummy_letzshop_order.py --vendor-id 1 --status confirmed
|
||||
python scripts/create_dummy_letzshop_order.py --vendor-id 1 --with-tracking
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.utils.money import cents_to_euros, euros_to_cents
|
||||
from models.database import Order, OrderItem, Product, Vendor
|
||||
|
||||
|
||||
def generate_order_number():
|
||||
"""Generate a realistic Letzshop order number like R532332163."""
|
||||
return f"R{random.randint(100000000, 999999999)}"
|
||||
|
||||
|
||||
def generate_shipment_number():
|
||||
"""Generate a realistic shipment number like H74683403433."""
|
||||
return f"H{random.randint(10000000000, 99999999999)}"
|
||||
|
||||
|
||||
def generate_hash_id():
|
||||
"""Generate a realistic hash ID like nvDv5RQEmCwbjo."""
|
||||
chars = string.ascii_letters + string.digits
|
||||
return ''.join(random.choice(chars) for _ in range(14))
|
||||
|
||||
|
||||
def create_dummy_order(
|
||||
db,
|
||||
vendor_id: int,
|
||||
status: str = "pending",
|
||||
with_tracking: bool = False,
|
||||
carrier: str = "greco",
|
||||
items_count: int = 2,
|
||||
):
|
||||
"""Create a dummy Letzshop order with realistic data."""
|
||||
|
||||
# Verify vendor exists
|
||||
vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first()
|
||||
if not vendor:
|
||||
print(f"Error: Vendor with ID {vendor_id} not found")
|
||||
return None
|
||||
|
||||
# Get some products from the vendor (or create placeholder if none exist)
|
||||
products = db.query(Product).filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_active == True
|
||||
).limit(items_count).all()
|
||||
|
||||
if not products:
|
||||
print(f"Warning: No active products found for vendor {vendor_id}, creating placeholder")
|
||||
# Create placeholder products with prices in cents
|
||||
products = [
|
||||
Product(
|
||||
vendor_id=vendor_id,
|
||||
vendor_sku="TEST-001",
|
||||
gtin="4006381333931",
|
||||
gtin_type="ean13",
|
||||
price_cents=2999, # €29.99
|
||||
is_active=True,
|
||||
is_featured=False,
|
||||
),
|
||||
Product(
|
||||
vendor_id=vendor_id,
|
||||
vendor_sku="TEST-002",
|
||||
gtin="5901234123457",
|
||||
gtin_type="ean13",
|
||||
price_cents=4999, # €49.99
|
||||
is_active=True,
|
||||
is_featured=False,
|
||||
),
|
||||
]
|
||||
for p in products:
|
||||
db.add(p)
|
||||
db.flush()
|
||||
|
||||
# Generate order data
|
||||
order_number = generate_order_number()
|
||||
shipment_number = generate_shipment_number()
|
||||
hash_id = generate_hash_id()
|
||||
order_date = datetime.now(timezone.utc) - timedelta(days=random.randint(0, 7))
|
||||
|
||||
# Customer data
|
||||
first_names = ["Jean", "Marie", "Pierre", "Sophie", "Michel", "Anne", "Thomas", "Claire"]
|
||||
last_names = ["Dupont", "Martin", "Bernard", "Dubois", "Thomas", "Robert", "Richard", "Petit"]
|
||||
cities = ["Luxembourg", "Esch-sur-Alzette", "Differdange", "Dudelange", "Ettelbruck"]
|
||||
|
||||
customer_first = random.choice(first_names)
|
||||
customer_last = random.choice(last_names)
|
||||
customer_email = f"{customer_first.lower()}.{customer_last.lower()}@example.lu"
|
||||
|
||||
# Calculate totals in cents
|
||||
subtotal_cents = sum((p.effective_price_cents or 0) * random.randint(1, 3) for p in products[:items_count])
|
||||
shipping_cents = 595 # €5.95
|
||||
total_cents = subtotal_cents + shipping_cents
|
||||
|
||||
# Create the order
|
||||
order = Order(
|
||||
vendor_id=vendor_id,
|
||||
customer_id=1, # Placeholder customer ID
|
||||
order_number=f"LS-{vendor_id}-{order_number}",
|
||||
channel="letzshop",
|
||||
external_order_id=f"gid://letzshop/Order/{random.randint(10000, 99999)}",
|
||||
external_order_number=order_number,
|
||||
external_shipment_id=hash_id,
|
||||
shipment_number=shipment_number,
|
||||
shipping_carrier=carrier,
|
||||
status=status,
|
||||
subtotal_cents=subtotal_cents,
|
||||
tax_amount_cents=0,
|
||||
shipping_amount_cents=shipping_cents,
|
||||
discount_amount_cents=0,
|
||||
total_amount_cents=total_cents,
|
||||
currency="EUR",
|
||||
# Customer snapshot
|
||||
customer_first_name=customer_first,
|
||||
customer_last_name=customer_last,
|
||||
customer_email=customer_email,
|
||||
customer_phone=f"+352 {random.randint(600000, 699999)}",
|
||||
customer_locale="fr",
|
||||
# Shipping address
|
||||
ship_first_name=customer_first,
|
||||
ship_last_name=customer_last,
|
||||
ship_company=None,
|
||||
ship_address_line_1=f"{random.randint(1, 200)} Rue du Test",
|
||||
ship_address_line_2=None,
|
||||
ship_city=random.choice(cities),
|
||||
ship_postal_code=f"L-{random.randint(1000, 9999)}",
|
||||
ship_country_iso="LU",
|
||||
# Billing address (same as shipping)
|
||||
bill_first_name=customer_first,
|
||||
bill_last_name=customer_last,
|
||||
bill_company=None,
|
||||
bill_address_line_1=f"{random.randint(1, 200)} Rue du Test",
|
||||
bill_address_line_2=None,
|
||||
bill_city=random.choice(cities),
|
||||
bill_postal_code=f"L-{random.randint(1000, 9999)}",
|
||||
bill_country_iso="LU",
|
||||
# Timestamps
|
||||
order_date=order_date,
|
||||
)
|
||||
|
||||
# Set status-specific timestamps
|
||||
if status in ["processing", "shipped", "delivered"]:
|
||||
order.confirmed_at = order_date + timedelta(hours=random.randint(1, 24))
|
||||
if status in ["shipped", "delivered"]:
|
||||
order.shipped_at = order.confirmed_at + timedelta(days=random.randint(1, 3))
|
||||
if status == "delivered":
|
||||
order.delivered_at = order.shipped_at + timedelta(days=random.randint(1, 5))
|
||||
if status == "cancelled":
|
||||
order.cancelled_at = order_date + timedelta(hours=random.randint(1, 48))
|
||||
|
||||
# Add tracking if requested
|
||||
if with_tracking or status == "shipped":
|
||||
order.tracking_number = f"LU{random.randint(100000000, 999999999)}"
|
||||
order.tracking_provider = carrier
|
||||
if carrier == "greco":
|
||||
order.tracking_url = f"https://dispatchweb.fr/Tracky/Home/{shipment_number}"
|
||||
|
||||
db.add(order)
|
||||
db.flush()
|
||||
|
||||
# Create order items with prices in cents
|
||||
for i, product in enumerate(products[:items_count]):
|
||||
quantity = random.randint(1, 3)
|
||||
unit_price_cents = product.effective_price_cents or 0
|
||||
product_name = product.get_effective_title("en") or f"Product {product.id}"
|
||||
item = OrderItem(
|
||||
order_id=order.id,
|
||||
product_id=product.id,
|
||||
product_name=product_name,
|
||||
product_sku=product.vendor_sku,
|
||||
gtin=product.gtin,
|
||||
gtin_type=product.gtin_type,
|
||||
quantity=quantity,
|
||||
unit_price_cents=unit_price_cents,
|
||||
total_price_cents=unit_price_cents * quantity,
|
||||
external_item_id=f"gid://letzshop/InventoryUnit/{random.randint(10000, 99999)}",
|
||||
item_state="confirmed_available" if status != "pending" else None,
|
||||
inventory_reserved=status != "pending",
|
||||
inventory_fulfilled=status in ["shipped", "delivered"],
|
||||
needs_product_match=False,
|
||||
)
|
||||
db.add(item)
|
||||
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
|
||||
return order
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Create a dummy Letzshop order for testing")
|
||||
parser.add_argument("--vendor-id", type=int, required=True, help="Vendor ID to create order for")
|
||||
parser.add_argument(
|
||||
"--status",
|
||||
choices=["pending", "processing", "shipped", "delivered", "cancelled"],
|
||||
default="pending",
|
||||
help="Order status (default: pending)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--carrier",
|
||||
choices=["greco", "colissimo", "xpresslogistics"],
|
||||
default="greco",
|
||||
help="Shipping carrier (default: greco)"
|
||||
)
|
||||
parser.add_argument("--with-tracking", action="store_true", help="Add tracking information")
|
||||
parser.add_argument("--items", type=int, default=2, help="Number of items in order (default: 2)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
print(f"Creating dummy Letzshop order for vendor {args.vendor_id}...")
|
||||
print(f" Status: {args.status}")
|
||||
print(f" Carrier: {args.carrier}")
|
||||
print(f" Items: {args.items}")
|
||||
print(f" With tracking: {args.with_tracking}")
|
||||
print()
|
||||
|
||||
order = create_dummy_order(
|
||||
db,
|
||||
vendor_id=args.vendor_id,
|
||||
status=args.status,
|
||||
with_tracking=args.with_tracking,
|
||||
carrier=args.carrier,
|
||||
items_count=args.items,
|
||||
)
|
||||
|
||||
if order:
|
||||
print("Order created successfully!")
|
||||
print()
|
||||
print("Order Details:")
|
||||
print(f" ID: {order.id}")
|
||||
print(f" Internal Number: {order.order_number}")
|
||||
print(f" Letzshop Order Number: {order.external_order_number}")
|
||||
print(f" Shipment Number: {order.shipment_number}")
|
||||
print(f" Hash ID: {order.external_shipment_id}")
|
||||
print(f" Carrier: {order.shipping_carrier}")
|
||||
print(f" Status: {order.status}")
|
||||
print(f" Total: {order.total_amount} {order.currency}")
|
||||
print(f" Customer: {order.customer_first_name} {order.customer_last_name}")
|
||||
print(f" Email: {order.customer_email}")
|
||||
print(f" Items: {len(order.items)}")
|
||||
if order.tracking_number:
|
||||
print(f" Tracking: {order.tracking_number}")
|
||||
if order.tracking_url:
|
||||
print(f" Tracking URL: {order.tracking_url}")
|
||||
print()
|
||||
print(f"View order at: http://localhost:8000/admin/letzshop/orders/{order.id}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user