Add Mago4 demo ERP with Python FastAPI
Implements a working in-memory ERP demo inspired by Mago4, covering the core modules: dashboard with KPI widgets, customers, suppliers, products, sales orders, purchase orders, and warehouse inventory. - FastAPI backend with Pydantic models and REST API (auto-docs at /docs) - Jinja2 HTML templates with Mago4-style UI (dark sidebar, blue/orange/ green/red palette, Material Design icons) - In-memory fake database seeded with sample customers, suppliers, products, and orders — ready to swap for SQLAlchemy + SQL Server - Workarounds for Python 3.14 + Starlette 1.3 / Jinja2 compatibility (cache_size=0, new TemplateResponse(request, name, ctx) signature) - launch.json for one-click preview; run.bat for manual startup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+287
@@ -0,0 +1,287 @@
|
||||
from datetime import datetime, timedelta
|
||||
from models import (
|
||||
Customer, Supplier, Product, OrderLine, SalesOrder, PurchaseOrder, OrderStatus
|
||||
)
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class FakeDatabase:
|
||||
def __init__(self):
|
||||
self.customers = self._init_customers()
|
||||
self.suppliers = self._init_suppliers()
|
||||
self.products = self._init_products()
|
||||
self.sales_orders = self._init_sales_orders()
|
||||
self.purchase_orders = self._init_purchase_orders()
|
||||
self.next_customer_id = max([c.id for c in self.customers]) + 1
|
||||
self.next_supplier_id = max([s.id for s in self.suppliers]) + 1
|
||||
self.next_product_id = max([p.id for p in self.products]) + 1
|
||||
self.next_so_id = max([o.id for o in self.sales_orders]) + 1
|
||||
self.next_po_id = max([o.id for o in self.purchase_orders]) + 1
|
||||
|
||||
def _init_customers(self) -> List[Customer]:
|
||||
return [
|
||||
Customer(
|
||||
id=1, code="CUST001", name="Acme Corporation",
|
||||
email="info@acme.com", phone="+39 02 1234567",
|
||||
address="Via Roma 10", city="Milano", country="IT",
|
||||
credit_limit=50000, balance=15000
|
||||
),
|
||||
Customer(
|
||||
id=2, code="CUST002", name="TechFlow Industries",
|
||||
email="contact@techflow.de", phone="+49 30 555888",
|
||||
address="Hauptstrasse 45", city="Berlin", country="DE",
|
||||
credit_limit=75000, balance=32000
|
||||
),
|
||||
Customer(
|
||||
id=3, code="CUST003", name="European Logistics",
|
||||
email="sales@eulog.fr", phone="+33 1 4520 3580",
|
||||
address="Avenue des Champs 20", city="Paris", country="FR",
|
||||
credit_limit=100000, balance=8500
|
||||
),
|
||||
Customer(
|
||||
id=4, code="CUST004", name="Alpine Manufacturing",
|
||||
email="orders@alpmfg.ch", phone="+41 44 5678900",
|
||||
address="Industriestrasse 12", city="Zurich", country="CH",
|
||||
credit_limit=60000, balance=22000
|
||||
),
|
||||
]
|
||||
|
||||
def _init_suppliers(self) -> List[Supplier]:
|
||||
return [
|
||||
Supplier(
|
||||
id=1, code="SUPP001", name="Global Components Ltd",
|
||||
email="sales@globalcomp.com", phone="+44 20 7946 0958",
|
||||
address="123 Oxford Street", city="London", country="UK",
|
||||
payment_terms_days=30, balance=5000
|
||||
),
|
||||
Supplier(
|
||||
id=2, code="SUPP002", name="Asia Electronics Co.",
|
||||
email="export@asiaelec.com", phone="+886 2 8797 3000",
|
||||
address="Taipei Industrial Park", city="Taipei", country="TW",
|
||||
payment_terms_days=45, balance=18000
|
||||
),
|
||||
Supplier(
|
||||
id=3, code="SUPP003", name="Nordic Materials AB",
|
||||
email="info@nordicmat.se", phone="+46 8 123 4567",
|
||||
address="Industrígatan 5", city="Stockholm", country="SE",
|
||||
payment_terms_days=60, balance=3500
|
||||
),
|
||||
]
|
||||
|
||||
def _init_products(self) -> List[Product]:
|
||||
return [
|
||||
Product(
|
||||
id=1, code="PROD001", description="Industrial Widget A",
|
||||
category="Electronics", unit_price=125.50,
|
||||
quantity_in_stock=250, reorder_level=50, supplier_id=1
|
||||
),
|
||||
Product(
|
||||
id=2, code="PROD002", description="Premium Connector Set",
|
||||
category="Hardware", unit_price=45.00,
|
||||
quantity_in_stock=180, reorder_level=30, supplier_id=2
|
||||
),
|
||||
Product(
|
||||
id=3, code="PROD003", description="Steel Bracket Type B",
|
||||
category="Structural", unit_price=22.75,
|
||||
quantity_in_stock=500, reorder_level=100, supplier_id=3
|
||||
),
|
||||
Product(
|
||||
id=4, code="PROD004", description="Precision Bearing Set",
|
||||
category="Mechanical", unit_price=89.99,
|
||||
quantity_in_stock=120, reorder_level=40, supplier_id=1
|
||||
),
|
||||
Product(
|
||||
id=5, code="PROD005", description="Control Module MCU-32",
|
||||
category="Electronics", unit_price=234.50,
|
||||
quantity_in_stock=75, reorder_level=20, supplier_id=2
|
||||
),
|
||||
]
|
||||
|
||||
def _init_sales_orders(self) -> List[SalesOrder]:
|
||||
now = datetime.now()
|
||||
return [
|
||||
SalesOrder(
|
||||
id=1, order_number="SO-2024-0001",
|
||||
order_date=now - timedelta(days=10),
|
||||
customer_id=1, customer_name="Acme Corporation",
|
||||
status=OrderStatus.CONFIRMED,
|
||||
lines=[
|
||||
OrderLine(id=1, product_id=1, product_code="PROD001",
|
||||
product_description="Industrial Widget A",
|
||||
quantity=10, unit_price=125.50, total_amount=1255.00),
|
||||
OrderLine(id=2, product_id=4, product_code="PROD004",
|
||||
product_description="Precision Bearing Set",
|
||||
quantity=5, unit_price=89.99, total_amount=449.95),
|
||||
],
|
||||
total_amount=1704.95,
|
||||
delivery_date=now + timedelta(days=15)
|
||||
),
|
||||
SalesOrder(
|
||||
id=2, order_number="SO-2024-0002",
|
||||
order_date=now - timedelta(days=5),
|
||||
customer_id=2, customer_name="TechFlow Industries",
|
||||
status=OrderStatus.CONFIRMED,
|
||||
lines=[
|
||||
OrderLine(id=3, product_id=5, product_code="PROD005",
|
||||
product_description="Control Module MCU-32",
|
||||
quantity=8, unit_price=234.50, total_amount=1876.00),
|
||||
],
|
||||
total_amount=1876.00,
|
||||
delivery_date=now + timedelta(days=10)
|
||||
),
|
||||
SalesOrder(
|
||||
id=3, order_number="SO-2024-0003",
|
||||
order_date=now - timedelta(days=2),
|
||||
customer_id=3, customer_name="European Logistics",
|
||||
status=OrderStatus.DRAFT,
|
||||
lines=[
|
||||
OrderLine(id=4, product_id=2, product_code="PROD002",
|
||||
product_description="Premium Connector Set",
|
||||
quantity=20, unit_price=45.00, total_amount=900.00),
|
||||
OrderLine(id=5, product_id=3, product_code="PROD003",
|
||||
product_description="Steel Bracket Type B",
|
||||
quantity=15, unit_price=22.75, total_amount=341.25),
|
||||
],
|
||||
total_amount=1241.25,
|
||||
delivery_date=now + timedelta(days=20)
|
||||
),
|
||||
]
|
||||
|
||||
def _init_purchase_orders(self) -> List[PurchaseOrder]:
|
||||
now = datetime.now()
|
||||
return [
|
||||
PurchaseOrder(
|
||||
id=1, order_number="PO-2024-0001",
|
||||
order_date=now - timedelta(days=15),
|
||||
supplier_id=1, supplier_name="Global Components Ltd",
|
||||
status=OrderStatus.PARTIALLY_RECEIVED,
|
||||
lines=[
|
||||
OrderLine(id=1, product_id=1, product_code="PROD001",
|
||||
product_description="Industrial Widget A",
|
||||
quantity=100, unit_price=110.00, total_amount=11000.00),
|
||||
],
|
||||
total_amount=11000.00,
|
||||
expected_delivery_date=now + timedelta(days=5)
|
||||
),
|
||||
PurchaseOrder(
|
||||
id=2, order_number="PO-2024-0002",
|
||||
order_date=now - timedelta(days=8),
|
||||
supplier_id=2, supplier_name="Asia Electronics Co.",
|
||||
status=OrderStatus.CONFIRMED,
|
||||
lines=[
|
||||
OrderLine(id=2, product_id=5, product_code="PROD005",
|
||||
product_description="Control Module MCU-32",
|
||||
quantity=50, unit_price=195.00, total_amount=9750.00),
|
||||
],
|
||||
total_amount=9750.00,
|
||||
expected_delivery_date=now + timedelta(days=25)
|
||||
),
|
||||
PurchaseOrder(
|
||||
id=3, order_number="PO-2024-0003",
|
||||
order_date=now - timedelta(days=1),
|
||||
supplier_id=3, supplier_name="Nordic Materials AB",
|
||||
status=OrderStatus.DRAFT,
|
||||
lines=[
|
||||
OrderLine(id=3, product_id=3, product_code="PROD003",
|
||||
product_description="Steel Bracket Type B",
|
||||
quantity=200, unit_price=20.00, total_amount=4000.00),
|
||||
],
|
||||
total_amount=4000.00,
|
||||
expected_delivery_date=now + timedelta(days=30)
|
||||
),
|
||||
]
|
||||
|
||||
# CRUD Operations
|
||||
def get_customers(self) -> List[Customer]:
|
||||
return self.customers
|
||||
|
||||
def get_customer(self, customer_id: int) -> Optional[Customer]:
|
||||
return next((c for c in self.customers if c.id == customer_id), None)
|
||||
|
||||
def create_customer(self, customer: Customer) -> Customer:
|
||||
customer.id = self.next_customer_id
|
||||
self.next_customer_id += 1
|
||||
self.customers.append(customer)
|
||||
return customer
|
||||
|
||||
def update_customer(self, customer_id: int, customer_data: dict) -> Optional[Customer]:
|
||||
customer = self.get_customer(customer_id)
|
||||
if customer:
|
||||
for key, value in customer_data.items():
|
||||
if hasattr(customer, key) and value is not None:
|
||||
setattr(customer, key, value)
|
||||
return customer
|
||||
|
||||
def get_suppliers(self) -> List[Supplier]:
|
||||
return self.suppliers
|
||||
|
||||
def get_supplier(self, supplier_id: int) -> Optional[Supplier]:
|
||||
return next((s for s in self.suppliers if s.id == supplier_id), None)
|
||||
|
||||
def create_supplier(self, supplier: Supplier) -> Supplier:
|
||||
supplier.id = self.next_supplier_id
|
||||
self.next_supplier_id += 1
|
||||
self.suppliers.append(supplier)
|
||||
return supplier
|
||||
|
||||
def get_products(self) -> List[Product]:
|
||||
return self.products
|
||||
|
||||
def get_product(self, product_id: int) -> Optional[Product]:
|
||||
return next((p for p in self.products if p.id == product_id), None)
|
||||
|
||||
def create_product(self, product: Product) -> Product:
|
||||
product.id = self.next_product_id
|
||||
self.next_product_id += 1
|
||||
self.products.append(product)
|
||||
return product
|
||||
|
||||
def get_sales_orders(self) -> List[SalesOrder]:
|
||||
return self.sales_orders
|
||||
|
||||
def get_sales_order(self, order_id: int) -> Optional[SalesOrder]:
|
||||
return next((o for o in self.sales_orders if o.id == order_id), None)
|
||||
|
||||
def create_sales_order(self, order: SalesOrder) -> SalesOrder:
|
||||
order.id = self.next_so_id
|
||||
self.next_so_id += 1
|
||||
self.sales_orders.append(order)
|
||||
return order
|
||||
|
||||
def get_purchase_orders(self) -> List[PurchaseOrder]:
|
||||
return self.purchase_orders
|
||||
|
||||
def get_purchase_order(self, order_id: int) -> Optional[PurchaseOrder]:
|
||||
return next((o for o in self.purchase_orders if o.id == order_id), None)
|
||||
|
||||
def create_purchase_order(self, order: PurchaseOrder) -> PurchaseOrder:
|
||||
order.id = self.next_po_id
|
||||
self.next_po_id += 1
|
||||
self.purchase_orders.append(order)
|
||||
return order
|
||||
|
||||
# Dashboard statistics
|
||||
def get_dashboard_stats(self):
|
||||
sales_to_fulfill = sum(1 for o in self.sales_orders if o.status in [OrderStatus.CONFIRMED, OrderStatus.DRAFT])
|
||||
purchase_to_receive = sum(1 for o in self.purchase_orders if o.status in [OrderStatus.CONFIRMED, OrderStatus.DRAFT, OrderStatus.PARTIALLY_RECEIVED])
|
||||
|
||||
total_receivable = sum(c.balance for c in self.customers)
|
||||
total_payable = sum(s.balance for s in self.suppliers)
|
||||
|
||||
low_stock_items = [p for p in self.products if p.quantity_in_stock <= p.reorder_level]
|
||||
|
||||
return {
|
||||
"sales_to_fulfill": sales_to_fulfill,
|
||||
"purchase_to_receive": purchase_to_receive,
|
||||
"total_receivable": total_receivable,
|
||||
"total_payable": total_payable,
|
||||
"low_stock_count": len(low_stock_items),
|
||||
"low_stock_items": low_stock_items,
|
||||
"total_customers": len(self.customers),
|
||||
"total_suppliers": len(self.suppliers),
|
||||
"total_products": len(self.products),
|
||||
}
|
||||
|
||||
|
||||
# Global database instance
|
||||
db = FakeDatabase()
|
||||
Reference in New Issue
Block a user