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:
@@ -20,6 +20,7 @@ env/
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
.vs/
|
||||
*.swp
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
@@ -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
@@ -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()),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+44
-108
@@ -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);
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 & 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 %}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 %}
|
||||
|
||||
@@ -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
@@ -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 %}
|
||||
|
||||
@@ -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
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user