Files
Alby96andClaude Sonnet 4.6 b6b6759b9a Restyle UI to match Mago4 and add invoicing, warehouse, scadenzario
Rework the demo into a faithful Mago4 look-and-feel and expand it with
core ERP modules modelled on the real Mago4 database schema.

UI (ispirata a Mago4):
- Blue header with logo/search/operation date, desktop-style window tabs
- White sidebar with coloured module icons (Anagrafiche, Vendite, Acquisti,
  Amministrazione, Logistica)
- Card-based module landing pages with breadcrumb + tab strip
- Document detail forms with left tabs, line grid and totals box

Fatturazione (InvoiceMng / MA_SaleDoc):
- Document types (Fattura Immediata, DDT, Nota di Credito, ProForma)
- Lines with discount/IVA, automatic taxable/tax/total computation
- Document states (draft -> printed -> issued / posted_to_inventory)

Magazzino (Inventory / MA_InventoryEntries + MA_InventoryReasons):
- Inventory movements with load/unload reasons (DebitCreditSign)
- Posting a movement actually updates item balances
- Causali and stock valuation pages

Amministrazione / Scadenzario (AP_AR / MA_PyblsRcvbls):
- Open items (receivables/payables) with due date, method, state
- Settle action closes the item and recomputes the cash forecast

Dashboard riprogettata (focus su scadenze e to-do):
- Overdue alert banner, KPI for overdue payables/receivables
- Upcoming deadlines table, prioritised "Cose da fare" action list
- 30-day cash-flow forecast and customer credit-limit check

Adds REST endpoints for invoices, movements, schedules and dashboard data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 11:47:32 +02:00

324 lines
13 KiB
Python

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)