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>
This commit is contained in:
2026-06-17 11:47:32 +02:00
co-authored by Claude Sonnet 4.6
parent 8a9fb5085a
commit b6b6759b9a
26 changed files with 2900 additions and 2125 deletions
+1
View File
@@ -20,6 +20,7 @@ env/
# IDEs
.vscode/
.idea/
.vs/
*.swp
*~
.DS_Store
+15 -10
View File
@@ -4,16 +4,21 @@ Demo funzionante di un gestionale ERP ispirato a **Mago4**, realizzato con **Pyt
## 🎯 Caratteristiche
- **Dashboard interattivo** con KPI e widget
- **Gestione Clienti** - Anagrafica e crediti
- **Gestione Fornitori** - Anagrafica e debiti
- **Catalogo Articoli** - Inventario con alert sottosorta
- **Ordini Vendita** - Creazione e tracciamento
- **Ordini Acquisto** - Gestione forniture
- **Magazzino** - Inventario con controllo giacenze
- **API REST** completamente documentata
- **Database in-memoria** per demo (facilmente migrabile a SQL)
- **UI ispirata a Mago4** - Design moderno con Material Icons
- **Interfaccia fedele a Mago4** — header blu, sidebar a moduli, menu a card con breadcrumb e tab, form documento con tab laterali e griglia righe
- **Cruscotto aziendale** orientato alle scadenze: banner allerta, KPI pagamenti/incassi scaduti, scadenze imminenti, **Cose da Fare** (paga/sollecita/ordina/conferma), **previsione di cassa** a 30 giorni, controllo fido clienti
- **Anagrafiche** — Clienti, Fornitori, Articoli (con P.IVA, codice fiscale, UM, IVA)
- **Vendite** — Ordini Cliente e **Fatturazione**: Fatture Immediate, DDT, Note di Credito, ProForma con calcolo imponibile/IVA/totale e stati documento (bozza → stampato → emesso)
- **Acquisti** — Ordini Fornitore
- **Amministrazione** — **Scadenzario** (partite aperte): incassi/pagamenti con data scadenza, stato (scaduta/in scadenza/aperta/saldata), registrazione incasso/pagamento, previsione di cassa
- **Logistica/Magazzino** **Movimenti di magazzino** con causali carico/scarico che **aggiornano realmente le giacenze**, **Causali** (DebitCreditSign), Giacenze e valorizzazione
- **API REST** completamente documentata (Swagger su `/docs`)
- **Database in-memoria** con logica di business (facilmente migrabile a SQL)
### Moduli ispirati al codice sorgente Mago4
La struttura dati e le funzionalità sono modellate sulle tabelle reali di Mago4:
- **Fatturazione**: `MA_SaleDoc` (InvoiceMng) — tipi documento, righe con sconto/IVA, stati `Printed`/`Issued`/`PostedToInventory`
- **Magazzino**: `MA_InventoryEntries` + `MA_InventoryReasons` (Inventory) — movimenti con causali e segno carico/scarico che aggiornano `MA_ItemsBalances`
- **Scadenzario**: `MA_PyblsRcvbls` + Details (AP_AR) — partite con `InstallmentDate`, `Amount`, `DebitCreditSign`, flag `Closed`/`Collected`
## 🚀 Quick Start
+505 -196
View File
@@ -1,8 +1,21 @@
from datetime import datetime, timedelta
from models import (
Customer, Supplier, Product, OrderLine, SalesOrder, PurchaseOrder, OrderStatus
)
from typing import List, Optional
from models import (
Customer, Supplier, Product, OrderLine, SalesOrder, PurchaseOrder, OrderStatus,
Invoice, InvoiceLine, DocumentType, DocumentStatus,
InventoryReason, InventoryMovement, InventoryMovementLine, MovementSign,
PaymentSchedule, PaymentScheduleType,
DOCUMENT_TYPE_LABELS,
)
def _today() -> datetime:
now = datetime.now()
return datetime(now.year, now.month, now.day)
def _round(v: float) -> float:
return round(v + 1e-9, 2)
class FakeDatabase:
@@ -12,191 +25,327 @@ class FakeDatabase:
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
self.inventory_reasons = self._init_inventory_reasons()
self.invoices = self._init_invoices()
self.inventory_movements = self._init_inventory_movements()
self.payment_schedules = self._init_payment_schedules()
self.next_customer_id = max((c.id for c in self.customers), default=0) + 1
self.next_supplier_id = max((s.id for s in self.suppliers), default=0) + 1
self.next_product_id = max((p.id for p in self.products), default=0) + 1
self.next_so_id = max((o.id for o in self.sales_orders), default=0) + 1
self.next_po_id = max((o.id for o in self.purchase_orders), default=0) + 1
self.next_invoice_id = max((i.id for i in self.invoices), default=0) + 1
self.next_movement_id = max((m.id for m in self.inventory_movements), default=0) + 1
self.next_schedule_id = max((s.id for s in self.payment_schedules), default=0) + 1
# ---------- Seed: Anagrafiche ----------
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
),
Customer(id=1, code="CUST001", name="Acme Corporation S.p.A.",
email="info@acme.it", phone="+39 02 1234567",
address="Via Roma 10", city="Milano", country="IT",
vat_number="IT01234567890", fiscal_code="CMRACM80A01F205X",
payment_terms="RB 30 gg FM", credit_limit=50000, balance=15000),
Customer(id=2, code="CUST002", name="TechFlow Industries GmbH",
email="contact@techflow.de", phone="+49 30 555888",
address="Hauptstrasse 45", city="Berlin", country="DE",
vat_number="DE811234567", payment_terms="Bonifico 60 gg",
credit_limit=75000, balance=32000),
Customer(id=3, code="CUST003", name="European Logistics SARL",
email="sales@eulog.fr", phone="+33 1 4520 3580",
address="Avenue des Champs 20", city="Paris", country="FR",
vat_number="FR40123456789", payment_terms="Bonifico 30 gg",
credit_limit=100000, balance=8500),
Customer(id=4, code="CUST004", name="Alpine Manufacturing AG",
email="orders@alpmfg.ch", phone="+41 44 5678900",
address="Industriestrasse 12", city="Zurich", country="CH",
vat_number="CHE116281456", payment_terms="Rimessa diretta",
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
),
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",
vat_number="GB123456789", 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",
vat_number="", 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="Industrigatan 5", city="Stockholm", country="SE",
vat_number="SE556677889901", 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
),
Product(id=1, code="PROD001", description="Industrial Widget A",
category="Electronics", uom="NR", unit_price=125.50,
purchase_price=85.00, tax_rate=22.0,
quantity_in_stock=250, reorder_level=50, max_stock=500, supplier_id=1),
Product(id=2, code="PROD002", description="Premium Connector Set",
category="Hardware", uom="NR", unit_price=45.00,
purchase_price=28.00, tax_rate=22.0,
quantity_in_stock=180, reorder_level=30, max_stock=300, supplier_id=2),
Product(id=3, code="PROD003", description="Steel Bracket Type B",
category="Structural", uom="KG", unit_price=22.75,
purchase_price=14.50, tax_rate=22.0,
quantity_in_stock=45, reorder_level=100, max_stock=600, supplier_id=3),
Product(id=4, code="PROD004", description="Precision Bearing Set",
category="Mechanical", uom="NR", unit_price=89.99,
purchase_price=55.00, tax_rate=22.0,
quantity_in_stock=120, reorder_level=40, max_stock=250, supplier_id=1),
Product(id=5, code="PROD005", description="Control Module MCU-32",
category="Electronics", uom="NR", unit_price=234.50,
purchase_price=160.00, tax_rate=22.0,
quantity_in_stock=18, reorder_level=20, max_stock=100, 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)
),
SalesOrder(id=1, order_number="SO-2024-0001",
order_date=now - timedelta(days=10),
customer_id=1, customer_name="Acme Corporation S.p.A.",
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 GmbH",
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 SARL",
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)
),
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=85.00, total_amount=8500.00)],
total_amount=8500.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=160.00, total_amount=8000.00)],
total_amount=8000.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=14.50, total_amount=2900.00)],
total_amount=2900.00, expected_delivery_date=now + timedelta(days=30)),
]
# CRUD Operations
def get_customers(self) -> List[Customer]:
return self.customers
# ---------- Seed: Magazzino ----------
def _init_inventory_reasons(self) -> List[InventoryReason]:
return [
InventoryReason(code="CAR-ACQ", description="Carico da acquisto",
sign=MovementSign.LOAD, fiscal=True),
InventoryReason(code="CAR-RET", description="Reso da cliente",
sign=MovementSign.LOAD, fiscal=True),
InventoryReason(code="CAR-RET+", description="Rettifica inventariale positiva",
sign=MovementSign.LOAD, fiscal=False),
InventoryReason(code="SCA-VEN", description="Scarico per vendita",
sign=MovementSign.UNLOAD, fiscal=True),
InventoryReason(code="SCA-PRO", description="Scarico per produzione",
sign=MovementSign.UNLOAD, fiscal=False),
InventoryReason(code="SCA-RET-", description="Rettifica inventariale negativa",
sign=MovementSign.UNLOAD, fiscal=False),
]
def get_customer(self, customer_id: int) -> Optional[Customer]:
return next((c for c in self.customers if c.id == customer_id), None)
def _init_inventory_movements(self) -> List[InventoryMovement]:
now = datetime.now()
return [
InventoryMovement(
id=1, reason_code="CAR-ACQ", reason_description="Carico da acquisto",
sign=MovementSign.LOAD, doc_no="MOV-0001",
doc_date=now - timedelta(days=12), posting_date=now - timedelta(days=12),
storage="MAG01", custsupp_name="Global Components Ltd",
lines=[InventoryMovementLine(line_no=1, item_code="PROD001",
description="Industrial Widget A", uom="NR",
quantity=100, unit_value=85.00, line_amount=8500.00,
location="A-01-03")],
total_amount=8500.00, posted=True, notes="Carico merce da OdA PO-2024-0001"),
InventoryMovement(
id=2, reason_code="SCA-VEN", reason_description="Scarico per vendita",
sign=MovementSign.UNLOAD, doc_no="MOV-0002",
doc_date=now - timedelta(days=6), posting_date=now - timedelta(days=6),
storage="MAG01", custsupp_name="Acme Corporation S.p.A.",
lines=[InventoryMovementLine(line_no=1, item_code="PROD001",
description="Industrial Widget A", uom="NR",
quantity=10, unit_value=125.50, line_amount=1255.00,
location="A-01-03")],
total_amount=1255.00, posted=True, notes="Scarico per fattura FT-2024-0001"),
InventoryMovement(
id=3, reason_code="CAR-RET+", reason_description="Rettifica inventariale positiva",
sign=MovementSign.LOAD, doc_no="MOV-0003",
doc_date=now - timedelta(days=1), posting_date=now - timedelta(days=1),
storage="MAG01", custsupp_name="",
lines=[InventoryMovementLine(line_no=1, item_code="PROD003",
description="Steel Bracket Type B", uom="KG",
quantity=5, unit_value=14.50, line_amount=72.50,
location="B-02-01")],
total_amount=72.50, posted=False, notes="Da confermare"),
]
# ---------- Seed: Fatture ----------
def _build_invoice_lines(self, raw_lines) -> List[InvoiceLine]:
lines = []
for i, (code, desc, uom, qty, val, disc, rate) in enumerate(raw_lines, start=1):
net_price = _round(val * (1 - disc / 100))
taxable = _round(net_price * qty)
tax = _round(taxable * rate / 100)
lines.append(InvoiceLine(line_no=i, item_code=code, description=desc, uom=uom,
quantity=qty, unit_value=val, discount_pct=disc,
net_price=net_price, taxable_amount=taxable,
tax_rate=rate, tax_amount=tax))
return lines
def _finalize_invoice(self, inv: Invoice) -> Invoice:
inv.taxable_total = _round(sum(l.taxable_amount for l in inv.lines))
inv.tax_total = _round(sum(l.tax_amount for l in inv.lines))
inv.total = _round(inv.taxable_total + inv.tax_total)
return inv
def _init_invoices(self) -> List[Invoice]:
now = datetime.now()
invoices = [
Invoice(id=1, doc_type=DocumentType.IMMEDIATE_INVOICE, doc_no="FT-2024-0001",
doc_date=now - timedelta(days=6), posting_date=now - timedelta(days=6),
customer_id=1, customer_name="Acme Corporation S.p.A.",
customer_vat="IT01234567890", your_reference="ODA 4521",
payment_terms="RB 30 gg FM", salesperson="Rossi M.",
lines=self._build_invoice_lines([
("PROD001", "Industrial Widget A", "NR", 10, 125.50, 0, 22.0),
("PROD004", "Precision Bearing Set", "NR", 5, 89.99, 5, 22.0),
]),
status=DocumentStatus.ISSUED, printed=True, issued=True,
posted_to_inventory=True),
Invoice(id=2, doc_type=DocumentType.DELIVERY_NOTE, doc_no="DDT-2024-0014",
doc_date=now - timedelta(days=3), posting_date=now - timedelta(days=3),
customer_id=2, customer_name="TechFlow Industries GmbH",
customer_vat="DE811234567", payment_terms="Bonifico 60 gg",
salesperson="Bianchi L.",
lines=self._build_invoice_lines([
("PROD005", "Control Module MCU-32", "NR", 8, 234.50, 0, 22.0),
]),
status=DocumentStatus.PRINTED, printed=True),
Invoice(id=3, doc_type=DocumentType.IMMEDIATE_INVOICE, doc_no="FT-2024-0002",
doc_date=now - timedelta(days=1), posting_date=now - timedelta(days=1),
customer_id=3, customer_name="European Logistics SARL",
customer_vat="FR40123456789", payment_terms="Bonifico 30 gg",
salesperson="Rossi M.",
lines=self._build_invoice_lines([
("PROD002", "Premium Connector Set", "NR", 20, 45.00, 10, 22.0),
("PROD003", "Steel Bracket Type B", "KG", 15, 22.75, 0, 22.0),
]),
status=DocumentStatus.DRAFT),
Invoice(id=4, doc_type=DocumentType.CREDIT_NOTE, doc_no="NC-2024-0003",
doc_date=now - timedelta(days=2), posting_date=now - timedelta(days=2),
customer_id=1, customer_name="Acme Corporation S.p.A.",
customer_vat="IT01234567890", payment_terms="RB 30 gg FM",
salesperson="Rossi M.", notes="Reso merce difettosa",
lines=self._build_invoice_lines([
("PROD004", "Precision Bearing Set", "NR", 2, 89.99, 0, 22.0),
]),
status=DocumentStatus.ISSUED, printed=True, issued=True),
]
return [self._finalize_invoice(i) for i in invoices]
# ---------- Seed: Scadenzario (partite aperte) ----------
def _init_payment_schedules(self) -> List[PaymentSchedule]:
t = _today()
R, P = PaymentScheduleType.RECEIVABLE, PaymentScheduleType.PAYABLE
return [
# --- Incassi (receivables) da fatture clienti ---
PaymentSchedule(id=1, type=R, custsupp_name="Acme Corporation S.p.A.",
doc_no="FT-2024-0001", doc_date=t - timedelta(days=36),
due_date=t - timedelta(days=6), payment_method="RB",
amount=2052.59, bank="Intesa Sanpaolo", closed=False,
notes="Scaduta — da sollecitare"),
PaymentSchedule(id=2, type=R, custsupp_name="European Logistics SARL",
doc_no="FT-2024-0002", doc_date=t - timedelta(days=1),
due_date=t + timedelta(days=29), payment_method="Bonifico",
amount=1404.53, bank="Intesa Sanpaolo", closed=False),
PaymentSchedule(id=3, type=R, custsupp_name="TechFlow Industries GmbH",
doc_no="FT-2024-0009", doc_date=t - timedelta(days=50),
due_date=t + timedelta(days=4), payment_method="RID",
amount=3680.00, bank="Unicredit", closed=False),
PaymentSchedule(id=4, type=R, custsupp_name="Acme Corporation S.p.A.",
doc_no="NC-2024-0003", doc_date=t - timedelta(days=2),
due_date=t + timedelta(days=10), payment_method="RB",
amount=219.58, is_credit_note=True, closed=False,
notes="Nota di credito — riduce il credito"),
PaymentSchedule(id=5, type=R, custsupp_name="Alpine Manufacturing AG",
doc_no="FT-2024-0007", doc_date=t - timedelta(days=40),
due_date=t - timedelta(days=10), payment_method="Bonifico",
amount=5430.00, bank="Unicredit", closed=False,
notes="Scaduta da 10 giorni"),
PaymentSchedule(id=6, type=R, custsupp_name="European Logistics SARL",
doc_no="FT-2024-0005", doc_date=t - timedelta(days=60),
due_date=t - timedelta(days=30), payment_method="RB",
amount=2100.00, bank="Intesa Sanpaolo", closed=True,
notes="Incassata"),
# --- Pagamenti (payables) verso fornitori ---
PaymentSchedule(id=7, type=P, custsupp_name="Global Components Ltd",
doc_no="FTACQ-1187", doc_date=t - timedelta(days=33),
due_date=t - timedelta(days=3), payment_method="Bonifico",
amount=8500.00, closed=False, notes="Scaduta — pagamento urgente"),
PaymentSchedule(id=8, type=P, custsupp_name="Asia Electronics Co.",
doc_no="FTACQ-2204", doc_date=t - timedelta(days=12),
due_date=t + timedelta(days=2), payment_method="Bonifico",
amount=8000.00, closed=False),
PaymentSchedule(id=9, type=P, custsupp_name="Nordic Materials AB",
doc_no="FTACQ-0991", doc_date=t - timedelta(days=5),
due_date=t + timedelta(days=25), payment_method="Bonifico",
amount=2900.00, closed=False),
PaymentSchedule(id=10, type=P, custsupp_name="Global Components Ltd",
doc_no="FTACQ-1150", doc_date=t - timedelta(days=70),
due_date=t - timedelta(days=40), payment_method="RIBA",
amount=3200.00, closed=True, notes="Pagata"),
]
# ==================== CRUD: Anagrafiche ====================
def get_customers(self): return self.customers
def get_customer(self, cid): return next((c for c in self.customers if c.id == cid), None)
def create_customer(self, customer: Customer) -> Customer:
customer.id = self.next_customer_id
@@ -204,19 +353,16 @@ class FakeDatabase:
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 update_customer(self, cid, data: dict):
c = self.get_customer(cid)
if c:
for k, v in data.items():
if hasattr(c, k) and v is not None:
setattr(c, k, v)
return c
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 get_suppliers(self): return self.suppliers
def get_supplier(self, sid): return next((s for s in self.suppliers if s.id == sid), None)
def create_supplier(self, supplier: Supplier) -> Supplier:
supplier.id = self.next_supplier_id
@@ -224,11 +370,9 @@ class FakeDatabase:
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 get_products(self): return self.products
def get_product(self, pid): return next((p for p in self.products if p.id == pid), None)
def get_product_by_code(self, code): return next((p for p in self.products if p.code == code), None)
def create_product(self, product: Product) -> Product:
product.id = self.next_product_id
@@ -236,11 +380,9 @@ class FakeDatabase:
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)
# ==================== CRUD: Ordini ====================
def get_sales_orders(self): return self.sales_orders
def get_sales_order(self, oid): return next((o for o in self.sales_orders if o.id == oid), None)
def create_sales_order(self, order: SalesOrder) -> SalesOrder:
order.id = self.next_so_id
@@ -248,11 +390,8 @@ class FakeDatabase:
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 get_purchase_orders(self): return self.purchase_orders
def get_purchase_order(self, oid): return next((o for o in self.purchase_orders if o.id == oid), None)
def create_purchase_order(self, order: PurchaseOrder) -> PurchaseOrder:
order.id = self.next_po_id
@@ -260,16 +399,173 @@ class FakeDatabase:
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])
# ==================== CRUD: Fatture ====================
def get_invoices(self): return self.invoices
def get_invoice(self, iid): return next((i for i in self.invoices if i.id == iid), None)
def get_invoices_by_type(self, doc_type: DocumentType):
return [i for i in self.invoices if i.doc_type == doc_type]
def create_invoice(self, invoice: Invoice) -> Invoice:
invoice.id = self.next_invoice_id
self.next_invoice_id += 1
self._finalize_invoice(invoice)
self.invoices.append(invoice)
return invoice
# ==================== CRUD: Magazzino ====================
def get_inventory_reasons(self): return self.inventory_reasons
def get_inventory_reason(self, code): return next((r for r in self.inventory_reasons if r.code == code), None)
def get_inventory_movements(self): return self.inventory_movements
def get_inventory_movement(self, mid): return next((m for m in self.inventory_movements if m.id == mid), None)
def create_inventory_movement(self, movement: InventoryMovement) -> InventoryMovement:
movement.id = self.next_movement_id
self.next_movement_id += 1
movement.total_amount = _round(sum(l.line_amount for l in movement.lines))
self.inventory_movements.append(movement)
if movement.posted:
self._apply_movement_to_balances(movement)
return movement
def post_movement(self, mid) -> Optional[InventoryMovement]:
"""Conferma un movimento e aggiorna le giacenze (Mago4: PostedToInventory)."""
m = self.get_inventory_movement(mid)
if m and not m.posted:
m.posted = True
self._apply_movement_to_balances(m)
return m
def _apply_movement_to_balances(self, movement: InventoryMovement):
"""Applica il movimento alle giacenze articoli secondo il segno della causale."""
delta_sign = 1 if movement.sign == MovementSign.LOAD else -1
for line in movement.lines:
product = self.get_product_by_code(line.item_code)
if product:
product.quantity_in_stock += int(delta_sign * line.quantity)
# ==================== Scadenzario (AP_AR) ====================
def get_payment_schedules(self): return self.payment_schedules
def get_payment_schedule(self, sid): return next((s for s in self.payment_schedules if s.id == sid), None)
def schedule_status(self, s: PaymentSchedule) -> str:
"""Stato derivato: paid / overdue / due_soon / open"""
if s.closed:
return "paid"
days = (s.due_date - _today()).days
if days < 0:
return "overdue"
if days <= 7:
return "due_soon"
return "open"
def schedule_days_to_due(self, s: PaymentSchedule) -> int:
return (s.due_date - _today()).days
def settle_schedule(self, sid) -> Optional[PaymentSchedule]:
"""Salda/incassa una partita (Mago4: Closed/Collected)."""
s = self.get_payment_schedule(sid)
if s and not s.closed:
s.closed = True
return s
def get_open_schedules(self, stype: Optional[PaymentScheduleType] = None):
items = [s for s in self.payment_schedules if not s.closed]
if stype:
items = [s for s in items if s.type == stype]
return items
def get_overdue_schedules(self, stype: Optional[PaymentScheduleType] = None):
return [s for s in self.get_open_schedules(stype) if self.schedule_status(s) == "overdue"]
def get_upcoming_schedules(self, days: int = 30):
"""Partite aperte in scadenza nei prossimi 'days' giorni (incluse scadute)."""
limit = _today() + timedelta(days=days)
items = [s for s in self.get_open_schedules() if s.due_date <= limit]
return sorted(items, key=lambda s: s.due_date)
def get_credit_exceeded_customers(self):
"""Clienti che hanno superato il limite di fido (modulo CreditLimit)."""
return [c for c in self.customers if c.credit_limit > 0 and c.balance > c.credit_limit]
def get_todo_items(self):
"""Aggrega le 'cose da fare' per la dashboard."""
draft_invoices = [i for i in self.invoices if i.status == DocumentStatus.DRAFT]
draft_movements = [m for m in self.inventory_movements if not m.posted]
orders_to_fulfill = [o for o in self.sales_orders
if o.status in (OrderStatus.CONFIRMED, OrderStatus.DRAFT)]
low_stock = [p for p in self.products if p.quantity_in_stock <= p.reorder_level]
overdue_recv = self.get_overdue_schedules(PaymentScheduleType.RECEIVABLE)
overdue_pay = self.get_overdue_schedules(PaymentScheduleType.PAYABLE)
credit_exceeded = self.get_credit_exceeded_customers()
todos = []
if overdue_pay:
todos.append({"icon": "payments", "color": "red", "priority": 1,
"text": f"{len(overdue_pay)} pagamenti scaduti da effettuare",
"amount": sum(s.amount for s in overdue_pay),
"link": "/scadenzario?type=payable", "action": "Paga"})
if overdue_recv:
todos.append({"icon": "request_quote", "color": "orange", "priority": 2,
"text": f"{len(overdue_recv)} incassi scaduti da sollecitare",
"amount": sum(s.amount for s in overdue_recv),
"link": "/scadenzario?type=receivable", "action": "Sollecita"})
if low_stock:
todos.append({"icon": "production_quantity_limits", "color": "red", "priority": 3,
"text": f"{len(low_stock)} articoli sotto scorta da ordinare",
"amount": None, "link": "/warehouse", "action": "Ordina"})
if draft_invoices:
todos.append({"icon": "edit_document", "color": "blue", "priority": 4,
"text": f"{len(draft_invoices)} documenti in bozza da confermare",
"amount": None, "link": "/invoices", "action": "Apri"})
if orders_to_fulfill:
todos.append({"icon": "local_shipping", "color": "blue", "priority": 5,
"text": f"{len(orders_to_fulfill)} ordini cliente da evadere",
"amount": None, "link": "/sales-orders", "action": "Apri"})
if draft_movements:
todos.append({"icon": "swap_vert", "color": "teal", "priority": 6,
"text": f"{len(draft_movements)} movimenti di magazzino da confermare",
"amount": None, "link": "/inventory-movements", "action": "Apri"})
if credit_exceeded:
todos.append({"icon": "credit_card_off", "color": "red", "priority": 7,
"text": f"{len(credit_exceeded)} clienti con fido superato",
"amount": None, "link": "/customers", "action": "Verifica"})
return sorted(todos, key=lambda x: x["priority"])
def get_cashflow_forecast(self, days: int = 30):
"""Previsione di cassa a 'days' giorni: incassi vs pagamenti previsti."""
upcoming = self.get_upcoming_schedules(days)
inflow = sum(s.signed_amount for s in upcoming if s.type == PaymentScheduleType.RECEIVABLE)
outflow = sum(s.amount for s in upcoming if s.type == PaymentScheduleType.PAYABLE)
return {"days": days, "inflow": _round(inflow), "outflow": _round(outflow),
"net": _round(inflow - outflow)}
# ==================== Statistiche ====================
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]
# Fatturato emesso (fatture/note credito issued)
invoiced_total = sum(
(i.total if i.doc_type != DocumentType.CREDIT_NOTE else -i.total)
for i in self.invoices if i.issued
)
pending_invoices = sum(1 for i in self.invoices if i.status == DocumentStatus.DRAFT)
warehouse_value = sum(p.quantity_in_stock * p.purchase_price for p in self.products)
# Scadenzario
overdue_recv = self.get_overdue_schedules(PaymentScheduleType.RECEIVABLE)
overdue_pay = self.get_overdue_schedules(PaymentScheduleType.PAYABLE)
open_recv = self.get_open_schedules(PaymentScheduleType.RECEIVABLE)
open_pay = self.get_open_schedules(PaymentScheduleType.PAYABLE)
return {
"sales_to_fulfill": sales_to_fulfill,
"purchase_to_receive": purchase_to_receive,
@@ -280,6 +576,19 @@ class FakeDatabase:
"total_customers": len(self.customers),
"total_suppliers": len(self.suppliers),
"total_products": len(self.products),
"invoiced_total": invoiced_total,
"pending_invoices": pending_invoices,
"warehouse_value": warehouse_value,
"total_invoices": len(self.invoices),
"total_movements": len(self.inventory_movements),
# Scadenze
"overdue_recv_count": len(overdue_recv),
"overdue_recv_amount": _round(sum(s.amount for s in overdue_recv)),
"overdue_pay_count": len(overdue_pay),
"overdue_pay_amount": _round(sum(s.amount for s in overdue_pay)),
"open_recv_amount": _round(sum(s.signed_amount for s in open_recv)),
"open_pay_amount": _round(sum(s.amount for s in open_pay)),
"credit_exceeded_count": len(self.get_credit_exceeded_customers()),
}
+244 -225
View File
@@ -8,24 +8,16 @@ from datetime import datetime
import os
from models import (
Customer, Supplier, Product, SalesOrder, PurchaseOrder,
OrderLine, OrderStatus, Dashboard, DashboardWidget
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 con Python FastAPI",
version="1.0.0"
)
app = FastAPI(title="Mago4 Demo", description="Demo gestionale ERP — Python FastAPI", version="2.0.0")
# Setup static e template files
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
os.makedirs(os.path.join(BASE_DIR, "static"), exist_ok=True)
os.makedirs(os.path.join(BASE_DIR, "static", "css"), exist_ok=True)
os.makedirs(os.path.join(BASE_DIR, "static", "js"), exist_ok=True)
os.makedirs(os.path.join(BASE_DIR, "templates"), exist_ok=True)
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)
@@ -34,268 +26,295 @@ _jinja_env = Environment(
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)
# ==================== DASHBOARD ====================
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()
widgets = [
[
DashboardWidget(
title="Ordini Vendita da Evadere",
style="stats",
size="small",
icon="shopping_cart",
color="blue",
value=str(stats["sales_to_fulfill"])
),
DashboardWidget(
title="Ordini Acquisto da Ricevere",
style="stats",
size="small",
icon="local_shipping",
color="orange",
value=str(stats["purchase_to_receive"])
),
DashboardWidget(
title="Crediti Clienti",
style="stats",
size="small",
icon="euro_symbol",
color="green",
value=f"{stats['total_receivable']:,.2f}",
format="money"
),
DashboardWidget(
title="Articoli Sotto Soglia",
style="stats",
size="small",
icon="warning",
color="red",
value=str(stats["low_stock_count"])
),
],
[
DashboardWidget(
title="Ultimi Ordini Vendita",
subtitle="per data ordine",
style="grid",
size="medium",
data={"orders": [
{
"order_number": o.order_number,
"order_date": o.order_date.strftime("%d/%m/%Y"),
"customer_name": o.customer_name,
"total_amount": f"{o.total_amount:,.2f}",
"status": o.status.value
}
for o in sorted(db.get_sales_orders(), key=lambda x: x.order_date, reverse=True)[:5]
]}
),
DashboardWidget(
title="Ultimi Ordini Acquisto",
subtitle="per data ordine",
style="grid",
size="medium",
data={"orders": [
{
"order_number": o.order_number,
"order_date": o.order_date.strftime("%d/%m/%Y"),
"supplier_name": o.supplier_name,
"total_amount": f"{o.total_amount:,.2f}",
"status": o.status.value
}
for o in sorted(db.get_purchase_orders(), key=lambda x: x.order_date, reverse=True)[:5]
]}
),
]
]
return templates.TemplateResponse(request, "dashboard.html", {
"widgets": widgets,
"stats": 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)
# ==================== CUSTOMERS ====================
@app.get("/api/customers", tags=["Customers"])
async def list_customers():
return db.get_customers()
@app.get("/api/customers/{customer_id}", tags=["Customers"])
async def get_customer(customer_id: int):
customer = db.get_customer(customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Cliente non trovato")
return customer
@app.post("/api/customers", tags=["Customers"])
async def create_customer(customer: Customer):
return db.create_customer(customer)
@app.put("/api/customers/{customer_id}", tags=["Customers"])
async def update_customer(customer_id: int, customer_data: dict):
result = db.update_customer(customer_id, customer_data)
if not result:
raise HTTPException(status_code=404, detail="Cliente non trovato")
return result
# ============================================================
# 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 templates.TemplateResponse(request, "customers.html", {
"customers": db.get_customers()
})
# ==================== SUPPLIERS ====================
@app.get("/api/suppliers", tags=["Suppliers"])
async def list_suppliers():
return db.get_suppliers()
@app.get("/api/suppliers/{supplier_id}", tags=["Suppliers"])
async def get_supplier(supplier_id: int):
supplier = db.get_supplier(supplier_id)
if not supplier:
raise HTTPException(status_code=404, detail="Fornitore non trovato")
return supplier
@app.post("/api/suppliers", tags=["Suppliers"])
async def create_supplier(supplier: Supplier):
return db.create_supplier(supplier)
return render(request, "customers.html", "anagrafiche", customers=db.get_customers())
@app.get("/suppliers", response_class=HTMLResponse)
async def suppliers_page(request: Request):
return templates.TemplateResponse(request, "suppliers.html", {
"suppliers": db.get_suppliers()
})
# ==================== PRODUCTS ====================
@app.get("/api/products", tags=["Products"])
async def list_products():
return db.get_products()
@app.get("/api/products/{product_id}", tags=["Products"])
async def get_product(product_id: int):
product = db.get_product(product_id)
if not product:
raise HTTPException(status_code=404, detail="Articolo non trovato")
return product
@app.post("/api/products", tags=["Products"])
async def create_product(product: Product):
return db.create_product(product)
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 templates.TemplateResponse(request, "products.html", {
"products": products,
"low_stock": [p for p in products if p.quantity_in_stock <= p.reorder_level]
})
return render(request, "products.html", "anagrafiche", products=products,
low_stock=[p for p in products if p.quantity_in_stock <= p.reorder_level])
# ==================== SALES ORDERS ====================
@app.get("/api/sales-orders", tags=["Sales Orders"])
async def list_sales_orders():
return db.get_sales_orders()
@app.get("/api/sales-orders/{order_id}", tags=["Sales Orders"])
async def get_sales_order(order_id: int):
order = db.get_sales_order(order_id)
if not order:
raise HTTPException(status_code=404, detail="Ordine vendita non trovato")
return order
@app.post("/api/sales-orders", tags=["Sales Orders"])
async def create_sales_order(order: SalesOrder):
return db.create_sales_order(order)
# ============================================================
# 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 templates.TemplateResponse(request, "sales_orders.html", {
"orders": orders,
"pending": [o for o in orders if o.status != OrderStatus.RECEIVED]
})
return render(request, "sales_orders.html", "vendite", orders=orders,
pending=[o for o in orders if o.status != OrderStatus.RECEIVED])
# ==================== PURCHASE ORDERS ====================
@app.get("/api/purchase-orders", tags=["Purchase Orders"])
async def list_purchase_orders():
return db.get_purchase_orders()
@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("/api/purchase-orders/{order_id}", tags=["Purchase Orders"])
async def get_purchase_order(order_id: int):
order = db.get_purchase_order(order_id)
if not order:
raise HTTPException(status_code=404, detail="Ordine acquisto non trovato")
return order
@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)
@app.post("/api/purchase-orders", tags=["Purchase Orders"])
async def create_purchase_order(order: PurchaseOrder):
return db.create_purchase_order(order)
# ============================================================
# 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 templates.TemplateResponse(request, "purchase_orders.html", {
"orders": orders,
"pending": [o for o in orders if o.status != OrderStatus.RECEIVED]
})
return render(request, "purchase_orders.html", "acquisti", orders=orders,
pending=[o for o in orders if o.status != OrderStatus.RECEIVED])
# ==================== WAREHOUSE ====================
@app.get("/api/warehouse/stats", tags=["Warehouse"])
async def warehouse_stats():
stats = db.get_dashboard_stats()
return {
"total_products": stats["total_products"],
"low_stock_count": stats["low_stock_count"],
"low_stock_items": stats["low_stock_items"],
"all_products": db.get_products()
}
# ============================================================
# 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 templates.TemplateResponse(request, "warehouse.html", {
"products": db.get_products(),
"low_stock": stats["low_stock_items"]
})
return render(request, "warehouse.html", "magazzino", products=db.get_products(),
low_stock=stats["low_stock_items"], warehouse_value=stats["warehouse_value"])
# ==================== INFO ====================
@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():
stats = db.get_dashboard_stats()
return {
"name": "Mago4 Demo",
"version": "1.0.0",
"status": "active",
"database_type": "in-memory",
"statistics": stats
}
return {"name": "Mago4 Demo", "version": "2.0.0", "status": "active",
"database_type": "in-memory", "statistics": db.get_dashboard_stats()}
if __name__ == "__main__":
+183 -102
View File
@@ -4,6 +4,7 @@ from datetime import datetime
from enum import Enum
# ==================== ENUMS ====================
class OrderStatus(str, Enum):
DRAFT = "draft"
CONFIRMED = "confirmed"
@@ -17,6 +18,51 @@ class OrderType(str, Enum):
PURCHASE = "purchase"
class DocumentType(str, Enum):
"""Tipi documento di vendita (Mago4: MA_SaleDoc.DocumentType)"""
IMMEDIATE_INVOICE = "immediate_invoice" # Fattura Immediata
DELIVERY_NOTE = "delivery_note" # Documento di Trasporto (DDT)
ACCOMPANYING_INVOICE = "accompanying_invoice" # Fattura Accompagnatoria
PROFORMA_INVOICE = "proforma_invoice" # Fattura ProForma
CREDIT_NOTE = "credit_note" # Nota di Credito
DEBIT_NOTE = "debit_note" # Nota di Debito
DOCUMENT_TYPE_LABELS = {
DocumentType.IMMEDIATE_INVOICE: "Fattura Immediata",
DocumentType.DELIVERY_NOTE: "Documento di Trasporto",
DocumentType.ACCOMPANYING_INVOICE: "Fattura Accompagnatoria",
DocumentType.PROFORMA_INVOICE: "Fattura ProForma",
DocumentType.CREDIT_NOTE: "Nota di Credito",
DocumentType.DEBIT_NOTE: "Nota di Debito",
}
class DocumentStatus(str, Enum):
"""Stato documento (Mago4: Printed / Issued / PostedToInventory)"""
DRAFT = "draft" # Bozza
CONFIRMED = "confirmed" # Confermato
PRINTED = "printed" # Stampato
ISSUED = "issued" # Emesso (definitivo)
POSTED = "posted" # Contabilizzato
DOCUMENT_STATUS_LABELS = {
DocumentStatus.DRAFT: "Bozza",
DocumentStatus.CONFIRMED: "Confermato",
DocumentStatus.PRINTED: "Stampato",
DocumentStatus.ISSUED: "Emesso",
DocumentStatus.POSTED: "Contabilizzato",
}
class MovementSign(str, Enum):
"""Segno movimento di magazzino (Mago4: InventoryReasons.DebitCreditSign)"""
LOAD = "load" # Carico (+)
UNLOAD = "unload" # Scarico (-)
# ==================== ANAGRAFICHE ====================
class Customer(BaseModel):
id: int
code: str
@@ -26,24 +72,11 @@ class Customer(BaseModel):
address: str
city: str
country: str
credit_limit: float
balance: float
class Config:
json_schema_extra = {
"example": {
"id": 1,
"code": "CUST001",
"name": "Acme Corp",
"email": "info@acme.com",
"phone": "+39 02 1234567",
"address": "Via Roma 10",
"city": "Milano",
"country": "IT",
"credit_limit": 50000,
"balance": 15000
}
}
vat_number: str = "" # Partita IVA
fiscal_code: str = "" # Codice Fiscale
payment_terms: str = "RB 30 gg" # Pagamento
credit_limit: float = 0.0
balance: float = 0.0
class Supplier(BaseModel):
@@ -55,24 +88,9 @@ class Supplier(BaseModel):
address: str
city: str
country: str
payment_terms_days: int
balance: float
class Config:
json_schema_extra = {
"example": {
"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
}
}
vat_number: str = ""
payment_terms_days: int = 30
balance: float = 0.0
class Product(BaseModel):
@@ -80,26 +98,17 @@ class Product(BaseModel):
code: str
description: str
category: str
unit_price: float
quantity_in_stock: int
reorder_level: int
uom: str = "NR" # Unità di misura
unit_price: float = 0.0 # Prezzo di vendita
purchase_price: float = 0.0 # Prezzo di acquisto / costo
tax_rate: float = 22.0 # Aliquota IVA %
quantity_in_stock: int = 0 # Giacenza (On-hand)
reorder_level: int = 0 # Scorta minima (Min. Stock)
max_stock: int = 0 # Scorta massima
supplier_id: Optional[int] = None
class Config:
json_schema_extra = {
"example": {
"id": 1,
"code": "PROD001",
"description": "Industrial Widget A",
"category": "Electronics",
"unit_price": 125.50,
"quantity_in_stock": 250,
"reorder_level": 50,
"supplier_id": 1
}
}
# ==================== ORDINI ====================
class OrderLine(BaseModel):
id: int
product_id: int
@@ -109,19 +118,6 @@ class OrderLine(BaseModel):
unit_price: float
total_amount: float
class Config:
json_schema_extra = {
"example": {
"id": 1,
"product_id": 1,
"product_code": "PROD001",
"product_description": "Industrial Widget A",
"quantity": 10,
"unit_price": 125.50,
"total_amount": 1255.00
}
}
class SalesOrder(BaseModel):
id: int
@@ -134,21 +130,6 @@ class SalesOrder(BaseModel):
total_amount: float
delivery_date: Optional[datetime] = None
class Config:
json_schema_extra = {
"example": {
"id": 1,
"order_number": "SO-2024-001",
"order_date": "2024-01-15T10:30:00",
"customer_id": 1,
"customer_name": "Acme Corp",
"status": "confirmed",
"lines": [],
"total_amount": 2500.00,
"delivery_date": "2024-02-15T00:00:00"
}
}
class PurchaseOrder(BaseModel):
id: int
@@ -161,33 +142,133 @@ class PurchaseOrder(BaseModel):
total_amount: float
expected_delivery_date: Optional[datetime] = None
class Config:
json_schema_extra = {
"example": {
"id": 1,
"order_number": "PO-2024-001",
"order_date": "2024-01-10T14:00:00",
"supplier_id": 1,
"supplier_name": "Global Components Ltd",
"status": "confirmed",
"lines": [],
"total_amount": 5000.00,
"expected_delivery_date": "2024-02-10T00:00:00"
}
}
# ==================== FATTURAZIONE (InvoiceMng) ====================
class InvoiceLine(BaseModel):
"""Riga documento (Mago4: MA_SaleDocDetail)"""
line_no: int
item_code: str
description: str
uom: str = "NR"
quantity: float = 1.0
unit_value: float = 0.0 # Valore unitario
discount_pct: float = 0.0 # Sconto %
net_price: float = 0.0 # Prezzo netto unitario
taxable_amount: float = 0.0 # Imponibile riga
tax_rate: float = 22.0 # Aliquota IVA
tax_amount: float = 0.0 # Imposta riga
class Invoice(BaseModel):
"""Documento di vendita (Mago4: MA_SaleDoc)"""
id: int
doc_type: DocumentType
doc_no: str # Numero documento
doc_date: datetime # Data documento
posting_date: datetime # Data registrazione
customer_id: int
customer_name: str
customer_vat: str = ""
our_reference: str = "" # Nostro riferimento
your_reference: str = "" # Vostro riferimento
payment_terms: str = "RB 30 gg"
price_list: str = "Listino Base"
currency: str = "EUR"
salesperson: str = "" # Agente
net_of_tax: bool = False # Prezzi IVA inclusa
lines: List[InvoiceLine] = []
taxable_total: float = 0.0 # Totale imponibile
tax_total: float = 0.0 # Totale imposta
total: float = 0.0 # Totale documento
status: DocumentStatus = DocumentStatus.DRAFT
printed: bool = False
issued: bool = False
posted_to_inventory: bool = False
notes: str = ""
# ==================== MAGAZZINO (Inventory) ====================
class InventoryReason(BaseModel):
"""Causale di magazzino (Mago4: MA_InventoryReasons)"""
code: str # Causale (es. CAR, SCA)
description: str
sign: MovementSign # Carico / Scarico
fiscal: bool = False # Numerazione fiscale
updates_balance: bool = True # Aggiorna giacenze
class InventoryMovementLine(BaseModel):
"""Riga movimento (Mago4: MA_InventoryEntriesDetail)"""
line_no: int
item_code: str
description: str
uom: str = "NR"
quantity: float = 0.0
unit_value: float = 0.0
line_amount: float = 0.0
lot: str = "" # Lotto
location: str = "" # Ubicazione
class InventoryMovement(BaseModel):
"""Movimento di magazzino (Mago4: MA_InventoryEntries)"""
id: int
reason_code: str # Causale
reason_description: str
sign: MovementSign
doc_no: str
doc_date: datetime
posting_date: datetime
storage: str = "MAG01" # Deposito
custsupp_name: str = "" # Cliente/Fornitore
lines: List[InventoryMovementLine] = []
total_amount: float = 0.0
posted: bool = False # Movimento confermato (aggiorna giacenze)
notes: str = ""
# ==================== SCADENZARIO (AP_AR — Partite/Effetti) ====================
class PaymentScheduleType(str, Enum):
"""Tipo partita (Mago4: DebitCreditSign)"""
RECEIVABLE = "receivable" # Credito / Incasso da ricevere
PAYABLE = "payable" # Debito / Pagamento da effettuare
PAYMENT_SCHEDULE_TYPE_LABELS = {
PaymentScheduleType.RECEIVABLE: "Incasso",
PaymentScheduleType.PAYABLE: "Pagamento",
}
class PaymentSchedule(BaseModel):
"""Partita aperta / scadenza (Mago4: MA_PyblsRcvbls + Details)"""
id: int
type: PaymentScheduleType
custsupp_name: str # Cliente o Fornitore
doc_no: str # Numero documento di origine
doc_date: datetime
due_date: datetime # Data scadenza (InstallmentDate)
installment_no: int = 1 # Numero rata
payment_method: str = "RB" # Modalità (RB, Bonifico, RID...)
amount: float = 0.0 # Importo rata
is_credit_note: bool = False # Nota di credito (segno opposto)
closed: bool = False # Saldata / Incassata (Closed/Collected)
bank: str = "" # Banca di presentazione
notes: str = ""
@property
def signed_amount(self) -> float:
return -self.amount if self.is_credit_note else self.amount
# ==================== UI / DASHBOARD ====================
class DashboardWidget(BaseModel):
title: str
subtitle: Optional[str] = None
style: str # "stats", "chart", "grid"
size: str = "small" # "small", "medium", "large"
style: str
size: str = "small"
icon: Optional[str] = None
color: Optional[str] = None
value: Optional[str] = None
format: Optional[str] = None # "money", "number", "date"
format: Optional[str] = None
data: Optional[dict] = None
class Dashboard(BaseModel):
widgets: List[List[DashboardWidget]]
+592 -614
View File
File diff suppressed because it is too large Load Diff
+44 -108
View File
@@ -1,120 +1,56 @@
// Utility functions for API calls
async function apiCall(endpoint, method = 'GET', data = null) {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
}
};
// ============================================================
// Mago4 Demo — helper frontend
// ============================================================
if (data) {
options.body = JSON.stringify(data);
}
try {
const response = await fetch(endpoint, options);
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
showNotification('Errore nella richiesta', 'error');
throw error;
}
// Genera una field-row in stile form Mago4
function field(label, value) {
return `<div class="field-row"><label>${label}</label><div class="field-value">${value ?? ''}</div></div>`;
}
// Notification system
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
background-color: ${type === 'error' ? '#D32F2F' : '#1976D2'};
color: white;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
z-index: 2000;
animation: slideIn 0.3s ease-in-out;
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
// Modal generico
function openGenericModal(title, html) {
const t = document.getElementById('gmTitle');
const b = document.getElementById('gmBody');
if (t) t.textContent = title;
if (b) b.innerHTML = html;
const m = document.getElementById('genericModal');
if (m) m.classList.add('show');
}
function closeGenericModal() {
const m = document.getElementById('genericModal');
if (m) m.classList.remove('show');
}
// Format currency
function formatCurrency(value) {
return new Intl.NumberFormat('it-IT', {
style: 'currency',
currency: 'EUR'
}).format(value);
// Ricerca globale (demo)
function globalSearch(q) {
if (!q || !q.trim()) return;
alert('Ricerca globale (demo): "' + q + '"');
}
// Format date
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('it-IT');
// Formattazione valuta
function fmtEur(v) {
return new Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' }).format(v || 0);
}
function fmtDate(s) {
if (!s) return '—';
return new Date(s).toLocaleDateString('it-IT');
}
// Initialize page animations
document.addEventListener('DOMContentLoaded', function() {
// Add smooth scrolling
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
// Toast notifiche
function showNotification(message, type) {
const n = document.createElement('div');
n.textContent = message;
n.style.cssText = `position:fixed;top:64px;right:20px;padding:11px 18px;border-radius:4px;
background:${type === 'error' ? '#D32F2F' : '#1577be'};color:#fff;
box-shadow:0 4px 16px rgba(0,0,0,0.2);z-index:2000;font-size:13px;animation:slideIn .25s ease;`;
document.body.appendChild(n);
setTimeout(() => n.remove(), 3000);
}
// Add keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Close modals with Escape
if (e.key === 'Escape') {
document.querySelectorAll('.modal.show').forEach(modal => {
modal.classList.remove('show');
});
}
});
// Initialize tooltips
document.querySelectorAll('[title]').forEach(element => {
element.style.cursor = 'help';
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') document.querySelectorAll('.modal.show').forEach(m => m.classList.remove('show'));
});
// Add CSS animation for notifications
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
}
`;
document.head.appendChild(style);
const _style = document.createElement('style');
_style.textContent = `@keyframes slideIn{from{transform:translateX(60px);opacity:0}to{transform:none;opacity:1}}`;
document.head.appendChild(_style);
+42
View File
@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block title %}Acquisti - Mago4 Demo{% endblock %}
{% block window_tabs %}<a href="/acquisti" class="window-tab active"><i class="material-icons">shopping_cart</i> Acquisti</a>{% endblock %}
{% block content %}
<div class="module-bar">
<div class="module-title">Acquisti</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><span class="current">Acquisti</span></div>
</div>
<div class="tab-strip">
<a href="/purchase-orders">Ordini Fornitore</a>
<a href="/acquisti" class="active">Documenti Acquisto</a>
<a href="#">Politiche Acquisto</a>
</div>
<div class="cards-grid">
<div class="menu-card">
<div class="menu-card-header"><h3>Ordini Fornitore</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="/purchase-orders" class="menu-link"><i class="material-icons ml-icon mtype-form">shopping_cart</i><span class="ml-text">Ordini Fornitore</span><i class="material-icons ml-star">star_border</i></a>
<a href="/purchase-orders" class="menu-link"><i class="material-icons ml-icon mtype-list">list_alt</i><span class="ml-text">Lista Ordini Fornitore</span><i class="material-icons ml-star">star_border</i></a>
<a href="/purchase-orders" class="menu-link"><i class="material-icons ml-icon mtype-print">print</i><span class="ml-text">Portafoglio Fornitori</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Documenti di Acquisto</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-form">receipt_long</i><span class="ml-text">Fatture di Acquisto</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-form">inventory</i><span class="ml-text">Documenti di Carico</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Procedure</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="/magazzino" class="menu-link"><i class="material-icons ml-icon mtype-proc">bolt</i><span class="ml-text">Generazione Ordini da Sottoscorta</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-print">bar_chart</i><span class="ml-text">Statistiche Acquisti</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
</div>
{% endblock %}
+52
View File
@@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block title %}Amministrazione - Mago4 Demo{% endblock %}
{% block window_tabs %}<a href="/amministrazione" class="window-tab active"><i class="material-icons">account_balance</i> Amministrazione</a>{% endblock %}
{% block content %}
<div class="module-bar">
<div class="module-title">Amministrazione</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><span class="current">Amministrazione</span></div>
</div>
<div class="tab-strip">
<a href="/amministrazione" class="active">Amministrazione</a>
<a href="/scadenzario">Scadenzario</a>
</div>
<div class="cards-grid">
<div class="menu-card">
<div class="menu-card-header"><h3>Pagamenti / Incassi</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="/scadenzario" class="menu-link"><i class="material-icons ml-icon mtype-form">event_note</i><span class="ml-text">Scadenzario</span><i class="material-icons ml-star">star_border</i></a>
<a href="/scadenzario?type=receivable" class="menu-link"><i class="material-icons ml-icon mtype-list">request_quote</i><span class="ml-text">Partite a Credito (Incassi)</span><i class="material-icons ml-star">star_border</i></a>
<a href="/scadenzario?type=payable" class="menu-link"><i class="material-icons ml-icon mtype-list">payments</i><span class="ml-text">Partite a Debito (Pagamenti)</span><i class="material-icons ml-star">star_border</i></a>
<a href="/scadenzario" class="menu-link"><i class="material-icons ml-icon mtype-print">print</i><span class="ml-text">Stampa Scadenzario</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Contabilità</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-form">menu_book</i><span class="ml-text">Prima Nota</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-list">account_tree</i><span class="ml-text">Piano dei Conti</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-print">summarize</i><span class="ml-text">Bilancio</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>IVA</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-print">receipt_long</i><span class="ml-text">Registri IVA</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-proc">calculate</i><span class="ml-text">Liquidazione IVA</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Banche</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-list">account_balance</i><span class="ml-text">Banche Aziendali</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-proc">sync_alt</i><span class="ml-text">Riconciliazione Bancaria</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-proc">request_page</i><span class="ml-text">Presentazione Effetti (RiBa)</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
</div>
{% endblock %}
+53
View File
@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block title %}Anagrafiche - Mago4 Demo{% endblock %}
{% block window_tabs %}<a href="/anagrafiche" class="window-tab active"><i class="material-icons">contacts</i> Anagrafiche</a>{% endblock %}
{% block content %}
<div class="module-bar">
<div class="module-title">Anagrafiche</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><span class="current">Anagrafiche</span></div>
</div>
<div class="tab-strip">
<a href="/anagrafiche" class="active">Anagrafiche</a>
<a href="/customers">Clienti</a>
<a href="/suppliers">Fornitori</a>
<a href="/products">Articoli</a>
</div>
<div class="cards-grid">
<div class="menu-card">
<div class="menu-card-header"><h3>Clienti</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="/customers" class="menu-link"><i class="material-icons ml-icon mtype-form">people</i><span class="ml-text">Clienti</span><i class="material-icons ml-star">star_border</i></a>
<a href="/customers" class="menu-link"><i class="material-icons ml-icon mtype-print">badge</i><span class="ml-text">Schede Cliente</span><i class="material-icons ml-star">star_border</i></a>
<a href="/customers" class="menu-link"><i class="material-icons ml-icon mtype-list">account_balance_wallet</i><span class="ml-text">Fido Cliente</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Fornitori</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="/suppliers" class="menu-link"><i class="material-icons ml-icon mtype-form">local_shipping</i><span class="ml-text">Fornitori</span><i class="material-icons ml-star">star_border</i></a>
<a href="/suppliers" class="menu-link"><i class="material-icons ml-icon mtype-print">badge</i><span class="ml-text">Schede Fornitore</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Articoli</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="/products" class="menu-link"><i class="material-icons ml-icon mtype-form">inventory_2</i><span class="ml-text">Articoli</span><i class="material-icons ml-star">star_border</i></a>
<a href="/warehouse" class="menu-link"><i class="material-icons ml-icon mtype-list">inventory</i><span class="ml-text">Giacenze Articoli</span><i class="material-icons ml-star">star_border</i></a>
<a href="/products" class="menu-link"><i class="material-icons ml-icon mtype-print">sell</i><span class="ml-text">Listini</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Tabelle</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-list">category</i><span class="ml-text">Categorie Clienti</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-list">payments</i><span class="ml-text">Condizioni di Pagamento</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-list">public</i><span class="ml-text">Paesi ISO</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
</div>
{% endblock %}
+80 -63
View File
@@ -3,81 +3,98 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Mago4 Demo - ERP{% endblock %}</title>
<title>{% block title %}Mago4 Demo{% endblock %}</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">
<link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}">
{% block extra_css %}{% endblock %}
</head>
<body>
<div class="app-container">
<!-- Sidebar Navigation -->
<!-- Window tabs (stile desktop Mago4) -->
<div class="window-tabs">
<a href="/" class="window-tab {% if active_module == 'dashboard' %}active{% endif %}">
<i class="material-icons">home</i> Home
</a>
{% block window_tabs %}{% endblock %}
</div>
<!-- Header -->
<header class="app-header">
<button class="header-burger"><i class="material-icons">menu</i></button>
<a href="/" class="logo">
<span class="logo-mark">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M2 20 L8 4 L12 14 L16 4 L22 20 L18 20 L15 11 L12 18 L9 11 L6 20 Z" fill="#1577be"/>
<path d="M16 14 L20 14 L20 9 L22 9 L22 20 L18 20 Z" fill="#5aa632"/>
</svg>
</span>
<span class="logo-text">MAGO</span>
</a>
<div class="header-search">
<i class="material-icons">search</i>
<input type="text" placeholder="Cerca..." onkeydown="if(event.key==='Enter')globalSearch(this.value)">
</div>
<div class="header-right">
<button class="header-icon-btn" title="Lingua"><i class="material-icons">language</i></button>
<div class="header-date">
<span class="label">Data delle operazioni</span>
<span class="value">{{ operation_date }}</span>
</div>
<button class="header-icon-btn" title="Calendario"><i class="material-icons">event</i></button>
<button class="header-icon-btn" title="Profilo"><i class="material-icons">account_circle</i></button>
<button class="header-icon-btn" title="Aggiorna" onclick="location.reload()"><i class="material-icons">refresh</i></button>
</div>
</header>
<div class="app-body">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="logo">
<span class="logo-text">Mago4</span>
<span class="logo-version">Demo</span>
</div>
<div class="sidebar-group-title">
<span class="gt-left">
<span class="gt-mark"><i class="material-icons">apps</i></span>
<span class="gt-text">Mago4</span>
</span>
<i class="material-icons chev">expand_less</i>
</div>
<nav class="sidebar-nav">
<a href="/" class="nav-item {% if request.url.path == '/' %}active{% endif %}">
<i class="material-icons">dashboard</i>
<span>Dashboard</span>
</a>
<a href="/customers" class="nav-item {% if 'customers' in request.url.path %}active{% endif %}">
<i class="material-icons">people</i>
<span>Clienti</span>
</a>
<a href="/suppliers" class="nav-item {% if 'suppliers' in request.url.path %}active{% endif %}">
<i class="material-icons">local_shipping</i>
<span>Fornitori</span>
</a>
<a href="/products" class="nav-item {% if 'products' in request.url.path %}active{% endif %}">
<i class="material-icons">inventory_2</i>
<span>Articoli</span>
</a>
<a href="/sales-orders" class="nav-item {% if 'sales' in request.url.path %}active{% endif %}">
<i class="material-icons">receipt</i>
<span>Ordini Vendita</span>
</a>
<a href="/purchase-orders" class="nav-item {% if 'purchase' in request.url.path %}active{% endif %}">
<i class="material-icons">shopping_cart</i>
<span>Ordini Acquisto</span>
</a>
<a href="/warehouse" class="nav-item {% if 'warehouse' in request.url.path %}active{% endif %}">
<i class="material-icons">warehouse</i>
<span>Magazzino</span>
</a>
</nav>
<a href="/" class="nav-item {% if active_module == 'dashboard' %}active{% endif %}">
<i class="material-icons ic-anagrafiche">dashboard</i> Dashboard
</a>
<a href="/anagrafiche" class="nav-item {% if active_module == 'anagrafiche' %}active{% endif %}">
<i class="material-icons ic-anagrafiche">contacts</i> Anagrafiche
</a>
<a href="/amministrazione" class="nav-item {% if active_module == 'amministrazione' %}active{% endif %}">
<i class="material-icons ic-amministrazione">account_balance</i> Amministrazione
</a>
<a href="/vendite" class="nav-item {% if active_module == 'vendite' %}active{% endif %}">
<i class="material-icons ic-vendite">point_of_sale</i> Vendite
</a>
<a href="/acquisti" class="nav-item {% if active_module == 'acquisti' %}active{% endif %}">
<i class="material-icons ic-acquisti">shopping_cart</i> Acquisti
</a>
<a href="/magazzino" class="nav-item {% if active_module == 'magazzino' %}active{% endif %}">
<i class="material-icons ic-logistica">warehouse</i> Logistica
</a>
<a href="#" class="nav-item disabled">
<i class="material-icons ic-produzione">precision_manufacturing</i> Produzione
</a>
<a href="#" class="nav-item disabled">
<i class="material-icons ic-servizi">build</i> Servizi
</a>
<a href="#" class="nav-item disabled">
<i class="material-icons ic-preferenze">settings</i> Preferenze
</a>
<div class="sidebar-footer">
<div class="version-info">
<span>v1.0.0</span>
<span>In-Memory DB</span>
</div>
<div class="sidebar-sub-group">
<span>TaskBuilder Studio</span>
<i class="material-icons chev" style="font-size:16px;">expand_more</i>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<header class="top-bar">
<div class="top-bar-content">
<h1 class="page-title">{% block page_title %}{% endblock %}</h1>
<div class="top-bar-actions">
<button class="btn-icon" title="Notifiche">
<i class="material-icons">notifications</i>
</button>
<button class="btn-icon" title="Profilo">
<i class="material-icons">account_circle</i>
</button>
</div>
</div>
</header>
<section class="content-area">
{% block content %}{% endblock %}
</section>
<!-- Main content -->
<main class="main">
{% block content %}{% endblock %}
</main>
</div>
+60 -111
View File
@@ -1,53 +1,46 @@
{% extends "base.html" %}
{% block title %}Clienti - Mago4 Demo{% endblock %}
{% block page_title %}Clienti{% endblock %}
{% block window_tabs %}<a href="/customers" class="window-tab active"><i class="material-icons">people</i> Clienti</a>{% endblock %}
{% block content %}
<div class="page-header">
<button class="btn btn-primary" onclick="openCreateCustomerModal()">
<i class="material-icons">add</i> Nuovo Cliente
</button>
<div class="module-bar">
<div class="module-title">Clienti</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/anagrafiche">Anagrafiche</a><span class="sep">»</span><span class="current">Clienti</span></div>
</div>
<div class="tab-strip">
<a href="/anagrafiche">Anagrafiche</a>
<a href="/customers" class="active">Clienti</a>
<a href="/suppliers">Fornitori</a>
<a href="/products">Articoli</a>
</div>
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">Elenco Clienti</h3>
</div>
<div class="widget-body">
<div class="doc-toolbar">
<button class="tb-btn" title="Nuovo" onclick="alert('Nuovo cliente (demo)')"><i class="material-icons">add_box</i></button>
<button class="tb-btn" title="Cerca"><i class="material-icons">search</i></button>
<div class="tb-sep"></div>
<button class="tb-btn" title="Stampa"><i class="material-icons">print</i></button>
<button class="tb-btn" title="Esporta"><i class="material-icons">file_download</i></button>
</div>
<div class="content-pad">
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">people</i> Elenco Clienti</h3></div>
<table class="data-table">
<thead>
<tr>
<th>Codice</th>
<th>Ragione Sociale</th>
<th>Città</th>
<th>Paese</th>
<th>Telefono</th>
<th>Limite Credito</th>
<th>Saldo</th>
<th>Azioni</th>
</tr>
<tr><th>Codice</th><th>Ragione Sociale</th><th>Partita IVA</th><th>Città</th><th>Paese</th><th>Pagamento</th><th class="text-right">Fido</th><th class="text-right">Saldo</th><th></th></tr>
</thead>
<tbody>
{% for customer in customers %}
{% for c in customers %}
<tr>
<td><strong>{{ customer.code }}</strong></td>
<td>{{ customer.name }}</td>
<td>{{ customer.city }}</td>
<td>{{ customer.country }}</td>
<td>{{ customer.phone }}</td>
<td class="text-right">{{ "%.2f"|format(customer.credit_limit) }}</td>
<td class="text-right {% if customer.balance > 0 %}text-danger{% endif %}">
€ {{ "%.2f"|format(customer.balance) }}
</td>
<td>
<button class="btn-small btn-info" onclick="viewCustomer({{ customer.id }})">
<i class="material-icons">visibility</i>
</button>
<button class="btn-small btn-warning" onclick="editCustomer({{ customer.id }})">
<i class="material-icons">edit</i>
</button>
</td>
<td class="text-bold mono">{{ c.code }}</td>
<td>{{ c.name }}</td>
<td class="mono">{{ c.vat_number or '—' }}</td>
<td>{{ c.city }}</td>
<td>{{ c.country }}</td>
<td>{{ c.payment_terms }}</td>
<td class="text-right">€ {{ "%.0f"|format(c.credit_limit) }}</td>
<td class="text-right {% if c.balance > 0 %}text-danger{% endif %}">€ {{ "%.2f"|format(c.balance) }}</td>
<td class="text-center"><button class="btn-icon-sm act-view" onclick="viewCustomer({{ c.id }})"><i class="material-icons">visibility</i></button></td>
</tr>
{% endfor %}
</tbody>
@@ -55,82 +48,38 @@
</div>
</div>
<!-- Modal Detail -->
<div id="customerModal" class="modal" onclick="closeModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modalTitle">Dettagli Cliente</h2>
<button class="btn-close" onclick="closeModal()">
<i class="material-icons">close</i>
</button>
</div>
<div class="modal-body" id="modalBody">
<!-- Content loaded via JS -->
</div>
<div id="genericModal" class="modal" onclick="if(event.target.id==='genericModal')closeGenericModal()">
<div class="modal-content">
<div class="modal-header"><h2 id="gmTitle">Dettagli</h2><button class="modal-close" onclick="closeGenericModal()"><i class="material-icons">close</i></button></div>
<div class="modal-body" id="gmBody"></div>
</div>
</div>
<script>
function viewCustomer(customerId) {
fetch(`/api/customers/${customerId}`)
.then(r => r.json())
.then(data => {
document.getElementById('modalTitle').textContent = `Cliente: ${data.name}`;
document.getElementById('modalBody').innerHTML = `
<div class="detail-grid">
<div class="detail-item">
<label>Codice</label>
<span>${data.code}</span>
</div>
<div class="detail-item">
<label>Ragione Sociale</label>
<span>${data.name}</span>
</div>
<div class="detail-item">
<label>Email</label>
<span>${data.email}</span>
</div>
<div class="detail-item">
<label>Telefono</label>
<span>${data.phone}</span>
</div>
<div class="detail-item">
<label>Indirizzo</label>
<span>${data.address}</span>
</div>
<div class="detail-item">
<label>Città</label>
<span>${data.city}</span>
</div>
<div class="detail-item">
<label>Paese</label>
<span>${data.country}</span>
</div>
<div class="detail-item">
<label>Limite Credito</label>
<span>€ ${data.credit_limit.toFixed(2)}</span>
</div>
<div class="detail-item">
<label>Saldo</label>
<span class="${data.balance > 0 ? 'text-danger' : ''}">${data.balance > 0 ? '€ ' : ''}${Math.abs(data.balance).toFixed(2)}</span>
</div>
</div>
`;
document.getElementById('customerModal').classList.add('show');
});
}
function editCustomer(customerId) {
alert('Modifica cliente: ' + customerId + ' (da implementare)');
}
function openCreateCustomerModal() {
alert('Creazione nuovo cliente (da implementare)');
}
function closeModal(event) {
if (event && event.target.id !== 'customerModal') return;
document.getElementById('customerModal').classList.remove('show');
function viewCustomer(id) {
fetch(`/api/customers/${id}`).then(r=>r.json()).then(d=>{
document.getElementById('gmTitle').textContent = 'Cliente: ' + d.name;
document.getElementById('gmBody').innerHTML = `
<div style="padding:16px">
<div class="form-section-title">Dati Principali</div>
<div class="form-grid-2">
${field('Codice', d.code)}${field('Ragione Sociale', d.name)}
${field('Partita IVA', d.vat_number || '—')}${field('Codice Fiscale', d.fiscal_code || '—')}
${field('Email', d.email)}${field('Telefono', d.phone)}
</div>
<div class="form-section-title">Indirizzo</div>
<div class="form-grid-2">
${field('Indirizzo', d.address)}${field('Città', d.city)}
${field('Paese', d.country)}${field('Pagamento', d.payment_terms)}
</div>
<div class="form-section-title">Dati Commerciali</div>
<div class="form-grid-2">
${field('Limite Credito', '€ ' + d.credit_limit.toFixed(2))}
${field('Saldo', '€ ' + d.balance.toFixed(2))}
</div>
</div>`;
document.getElementById('genericModal').classList.add('show');
});
}
</script>
{% endblock %}
+173 -99
View File
@@ -1,106 +1,180 @@
{% extends "base.html" %}
{% block title %}Dashboard - Mago4 Demo{% endblock %}
{% block page_title %}Dashboard{% endblock %}
{% set status_badge = {'overdue':'badge-danger','due_soon':'badge-warning','open':'badge-info','paid':'badge-success'} %}
{% set status_label = {'overdue':'Scaduta','due_soon':'In scadenza','open':'Aperta','paid':'Saldata'} %}
{% block content %}
<div class="dashboard-container">
<!-- First Row - KPI Cards -->
<div class="widget-row">
{% for widget in widgets[0] %}
<div class="widget-card stat-card" style="border-left-color: var(--color-{{ widget.color }})">
<div class="widget-header">
<h3 class="widget-title">{{ widget.title }}</h3>
</div>
<div class="widget-body stat-body">
<div class="stat-icon" style="color: var(--color-{{ widget.color }})">
<i class="material-icons">{{ widget.icon }}</i>
</div>
<div class="stat-value">{{ widget.value }}</div>
</div>
</div>
{% endfor %}
</div>
<!-- Second Row - Data Tables -->
<div class="widget-row">
{% for widget in widgets[1] %}
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">{{ widget.title }}</h3>
{% if widget.subtitle %}<p class="widget-subtitle">{{ widget.subtitle }}</p>{% endif %}
</div>
<div class="widget-body grid-body">
<table class="data-table">
<thead>
<tr>
{% if "Vendita" in widget.title %}
<th>Data</th>
<th>Numero Ordine</th>
<th>Cliente</th>
<th>Importo</th>
<th>Stato</th>
{% else %}
<th>Data</th>
<th>Numero Ordine</th>
<th>Fornitore</th>
<th>Importo</th>
<th>Stato</th>
{% endif %}
</tr>
</thead>
<tbody>
{% if "Vendita" in widget.title %}
{% for order in widget.data.orders %}
<tr>
<td>{{ order.order_date }}</td>
<td><strong>{{ order.order_number }}</strong></td>
<td>{{ order.customer_name }}</td>
<td class="text-right">{{ order.total_amount }}</td>
<td>
<span class="badge status-{{ order.status }}">{{ order.status }}</span>
</td>
</tr>
{% endfor %}
{% else %}
{% for order in widget.data.orders %}
<tr>
<td>{{ order.order_date }}</td>
<td><strong>{{ order.order_number }}</strong></td>
<td>{{ order.supplier_name }}</td>
<td class="text-right">{{ order.total_amount }}</td>
<td>
<span class="badge status-{{ order.status }}">{{ order.status }}</span>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</div>
<!-- Summary Stats -->
<div class="summary-stats">
<div class="stat-summary">
<div class="stat-label">Totale Clienti</div>
<div class="stat-number">{{ stats.total_customers }}</div>
</div>
<div class="stat-summary">
<div class="stat-label">Totale Fornitori</div>
<div class="stat-number">{{ stats.total_suppliers }}</div>
</div>
<div class="stat-summary">
<div class="stat-label">Articoli in Catalogo</div>
<div class="stat-number">{{ stats.total_products }}</div>
</div>
<div class="stat-summary">
<div class="stat-label">Debiti Fornitori</div>
<div class="stat-number text-orange">€ {{ "%.2f"|format(stats.total_payable) }}</div>
</div>
</div>
<div class="module-bar">
<div class="module-title">Cruscotto Aziendale</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><span class="current">Dashboard</span></div>
</div>
<div class="content-pad">
<!-- Banner allerta scadenze -->
{% if stats.overdue_pay_count or stats.overdue_recv_count %}
<div class="alert alert-danger">
<i class="material-icons">notification_important</i>
<span>
Attenzione: hai
{% if stats.overdue_pay_count %}<b>{{ stats.overdue_pay_count }} pagamenti scaduti</b> (€ {{ "%.2f"|format(stats.overdue_pay_amount) }}){% endif %}
{% if stats.overdue_pay_count and stats.overdue_recv_count %} e {% endif %}
{% if stats.overdue_recv_count %}<b>{{ stats.overdue_recv_count }} incassi scaduti</b> (€ {{ "%.2f"|format(stats.overdue_recv_amount) }}){% endif %}.
<a href="/scadenzario" style="color:inherit;font-weight:700;text-decoration:underline;">Vai allo scadenzario »</a>
</span>
</div>
{% endif %}
<!-- KPI: focus scadenze -->
<div class="kpi-grid">
<div class="kpi-card" style="border-left-color: var(--c-red)">
<div class="kpi-title">Pagamenti Scaduti</div>
<div class="kpi-row">
<div class="kpi-icon"><i class="material-icons" style="color:var(--c-red)">payments</i></div>
<div><div class="kpi-value">€ {{ "{:,.0f}".format(stats.overdue_pay_amount) }}</div>
<div class="text-mute" style="font-size:11px;">{{ stats.overdue_pay_count }} partite</div></div>
</div>
</div>
<div class="kpi-card" style="border-left-color: var(--c-orange)">
<div class="kpi-title">Incassi Scaduti</div>
<div class="kpi-row">
<div class="kpi-icon"><i class="material-icons" style="color:var(--c-orange)">request_quote</i></div>
<div><div class="kpi-value">€ {{ "{:,.0f}".format(stats.overdue_recv_amount) }}</div>
<div class="text-mute" style="font-size:11px;">{{ stats.overdue_recv_count }} partite</div></div>
</div>
</div>
<div class="kpi-card" style="border-left-color: var(--c-blue)">
<div class="kpi-title">Ordini da Evadere</div>
<div class="kpi-row">
<div class="kpi-icon"><i class="material-icons" style="color:var(--c-blue)">local_shipping</i></div>
<div class="kpi-value">{{ stats.sales_to_fulfill }}</div>
</div>
</div>
<div class="kpi-card" style="border-left-color: var(--c-purple)">
<div class="kpi-title">Articoli da Ordinare</div>
<div class="kpi-row">
<div class="kpi-icon"><i class="material-icons" style="color:var(--c-purple)">production_quantity_limits</i></div>
<div class="kpi-value">{{ stats.low_stock_count }}</div>
</div>
</div>
</div>
<!-- Colonna sinistra: scadenze | destra: cose da fare + cashflow -->
<div class="dash-grid-2">
<!-- Scadenze imminenti -->
<div class="panel">
<div class="panel-header">
<h3><i class="material-icons">event_note</i> Scadenze Imminenti (30 giorni)</h3>
<a href="/scadenzario" class="text-blue" style="font-size:12px;text-decoration:none;">Vedi tutto »</a>
</div>
<table class="data-table">
<thead><tr><th>Scadenza</th><th>Tipo</th><th>Soggetto</th><th class="text-right">Importo</th><th class="text-center">Stato</th></tr></thead>
<tbody>
{% for s in upcoming %}
{% set st = sched_status(s) %}
<tr class="row-link {% if st == 'overdue' %}low-stock{% endif %}" onclick="location.href='/scadenzario?type={{ s.type.value }}'">
<td class="text-bold">{{ s.due_date.strftime("%d/%m") }}</td>
<td>
{% if s.type == RECEIVABLE %}<span class="badge badge-load"><i class="material-icons">south_west</i> Inc.</span>
{% else %}<span class="badge badge-unload"><i class="material-icons">north_east</i> Pag.</span>{% endif %}
</td>
<td>{{ s.custsupp_name }}</td>
<td class="text-right text-bold">€ {{ "%.0f"|format(s.amount) }}</td>
<td class="text-center"><span class="badge {{ status_badge[st] }}">{{ status_label[st] }}</span></td>
</tr>
{% endfor %}
{% if not upcoming %}<tr><td colspan="5" class="text-center text-mute" style="padding:20px;">Nessuna scadenza nei prossimi 30 giorni</td></tr>{% endif %}
</tbody>
</table>
</div>
<div>
<!-- Cose da fare -->
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">checklist</i> Cose da Fare</h3></div>
<div class="todo-list">
{% for t in todos %}
<div class="todo-item">
<div class="todo-icon c-{{ t.color }}"><i class="material-icons">{{ t.icon }}</i></div>
<div class="todo-body">
<div class="todo-text">{{ t.text }}</div>
{% if t.amount %}<div class="todo-amount">€ {{ "%.2f"|format(t.amount) }}</div>{% endif %}
</div>
<a href="{{ t.link }}" class="todo-action">{{ t.action }}</a>
</div>
{% endfor %}
{% if not todos %}
<div class="todo-empty"><i class="material-icons">check_circle</i>Tutto sotto controllo!<br>Nessuna attività urgente.</div>
{% endif %}
</div>
</div>
<!-- Previsione di cassa -->
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">account_balance_wallet</i> Previsione di Cassa (30 gg)</h3></div>
<div class="cashflow-box">
<div class="cashflow-row">
<div class="cf-label"><i class="material-icons text-success">south_west</i> Incassi previsti</div>
<div class="cf-value text-success">€ {{ "{:,.2f}".format(cashflow.inflow) }}</div>
</div>
<div class="cashflow-row">
<div class="cf-label"><i class="material-icons text-danger">north_east</i> Pagamenti previsti</div>
<div class="cf-value text-danger"> € {{ "{:,.2f}".format(cashflow.outflow) }}</div>
</div>
<div class="cashflow-row cf-net">
<div class="cf-label">Saldo previsto</div>
<div class="cf-value {% if cashflow.net >= 0 %}text-success{% else %}text-danger{% endif %}">€ {{ "{:,.2f}".format(cashflow.net) }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Riga inferiore: fido superato + ultimi documenti -->
<div class="dash-grid-2">
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">credit_card</i> Controllo Fido Clienti</h3></div>
<table class="data-table">
<thead><tr><th>Cliente</th><th class="text-right">Fido</th><th class="text-right">Esposizione</th><th class="text-center">Stato</th></tr></thead>
<tbody>
{% for c in credit_exceeded %}
<tr class="low-stock">
<td>{{ c.name }}</td>
<td class="text-right">€ {{ "%.0f"|format(c.credit_limit) }}</td>
<td class="text-right text-danger text-bold">€ {{ "%.0f"|format(c.balance) }}</td>
<td class="text-center"><span class="badge badge-danger"><i class="material-icons">warning</i> Superato</span></td>
</tr>
{% endfor %}
{% if not credit_exceeded %}<tr><td colspan="4" class="text-center text-mute" style="padding:20px;">Nessun cliente ha superato il fido</td></tr>{% endif %}
</tbody>
</table>
</div>
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">description</i> Ultimi Documenti di Vendita</h3></div>
<table class="data-table">
<thead><tr><th>Data</th><th>Numero</th><th>Cliente</th><th class="text-right">Totale</th><th>Stato</th></tr></thead>
<tbody>
{% for i in last_invoices %}
<tr class="row-link" onclick="location.href='/invoices/{{ i.id }}'">
<td>{{ i.doc_date.strftime("%d/%m/%Y") }}</td>
<td class="text-bold mono">{{ i.doc_no }}</td>
<td>{{ i.customer_name }}</td>
<td class="text-right">€ {{ "%.2f"|format(i.total) }}</td>
<td><span class="badge status-{{ i.status.value }}">{{ doc_status_label(i.status) }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- KPI riepilogo -->
<div class="kpi-grid" style="margin-top:16px;">
<div class="kpi-card"><div class="kpi-title">Fatturato Emesso</div><div class="kpi-value text-success">€ {{ "{:,.0f}".format(stats.invoiced_total) }}</div></div>
<div class="kpi-card" style="border-left-color: var(--c-teal)"><div class="kpi-title">Crediti Aperti</div><div class="kpi-value">€ {{ "{:,.0f}".format(stats.open_recv_amount) }}</div></div>
<div class="kpi-card" style="border-left-color: var(--c-orange)"><div class="kpi-title">Debiti Aperti</div><div class="kpi-value">€ {{ "{:,.0f}".format(stats.open_pay_amount) }}</div></div>
<div class="kpi-card" style="border-left-color: var(--c-purple)"><div class="kpi-title">Valore Magazzino</div><div class="kpi-value">€ {{ "{:,.0f}".format(stats.warehouse_value) }}</div></div>
</div>
</div>
{% endblock %}
+55
View File
@@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block title %}Movimenti di Magazzino - Mago4 Demo{% endblock %}
{% block window_tabs %}<a href="/inventory-movements" class="window-tab active"><i class="material-icons">swap_vert</i> Movimenti</a>{% endblock %}
{% block content %}
<div class="module-bar">
<div class="module-title">Movimenti di Magazzino</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/magazzino">Logistica</a><span class="sep">»</span><span class="current">Movimenti</span></div>
</div>
<div class="tab-strip">
<a href="/magazzino">Magazzino</a>
<a href="/inventory-movements" class="active">Movimenti</a>
<a href="/warehouse">Giacenze</a>
<a href="/inventory-reasons">Causali</a>
</div>
<div class="doc-toolbar">
<button class="tb-btn" title="Nuovo movimento" onclick="alert('Nuovo movimento (demo)')"><i class="material-icons">add_box</i></button>
<button class="tb-btn" title="Cerca"><i class="material-icons">search</i></button>
<div class="tb-sep"></div>
<button class="tb-btn" title="Stampa giornale"><i class="material-icons">print</i></button>
</div>
<div class="content-pad">
<div class="alert alert-info"><i class="material-icons">info</i> I movimenti <b>confermati</b> aggiornano le giacenze secondo il segno della causale (carico +, scarico ).</div>
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">swap_vert</i> Elenco Movimenti</h3></div>
<table class="data-table">
<thead><tr><th>Data Reg.</th><th>Numero</th><th>Causale</th><th class="text-center">Segno</th><th>Cliente/Fornitore</th><th class="text-center">Deposito</th><th class="text-right">Valore</th><th class="text-center">Stato</th><th></th></tr></thead>
<tbody>
{% for m in movements %}
<tr class="row-link" onclick="location.href='/inventory-movements/{{ m.id }}'">
<td>{{ m.posting_date.strftime("%d/%m/%Y") }}</td>
<td class="text-bold mono">{{ m.doc_no }}</td>
<td><span class="mono">{{ m.reason_code }}</span> — {{ m.reason_description }}</td>
<td class="text-center">
{% if m.sign.value == 'load' %}<span class="badge badge-load"><i class="material-icons">add</i> Carico</span>
{% else %}<span class="badge badge-unload"><i class="material-icons">remove</i> Scarico</span>{% endif %}
</td>
<td>{{ m.custsupp_name or '—' }}</td>
<td class="text-center">{{ m.storage }}</td>
<td class="text-right">€ {{ "%.2f"|format(m.total_amount) }}</td>
<td class="text-center">
{% if m.posted %}<span class="badge badge-success"><i class="material-icons">check</i> Confermato</span>
{% else %}<span class="badge badge-grey">Bozza</span>{% endif %}
</td>
<td class="text-center"><i class="material-icons text-blue" style="font-size:17px;">chevron_right</i></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
+46
View File
@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}Causali di Magazzino - Mago4 Demo{% endblock %}
{% block window_tabs %}<a href="/inventory-reasons" class="window-tab active"><i class="material-icons">rule</i> Causali</a>{% endblock %}
{% block content %}
<div class="module-bar">
<div class="module-title">Causali di Magazzino</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/magazzino">Logistica</a><span class="sep">»</span><span class="current">Causali</span></div>
</div>
<div class="tab-strip">
<a href="/magazzino">Magazzino</a>
<a href="/inventory-movements">Movimenti</a>
<a href="/warehouse">Giacenze</a>
<a href="/inventory-reasons" class="active">Causali</a>
</div>
<div class="doc-toolbar">
<button class="tb-btn" title="Nuova causale" onclick="alert('Nuova causale (demo)')"><i class="material-icons">add_box</i></button>
<button class="tb-btn" title="Stampa"><i class="material-icons">print</i></button>
</div>
<div class="content-pad">
<div class="alert alert-info"><i class="material-icons">info</i> Le causali determinano se un movimento <b>carica</b> o <b>scarica</b> la giacenza (campo <span class="mono">DebitCreditSign</span> in Mago4).</div>
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">rule</i> Elenco Causali</h3></div>
<table class="data-table">
<thead><tr><th>Codice</th><th>Descrizione</th><th class="text-center">Segno</th><th class="text-center">Numerazione Fiscale</th><th class="text-center">Aggiorna Giacenze</th></tr></thead>
<tbody>
{% for r in reasons %}
<tr>
<td class="text-bold mono">{{ r.code }}</td>
<td>{{ r.description }}</td>
<td class="text-center">
{% if r.sign.value == 'load' %}<span class="badge badge-load"><i class="material-icons">add</i> Carico (+)</span>
{% else %}<span class="badge badge-unload"><i class="material-icons">remove</i> Scarico ()</span>{% endif %}
</td>
<td class="text-center">{% if r.fiscal %}<i class="material-icons text-success">check_circle</i>{% else %}<i class="material-icons text-mute">remove</i>{% endif %}</td>
<td class="text-center">{% if r.updates_balance %}<i class="material-icons text-success">check_circle</i>{% else %}<i class="material-icons text-mute">remove</i>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
+141
View File
@@ -0,0 +1,141 @@
{% extends "base.html" %}
{% block title %}{{ invoice.doc_no }} - Mago4 Demo{% endblock %}
{% block window_tabs %}
<a href="/invoices" class="window-tab"><i class="material-icons">description</i> Documenti Vendita</a>
<a href="/invoices/{{ invoice.id }}" class="window-tab active"><i class="material-icons">request_quote</i> {{ invoice.doc_no }}</a>
{% endblock %}
{% block content %}
<div class="module-bar">
<div class="module-title">{{ doc_type_label(invoice.doc_type) }} — {{ invoice.doc_no }}</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/vendite">Vendite</a><span class="sep">»</span><a href="/invoices">Documenti Vendita</a><span class="sep">»</span><span class="current">{{ invoice.doc_no }}</span></div>
</div>
<!-- Toolbar documento -->
<div class="doc-toolbar">
<button class="tb-btn" title="Primo"><i class="material-icons">first_page</i></button>
<button class="tb-btn" title="Precedente"><i class="material-icons">chevron_left</i></button>
<button class="tb-btn" title="Successivo"><i class="material-icons">chevron_right</i></button>
<button class="tb-btn" title="Ultimo"><i class="material-icons">last_page</i></button>
<div class="tb-sep"></div>
<button class="tb-btn" title="Nuovo"><i class="material-icons">note_add</i></button>
<button class="tb-btn" title="Modifica"><i class="material-icons">edit</i></button>
<button class="tb-btn" title="Salva"><i class="material-icons">save</i></button>
<button class="tb-btn" title="Elimina"><i class="material-icons">delete</i></button>
<div class="tb-sep"></div>
<button class="tb-btn" title="Stampa"><i class="material-icons">print</i></button>
<button class="tb-btn" title="Invia email"><i class="material-icons">email</i></button>
<button class="tb-btn" title="Esporta XML (FatturaPA)"><i class="material-icons">file_download</i></button>
<div class="tb-sep"></div>
<a class="tb-btn" href="/invoices" title="Chiudi"><i class="material-icons">logout</i></a>
</div>
<!-- Intestazione documento -->
<div style="background:#fff;border-bottom:1px solid var(--border);padding:14px 20px;">
<div class="form-grid-2" style="margin-bottom:0;">
<div>
<div class="field-row"><label>Numero</label><div class="field-value text-bold">{{ invoice.doc_no }}</div></div>
<div class="field-row"><label>Del</label><div class="field-value">{{ invoice.doc_date.strftime("%d/%m/%Y") }}</div></div>
<div class="field-row"><label>Registrato il</label><div class="field-value">{{ invoice.posting_date.strftime("%d/%m/%Y") }}</div></div>
</div>
<div>
<div class="field-row"><label>Cliente</label><div class="field-value">{{ invoice.customer_name }}</div></div>
<div class="field-row"><label>Partita IVA</label><div class="field-value mono">{{ invoice.customer_vat or '—' }}</div></div>
<div class="field-row"><label>Stato</label><div class="field-value"><span class="badge status-{{ invoice.status.value }}">{{ doc_status_label(invoice.status) }}</span></div></div>
</div>
</div>
</div>
<!-- Form con tab laterali -->
<div class="doc-form">
<div class="doc-form-tabs">
<div class="doc-form-tab active" onclick="showTab(this,'tab-main')"><i class="material-icons">article</i> Dati Principali</div>
<div class="doc-form-tab" onclick="showTab(this,'tab-lines')"><i class="material-icons">list</i> Righe Documento</div>
<div class="doc-form-tab" onclick="showTab(this,'tab-pay')"><i class="material-icons">payments</i> Dati Pagamento</div>
<div class="doc-form-tab" onclick="showTab(this,'tab-notes')"><i class="material-icons">notes</i> Note</div>
</div>
<div class="doc-form-content">
<!-- TAB: Dati principali -->
<div id="tab-main">
<div class="form-section-title">Dati Principali</div>
<div class="form-grid-2">
<div class="field-row"><label>Tipo Documento</label><div class="field-value">{{ doc_type_label(invoice.doc_type) }}</div></div>
<div class="field-row"><label>Valuta</label><div class="field-value">{{ invoice.currency }}</div></div>
<div class="field-row"><label>Listino</label><div class="field-value">{{ invoice.price_list }}</div></div>
<div class="field-row"><label>Agente</label><div class="field-value">{{ invoice.salesperson or '—' }}</div></div>
<div class="field-row"><label>Nostro Rif.</label><div class="field-value">{{ invoice.our_reference or '—' }}</div></div>
<div class="field-row"><label>Vostro Rif.</label><div class="field-value">{{ invoice.your_reference or '—' }}</div></div>
</div>
<div class="form-section-title">Stato Elaborazione</div>
<div class="form-grid-2">
<div class="field-row"><label>Stampato</label><div class="field-value">{{ 'Sì' if invoice.printed else 'No' }}</div></div>
<div class="field-row"><label>Emesso</label><div class="field-value">{{ 'Sì' if invoice.issued else 'No' }}</div></div>
<div class="field-row"><label>Scaricato a Magazzino</label><div class="field-value">{{ 'Sì' if invoice.posted_to_inventory else 'No' }}</div></div>
</div>
</div>
<!-- TAB: Righe -->
<div id="tab-lines" style="display:none;">
<div class="form-section-title">Righe Documento</div>
<table class="data-table-nested">
<thead><tr><th>#</th><th>Articolo</th><th>Descrizione</th><th class="text-center">UM</th><th class="text-right">Q.tà</th><th class="text-right">Val. Unit.</th><th class="text-right">Sconto</th><th class="text-right">Prezzo Netto</th><th class="text-center">IVA</th><th class="text-right">Imponibile</th></tr></thead>
<tbody>
{% for l in invoice.lines %}
<tr>
<td>{{ l.line_no }}</td>
<td class="mono text-bold">{{ l.item_code }}</td>
<td>{{ l.description }}</td>
<td class="text-center">{{ l.uom }}</td>
<td class="text-right">{{ "%.2f"|format(l.quantity) }}</td>
<td class="text-right">€ {{ "%.2f"|format(l.unit_value) }}</td>
<td class="text-right">{{ "%.0f"|format(l.discount_pct) }}%</td>
<td class="text-right">€ {{ "%.2f"|format(l.net_price) }}</td>
<td class="text-center">{{ "%.0f"|format(l.tax_rate) }}%</td>
<td class="text-right text-bold">€ {{ "%.2f"|format(l.taxable_amount) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- TAB: Pagamento -->
<div id="tab-pay" style="display:none;">
<div class="form-section-title">Dati di Pagamento</div>
<div class="form-grid-2">
<div class="field-row"><label>Condizione</label><div class="field-value">{{ invoice.payment_terms }}</div></div>
<div class="field-row"><label>Totale Documento</label><div class="field-value text-bold">€ {{ "%.2f"|format(invoice.total) }}</div></div>
</div>
<div class="form-section-title">Riepilogo IVA</div>
<table class="data-table-nested">
<thead><tr><th>Aliquota</th><th class="text-right">Imponibile</th><th class="text-right">Imposta</th></tr></thead>
<tbody>
<tr><td>22%</td><td class="text-right">€ {{ "%.2f"|format(invoice.taxable_total) }}</td><td class="text-right">€ {{ "%.2f"|format(invoice.tax_total) }}</td></tr>
</tbody>
</table>
</div>
<!-- TAB: Note -->
<div id="tab-notes" style="display:none;">
<div class="form-section-title">Note</div>
<div class="field-value" style="min-height:80px;align-items:flex-start;">{{ invoice.notes or 'Nessuna nota.' }}</div>
</div>
</div>
</div>
<!-- Totali -->
<div class="totals-box">
<div class="t-item"><div class="t-label">Totale Imponibile</div><div class="t-value">€ {{ "%.2f"|format(invoice.taxable_total) }}</div></div>
<div class="t-item"><div class="t-label">Totale IVA</div><div class="t-value">€ {{ "%.2f"|format(invoice.tax_total) }}</div></div>
<div class="t-item grand"><div class="t-label">TOTALE DOCUMENTO</div><div class="t-value">€ {{ "%.2f"|format(invoice.total) }}</div></div>
</div>
<script>
function showTab(el, id) {
document.querySelectorAll('.doc-form-tab').forEach(t => t.classList.remove('active'));
el.classList.add('active');
['tab-main','tab-lines','tab-pay','tab-notes'].forEach(t =>
document.getElementById(t).style.display = (t === id ? 'block' : 'none'));
}
</script>
{% endblock %}
+55
View File
@@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block title %}Documenti Vendita - Mago4 Demo{% endblock %}
{% block window_tabs %}<a href="/invoices" class="window-tab active"><i class="material-icons">description</i> Documenti Vendita</a>{% endblock %}
{% block content %}
<div class="module-bar">
<div class="module-title">Documenti di Vendita</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/vendite">Vendite</a><span class="sep">»</span><span class="current">Documenti Vendita</span></div>
</div>
<div class="tab-strip">
<a href="/invoices" class="{% if selected_type == 'all' %}active{% endif %}">Tutti</a>
<a href="/invoices?type=immediate_invoice" class="{% if selected_type == 'immediate_invoice' %}active{% endif %}">Fatture Immediate</a>
<a href="/invoices?type=delivery_note" class="{% if selected_type == 'delivery_note' %}active{% endif %}">DDT</a>
<a href="/invoices?type=credit_note" class="{% if selected_type == 'credit_note' %}active{% endif %}">Note di Credito</a>
<a href="/invoices?type=proforma_invoice" class="{% if selected_type == 'proforma_invoice' %}active{% endif %}">ProForma</a>
</div>
<div class="doc-toolbar">
<button class="tb-btn" title="Nuovo documento" onclick="alert('Nuovo documento (demo)')"><i class="material-icons">add_box</i></button>
<button class="tb-btn" title="Cerca"><i class="material-icons">search</i></button>
<div class="tb-sep"></div>
<button class="tb-btn" title="Stampa"><i class="material-icons">print</i></button>
<button class="tb-btn" title="Invia email"><i class="material-icons">email</i></button>
<button class="tb-btn" title="Esporta XML"><i class="material-icons">file_download</i></button>
</div>
<div class="content-pad">
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">description</i> Elenco Documenti ({{ invoices|length }})</h3></div>
<table class="data-table">
<thead><tr><th>Tipo</th><th>Numero</th><th>Data</th><th>Cliente</th><th class="text-right">Imponibile</th><th class="text-right">IVA</th><th class="text-right">Totale</th><th>Stato</th><th class="text-center">Fatt.</th><th></th></tr></thead>
<tbody>
{% for i in invoices %}
<tr class="row-link" onclick="location.href='/invoices/{{ i.id }}'">
<td>{{ doc_type_label(i.doc_type) }}</td>
<td class="text-bold mono">{{ i.doc_no }}</td>
<td>{{ i.doc_date.strftime("%d/%m/%Y") }}</td>
<td>{{ i.customer_name }}</td>
<td class="text-right">€ {{ "%.2f"|format(i.taxable_total) }}</td>
<td class="text-right text-mute">€ {{ "%.2f"|format(i.tax_total) }}</td>
<td class="text-right text-bold">€ {{ "%.2f"|format(i.total) }}</td>
<td><span class="badge status-{{ i.status.value }}">{{ doc_status_label(i.status) }}</span></td>
<td class="text-center">
{% if i.issued %}<i class="material-icons text-success" title="Emesso" style="font-size:17px;">check_circle</i>
{% elif i.printed %}<i class="material-icons text-warning" title="Stampato" style="font-size:17px;">print</i>
{% else %}<i class="material-icons text-mute" title="Bozza" style="font-size:17px;">edit</i>{% endif %}
</td>
<td class="text-center"><i class="material-icons text-blue" style="font-size:17px;">chevron_right</i></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
+53
View File
@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block title %}Logistica - Mago4 Demo{% endblock %}
{% block window_tabs %}<a href="/magazzino" class="window-tab active"><i class="material-icons">warehouse</i> Logistica</a>{% endblock %}
{% block content %}
<div class="module-bar">
<div class="module-title">Logistica &amp; Magazzino</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><span class="current">Logistica</span></div>
</div>
<div class="tab-strip">
<a href="/magazzino" class="active">Magazzino</a>
<a href="/inventory-movements">Movimenti</a>
<a href="/warehouse">Giacenze</a>
<a href="/inventory-reasons">Tabelle</a>
</div>
<div class="cards-grid">
<div class="menu-card">
<div class="menu-card-header"><h3>Movimenti di Magazzino</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="/inventory-movements" class="menu-link"><i class="material-icons ml-icon mtype-form">swap_vert</i><span class="ml-text">Movimenti di Magazzino</span><i class="material-icons ml-star">star_border</i></a>
<a href="/inventory-movements" class="menu-link"><i class="material-icons ml-icon mtype-list">list_alt</i><span class="ml-text">Lista Movimenti</span><i class="material-icons ml-star">star_border</i></a>
<a href="/inventory-movements" class="menu-link"><i class="material-icons ml-icon mtype-print">print</i><span class="ml-text">Giornale di Magazzino</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Giacenze</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="/warehouse" class="menu-link"><i class="material-icons ml-icon mtype-list">inventory</i><span class="ml-text">Giacenze Articoli</span><i class="material-icons ml-star">star_border</i></a>
<a href="/warehouse" class="menu-link"><i class="material-icons ml-icon mtype-print">warning</i><span class="ml-text">Articoli Sotto Scorta</span><i class="material-icons ml-star">star_border</i></a>
<a href="/warehouse" class="menu-link"><i class="material-icons ml-icon mtype-list">euro</i><span class="ml-text">Valorizzazione Magazzino</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Tabelle</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="/inventory-reasons" class="menu-link"><i class="material-icons ml-icon mtype-list">rule</i><span class="ml-text">Causali di Magazzino</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-list">warehouse</i><span class="ml-text">Depositi</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-list">place</i><span class="ml-text">Ubicazioni</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Procedure</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-proc">bolt</i><span class="ml-text">Inventario Fisico</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-proc">bolt</i><span class="ml-text">Ricalcolo Giacenze</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
</div>
{% endblock %}
+100
View File
@@ -0,0 +1,100 @@
{% extends "base.html" %}
{% block title %}{{ movement.doc_no }} - Mago4 Demo{% endblock %}
{% block window_tabs %}
<a href="/inventory-movements" class="window-tab"><i class="material-icons">swap_vert</i> Movimenti</a>
<a href="/inventory-movements/{{ movement.id }}" class="window-tab active"><i class="material-icons">inventory</i> {{ movement.doc_no }}</a>
{% endblock %}
{% block content %}
<div class="module-bar">
<div class="module-title">Movimento di Magazzino — {{ movement.doc_no }}</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/magazzino">Logistica</a><span class="sep">»</span><a href="/inventory-movements">Movimenti</a><span class="sep">»</span><span class="current">{{ movement.doc_no }}</span></div>
</div>
<div class="doc-toolbar">
<button class="tb-btn" title="Precedente"><i class="material-icons">chevron_left</i></button>
<button class="tb-btn" title="Successivo"><i class="material-icons">chevron_right</i></button>
<div class="tb-sep"></div>
<button class="tb-btn" title="Modifica"><i class="material-icons">edit</i></button>
<button class="tb-btn" title="Salva"><i class="material-icons">save</i></button>
<button class="tb-btn" title="Stampa"><i class="material-icons">print</i></button>
<div class="tb-sep"></div>
{% if not movement.posted %}
<button class="btn btn-primary" style="margin-left:6px;" onclick="postMovement({{ movement.id }})"><i class="material-icons">check</i> Conferma e aggiorna giacenze</button>
{% endif %}
<a class="tb-btn" href="/inventory-movements" title="Chiudi" style="margin-left:auto;"><i class="material-icons">logout</i></a>
</div>
<div style="background:#fff;border-bottom:1px solid var(--border);padding:14px 20px;">
<div class="form-grid-2" style="margin-bottom:0;">
<div>
<div class="field-row"><label>Numero</label><div class="field-value text-bold">{{ movement.doc_no }}</div></div>
<div class="field-row"><label>Data Documento</label><div class="field-value">{{ movement.doc_date.strftime("%d/%m/%Y") }}</div></div>
<div class="field-row"><label>Data Registrazione</label><div class="field-value">{{ movement.posting_date.strftime("%d/%m/%Y") }}</div></div>
<div class="field-row"><label>Deposito</label><div class="field-value">{{ movement.storage }}</div></div>
</div>
<div>
<div class="field-row"><label>Causale</label><div class="field-value"><span class="mono">{{ movement.reason_code }}</span> — {{ movement.reason_description }}</div></div>
<div class="field-row"><label>Segno</label><div class="field-value">
{% if movement.sign.value == 'load' %}<span class="badge badge-load"><i class="material-icons">add</i> Carico (+)</span>
{% else %}<span class="badge badge-unload"><i class="material-icons">remove</i> Scarico ()</span>{% endif %}
</div></div>
<div class="field-row"><label>Cliente/Fornitore</label><div class="field-value">{{ movement.custsupp_name or '—' }}</div></div>
<div class="field-row"><label>Stato</label><div class="field-value" id="statusField">
{% if movement.posted %}<span class="badge badge-success"><i class="material-icons">check</i> Confermato</span>
{% else %}<span class="badge badge-grey">Bozza</span>{% endif %}
</div></div>
</div>
</div>
</div>
<div class="content-pad">
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">list</i> Righe Movimento</h3></div>
<table class="data-table">
<thead><tr><th>#</th><th>Articolo</th><th>Descrizione</th><th class="text-center">UM</th><th class="text-right">Q.tà</th><th class="text-right">Valore Unit.</th><th class="text-right">Importo</th><th>Lotto</th><th>Ubicazione</th></tr></thead>
<tbody>
{% for l in movement.lines %}
<tr>
<td>{{ l.line_no }}</td>
<td class="mono text-bold">{{ l.item_code }}</td>
<td>{{ l.description }}</td>
<td class="text-center">{{ l.uom }}</td>
<td class="text-right {% if movement.sign.value == 'load' %}text-success{% else %}text-danger{% endif %} text-bold">
{{ '+' if movement.sign.value == 'load' else '' }}{{ "%.2f"|format(l.quantity) }}
</td>
<td class="text-right">€ {{ "%.2f"|format(l.unit_value) }}</td>
<td class="text-right text-bold">€ {{ "%.2f"|format(l.line_amount) }}</td>
<td>{{ l.lot or '—' }}</td>
<td>{{ l.location or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if movement.notes %}
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">notes</i> Note</h3></div>
<div class="panel-body padded">{{ movement.notes }}</div>
</div>
{% endif %}
</div>
<div class="totals-box">
<div class="t-item grand"><div class="t-label">VALORE TOTALE MOVIMENTO</div><div class="t-value">€ {{ "%.2f"|format(movement.total_amount) }}</div></div>
</div>
<script>
function postMovement(id) {
if (!confirm('Confermare il movimento? Le giacenze degli articoli verranno aggiornate.')) return;
fetch(`/inventory-movements/${id}/post`, { method: 'POST' })
.then(r => r.json())
.then(d => {
showNotification('Movimento confermato — giacenze aggiornate');
setTimeout(() => location.reload(), 800);
})
.catch(() => showNotification('Errore nella conferma', 'error'));
}
</script>
{% endblock %}
+67 -121
View File
@@ -1,68 +1,50 @@
{% extends "base.html" %}
{% block title %}Articoli - Mago4 Demo{% endblock %}
{% block page_title %}Articoli{% endblock %}
{% block window_tabs %}<a href="/products" class="window-tab active"><i class="material-icons">inventory_2</i> Articoli</a>{% endblock %}
{% block content %}
<div class="page-header">
<button class="btn btn-primary" onclick="openCreateProductModal()">
<i class="material-icons">add</i> Nuovo Articolo
</button>
{% if low_stock %}
<div class="alert alert-warning">
<i class="material-icons">warning</i>
{{ low_stock|length }} articolo/i sotto la soglia di riordino
</div>
{% endif %}
<div class="module-bar">
<div class="module-title">Articoli</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/anagrafiche">Anagrafiche</a><span class="sep">»</span><span class="current">Articoli</span></div>
</div>
<div class="tab-strip">
<a href="/anagrafiche">Anagrafiche</a>
<a href="/customers">Clienti</a>
<a href="/suppliers">Fornitori</a>
<a href="/products" class="active">Articoli</a>
</div>
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">Catalogo Articoli</h3>
</div>
<div class="widget-body">
<div class="doc-toolbar">
<button class="tb-btn" title="Nuovo" onclick="alert('Nuovo articolo (demo)')"><i class="material-icons">add_box</i></button>
<button class="tb-btn" title="Cerca"><i class="material-icons">search</i></button>
<div class="tb-sep"></div>
<button class="tb-btn" title="Stampa"><i class="material-icons">print</i></button>
</div>
<div class="content-pad">
{% if low_stock %}
<div class="alert alert-warning"><i class="material-icons">warning</i> {{ low_stock|length }} articolo/i sotto la scorta minima</div>
{% endif %}
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">inventory_2</i> Catalogo Articoli</h3></div>
<table class="data-table">
<thead>
<tr>
<th>Codice</th>
<th>Descrizione</th>
<th>Categoria</th>
<th>Prezzo Unitario</th>
<th>Giacenza</th>
<th>Soglia Riordino</th>
<th>Fornitore</th>
<th>Azioni</th>
</tr>
</thead>
<thead><tr><th>Codice</th><th>Descrizione</th><th>Categoria</th><th class="text-center">UM</th><th class="text-right">Prezzo Vend.</th><th class="text-right">Costo</th><th class="text-center">IVA</th><th class="text-right">Giacenza</th><th></th></tr></thead>
<tbody>
{% for product in products %}
<tr class="{% if product.quantity_in_stock <= product.reorder_level %}low-stock{% endif %}">
<td><strong>{{ product.code }}</strong></td>
<td>{{ product.description }}</td>
<td>{{ product.category }}</td>
<td class="text-right">{{ "%.2f"|format(product.unit_price) }}</td>
<td class="text-right {% if product.quantity_in_stock <= product.reorder_level %}text-danger{% endif %}">
{{ product.quantity_in_stock }}
{% if product.quantity_in_stock <= product.reorder_level %}
<i class="material-icons small-icon">warning</i>
{% endif %}
</td>
<td class="text-right">{{ product.reorder_level }}</td>
<td>
{% if product.supplier_id %}
Fornitore #{{ product.supplier_id }}
{% else %}
{% endif %}
</td>
<td>
<button class="btn-small btn-info" onclick="viewProduct({{ product.id }})">
<i class="material-icons">visibility</i>
</button>
<button class="btn-small btn-warning" onclick="editProduct({{ product.id }})">
<i class="material-icons">edit</i>
</button>
{% for p in products %}
<tr class="{% if p.quantity_in_stock <= p.reorder_level %}low-stock{% endif %}">
<td class="text-bold mono">{{ p.code }}</td>
<td>{{ p.description }}</td>
<td>{{ p.category }}</td>
<td class="text-center">{{ p.uom }}</td>
<td class="text-right">€ {{ "%.2f"|format(p.unit_price) }}</td>
<td class="text-right text-mute">€ {{ "%.2f"|format(p.purchase_price) }}</td>
<td class="text-center">{{ "%.0f"|format(p.tax_rate) }}%</td>
<td class="text-right {% if p.quantity_in_stock <= p.reorder_level %}text-danger text-bold{% endif %}">
{{ p.quantity_in_stock }} {{ p.uom }}
{% if p.quantity_in_stock <= p.reorder_level %}<i class="material-icons" style="font-size:14px;vertical-align:middle;">warning</i>{% endif %}
</td>
<td class="text-center"><button class="btn-icon-sm act-view" onclick="viewProduct({{ p.id }})"><i class="material-icons">visibility</i></button></td>
</tr>
{% endfor %}
</tbody>
@@ -70,75 +52,39 @@
</div>
</div>
<div id="productModal" class="modal" onclick="closeModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modalTitle">Dettagli Articolo</h2>
<button class="btn-close" onclick="closeModal()">
<i class="material-icons">close</i>
</button>
</div>
<div class="modal-body" id="modalBody">
</div>
<div id="genericModal" class="modal" onclick="if(event.target.id==='genericModal')closeGenericModal()">
<div class="modal-content">
<div class="modal-header"><h2 id="gmTitle">Dettagli</h2><button class="modal-close" onclick="closeGenericModal()"><i class="material-icons">close</i></button></div>
<div class="modal-body" id="gmBody"></div>
</div>
</div>
<script>
function viewProduct(productId) {
fetch(`/api/products/${productId}`)
.then(r => r.json())
.then(data => {
const isLowStock = data.quantity_in_stock <= data.reorder_level;
document.getElementById('modalTitle').textContent = `Articolo: ${data.code}`;
document.getElementById('modalBody').innerHTML = `
<div class="detail-grid">
<div class="detail-item">
<label>Codice</label>
<span>${data.code}</span>
</div>
<div class="detail-item">
<label>Descrizione</label>
<span>${data.description}</span>
</div>
<div class="detail-item">
<label>Categoria</label>
<span>${data.category}</span>
</div>
<div class="detail-item">
<label>Prezzo Unitario</label>
<span>€ ${data.unit_price.toFixed(2)}</span>
</div>
<div class="detail-item">
<label>Giacenza Attuale</label>
<span class="${isLowStock ? 'text-danger' : ''}">
${data.quantity_in_stock} ${isLowStock ? '⚠️ SOTTOSORTA' : ''}
</span>
</div>
<div class="detail-item">
<label>Soglia Riordino</label>
<span>${data.reorder_level}</span>
</div>
<div class="detail-item">
<label>Fornitore</label>
<span>${data.supplier_id ? 'Fornitore #' + data.supplier_id : '—'}</span>
</div>
</div>
`;
document.getElementById('productModal').classList.add('show');
});
}
function editProduct(productId) {
alert('Modifica articolo: ' + productId + ' (da implementare)');
}
function openCreateProductModal() {
alert('Creazione nuovo articolo (da implementare)');
}
function closeModal(event) {
if (event && event.target.id !== 'productModal') return;
document.getElementById('productModal').classList.remove('show');
function viewProduct(id) {
fetch(`/api/products/${id}`).then(r=>r.json()).then(d=>{
const low = d.quantity_in_stock <= d.reorder_level;
openGenericModal('Articolo: ' + d.code, `
<div style="padding:16px">
<div class="form-section-title">Dati Principali</div>
<div class="form-grid-2">
${field('Codice', d.code)}${field('Descrizione', d.description)}
${field('Categoria', d.category)}${field('Unità di Misura', d.uom)}
</div>
<div class="form-section-title">Dati Commerciali</div>
<div class="form-grid-2">
${field('Prezzo di Vendita', '€ ' + d.unit_price.toFixed(2))}
${field('Costo di Acquisto', '€ ' + d.purchase_price.toFixed(2))}
${field('Aliquota IVA', d.tax_rate.toFixed(0) + '%')}
</div>
<div class="form-section-title">Dati Magazzino</div>
<div class="form-grid-2">
${field('Giacenza', '<span class="' + (low?'text-danger text-bold':'') + '">' + d.quantity_in_stock + ' ' + d.uom + (low?' ⚠ sottoscorta':'') + '</span>')}
${field('Scorta Minima', d.reorder_level + ' ' + d.uom)}
${field('Scorta Massima', d.max_stock + ' ' + d.uom)}
${field('Valore Giacenza', '€ ' + (d.quantity_in_stock * d.purchase_price).toFixed(2))}
</div>
</div>`);
});
}
</script>
{% endblock %}
+49 -123
View File
@@ -1,60 +1,40 @@
{% extends "base.html" %}
{% block title %}Ordini Acquisto - Mago4 Demo{% endblock %}
{% block page_title %}Ordini Acquisto{% endblock %}
{% block title %}Ordini Fornitore - Mago4 Demo{% endblock %}
{% block window_tabs %}<a href="/purchase-orders" class="window-tab active"><i class="material-icons">shopping_cart</i> Ordini Fornitore</a>{% endblock %}
{% block content %}
<div class="page-header">
<button class="btn btn-primary" onclick="openCreateOrderModal()">
<i class="material-icons">add</i> Nuovo Ordine
</button>
{% if pending %}
<div class="alert alert-info">
<i class="material-icons">info</i>
{{ pending|length }} ordine/i in sospeso
</div>
{% endif %}
<div class="module-bar">
<div class="module-title">Ordini Fornitore</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/acquisti">Acquisti</a><span class="sep">»</span><span class="current">Ordini Fornitore</span></div>
</div>
<div class="tab-strip">
<a href="/purchase-orders" class="active">Ordini Fornitore</a>
<a href="/acquisti">Documenti Acquisto</a>
</div>
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">Elenco Ordini Acquisto</h3>
</div>
<div class="widget-body">
<div class="doc-toolbar">
<button class="tb-btn" title="Nuovo" onclick="alert('Nuovo ordine (demo)')"><i class="material-icons">add_box</i></button>
<button class="tb-btn" title="Cerca"><i class="material-icons">search</i></button>
<div class="tb-sep"></div>
<button class="tb-btn" title="Stampa"><i class="material-icons">print</i></button>
</div>
<div class="content-pad">
{% if pending %}<div class="alert alert-info"><i class="material-icons">info</i> {{ pending|length }} ordine/i in sospeso</div>{% endif %}
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">shopping_cart</i> Elenco Ordini Fornitore</h3></div>
<table class="data-table">
<thead>
<tr>
<th>Data</th>
<th>Numero Ordine</th>
<th>Fornitore</th>
<th>Importo Totale</th>
<th>Data Consegna Prevista</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<thead><tr><th>Data</th><th>Numero</th><th>Fornitore</th><th class="text-right">Importo</th><th>Consegna Prevista</th><th>Stato</th><th></th></tr></thead>
<tbody>
{% for order in orders %}
{% for o in orders %}
<tr>
<td>{{ order.order_date.strftime("%d/%m/%Y") }}</td>
<td><strong>{{ order.order_number }}</strong></td>
<td>{{ order.supplier_name }}</td>
<td class="text-right">€ {{ "%.2f"|format(order.total_amount) }}</td>
<td>
{% if order.expected_delivery_date %}
{{ order.expected_delivery_date.strftime("%d/%m/%Y") }}
{% else %}
{% endif %}
</td>
<td>
<span class="badge status-{{ order.status.value }}">{{ order.status.value }}</span>
</td>
<td>
<button class="btn-small btn-info" onclick="viewOrder({{ order.id }})">
<i class="material-icons">visibility</i>
</button>
</td>
<td>{{ o.order_date.strftime("%d/%m/%Y") }}</td>
<td class="text-bold mono">{{ o.order_number }}</td>
<td>{{ o.supplier_name }}</td>
<td class="text-right">€ {{ "%.2f"|format(o.total_amount) }}</td>
<td>{{ o.expected_delivery_date.strftime("%d/%m/%Y") if o.expected_delivery_date else '—' }}</td>
<td><span class="badge status-{{ o.status.value }}">{{ o.status.value }}</span></td>
<td class="text-center"><button class="btn-icon-sm act-view" onclick="viewOrder({{ o.id }})"><i class="material-icons">visibility</i></button></td>
</tr>
{% endfor %}
</tbody>
@@ -62,84 +42,30 @@
</div>
</div>
<div id="orderModal" class="modal" onclick="closeModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modalTitle">Dettagli Ordine Acquisto</h2>
<button class="btn-close" onclick="closeModal()">
<i class="material-icons">close</i>
</button>
</div>
<div class="modal-body" id="modalBody">
</div>
<div id="genericModal" class="modal" onclick="if(event.target.id==='genericModal')closeGenericModal()">
<div class="modal-content">
<div class="modal-header"><h2 id="gmTitle">Ordine</h2><button class="modal-close" onclick="closeGenericModal()"><i class="material-icons">close</i></button></div>
<div class="modal-body" id="gmBody"></div>
</div>
</div>
<script>
function viewOrder(orderId) {
fetch(`/api/purchase-orders/${orderId}`)
.then(r => r.json())
.then(data => {
const deliveryDate = data.expected_delivery_date ? new Date(data.expected_delivery_date).toLocaleDateString('it-IT') : '—';
const orderDate = new Date(data.order_date).toLocaleDateString('it-IT');
let linesHtml = '<table class="data-table-nested"><thead><tr><th>Articolo</th><th>Quantità</th><th>Prezzo Unit.</th><th>Totale</th></tr></thead><tbody>';
data.lines.forEach(line => {
linesHtml += `<tr>
<td>${line.product_description} (${line.product_code})</td>
<td class="text-right">${line.quantity}</td>
<td class="text-right">€ ${line.unit_price.toFixed(2)}</td>
<td class="text-right">€ ${line.total_amount.toFixed(2)}</td>
</tr>`;
});
linesHtml += '</tbody></table>';
document.getElementById('modalTitle').textContent = `Ordine: ${data.order_number}`;
document.getElementById('modalBody').innerHTML = `
<div class="detail-grid">
<div class="detail-item">
<label>Numero Ordine</label>
<span>${data.order_number}</span>
</div>
<div class="detail-item">
<label>Data Ordine</label>
<span>${orderDate}</span>
</div>
<div class="detail-item">
<label>Fornitore</label>
<span>${data.supplier_name}</span>
</div>
<div class="detail-item">
<label>Stato</label>
<span class="badge status-${data.status}">
${data.status.replace('_', ' ').toUpperCase()}
</span>
</div>
<div class="detail-item">
<label>Data Consegna Prevista</label>
<span>${deliveryDate}</span>
</div>
<div class="detail-item">
<label>Importo Totale</label>
<span class="text-orange" style="font-size: 1.2em; font-weight: bold;">€ ${data.total_amount.toFixed(2)}</span>
</div>
</div>
<div style="margin-top: 20px;">
<h4>Righe Ordine</h4>
${linesHtml}
</div>
`;
document.getElementById('orderModal').classList.add('show');
});
}
function openCreateOrderModal() {
alert('Creazione nuovo ordine (da implementare)');
}
function closeModal(event) {
if (event && event.target.id !== 'orderModal') return;
document.getElementById('orderModal').classList.remove('show');
function viewOrder(id) {
fetch(`/api/purchase-orders/${id}`).then(r=>r.json()).then(d=>{
let rows = d.lines.map(l=>`<tr><td>${l.product_description} <span class="text-mute">(${l.product_code})</span></td>
<td class="text-right">${l.quantity}</td><td class="text-right">€ ${l.unit_price.toFixed(2)}</td>
<td class="text-right">€ ${l.total_amount.toFixed(2)}</td></tr>`).join('');
openGenericModal('Ordine: ' + d.order_number, `
<div style="padding:16px">
<div class="form-grid-2">
${field('Numero Ordine', d.order_number)}${field('Data', fmtDate(d.order_date))}
${field('Fornitore', d.supplier_name)}${field('Stato', d.status)}
${field('Consegna Prevista', fmtDate(d.expected_delivery_date))}${field('Totale', '€ ' + d.total_amount.toFixed(2))}
</div>
<div class="form-section-title">Righe Ordine</div>
<table class="data-table-nested"><thead><tr><th>Articolo</th><th class="text-right">Q.tà</th><th class="text-right">Prezzo</th><th class="text-right">Totale</th></tr></thead><tbody>${rows}</tbody></table>
</div>`);
});
}
</script>
{% endblock %}
+49 -123
View File
@@ -1,60 +1,40 @@
{% extends "base.html" %}
{% block title %}Ordini Vendita - Mago4 Demo{% endblock %}
{% block page_title %}Ordini Vendita{% endblock %}
{% block title %}Ordini Cliente - Mago4 Demo{% endblock %}
{% block window_tabs %}<a href="/sales-orders" class="window-tab active"><i class="material-icons">receipt_long</i> Ordini Cliente</a>{% endblock %}
{% block content %}
<div class="page-header">
<button class="btn btn-primary" onclick="openCreateOrderModal()">
<i class="material-icons">add</i> Nuovo Ordine
</button>
{% if pending %}
<div class="alert alert-info">
<i class="material-icons">info</i>
{{ pending|length }} ordine/i in sospeso
</div>
{% endif %}
<div class="module-bar">
<div class="module-title">Ordini Cliente</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/vendite">Vendite</a><span class="sep">»</span><span class="current">Ordini Cliente</span></div>
</div>
<div class="tab-strip">
<a href="/sales-orders" class="active">Ordini Cliente</a>
<a href="/vendite">Documenti Vendita</a>
</div>
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">Elenco Ordini Vendita</h3>
</div>
<div class="widget-body">
<div class="doc-toolbar">
<button class="tb-btn" title="Nuovo" onclick="alert('Nuovo ordine (demo)')"><i class="material-icons">add_box</i></button>
<button class="tb-btn" title="Cerca"><i class="material-icons">search</i></button>
<div class="tb-sep"></div>
<button class="tb-btn" title="Stampa"><i class="material-icons">print</i></button>
</div>
<div class="content-pad">
{% if pending %}<div class="alert alert-info"><i class="material-icons">info</i> {{ pending|length }} ordine/i in sospeso</div>{% endif %}
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">receipt_long</i> Elenco Ordini Cliente</h3></div>
<table class="data-table">
<thead>
<tr>
<th>Data</th>
<th>Numero Ordine</th>
<th>Cliente</th>
<th>Importo Totale</th>
<th>Data Consegna</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<thead><tr><th>Data</th><th>Numero</th><th>Cliente</th><th class="text-right">Importo</th><th>Consegna</th><th>Stato</th><th></th></tr></thead>
<tbody>
{% for order in orders %}
{% for o in orders %}
<tr>
<td>{{ order.order_date.strftime("%d/%m/%Y") }}</td>
<td><strong>{{ order.order_number }}</strong></td>
<td>{{ order.customer_name }}</td>
<td class="text-right">€ {{ "%.2f"|format(order.total_amount) }}</td>
<td>
{% if order.delivery_date %}
{{ order.delivery_date.strftime("%d/%m/%Y") }}
{% else %}
{% endif %}
</td>
<td>
<span class="badge status-{{ order.status.value }}">{{ order.status.value }}</span>
</td>
<td>
<button class="btn-small btn-info" onclick="viewOrder({{ order.id }})">
<i class="material-icons">visibility</i>
</button>
</td>
<td>{{ o.order_date.strftime("%d/%m/%Y") }}</td>
<td class="text-bold mono">{{ o.order_number }}</td>
<td>{{ o.customer_name }}</td>
<td class="text-right">€ {{ "%.2f"|format(o.total_amount) }}</td>
<td>{{ o.delivery_date.strftime("%d/%m/%Y") if o.delivery_date else '—' }}</td>
<td><span class="badge status-{{ o.status.value }}">{{ o.status.value }}</span></td>
<td class="text-center"><button class="btn-icon-sm act-view" onclick="viewOrder({{ o.id }})"><i class="material-icons">visibility</i></button></td>
</tr>
{% endfor %}
</tbody>
@@ -62,84 +42,30 @@
</div>
</div>
<div id="orderModal" class="modal" onclick="closeModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modalTitle">Dettagli Ordine Vendita</h2>
<button class="btn-close" onclick="closeModal()">
<i class="material-icons">close</i>
</button>
</div>
<div class="modal-body" id="modalBody">
</div>
<div id="genericModal" class="modal" onclick="if(event.target.id==='genericModal')closeGenericModal()">
<div class="modal-content">
<div class="modal-header"><h2 id="gmTitle">Ordine</h2><button class="modal-close" onclick="closeGenericModal()"><i class="material-icons">close</i></button></div>
<div class="modal-body" id="gmBody"></div>
</div>
</div>
<script>
function viewOrder(orderId) {
fetch(`/api/sales-orders/${orderId}`)
.then(r => r.json())
.then(data => {
const deliveryDate = data.delivery_date ? new Date(data.delivery_date).toLocaleDateString('it-IT') : '—';
const orderDate = new Date(data.order_date).toLocaleDateString('it-IT');
let linesHtml = '<table class="data-table-nested"><thead><tr><th>Articolo</th><th>Quantità</th><th>Prezzo Unit.</th><th>Totale</th></tr></thead><tbody>';
data.lines.forEach(line => {
linesHtml += `<tr>
<td>${line.product_description} (${line.product_code})</td>
<td class="text-right">${line.quantity}</td>
<td class="text-right">€ ${line.unit_price.toFixed(2)}</td>
<td class="text-right">€ ${line.total_amount.toFixed(2)}</td>
</tr>`;
});
linesHtml += '</tbody></table>';
document.getElementById('modalTitle').textContent = `Ordine: ${data.order_number}`;
document.getElementById('modalBody').innerHTML = `
<div class="detail-grid">
<div class="detail-item">
<label>Numero Ordine</label>
<span>${data.order_number}</span>
</div>
<div class="detail-item">
<label>Data Ordine</label>
<span>${orderDate}</span>
</div>
<div class="detail-item">
<label>Cliente</label>
<span>${data.customer_name}</span>
</div>
<div class="detail-item">
<label>Stato</label>
<span class="badge status-${data.status}">
${data.status.replace('_', ' ').toUpperCase()}
</span>
</div>
<div class="detail-item">
<label>Data Consegna Prevista</label>
<span>${deliveryDate}</span>
</div>
<div class="detail-item">
<label>Importo Totale</label>
<span class="text-success" style="font-size: 1.2em; font-weight: bold;">€ ${data.total_amount.toFixed(2)}</span>
</div>
</div>
<div style="margin-top: 20px;">
<h4>Righe Ordine</h4>
${linesHtml}
</div>
`;
document.getElementById('orderModal').classList.add('show');
});
}
function openCreateOrderModal() {
alert('Creazione nuovo ordine (da implementare)');
}
function closeModal(event) {
if (event && event.target.id !== 'orderModal') return;
document.getElementById('orderModal').classList.remove('show');
function viewOrder(id) {
fetch(`/api/sales-orders/${id}`).then(r=>r.json()).then(d=>{
let rows = d.lines.map(l=>`<tr><td>${l.product_description} <span class="text-mute">(${l.product_code})</span></td>
<td class="text-right">${l.quantity}</td><td class="text-right">€ ${l.unit_price.toFixed(2)}</td>
<td class="text-right">€ ${l.total_amount.toFixed(2)}</td></tr>`).join('');
openGenericModal('Ordine: ' + d.order_number, `
<div style="padding:16px">
<div class="form-grid-2">
${field('Numero Ordine', d.order_number)}${field('Data', fmtDate(d.order_date))}
${field('Cliente', d.customer_name)}${field('Stato', d.status)}
${field('Consegna Prevista', fmtDate(d.delivery_date))}${field('Totale', '€ ' + d.total_amount.toFixed(2))}
</div>
<div class="form-section-title">Righe Ordine</div>
<table class="data-table-nested"><thead><tr><th>Articolo</th><th class="text-right">Q.tà</th><th class="text-right">Prezzo</th><th class="text-right">Totale</th></tr></thead><tbody>${rows}</tbody></table>
</div>`);
});
}
</script>
{% endblock %}
+78
View File
@@ -0,0 +1,78 @@
{% extends "base.html" %}
{% block title %}Scadenzario - Mago4 Demo{% endblock %}
{% block window_tabs %}<a href="/scadenzario" class="window-tab active"><i class="material-icons">event_note</i> Scadenzario</a>{% endblock %}
{% set status_badge = {'overdue':'badge-danger','due_soon':'badge-warning','open':'badge-info','paid':'badge-success'} %}
{% set status_label = {'overdue':'Scaduta','due_soon':'In scadenza','open':'Aperta','paid':'Saldata'} %}
{% block content %}
<div class="module-bar">
<div class="module-title">Scadenzario — Partite Aperte</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/amministrazione">Amministrazione</a><span class="sep">»</span><span class="current">Scadenzario</span></div>
</div>
<div class="tab-strip">
<a href="/scadenzario" class="{% if selected_type == 'all' %}active{% endif %}">Tutte</a>
<a href="/scadenzario?type=receivable" class="{% if selected_type == 'receivable' %}active{% endif %}">Incassi (Crediti)</a>
<a href="/scadenzario?type=payable" class="{% if selected_type == 'payable' %}active{% endif %}">Pagamenti (Debiti)</a>
</div>
<div class="doc-toolbar">
<button class="tb-btn" title="Nuova partita" onclick="alert('Nuova partita (demo)')"><i class="material-icons">add_box</i></button>
<button class="tb-btn" title="Cerca"><i class="material-icons">search</i></button>
<div class="tb-sep"></div>
<button class="tb-btn" title="Stampa scadenzario"><i class="material-icons">print</i></button>
<button class="tb-btn" title="Presentazione effetti"><i class="material-icons">request_page</i></button>
</div>
<div class="content-pad">
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">event_note</i> Partite ({{ schedules|length }})</h3></div>
<table class="data-table">
<thead><tr><th>Tipo</th><th>Scadenza</th><th class="text-center">Giorni</th><th>Cliente/Fornitore</th><th>Documento</th><th class="text-center">Modalità</th><th class="text-right">Importo</th><th class="text-center">Stato</th><th></th></tr></thead>
<tbody>
{% for s in schedules %}
{% set st = sched_status(s) %}
{% set days = sched_days(s) %}
<tr {% if st == 'overdue' %}class="low-stock"{% endif %}>
<td>
{% if s.type == RECEIVABLE %}<span class="badge badge-load"><i class="material-icons">south_west</i> Incasso</span>
{% else %}<span class="badge badge-unload"><i class="material-icons">north_east</i> Pagamento</span>{% endif %}
</td>
<td class="text-bold">{{ s.due_date.strftime("%d/%m/%Y") }}</td>
<td class="text-center">
{% if s.closed %}<span class="text-mute"></span>
{% elif days < 0 %}<span class="text-danger text-bold">{{ days }}</span>
{% elif days <= 7 %}<span class="text-warning text-bold">+{{ days }}</span>
{% else %}<span class="text-mute">+{{ days }}</span>{% endif %}
</td>
<td>{{ s.custsupp_name }}</td>
<td><span class="mono">{{ s.doc_no }}</span>{% if s.is_credit_note %} <span class="badge badge-grey">NC</span>{% endif %}</td>
<td class="text-center">{{ s.payment_method }}</td>
<td class="text-right text-bold {% if s.is_credit_note %}text-danger{% endif %}">
{% if s.is_credit_note %}{% endif %}€ {{ "%.2f"|format(s.amount) }}
</td>
<td class="text-center"><span class="badge {{ status_badge[st] }}">{{ status_label[st] }}</span></td>
<td class="text-center">
{% if not s.closed %}
<button class="btn-icon-sm act-cart" title="{{ 'Registra incasso' if s.type == RECEIVABLE else 'Registra pagamento' }}" onclick="settle({{ s.id }})"><i class="material-icons">task_alt</i></button>
{% else %}
<i class="material-icons text-success" style="font-size:17px;" title="Saldata">check_circle</i>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script>
function settle(id) {
if (!confirm('Registrare l\'avvenuto pagamento/incasso della partita?')) return;
fetch(`/scadenzario/${id}/settle`, { method: 'POST' })
.then(r => r.json())
.then(() => { showNotification('Partita saldata'); setTimeout(() => location.reload(), 700); })
.catch(() => showNotification('Errore', 'error'));
}
</script>
{% endblock %}
+51 -111
View File
@@ -1,53 +1,42 @@
{% extends "base.html" %}
{% block title %}Fornitori - Mago4 Demo{% endblock %}
{% block page_title %}Fornitori{% endblock %}
{% block window_tabs %}<a href="/suppliers" class="window-tab active"><i class="material-icons">local_shipping</i> Fornitori</a>{% endblock %}
{% block content %}
<div class="page-header">
<button class="btn btn-primary" onclick="openCreateSupplierModal()">
<i class="material-icons">add</i> Nuovo Fornitore
</button>
<div class="module-bar">
<div class="module-title">Fornitori</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/anagrafiche">Anagrafiche</a><span class="sep">»</span><span class="current">Fornitori</span></div>
</div>
<div class="tab-strip">
<a href="/anagrafiche">Anagrafiche</a>
<a href="/customers">Clienti</a>
<a href="/suppliers" class="active">Fornitori</a>
<a href="/products">Articoli</a>
</div>
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">Elenco Fornitori</h3>
</div>
<div class="widget-body">
<div class="doc-toolbar">
<button class="tb-btn" title="Nuovo" onclick="alert('Nuovo fornitore (demo)')"><i class="material-icons">add_box</i></button>
<button class="tb-btn" title="Cerca"><i class="material-icons">search</i></button>
<div class="tb-sep"></div>
<button class="tb-btn" title="Stampa"><i class="material-icons">print</i></button>
</div>
<div class="content-pad">
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">local_shipping</i> Elenco Fornitori</h3></div>
<table class="data-table">
<thead>
<tr>
<th>Codice</th>
<th>Ragione Sociale</th>
<th>Città</th>
<th>Paese</th>
<th>Telefono</th>
<th>Termini Pagamento</th>
<th>Saldo</th>
<th>Azioni</th>
</tr>
</thead>
<thead><tr><th>Codice</th><th>Ragione Sociale</th><th>Partita IVA</th><th>Città</th><th>Paese</th><th class="text-center">Pagamento</th><th class="text-right">Saldo</th><th></th></tr></thead>
<tbody>
{% for supplier in suppliers %}
{% for s in suppliers %}
<tr>
<td><strong>{{ supplier.code }}</strong></td>
<td>{{ supplier.name }}</td>
<td>{{ supplier.city }}</td>
<td>{{ supplier.country }}</td>
<td>{{ supplier.phone }}</td>
<td class="text-center">{{ supplier.payment_terms_days }} gg</td>
<td class="text-right {% if supplier.balance > 0 %}text-warning{% endif %}">
€ {{ "%.2f"|format(supplier.balance) }}
</td>
<td>
<button class="btn-small btn-info" onclick="viewSupplier({{ supplier.id }})">
<i class="material-icons">visibility</i>
</button>
<button class="btn-small btn-warning" onclick="editSupplier({{ supplier.id }})">
<i class="material-icons">edit</i>
</button>
</td>
<td class="text-bold mono">{{ s.code }}</td>
<td>{{ s.name }}</td>
<td class="mono">{{ s.vat_number or '—' }}</td>
<td>{{ s.city }}</td>
<td>{{ s.country }}</td>
<td class="text-center">{{ s.payment_terms_days }} gg</td>
<td class="text-right {% if s.balance > 0 %}text-warning{% endif %}">€ {{ "%.2f"|format(s.balance) }}</td>
<td class="text-center"><button class="btn-icon-sm act-view" onclick="viewSupplier({{ s.id }})"><i class="material-icons">visibility</i></button></td>
</tr>
{% endfor %}
</tbody>
@@ -55,80 +44,31 @@
</div>
</div>
<div id="supplierModal" class="modal" onclick="closeModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modalTitle">Dettagli Fornitore</h2>
<button class="btn-close" onclick="closeModal()">
<i class="material-icons">close</i>
</button>
</div>
<div class="modal-body" id="modalBody">
</div>
<div id="genericModal" class="modal" onclick="if(event.target.id==='genericModal')closeGenericModal()">
<div class="modal-content">
<div class="modal-header"><h2 id="gmTitle">Dettagli</h2><button class="modal-close" onclick="closeGenericModal()"><i class="material-icons">close</i></button></div>
<div class="modal-body" id="gmBody"></div>
</div>
</div>
<script>
function viewSupplier(supplierId) {
fetch(`/api/suppliers/${supplierId}`)
.then(r => r.json())
.then(data => {
document.getElementById('modalTitle').textContent = `Fornitore: ${data.name}`;
document.getElementById('modalBody').innerHTML = `
<div class="detail-grid">
<div class="detail-item">
<label>Codice</label>
<span>${data.code}</span>
</div>
<div class="detail-item">
<label>Ragione Sociale</label>
<span>${data.name}</span>
</div>
<div class="detail-item">
<label>Email</label>
<span>${data.email}</span>
</div>
<div class="detail-item">
<label>Telefono</label>
<span>${data.phone}</span>
</div>
<div class="detail-item">
<label>Indirizzo</label>
<span>${data.address}</span>
</div>
<div class="detail-item">
<label>Città</label>
<span>${data.city}</span>
</div>
<div class="detail-item">
<label>Paese</label>
<span>${data.country}</span>
</div>
<div class="detail-item">
<label>Termini Pagamento</label>
<span>${data.payment_terms_days} giorni</span>
</div>
<div class="detail-item">
<label>Saldo Debito</label>
<span class="${data.balance > 0 ? 'text-warning' : ''}">€ ${data.balance.toFixed(2)}</span>
</div>
</div>
`;
document.getElementById('supplierModal').classList.add('show');
});
}
function editSupplier(supplierId) {
alert('Modifica fornitore: ' + supplierId + ' (da implementare)');
}
function openCreateSupplierModal() {
alert('Creazione nuovo fornitore (da implementare)');
}
function closeModal(event) {
if (event && event.target.id !== 'supplierModal') return;
document.getElementById('supplierModal').classList.remove('show');
function viewSupplier(id) {
fetch(`/api/suppliers/${id}`).then(r=>r.json()).then(d=>{
openGenericModal('Fornitore: ' + d.name, `
<div style="padding:16px">
<div class="form-section-title">Dati Principali</div>
<div class="form-grid-2">
${field('Codice', d.code)}${field('Ragione Sociale', d.name)}
${field('Partita IVA', d.vat_number || '—')}${field('Email', d.email)}
${field('Telefono', d.phone)}${field('Termini Pagamento', d.payment_terms_days + ' giorni')}
</div>
<div class="form-section-title">Indirizzo</div>
<div class="form-grid-2">
${field('Indirizzo', d.address)}${field('Città', d.city)}
${field('Paese', d.country)}${field('Saldo Debito', '€ ' + d.balance.toFixed(2))}
</div>
</div>`);
});
}
</script>
{% endblock %}
+53
View File
@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block title %}Vendite - Mago4 Demo{% endblock %}
{% block window_tabs %}<a href="/vendite" class="window-tab active"><i class="material-icons">point_of_sale</i> Vendite</a>{% endblock %}
{% block content %}
<div class="module-bar">
<div class="module-title">Documenti Vendita</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/vendite">Vendite</a><span class="sep">»</span><span class="current">Documenti Vendita</span></div>
</div>
<div class="tab-strip">
<a href="/sales-orders">Ordini Cliente</a>
<a href="/vendite" class="active">Documenti Vendita</a>
<a href="#">Politiche Vendita</a>
<a href="#">Statistiche</a>
</div>
<div class="cards-grid">
<div class="menu-card">
<div class="menu-card-header"><h3>Fatture</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="/invoices?type=immediate_invoice" class="menu-link"><i class="material-icons ml-icon mtype-form">request_quote</i><span class="ml-text">Fatture Immediate</span><i class="material-icons ml-star">star_border</i></a>
<a href="/invoices?type=accompanying_invoice" class="menu-link"><i class="material-icons ml-icon mtype-form">receipt</i><span class="ml-text">Fatture Accompagnatorie</span><i class="material-icons ml-star">star_border</i></a>
<a href="/invoices?type=proforma_invoice" class="menu-link"><i class="material-icons ml-icon mtype-list">description</i><span class="ml-text">Fatture ProForma</span><i class="material-icons ml-star">star_border</i></a>
<a href="/invoices" class="menu-link"><i class="material-icons ml-icon mtype-print">print</i><span class="ml-text">Portafoglio Clienti</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Documenti di Trasporto</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="/invoices?type=delivery_note" class="menu-link"><i class="material-icons ml-icon mtype-form">local_shipping</i><span class="ml-text">Documenti di Trasporto</span><i class="material-icons ml-star">star_border</i></a>
<a href="/invoices?type=delivery_note" class="menu-link"><i class="material-icons ml-icon mtype-list">list_alt</i><span class="ml-text">Lista Documenti di Trasporto</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Note di Credito / Debito</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="/invoices?type=credit_note" class="menu-link"><i class="material-icons ml-icon mtype-form">undo</i><span class="ml-text">Note di Credito</span><i class="material-icons ml-star">star_border</i></a>
<a href="/invoices?type=debit_note" class="menu-link"><i class="material-icons ml-icon mtype-form">redo</i><span class="ml-text">Note di Debito</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
<div class="menu-card">
<div class="menu-card-header"><h3>Procedure</h3><span class="collapse-dot">○—</span></div>
<div class="menu-card-body">
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-proc">bolt</i><span class="ml-text">Fatturazione Differita DDT</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-proc">bolt</i><span class="ml-text">Evasione Picking List</span><i class="material-icons ml-star">star_border</i></a>
<a href="#" class="menu-link"><i class="material-icons ml-icon mtype-print">bar_chart</i><span class="ml-text">Grafici di Fatturato</span><i class="material-icons ml-star">star_border</i></a>
</div>
</div>
</div>
{% endblock %}
+59 -119
View File
@@ -1,131 +1,77 @@
{% extends "base.html" %}
{% block title %}Magazzino - Mago4 Demo{% endblock %}
{% block page_title %}Magazzino{% endblock %}
{% block title %}Giacenze - Mago4 Demo{% endblock %}
{% block window_tabs %}<a href="/warehouse" class="window-tab active"><i class="material-icons">inventory</i> Giacenze</a>{% endblock %}
{% block content %}
<div class="page-header">
<div class="module-bar">
<div class="module-title">Giacenze Articoli</div>
<div class="breadcrumb"><a href="/">Mago4</a><span class="sep">»</span><a href="/magazzino">Logistica</a><span class="sep">»</span><span class="current">Giacenze</span></div>
</div>
<div class="tab-strip">
<a href="/magazzino">Magazzino</a>
<a href="/inventory-movements">Movimenti</a>
<a href="/warehouse" class="active">Giacenze</a>
<a href="/inventory-reasons">Causali</a>
</div>
<div class="content-pad">
<div class="kpi-grid">
<div class="kpi-card" style="border-left-color: var(--c-blue)">
<div class="kpi-title">Articoli in Catalogo</div>
<div class="kpi-row"><div class="kpi-icon"><i class="material-icons" style="color:var(--c-blue)">inventory_2</i></div><div class="kpi-value">{{ products|length }}</div></div>
</div>
<div class="kpi-card" style="border-left-color: var(--c-red)">
<div class="kpi-title">Articoli Sotto Scorta</div>
<div class="kpi-row"><div class="kpi-icon"><i class="material-icons" style="color:var(--c-red)">warning</i></div><div class="kpi-value">{{ low_stock|length }}</div></div>
</div>
<div class="kpi-card" style="border-left-color: var(--c-purple)">
<div class="kpi-title">Valore Magazzino (a costo)</div>
<div class="kpi-row"><div class="kpi-icon"><i class="material-icons" style="color:var(--c-purple)">euro_symbol</i></div><div class="kpi-value">€ {{ "{:,.0f}".format(warehouse_value) }}</div></div>
</div>
</div>
{% if low_stock %}
<div class="alert alert-danger" style="flex: 1;">
<i class="material-icons">error</i>
<strong>Attenzione!</strong> {{ low_stock|length }} articolo/i in sottosorta
</div>
{% endif %}
</div>
<!-- Summary Stats -->
<div class="widget-row" style="margin-bottom: 20px;">
<div class="widget-card stat-card" style="border-left-color: var(--color-blue)">
<div class="widget-header">
<h3 class="widget-title">Articoli in Catalogo</h3>
</div>
<div class="widget-body stat-body">
<div class="stat-value">{{ products|length }}</div>
</div>
</div>
<div class="widget-card stat-card" style="border-left-color: var(--color-red)">
<div class="widget-header">
<h3 class="widget-title">Articoli in Sottosorta</h3>
</div>
<div class="widget-body stat-body">
<div class="stat-icon" style="color: var(--color-red)">
<i class="material-icons">warning</i>
</div>
<div class="stat-value">{{ low_stock|length }}</div>
</div>
</div>
</div>
<!-- Low Stock Alert -->
{% if low_stock %}
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">
<i class="material-icons">warning</i> Articoli Sotto Soglia Riordino
</h3>
</div>
<div class="widget-body">
<div class="panel">
<div class="panel-header"><h3><i class="material-icons" style="color:var(--c-red)">warning</i> Articoli Sotto Scorta Minima</h3></div>
<table class="data-table">
<thead>
<tr>
<th>Codice</th>
<th>Descrizione</th>
<th>Giacenza</th>
<th>Soglia Riordino</th>
<th>Scostamento</th>
<th>Azioni</th>
</tr>
</thead>
<thead><tr><th>Codice</th><th>Descrizione</th><th class="text-right">Giacenza</th><th class="text-right">Scorta Min.</th><th class="text-right">Da Riordinare</th><th></th></tr></thead>
<tbody>
{% for product in low_stock %}
<tr class="highlight-danger">
<td><strong>{{ product.code }}</strong></td>
<td>{{ product.description }}</td>
<td class="text-danger text-bold">{{ product.quantity_in_stock }}</td>
<td class="text-right">{{ product.reorder_level }}</td>
<td class="text-right text-danger">
-{{ product.reorder_level - product.quantity_in_stock }} unità
</td>
<td>
<button class="btn-small btn-primary" onclick="createPOForProduct({{ product.id }})">
<i class="material-icons">add_shopping_cart</i>
</button>
</td>
{% for p in low_stock %}
<tr class="low-stock">
<td class="text-bold mono">{{ p.code }}</td>
<td>{{ p.description }}</td>
<td class="text-right text-danger text-bold">{{ p.quantity_in_stock }} {{ p.uom }}</td>
<td class="text-right">{{ p.reorder_level }} {{ p.uom }}</td>
<td class="text-right text-danger">{{ p.max_stock - p.quantity_in_stock }} {{ p.uom }}</td>
<td class="text-center"><button class="btn-icon-sm act-cart" title="Genera ordine" onclick="alert('Genera ordine di acquisto (demo)')"><i class="material-icons">add_shopping_cart</i></button></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% endif %}
<!-- All Products -->
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">Inventario Completo</h3>
</div>
<div class="widget-body">
<div class="panel">
<div class="panel-header"><h3><i class="material-icons">inventory</i> Inventario Completo — Deposito MAG01</h3></div>
<table class="data-table">
<thead>
<tr>
<th>Codice</th>
<th>Descrizione</th>
<th>Categoria</th>
<th>Giacenza</th>
<th>Soglia Riordino</th>
<th>Prezzo Unit.</th>
<th>Valore Magazzino</th>
<th>Stato</th>
</tr>
</thead>
<thead><tr><th>Codice</th><th>Descrizione</th><th>Categoria</th><th class="text-right">Giacenza</th><th class="text-right">Scorta Min.</th><th class="text-right">Costo Unit.</th><th class="text-right">Valore</th><th class="text-center">Stato</th></tr></thead>
<tbody>
{% for product in products %}
<tr class="{% if product.quantity_in_stock <= product.reorder_level %}low-stock{% endif %}">
<td><strong>{{ product.code }}</strong></td>
<td>{{ product.description }}</td>
<td>{{ product.category }}</td>
<td class="text-right {% if product.quantity_in_stock <= product.reorder_level %}text-danger text-bold{% endif %}">
{{ product.quantity_in_stock }}
</td>
<td class="text-right">{{ product.reorder_level }}</td>
<td class="text-right">€ {{ "%.2f"|format(product.unit_price) }}</td>
<td class="text-right text-bold">
€ {{ "%.2f"|format(product.quantity_in_stock * product.unit_price) }}
</td>
<td>
{% if product.quantity_in_stock <= product.reorder_level %}
<span class="badge badge-danger">
<i class="material-icons">warning</i> SOTTOSORTA
</span>
{% elif product.quantity_in_stock <= product.reorder_level + 20 %}
<span class="badge badge-warning">
<i class="material-icons">info</i> PROSSIMA SOGLIA
</span>
{% for p in products %}
<tr class="{% if p.quantity_in_stock <= p.reorder_level %}low-stock{% endif %}">
<td class="text-bold mono">{{ p.code }}</td>
<td>{{ p.description }}</td>
<td>{{ p.category }}</td>
<td class="text-right {% if p.quantity_in_stock <= p.reorder_level %}text-danger text-bold{% endif %}">{{ p.quantity_in_stock }} {{ p.uom }}</td>
<td class="text-right">{{ p.reorder_level }} {{ p.uom }}</td>
<td class="text-right text-mute">€ {{ "%.2f"|format(p.purchase_price) }}</td>
<td class="text-right text-bold">{{ "%.2f"|format(p.quantity_in_stock * p.purchase_price) }}</td>
<td class="text-center">
{% if p.quantity_in_stock <= p.reorder_level %}
<span class="badge badge-danger"><i class="material-icons">warning</i> Sottoscorta</span>
{% elif p.quantity_in_stock <= p.reorder_level + 20 %}
<span class="badge badge-warning"><i class="material-icons">info</i> In esaurimento</span>
{% else %}
<span class="badge badge-success">
<i class="material-icons">check</i> OK
</span>
<span class="badge badge-success"><i class="material-icons">check</i> OK</span>
{% endif %}
</td>
</tr>
@@ -134,10 +80,4 @@
</table>
</div>
</div>
<script>
function createPOForProduct(productId) {
alert('Creazione ordine di acquisto per articolo #' + productId + ' (da implementare)');
}
</script>
{% endblock %}