Restyle UI to match Mago4 and add invoicing, warehouse, scadenzario

Rework the demo into a faithful Mago4 look-and-feel and expand it with
core ERP modules modelled on the real Mago4 database schema.

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 11:47:32 +02:00
co-authored by Claude Sonnet 4.6
parent 8a9fb5085a
commit b6b6759b9a
26 changed files with 2900 additions and 2125 deletions
+505 -196
View File
@@ -1,8 +1,21 @@
from datetime import datetime, timedelta
from models import (
Customer, Supplier, Product, OrderLine, SalesOrder, PurchaseOrder, OrderStatus
)
from typing import List, Optional
from models import (
Customer, Supplier, Product, OrderLine, SalesOrder, PurchaseOrder, OrderStatus,
Invoice, InvoiceLine, DocumentType, DocumentStatus,
InventoryReason, InventoryMovement, InventoryMovementLine, MovementSign,
PaymentSchedule, PaymentScheduleType,
DOCUMENT_TYPE_LABELS,
)
def _today() -> datetime:
now = datetime.now()
return datetime(now.year, now.month, now.day)
def _round(v: float) -> float:
return round(v + 1e-9, 2)
class FakeDatabase:
@@ -12,191 +25,327 @@ class FakeDatabase:
self.products = self._init_products()
self.sales_orders = self._init_sales_orders()
self.purchase_orders = self._init_purchase_orders()
self.next_customer_id = max([c.id for c in self.customers]) + 1
self.next_supplier_id = max([s.id for s in self.suppliers]) + 1
self.next_product_id = max([p.id for p in self.products]) + 1
self.next_so_id = max([o.id for o in self.sales_orders]) + 1
self.next_po_id = max([o.id for o in self.purchase_orders]) + 1
self.inventory_reasons = self._init_inventory_reasons()
self.invoices = self._init_invoices()
self.inventory_movements = self._init_inventory_movements()
self.payment_schedules = self._init_payment_schedules()
self.next_customer_id = max((c.id for c in self.customers), default=0) + 1
self.next_supplier_id = max((s.id for s in self.suppliers), default=0) + 1
self.next_product_id = max((p.id for p in self.products), default=0) + 1
self.next_so_id = max((o.id for o in self.sales_orders), default=0) + 1
self.next_po_id = max((o.id for o in self.purchase_orders), default=0) + 1
self.next_invoice_id = max((i.id for i in self.invoices), default=0) + 1
self.next_movement_id = max((m.id for m in self.inventory_movements), default=0) + 1
self.next_schedule_id = max((s.id for s in self.payment_schedules), default=0) + 1
# ---------- Seed: Anagrafiche ----------
def _init_customers(self) -> List[Customer]:
return [
Customer(
id=1, code="CUST001", name="Acme Corporation",
email="info@acme.com", phone="+39 02 1234567",
address="Via Roma 10", city="Milano", country="IT",
credit_limit=50000, balance=15000
),
Customer(
id=2, code="CUST002", name="TechFlow Industries",
email="contact@techflow.de", phone="+49 30 555888",
address="Hauptstrasse 45", city="Berlin", country="DE",
credit_limit=75000, balance=32000
),
Customer(
id=3, code="CUST003", name="European Logistics",
email="sales@eulog.fr", phone="+33 1 4520 3580",
address="Avenue des Champs 20", city="Paris", country="FR",
credit_limit=100000, balance=8500
),
Customer(
id=4, code="CUST004", name="Alpine Manufacturing",
email="orders@alpmfg.ch", phone="+41 44 5678900",
address="Industriestrasse 12", city="Zurich", country="CH",
credit_limit=60000, balance=22000
),
Customer(id=1, code="CUST001", name="Acme Corporation S.p.A.",
email="info@acme.it", phone="+39 02 1234567",
address="Via Roma 10", city="Milano", country="IT",
vat_number="IT01234567890", fiscal_code="CMRACM80A01F205X",
payment_terms="RB 30 gg FM", credit_limit=50000, balance=15000),
Customer(id=2, code="CUST002", name="TechFlow Industries GmbH",
email="contact@techflow.de", phone="+49 30 555888",
address="Hauptstrasse 45", city="Berlin", country="DE",
vat_number="DE811234567", payment_terms="Bonifico 60 gg",
credit_limit=75000, balance=32000),
Customer(id=3, code="CUST003", name="European Logistics SARL",
email="sales@eulog.fr", phone="+33 1 4520 3580",
address="Avenue des Champs 20", city="Paris", country="FR",
vat_number="FR40123456789", payment_terms="Bonifico 30 gg",
credit_limit=100000, balance=8500),
Customer(id=4, code="CUST004", name="Alpine Manufacturing AG",
email="orders@alpmfg.ch", phone="+41 44 5678900",
address="Industriestrasse 12", city="Zurich", country="CH",
vat_number="CHE116281456", payment_terms="Rimessa diretta",
credit_limit=60000, balance=22000),
]
def _init_suppliers(self) -> List[Supplier]:
return [
Supplier(
id=1, code="SUPP001", name="Global Components Ltd",
email="sales@globalcomp.com", phone="+44 20 7946 0958",
address="123 Oxford Street", city="London", country="UK",
payment_terms_days=30, balance=5000
),
Supplier(
id=2, code="SUPP002", name="Asia Electronics Co.",
email="export@asiaelec.com", phone="+886 2 8797 3000",
address="Taipei Industrial Park", city="Taipei", country="TW",
payment_terms_days=45, balance=18000
),
Supplier(
id=3, code="SUPP003", name="Nordic Materials AB",
email="info@nordicmat.se", phone="+46 8 123 4567",
address="Industrígatan 5", city="Stockholm", country="SE",
payment_terms_days=60, balance=3500
),
Supplier(id=1, code="SUPP001", name="Global Components Ltd",
email="sales@globalcomp.com", phone="+44 20 7946 0958",
address="123 Oxford Street", city="London", country="UK",
vat_number="GB123456789", payment_terms_days=30, balance=5000),
Supplier(id=2, code="SUPP002", name="Asia Electronics Co.",
email="export@asiaelec.com", phone="+886 2 8797 3000",
address="Taipei Industrial Park", city="Taipei", country="TW",
vat_number="", payment_terms_days=45, balance=18000),
Supplier(id=3, code="SUPP003", name="Nordic Materials AB",
email="info@nordicmat.se", phone="+46 8 123 4567",
address="Industrigatan 5", city="Stockholm", country="SE",
vat_number="SE556677889901", payment_terms_days=60, balance=3500),
]
def _init_products(self) -> List[Product]:
return [
Product(
id=1, code="PROD001", description="Industrial Widget A",
category="Electronics", unit_price=125.50,
quantity_in_stock=250, reorder_level=50, supplier_id=1
),
Product(
id=2, code="PROD002", description="Premium Connector Set",
category="Hardware", unit_price=45.00,
quantity_in_stock=180, reorder_level=30, supplier_id=2
),
Product(
id=3, code="PROD003", description="Steel Bracket Type B",
category="Structural", unit_price=22.75,
quantity_in_stock=500, reorder_level=100, supplier_id=3
),
Product(
id=4, code="PROD004", description="Precision Bearing Set",
category="Mechanical", unit_price=89.99,
quantity_in_stock=120, reorder_level=40, supplier_id=1
),
Product(
id=5, code="PROD005", description="Control Module MCU-32",
category="Electronics", unit_price=234.50,
quantity_in_stock=75, reorder_level=20, supplier_id=2
),
Product(id=1, code="PROD001", description="Industrial Widget A",
category="Electronics", uom="NR", unit_price=125.50,
purchase_price=85.00, tax_rate=22.0,
quantity_in_stock=250, reorder_level=50, max_stock=500, supplier_id=1),
Product(id=2, code="PROD002", description="Premium Connector Set",
category="Hardware", uom="NR", unit_price=45.00,
purchase_price=28.00, tax_rate=22.0,
quantity_in_stock=180, reorder_level=30, max_stock=300, supplier_id=2),
Product(id=3, code="PROD003", description="Steel Bracket Type B",
category="Structural", uom="KG", unit_price=22.75,
purchase_price=14.50, tax_rate=22.0,
quantity_in_stock=45, reorder_level=100, max_stock=600, supplier_id=3),
Product(id=4, code="PROD004", description="Precision Bearing Set",
category="Mechanical", uom="NR", unit_price=89.99,
purchase_price=55.00, tax_rate=22.0,
quantity_in_stock=120, reorder_level=40, max_stock=250, supplier_id=1),
Product(id=5, code="PROD005", description="Control Module MCU-32",
category="Electronics", uom="NR", unit_price=234.50,
purchase_price=160.00, tax_rate=22.0,
quantity_in_stock=18, reorder_level=20, max_stock=100, supplier_id=2),
]
def _init_sales_orders(self) -> List[SalesOrder]:
now = datetime.now()
return [
SalesOrder(
id=1, order_number="SO-2024-0001",
order_date=now - timedelta(days=10),
customer_id=1, customer_name="Acme Corporation",
status=OrderStatus.CONFIRMED,
lines=[
OrderLine(id=1, product_id=1, product_code="PROD001",
product_description="Industrial Widget A",
quantity=10, unit_price=125.50, total_amount=1255.00),
OrderLine(id=2, product_id=4, product_code="PROD004",
product_description="Precision Bearing Set",
quantity=5, unit_price=89.99, total_amount=449.95),
],
total_amount=1704.95,
delivery_date=now + timedelta(days=15)
),
SalesOrder(
id=2, order_number="SO-2024-0002",
order_date=now - timedelta(days=5),
customer_id=2, customer_name="TechFlow Industries",
status=OrderStatus.CONFIRMED,
lines=[
OrderLine(id=3, product_id=5, product_code="PROD005",
product_description="Control Module MCU-32",
quantity=8, unit_price=234.50, total_amount=1876.00),
],
total_amount=1876.00,
delivery_date=now + timedelta(days=10)
),
SalesOrder(
id=3, order_number="SO-2024-0003",
order_date=now - timedelta(days=2),
customer_id=3, customer_name="European Logistics",
status=OrderStatus.DRAFT,
lines=[
OrderLine(id=4, product_id=2, product_code="PROD002",
product_description="Premium Connector Set",
quantity=20, unit_price=45.00, total_amount=900.00),
OrderLine(id=5, product_id=3, product_code="PROD003",
product_description="Steel Bracket Type B",
quantity=15, unit_price=22.75, total_amount=341.25),
],
total_amount=1241.25,
delivery_date=now + timedelta(days=20)
),
SalesOrder(id=1, order_number="SO-2024-0001",
order_date=now - timedelta(days=10),
customer_id=1, customer_name="Acme Corporation S.p.A.",
status=OrderStatus.CONFIRMED,
lines=[
OrderLine(id=1, product_id=1, product_code="PROD001",
product_description="Industrial Widget A",
quantity=10, unit_price=125.50, total_amount=1255.00),
OrderLine(id=2, product_id=4, product_code="PROD004",
product_description="Precision Bearing Set",
quantity=5, unit_price=89.99, total_amount=449.95),
],
total_amount=1704.95, delivery_date=now + timedelta(days=15)),
SalesOrder(id=2, order_number="SO-2024-0002",
order_date=now - timedelta(days=5),
customer_id=2, customer_name="TechFlow Industries GmbH",
status=OrderStatus.CONFIRMED,
lines=[
OrderLine(id=3, product_id=5, product_code="PROD005",
product_description="Control Module MCU-32",
quantity=8, unit_price=234.50, total_amount=1876.00),
],
total_amount=1876.00, delivery_date=now + timedelta(days=10)),
SalesOrder(id=3, order_number="SO-2024-0003",
order_date=now - timedelta(days=2),
customer_id=3, customer_name="European Logistics SARL",
status=OrderStatus.DRAFT,
lines=[
OrderLine(id=4, product_id=2, product_code="PROD002",
product_description="Premium Connector Set",
quantity=20, unit_price=45.00, total_amount=900.00),
OrderLine(id=5, product_id=3, product_code="PROD003",
product_description="Steel Bracket Type B",
quantity=15, unit_price=22.75, total_amount=341.25),
],
total_amount=1241.25, delivery_date=now + timedelta(days=20)),
]
def _init_purchase_orders(self) -> List[PurchaseOrder]:
now = datetime.now()
return [
PurchaseOrder(
id=1, order_number="PO-2024-0001",
order_date=now - timedelta(days=15),
supplier_id=1, supplier_name="Global Components Ltd",
status=OrderStatus.PARTIALLY_RECEIVED,
lines=[
OrderLine(id=1, product_id=1, product_code="PROD001",
product_description="Industrial Widget A",
quantity=100, unit_price=110.00, total_amount=11000.00),
],
total_amount=11000.00,
expected_delivery_date=now + timedelta(days=5)
),
PurchaseOrder(
id=2, order_number="PO-2024-0002",
order_date=now - timedelta(days=8),
supplier_id=2, supplier_name="Asia Electronics Co.",
status=OrderStatus.CONFIRMED,
lines=[
OrderLine(id=2, product_id=5, product_code="PROD005",
product_description="Control Module MCU-32",
quantity=50, unit_price=195.00, total_amount=9750.00),
],
total_amount=9750.00,
expected_delivery_date=now + timedelta(days=25)
),
PurchaseOrder(
id=3, order_number="PO-2024-0003",
order_date=now - timedelta(days=1),
supplier_id=3, supplier_name="Nordic Materials AB",
status=OrderStatus.DRAFT,
lines=[
OrderLine(id=3, product_id=3, product_code="PROD003",
product_description="Steel Bracket Type B",
quantity=200, unit_price=20.00, total_amount=4000.00),
],
total_amount=4000.00,
expected_delivery_date=now + timedelta(days=30)
),
PurchaseOrder(id=1, order_number="PO-2024-0001",
order_date=now - timedelta(days=15),
supplier_id=1, supplier_name="Global Components Ltd",
status=OrderStatus.PARTIALLY_RECEIVED,
lines=[OrderLine(id=1, product_id=1, product_code="PROD001",
product_description="Industrial Widget A",
quantity=100, unit_price=85.00, total_amount=8500.00)],
total_amount=8500.00, expected_delivery_date=now + timedelta(days=5)),
PurchaseOrder(id=2, order_number="PO-2024-0002",
order_date=now - timedelta(days=8),
supplier_id=2, supplier_name="Asia Electronics Co.",
status=OrderStatus.CONFIRMED,
lines=[OrderLine(id=2, product_id=5, product_code="PROD005",
product_description="Control Module MCU-32",
quantity=50, unit_price=160.00, total_amount=8000.00)],
total_amount=8000.00, expected_delivery_date=now + timedelta(days=25)),
PurchaseOrder(id=3, order_number="PO-2024-0003",
order_date=now - timedelta(days=1),
supplier_id=3, supplier_name="Nordic Materials AB",
status=OrderStatus.DRAFT,
lines=[OrderLine(id=3, product_id=3, product_code="PROD003",
product_description="Steel Bracket Type B",
quantity=200, unit_price=14.50, total_amount=2900.00)],
total_amount=2900.00, expected_delivery_date=now + timedelta(days=30)),
]
# CRUD Operations
def get_customers(self) -> List[Customer]:
return self.customers
# ---------- Seed: Magazzino ----------
def _init_inventory_reasons(self) -> List[InventoryReason]:
return [
InventoryReason(code="CAR-ACQ", description="Carico da acquisto",
sign=MovementSign.LOAD, fiscal=True),
InventoryReason(code="CAR-RET", description="Reso da cliente",
sign=MovementSign.LOAD, fiscal=True),
InventoryReason(code="CAR-RET+", description="Rettifica inventariale positiva",
sign=MovementSign.LOAD, fiscal=False),
InventoryReason(code="SCA-VEN", description="Scarico per vendita",
sign=MovementSign.UNLOAD, fiscal=True),
InventoryReason(code="SCA-PRO", description="Scarico per produzione",
sign=MovementSign.UNLOAD, fiscal=False),
InventoryReason(code="SCA-RET-", description="Rettifica inventariale negativa",
sign=MovementSign.UNLOAD, fiscal=False),
]
def get_customer(self, customer_id: int) -> Optional[Customer]:
return next((c for c in self.customers if c.id == customer_id), None)
def _init_inventory_movements(self) -> List[InventoryMovement]:
now = datetime.now()
return [
InventoryMovement(
id=1, reason_code="CAR-ACQ", reason_description="Carico da acquisto",
sign=MovementSign.LOAD, doc_no="MOV-0001",
doc_date=now - timedelta(days=12), posting_date=now - timedelta(days=12),
storage="MAG01", custsupp_name="Global Components Ltd",
lines=[InventoryMovementLine(line_no=1, item_code="PROD001",
description="Industrial Widget A", uom="NR",
quantity=100, unit_value=85.00, line_amount=8500.00,
location="A-01-03")],
total_amount=8500.00, posted=True, notes="Carico merce da OdA PO-2024-0001"),
InventoryMovement(
id=2, reason_code="SCA-VEN", reason_description="Scarico per vendita",
sign=MovementSign.UNLOAD, doc_no="MOV-0002",
doc_date=now - timedelta(days=6), posting_date=now - timedelta(days=6),
storage="MAG01", custsupp_name="Acme Corporation S.p.A.",
lines=[InventoryMovementLine(line_no=1, item_code="PROD001",
description="Industrial Widget A", uom="NR",
quantity=10, unit_value=125.50, line_amount=1255.00,
location="A-01-03")],
total_amount=1255.00, posted=True, notes="Scarico per fattura FT-2024-0001"),
InventoryMovement(
id=3, reason_code="CAR-RET+", reason_description="Rettifica inventariale positiva",
sign=MovementSign.LOAD, doc_no="MOV-0003",
doc_date=now - timedelta(days=1), posting_date=now - timedelta(days=1),
storage="MAG01", custsupp_name="",
lines=[InventoryMovementLine(line_no=1, item_code="PROD003",
description="Steel Bracket Type B", uom="KG",
quantity=5, unit_value=14.50, line_amount=72.50,
location="B-02-01")],
total_amount=72.50, posted=False, notes="Da confermare"),
]
# ---------- Seed: Fatture ----------
def _build_invoice_lines(self, raw_lines) -> List[InvoiceLine]:
lines = []
for i, (code, desc, uom, qty, val, disc, rate) in enumerate(raw_lines, start=1):
net_price = _round(val * (1 - disc / 100))
taxable = _round(net_price * qty)
tax = _round(taxable * rate / 100)
lines.append(InvoiceLine(line_no=i, item_code=code, description=desc, uom=uom,
quantity=qty, unit_value=val, discount_pct=disc,
net_price=net_price, taxable_amount=taxable,
tax_rate=rate, tax_amount=tax))
return lines
def _finalize_invoice(self, inv: Invoice) -> Invoice:
inv.taxable_total = _round(sum(l.taxable_amount for l in inv.lines))
inv.tax_total = _round(sum(l.tax_amount for l in inv.lines))
inv.total = _round(inv.taxable_total + inv.tax_total)
return inv
def _init_invoices(self) -> List[Invoice]:
now = datetime.now()
invoices = [
Invoice(id=1, doc_type=DocumentType.IMMEDIATE_INVOICE, doc_no="FT-2024-0001",
doc_date=now - timedelta(days=6), posting_date=now - timedelta(days=6),
customer_id=1, customer_name="Acme Corporation S.p.A.",
customer_vat="IT01234567890", your_reference="ODA 4521",
payment_terms="RB 30 gg FM", salesperson="Rossi M.",
lines=self._build_invoice_lines([
("PROD001", "Industrial Widget A", "NR", 10, 125.50, 0, 22.0),
("PROD004", "Precision Bearing Set", "NR", 5, 89.99, 5, 22.0),
]),
status=DocumentStatus.ISSUED, printed=True, issued=True,
posted_to_inventory=True),
Invoice(id=2, doc_type=DocumentType.DELIVERY_NOTE, doc_no="DDT-2024-0014",
doc_date=now - timedelta(days=3), posting_date=now - timedelta(days=3),
customer_id=2, customer_name="TechFlow Industries GmbH",
customer_vat="DE811234567", payment_terms="Bonifico 60 gg",
salesperson="Bianchi L.",
lines=self._build_invoice_lines([
("PROD005", "Control Module MCU-32", "NR", 8, 234.50, 0, 22.0),
]),
status=DocumentStatus.PRINTED, printed=True),
Invoice(id=3, doc_type=DocumentType.IMMEDIATE_INVOICE, doc_no="FT-2024-0002",
doc_date=now - timedelta(days=1), posting_date=now - timedelta(days=1),
customer_id=3, customer_name="European Logistics SARL",
customer_vat="FR40123456789", payment_terms="Bonifico 30 gg",
salesperson="Rossi M.",
lines=self._build_invoice_lines([
("PROD002", "Premium Connector Set", "NR", 20, 45.00, 10, 22.0),
("PROD003", "Steel Bracket Type B", "KG", 15, 22.75, 0, 22.0),
]),
status=DocumentStatus.DRAFT),
Invoice(id=4, doc_type=DocumentType.CREDIT_NOTE, doc_no="NC-2024-0003",
doc_date=now - timedelta(days=2), posting_date=now - timedelta(days=2),
customer_id=1, customer_name="Acme Corporation S.p.A.",
customer_vat="IT01234567890", payment_terms="RB 30 gg FM",
salesperson="Rossi M.", notes="Reso merce difettosa",
lines=self._build_invoice_lines([
("PROD004", "Precision Bearing Set", "NR", 2, 89.99, 0, 22.0),
]),
status=DocumentStatus.ISSUED, printed=True, issued=True),
]
return [self._finalize_invoice(i) for i in invoices]
# ---------- Seed: Scadenzario (partite aperte) ----------
def _init_payment_schedules(self) -> List[PaymentSchedule]:
t = _today()
R, P = PaymentScheduleType.RECEIVABLE, PaymentScheduleType.PAYABLE
return [
# --- Incassi (receivables) da fatture clienti ---
PaymentSchedule(id=1, type=R, custsupp_name="Acme Corporation S.p.A.",
doc_no="FT-2024-0001", doc_date=t - timedelta(days=36),
due_date=t - timedelta(days=6), payment_method="RB",
amount=2052.59, bank="Intesa Sanpaolo", closed=False,
notes="Scaduta — da sollecitare"),
PaymentSchedule(id=2, type=R, custsupp_name="European Logistics SARL",
doc_no="FT-2024-0002", doc_date=t - timedelta(days=1),
due_date=t + timedelta(days=29), payment_method="Bonifico",
amount=1404.53, bank="Intesa Sanpaolo", closed=False),
PaymentSchedule(id=3, type=R, custsupp_name="TechFlow Industries GmbH",
doc_no="FT-2024-0009", doc_date=t - timedelta(days=50),
due_date=t + timedelta(days=4), payment_method="RID",
amount=3680.00, bank="Unicredit", closed=False),
PaymentSchedule(id=4, type=R, custsupp_name="Acme Corporation S.p.A.",
doc_no="NC-2024-0003", doc_date=t - timedelta(days=2),
due_date=t + timedelta(days=10), payment_method="RB",
amount=219.58, is_credit_note=True, closed=False,
notes="Nota di credito — riduce il credito"),
PaymentSchedule(id=5, type=R, custsupp_name="Alpine Manufacturing AG",
doc_no="FT-2024-0007", doc_date=t - timedelta(days=40),
due_date=t - timedelta(days=10), payment_method="Bonifico",
amount=5430.00, bank="Unicredit", closed=False,
notes="Scaduta da 10 giorni"),
PaymentSchedule(id=6, type=R, custsupp_name="European Logistics SARL",
doc_no="FT-2024-0005", doc_date=t - timedelta(days=60),
due_date=t - timedelta(days=30), payment_method="RB",
amount=2100.00, bank="Intesa Sanpaolo", closed=True,
notes="Incassata"),
# --- Pagamenti (payables) verso fornitori ---
PaymentSchedule(id=7, type=P, custsupp_name="Global Components Ltd",
doc_no="FTACQ-1187", doc_date=t - timedelta(days=33),
due_date=t - timedelta(days=3), payment_method="Bonifico",
amount=8500.00, closed=False, notes="Scaduta — pagamento urgente"),
PaymentSchedule(id=8, type=P, custsupp_name="Asia Electronics Co.",
doc_no="FTACQ-2204", doc_date=t - timedelta(days=12),
due_date=t + timedelta(days=2), payment_method="Bonifico",
amount=8000.00, closed=False),
PaymentSchedule(id=9, type=P, custsupp_name="Nordic Materials AB",
doc_no="FTACQ-0991", doc_date=t - timedelta(days=5),
due_date=t + timedelta(days=25), payment_method="Bonifico",
amount=2900.00, closed=False),
PaymentSchedule(id=10, type=P, custsupp_name="Global Components Ltd",
doc_no="FTACQ-1150", doc_date=t - timedelta(days=70),
due_date=t - timedelta(days=40), payment_method="RIBA",
amount=3200.00, closed=True, notes="Pagata"),
]
# ==================== CRUD: Anagrafiche ====================
def get_customers(self): return self.customers
def get_customer(self, cid): return next((c for c in self.customers if c.id == cid), None)
def create_customer(self, customer: Customer) -> Customer:
customer.id = self.next_customer_id
@@ -204,19 +353,16 @@ class FakeDatabase:
self.customers.append(customer)
return customer
def update_customer(self, customer_id: int, customer_data: dict) -> Optional[Customer]:
customer = self.get_customer(customer_id)
if customer:
for key, value in customer_data.items():
if hasattr(customer, key) and value is not None:
setattr(customer, key, value)
return customer
def update_customer(self, cid, data: dict):
c = self.get_customer(cid)
if c:
for k, v in data.items():
if hasattr(c, k) and v is not None:
setattr(c, k, v)
return c
def get_suppliers(self) -> List[Supplier]:
return self.suppliers
def get_supplier(self, supplier_id: int) -> Optional[Supplier]:
return next((s for s in self.suppliers if s.id == supplier_id), None)
def get_suppliers(self): return self.suppliers
def get_supplier(self, sid): return next((s for s in self.suppliers if s.id == sid), None)
def create_supplier(self, supplier: Supplier) -> Supplier:
supplier.id = self.next_supplier_id
@@ -224,11 +370,9 @@ class FakeDatabase:
self.suppliers.append(supplier)
return supplier
def get_products(self) -> List[Product]:
return self.products
def get_product(self, product_id: int) -> Optional[Product]:
return next((p for p in self.products if p.id == product_id), None)
def get_products(self): return self.products
def get_product(self, pid): return next((p for p in self.products if p.id == pid), None)
def get_product_by_code(self, code): return next((p for p in self.products if p.code == code), None)
def create_product(self, product: Product) -> Product:
product.id = self.next_product_id
@@ -236,11 +380,9 @@ class FakeDatabase:
self.products.append(product)
return product
def get_sales_orders(self) -> List[SalesOrder]:
return self.sales_orders
def get_sales_order(self, order_id: int) -> Optional[SalesOrder]:
return next((o for o in self.sales_orders if o.id == order_id), None)
# ==================== CRUD: Ordini ====================
def get_sales_orders(self): return self.sales_orders
def get_sales_order(self, oid): return next((o for o in self.sales_orders if o.id == oid), None)
def create_sales_order(self, order: SalesOrder) -> SalesOrder:
order.id = self.next_so_id
@@ -248,11 +390,8 @@ class FakeDatabase:
self.sales_orders.append(order)
return order
def get_purchase_orders(self) -> List[PurchaseOrder]:
return self.purchase_orders
def get_purchase_order(self, order_id: int) -> Optional[PurchaseOrder]:
return next((o for o in self.purchase_orders if o.id == order_id), None)
def get_purchase_orders(self): return self.purchase_orders
def get_purchase_order(self, oid): return next((o for o in self.purchase_orders if o.id == oid), None)
def create_purchase_order(self, order: PurchaseOrder) -> PurchaseOrder:
order.id = self.next_po_id
@@ -260,16 +399,173 @@ class FakeDatabase:
self.purchase_orders.append(order)
return order
# Dashboard statistics
def get_dashboard_stats(self):
sales_to_fulfill = sum(1 for o in self.sales_orders if o.status in [OrderStatus.CONFIRMED, OrderStatus.DRAFT])
purchase_to_receive = sum(1 for o in self.purchase_orders if o.status in [OrderStatus.CONFIRMED, OrderStatus.DRAFT, OrderStatus.PARTIALLY_RECEIVED])
# ==================== CRUD: Fatture ====================
def get_invoices(self): return self.invoices
def get_invoice(self, iid): return next((i for i in self.invoices if i.id == iid), None)
def get_invoices_by_type(self, doc_type: DocumentType):
return [i for i in self.invoices if i.doc_type == doc_type]
def create_invoice(self, invoice: Invoice) -> Invoice:
invoice.id = self.next_invoice_id
self.next_invoice_id += 1
self._finalize_invoice(invoice)
self.invoices.append(invoice)
return invoice
# ==================== CRUD: Magazzino ====================
def get_inventory_reasons(self): return self.inventory_reasons
def get_inventory_reason(self, code): return next((r for r in self.inventory_reasons if r.code == code), None)
def get_inventory_movements(self): return self.inventory_movements
def get_inventory_movement(self, mid): return next((m for m in self.inventory_movements if m.id == mid), None)
def create_inventory_movement(self, movement: InventoryMovement) -> InventoryMovement:
movement.id = self.next_movement_id
self.next_movement_id += 1
movement.total_amount = _round(sum(l.line_amount for l in movement.lines))
self.inventory_movements.append(movement)
if movement.posted:
self._apply_movement_to_balances(movement)
return movement
def post_movement(self, mid) -> Optional[InventoryMovement]:
"""Conferma un movimento e aggiorna le giacenze (Mago4: PostedToInventory)."""
m = self.get_inventory_movement(mid)
if m and not m.posted:
m.posted = True
self._apply_movement_to_balances(m)
return m
def _apply_movement_to_balances(self, movement: InventoryMovement):
"""Applica il movimento alle giacenze articoli secondo il segno della causale."""
delta_sign = 1 if movement.sign == MovementSign.LOAD else -1
for line in movement.lines:
product = self.get_product_by_code(line.item_code)
if product:
product.quantity_in_stock += int(delta_sign * line.quantity)
# ==================== Scadenzario (AP_AR) ====================
def get_payment_schedules(self): return self.payment_schedules
def get_payment_schedule(self, sid): return next((s for s in self.payment_schedules if s.id == sid), None)
def schedule_status(self, s: PaymentSchedule) -> str:
"""Stato derivato: paid / overdue / due_soon / open"""
if s.closed:
return "paid"
days = (s.due_date - _today()).days
if days < 0:
return "overdue"
if days <= 7:
return "due_soon"
return "open"
def schedule_days_to_due(self, s: PaymentSchedule) -> int:
return (s.due_date - _today()).days
def settle_schedule(self, sid) -> Optional[PaymentSchedule]:
"""Salda/incassa una partita (Mago4: Closed/Collected)."""
s = self.get_payment_schedule(sid)
if s and not s.closed:
s.closed = True
return s
def get_open_schedules(self, stype: Optional[PaymentScheduleType] = None):
items = [s for s in self.payment_schedules if not s.closed]
if stype:
items = [s for s in items if s.type == stype]
return items
def get_overdue_schedules(self, stype: Optional[PaymentScheduleType] = None):
return [s for s in self.get_open_schedules(stype) if self.schedule_status(s) == "overdue"]
def get_upcoming_schedules(self, days: int = 30):
"""Partite aperte in scadenza nei prossimi 'days' giorni (incluse scadute)."""
limit = _today() + timedelta(days=days)
items = [s for s in self.get_open_schedules() if s.due_date <= limit]
return sorted(items, key=lambda s: s.due_date)
def get_credit_exceeded_customers(self):
"""Clienti che hanno superato il limite di fido (modulo CreditLimit)."""
return [c for c in self.customers if c.credit_limit > 0 and c.balance > c.credit_limit]
def get_todo_items(self):
"""Aggrega le 'cose da fare' per la dashboard."""
draft_invoices = [i for i in self.invoices if i.status == DocumentStatus.DRAFT]
draft_movements = [m for m in self.inventory_movements if not m.posted]
orders_to_fulfill = [o for o in self.sales_orders
if o.status in (OrderStatus.CONFIRMED, OrderStatus.DRAFT)]
low_stock = [p for p in self.products if p.quantity_in_stock <= p.reorder_level]
overdue_recv = self.get_overdue_schedules(PaymentScheduleType.RECEIVABLE)
overdue_pay = self.get_overdue_schedules(PaymentScheduleType.PAYABLE)
credit_exceeded = self.get_credit_exceeded_customers()
todos = []
if overdue_pay:
todos.append({"icon": "payments", "color": "red", "priority": 1,
"text": f"{len(overdue_pay)} pagamenti scaduti da effettuare",
"amount": sum(s.amount for s in overdue_pay),
"link": "/scadenzario?type=payable", "action": "Paga"})
if overdue_recv:
todos.append({"icon": "request_quote", "color": "orange", "priority": 2,
"text": f"{len(overdue_recv)} incassi scaduti da sollecitare",
"amount": sum(s.amount for s in overdue_recv),
"link": "/scadenzario?type=receivable", "action": "Sollecita"})
if low_stock:
todos.append({"icon": "production_quantity_limits", "color": "red", "priority": 3,
"text": f"{len(low_stock)} articoli sotto scorta da ordinare",
"amount": None, "link": "/warehouse", "action": "Ordina"})
if draft_invoices:
todos.append({"icon": "edit_document", "color": "blue", "priority": 4,
"text": f"{len(draft_invoices)} documenti in bozza da confermare",
"amount": None, "link": "/invoices", "action": "Apri"})
if orders_to_fulfill:
todos.append({"icon": "local_shipping", "color": "blue", "priority": 5,
"text": f"{len(orders_to_fulfill)} ordini cliente da evadere",
"amount": None, "link": "/sales-orders", "action": "Apri"})
if draft_movements:
todos.append({"icon": "swap_vert", "color": "teal", "priority": 6,
"text": f"{len(draft_movements)} movimenti di magazzino da confermare",
"amount": None, "link": "/inventory-movements", "action": "Apri"})
if credit_exceeded:
todos.append({"icon": "credit_card_off", "color": "red", "priority": 7,
"text": f"{len(credit_exceeded)} clienti con fido superato",
"amount": None, "link": "/customers", "action": "Verifica"})
return sorted(todos, key=lambda x: x["priority"])
def get_cashflow_forecast(self, days: int = 30):
"""Previsione di cassa a 'days' giorni: incassi vs pagamenti previsti."""
upcoming = self.get_upcoming_schedules(days)
inflow = sum(s.signed_amount for s in upcoming if s.type == PaymentScheduleType.RECEIVABLE)
outflow = sum(s.amount for s in upcoming if s.type == PaymentScheduleType.PAYABLE)
return {"days": days, "inflow": _round(inflow), "outflow": _round(outflow),
"net": _round(inflow - outflow)}
# ==================== Statistiche ====================
def get_dashboard_stats(self):
sales_to_fulfill = sum(1 for o in self.sales_orders
if o.status in (OrderStatus.CONFIRMED, OrderStatus.DRAFT))
purchase_to_receive = sum(1 for o in self.purchase_orders
if o.status in (OrderStatus.CONFIRMED, OrderStatus.DRAFT,
OrderStatus.PARTIALLY_RECEIVED))
total_receivable = sum(c.balance for c in self.customers)
total_payable = sum(s.balance for s in self.suppliers)
low_stock_items = [p for p in self.products if p.quantity_in_stock <= p.reorder_level]
# Fatturato emesso (fatture/note credito issued)
invoiced_total = sum(
(i.total if i.doc_type != DocumentType.CREDIT_NOTE else -i.total)
for i in self.invoices if i.issued
)
pending_invoices = sum(1 for i in self.invoices if i.status == DocumentStatus.DRAFT)
warehouse_value = sum(p.quantity_in_stock * p.purchase_price for p in self.products)
# Scadenzario
overdue_recv = self.get_overdue_schedules(PaymentScheduleType.RECEIVABLE)
overdue_pay = self.get_overdue_schedules(PaymentScheduleType.PAYABLE)
open_recv = self.get_open_schedules(PaymentScheduleType.RECEIVABLE)
open_pay = self.get_open_schedules(PaymentScheduleType.PAYABLE)
return {
"sales_to_fulfill": sales_to_fulfill,
"purchase_to_receive": purchase_to_receive,
@@ -280,6 +576,19 @@ class FakeDatabase:
"total_customers": len(self.customers),
"total_suppliers": len(self.suppliers),
"total_products": len(self.products),
"invoiced_total": invoiced_total,
"pending_invoices": pending_invoices,
"warehouse_value": warehouse_value,
"total_invoices": len(self.invoices),
"total_movements": len(self.inventory_movements),
# Scadenze
"overdue_recv_count": len(overdue_recv),
"overdue_recv_amount": _round(sum(s.amount for s in overdue_recv)),
"overdue_pay_count": len(overdue_pay),
"overdue_pay_amount": _round(sum(s.amount for s in overdue_pay)),
"open_recv_amount": _round(sum(s.signed_amount for s in open_recv)),
"open_pay_amount": _round(sum(s.amount for s in open_pay)),
"credit_exceeded_count": len(self.get_credit_exceeded_customers()),
}