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:
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user