from fastapi import FastAPI, HTTPException from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse from starlette.requests import Request from jinja2 import Environment, FileSystemLoader from datetime import datetime import os from models import ( Customer, Supplier, Product, SalesOrder, PurchaseOrder, OrderStatus, Invoice, InventoryMovement, DocumentType, DocumentStatus, PaymentSchedule, PaymentScheduleType, DOCUMENT_TYPE_LABELS, DOCUMENT_STATUS_LABELS, PAYMENT_SCHEDULE_TYPE_LABELS, MovementSign, ) from database import db app = FastAPI(title="Mago4 Demo", description="Demo gestionale ERP — Python FastAPI", version="2.0.0") BASE_DIR = os.path.dirname(os.path.abspath(__file__)) app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static") # Workaround Python 3.14 + Jinja2: disable template cache (globals dict not hashable as cache key) _jinja_env = Environment( loader=FileSystemLoader(os.path.join(BASE_DIR, "templates")), autoescape=True, cache_size=0, ) # Helper esposti ai template _jinja_env.globals["doc_type_label"] = lambda t: DOCUMENT_TYPE_LABELS.get(t, str(t)) _jinja_env.globals["doc_status_label"] = lambda s: DOCUMENT_STATUS_LABELS.get(s, str(s)) _jinja_env.globals["sched_type_label"] = lambda t: PAYMENT_SCHEDULE_TYPE_LABELS.get(t, str(t)) _jinja_env.globals["sched_status"] = db.schedule_status _jinja_env.globals["sched_days"] = db.schedule_days_to_due templates = Jinja2Templates(env=_jinja_env) def ctx(request: Request, active_module: str, **extra): base = { "request": request, "active_module": active_module, "operation_date": datetime.now().strftime("%d/%m/%Y"), } base.update(extra) return base def render(request: Request, name: str, active_module: str, **extra): return templates.TemplateResponse(request, name, ctx(request, active_module, **extra)) # ============================================================ # DASHBOARD # ============================================================ @app.get("/", response_class=HTMLResponse) async def dashboard(request: Request): stats = db.get_dashboard_stats() upcoming = db.get_upcoming_schedules(30) todos = db.get_todo_items() cashflow = db.get_cashflow_forecast(30) credit_exceeded = db.get_credit_exceeded_customers() last_invoices = sorted(db.get_invoices(), key=lambda x: x.doc_date, reverse=True)[:5] return render(request, "dashboard.html", "dashboard", stats=stats, upcoming=upcoming, todos=todos, cashflow=cashflow, credit_exceeded=credit_exceeded, last_invoices=last_invoices, RECEIVABLE=PaymentScheduleType.RECEIVABLE, PAYABLE=PaymentScheduleType.PAYABLE) # ============================================================ # ANAGRAFICHE # ============================================================ @app.get("/anagrafiche", response_class=HTMLResponse) async def anagrafiche(request: Request): return render(request, "anagrafiche.html", "anagrafiche") @app.get("/customers", response_class=HTMLResponse) async def customers_page(request: Request): return render(request, "customers.html", "anagrafiche", customers=db.get_customers()) @app.get("/suppliers", response_class=HTMLResponse) async def suppliers_page(request: Request): return render(request, "suppliers.html", "anagrafiche", suppliers=db.get_suppliers()) @app.get("/products", response_class=HTMLResponse) async def products_page(request: Request): products = db.get_products() return render(request, "products.html", "anagrafiche", products=products, low_stock=[p for p in products if p.quantity_in_stock <= p.reorder_level]) # ============================================================ # VENDITE # ============================================================ @app.get("/vendite", response_class=HTMLResponse) async def vendite(request: Request): return render(request, "vendite.html", "vendite") @app.get("/sales-orders", response_class=HTMLResponse) async def sales_orders_page(request: Request): orders = db.get_sales_orders() return render(request, "sales_orders.html", "vendite", orders=orders, pending=[o for o in orders if o.status != OrderStatus.RECEIVED]) @app.get("/invoices", response_class=HTMLResponse) async def invoices_page(request: Request, type: str = "all"): invoices = db.get_invoices() selected = "all" if type != "all": try: dt = DocumentType(type) invoices = [i for i in invoices if i.doc_type == dt] selected = type except ValueError: pass invoices = sorted(invoices, key=lambda x: x.doc_date, reverse=True) return render(request, "invoices.html", "vendite", invoices=invoices, selected_type=selected, doc_types=list(DocumentType)) @app.get("/invoices/{invoice_id}", response_class=HTMLResponse) async def invoice_detail_page(request: Request, invoice_id: int): invoice = db.get_invoice(invoice_id) if not invoice: raise HTTPException(status_code=404, detail="Documento non trovato") return render(request, "invoice_detail.html", "vendite", invoice=invoice) # ============================================================ # ACQUISTI # ============================================================ @app.get("/acquisti", response_class=HTMLResponse) async def acquisti(request: Request): return render(request, "acquisti.html", "acquisti") @app.get("/purchase-orders", response_class=HTMLResponse) async def purchase_orders_page(request: Request): orders = db.get_purchase_orders() return render(request, "purchase_orders.html", "acquisti", orders=orders, pending=[o for o in orders if o.status != OrderStatus.RECEIVED]) # ============================================================ # AMMINISTRAZIONE / SCADENZARIO (AP_AR) # ============================================================ @app.get("/amministrazione", response_class=HTMLResponse) async def amministrazione(request: Request): return render(request, "amministrazione.html", "amministrazione") @app.get("/scadenzario", response_class=HTMLResponse) async def scadenzario_page(request: Request, type: str = "all"): schedules = db.get_payment_schedules() selected = "all" if type in ("receivable", "payable"): st = PaymentScheduleType(type) schedules = [s for s in schedules if s.type == st] selected = type # ordina: prima le aperte per data scadenza, poi le chiuse schedules = sorted(schedules, key=lambda s: (s.closed, s.due_date)) return render(request, "scadenzario.html", "amministrazione", schedules=schedules, selected_type=selected, RECEIVABLE=PaymentScheduleType.RECEIVABLE, PAYABLE=PaymentScheduleType.PAYABLE) @app.post("/scadenzario/{schedule_id}/settle") async def settle_schedule_action(schedule_id: int): s = db.settle_schedule(schedule_id) if not s: raise HTTPException(status_code=404, detail="Partita non trovata o già saldata") return {"status": "ok", "schedule_id": schedule_id, "closed": s.closed} # ============================================================ # MAGAZZINO / LOGISTICA # ============================================================ @app.get("/magazzino", response_class=HTMLResponse) async def magazzino(request: Request): return render(request, "magazzino.html", "magazzino") @app.get("/warehouse", response_class=HTMLResponse) async def warehouse_page(request: Request): stats = db.get_dashboard_stats() return render(request, "warehouse.html", "magazzino", products=db.get_products(), low_stock=stats["low_stock_items"], warehouse_value=stats["warehouse_value"]) @app.get("/inventory-movements", response_class=HTMLResponse) async def inventory_movements_page(request: Request): movements = sorted(db.get_inventory_movements(), key=lambda m: m.posting_date, reverse=True) return render(request, "inventory_movements.html", "magazzino", movements=movements) @app.get("/inventory-movements/{movement_id}", response_class=HTMLResponse) async def movement_detail_page(request: Request, movement_id: int): movement = db.get_inventory_movement(movement_id) if not movement: raise HTTPException(status_code=404, detail="Movimento non trovato") return render(request, "movement_detail.html", "magazzino", movement=movement) @app.post("/inventory-movements/{movement_id}/post") async def post_movement_action(movement_id: int): m = db.post_movement(movement_id) if not m: raise HTTPException(status_code=404, detail="Movimento non trovato o già confermato") return {"status": "ok", "movement_id": movement_id, "posted": m.posted} @app.get("/inventory-reasons", response_class=HTMLResponse) async def inventory_reasons_page(request: Request): return render(request, "inventory_reasons.html", "magazzino", reasons=db.get_inventory_reasons()) # ============================================================ # API REST # ============================================================ @app.get("/api/customers", tags=["Customers"]) async def api_customers(): return db.get_customers() @app.get("/api/customers/{cid}", tags=["Customers"]) async def api_customer(cid: int): c = db.get_customer(cid) if not c: raise HTTPException(404, "Cliente non trovato") return c @app.post("/api/customers", tags=["Customers"]) async def api_create_customer(customer: Customer): return db.create_customer(customer) @app.get("/api/suppliers", tags=["Suppliers"]) async def api_suppliers(): return db.get_suppliers() @app.get("/api/suppliers/{sid}", tags=["Suppliers"]) async def api_supplier(sid: int): s = db.get_supplier(sid) if not s: raise HTTPException(404, "Fornitore non trovato") return s @app.get("/api/products", tags=["Products"]) async def api_products(): return db.get_products() @app.get("/api/products/{pid}", tags=["Products"]) async def api_product(pid: int): p = db.get_product(pid) if not p: raise HTTPException(404, "Articolo non trovato") return p @app.get("/api/sales-orders", tags=["Sales Orders"]) async def api_sales_orders(): return db.get_sales_orders() @app.get("/api/sales-orders/{oid}", tags=["Sales Orders"]) async def api_sales_order(oid: int): o = db.get_sales_order(oid) if not o: raise HTTPException(404, "Ordine non trovato") return o @app.get("/api/purchase-orders", tags=["Purchase Orders"]) async def api_purchase_orders(): return db.get_purchase_orders() @app.get("/api/purchase-orders/{oid}", tags=["Purchase Orders"]) async def api_purchase_order(oid: int): o = db.get_purchase_order(oid) if not o: raise HTTPException(404, "Ordine non trovato") return o @app.get("/api/invoices", tags=["Invoices"]) async def api_invoices(): return db.get_invoices() @app.get("/api/invoices/{iid}", tags=["Invoices"]) async def api_invoice(iid: int): i = db.get_invoice(iid) if not i: raise HTTPException(404, "Documento non trovato") return i @app.post("/api/invoices", tags=["Invoices"]) async def api_create_invoice(invoice: Invoice): return db.create_invoice(invoice) @app.get("/api/inventory-movements", tags=["Warehouse"]) async def api_movements(): return db.get_inventory_movements() @app.get("/api/inventory-movements/{mid}", tags=["Warehouse"]) async def api_movement(mid: int): m = db.get_inventory_movement(mid) if not m: raise HTTPException(404, "Movimento non trovato") return m @app.post("/api/inventory-movements", tags=["Warehouse"]) async def api_create_movement(movement: InventoryMovement): return db.create_inventory_movement(movement) @app.get("/api/inventory-reasons", tags=["Warehouse"]) async def api_reasons(): return db.get_inventory_reasons() @app.get("/api/payment-schedules", tags=["Scadenzario"]) async def api_schedules(): return db.get_payment_schedules() @app.get("/api/payment-schedules/{sid}", tags=["Scadenzario"]) async def api_schedule(sid: int): s = db.get_payment_schedule(sid) if not s: raise HTTPException(404, "Partita non trovata") return s @app.get("/api/dashboard/todos", tags=["Dashboard"]) async def api_todos(): return db.get_todo_items() @app.get("/api/dashboard/cashflow", tags=["Dashboard"]) async def api_cashflow(days: int = 30): return db.get_cashflow_forecast(days) @app.get("/api/info", tags=["Info"]) async def api_info(): return {"name": "Mago4 Demo", "version": "2.0.0", "status": "active", "database_type": "in-memory", "statistics": db.get_dashboard_stats()} if __name__ == "__main__": import uvicorn port = int(os.environ.get("PORT", 8000)) uvicorn.run(app, host="127.0.0.1", port=port)