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>
275 lines
8.4 KiB
Python
275 lines
8.4 KiB
Python
from pydantic import BaseModel
|
|
from typing import Optional, List
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
|
|
|
|
# ==================== ENUMS ====================
|
|
class OrderStatus(str, Enum):
|
|
DRAFT = "draft"
|
|
CONFIRMED = "confirmed"
|
|
PARTIALLY_RECEIVED = "partially_received"
|
|
RECEIVED = "received"
|
|
CANCELLED = "cancelled"
|
|
|
|
|
|
class OrderType(str, Enum):
|
|
SALES = "sales"
|
|
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
|
|
name: str
|
|
email: str
|
|
phone: str
|
|
address: str
|
|
city: str
|
|
country: str
|
|
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):
|
|
id: int
|
|
code: str
|
|
name: str
|
|
email: str
|
|
phone: str
|
|
address: str
|
|
city: str
|
|
country: str
|
|
vat_number: str = ""
|
|
payment_terms_days: int = 30
|
|
balance: float = 0.0
|
|
|
|
|
|
class Product(BaseModel):
|
|
id: int
|
|
code: str
|
|
description: str
|
|
category: str
|
|
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
|
|
|
|
|
|
# ==================== ORDINI ====================
|
|
class OrderLine(BaseModel):
|
|
id: int
|
|
product_id: int
|
|
product_code: str
|
|
product_description: str
|
|
quantity: float
|
|
unit_price: float
|
|
total_amount: float
|
|
|
|
|
|
class SalesOrder(BaseModel):
|
|
id: int
|
|
order_number: str
|
|
order_date: datetime
|
|
customer_id: int
|
|
customer_name: str
|
|
status: OrderStatus
|
|
lines: List[OrderLine]
|
|
total_amount: float
|
|
delivery_date: Optional[datetime] = None
|
|
|
|
|
|
class PurchaseOrder(BaseModel):
|
|
id: int
|
|
order_number: str
|
|
order_date: datetime
|
|
supplier_id: int
|
|
supplier_name: str
|
|
status: OrderStatus
|
|
lines: List[OrderLine]
|
|
total_amount: float
|
|
expected_delivery_date: Optional[datetime] = None
|
|
|
|
|
|
# ==================== 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
|
|
size: str = "small"
|
|
icon: Optional[str] = None
|
|
color: Optional[str] = None
|
|
value: Optional[str] = None
|
|
format: Optional[str] = None
|
|
data: Optional[dict] = None
|