Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@ La nota *"se i numeri sono questi cosa devo fare per crescere?"* suggerisce un l
| **4** | UI Business: Panoramica + Conto Economico + Stato Patrimoniale (§10.2-10.4) | Le 3 schermate con dati reali importati | ✅ **Fatta** (sessione 1) — menu laterale e grafici compresi |
| **5** | UI Capitale Circolante + Tesoreria/Cash Flow + Scadenziario (§10.5-10.6) | Previsione di cassa funzionante su dati reali | ✅ **Fatta** (sessione 2) |
| **6** | UI Banche e Finanziamenti (§10.7) + collegamento rate→Cash Flow | Fidi/finanziamenti con impatto visibile in Tesoreria | ✅ **Fatta** (sessione 2) |
| **7** | Analisi & Simulazioni (§10.8) | Scenario what-if salvabile e confrontabile | ⬜ Prossima |
| **8** | Sync Consulente↔Azienda via Tailscale (§6) + status bar (§7) | Due installazioni reali che si scambiano dati | ⬜ |
| **7** | Analisi & Simulazioni (§10.8) | Scenario what-if salvabile e confrontabile | ✅ **Fatta** (sessione 2) |
| **8** | Sync Consulente↔Azienda via Tailscale (§6) + status bar (§7) | Due installazioni reali che si scambiano dati | ⬜ Prossima |
| **9** | Import Excel avanzato: tolleranza a varianti di formato tra clienti/periodi (§11.1) | Import robusto su più file Excel reali diversi tra loro | ⬜ |
| **10** | Installer offline (electron-builder) per Consulente e Azienda | `.exe` funzionanti, Tailscale bundled | 🟡 **Parziale**: `.exe` installabile, portable e demo funzionanti. Mancano le due varianti separate e Tailscale bundled, che hanno senso solo dopo la Fase 8. **Regola del cliente (2026-09-16): una release a ogni aggiornamento importante, sempre con i tre eseguibili — installer, portable e demo** (`npm run dist` e `npm run dist:demo`). La prima così è la v0.0.5, con le Fasi 5 e 6 |
| **11+** | Integrazioni Fase futura: connettore IRIS, Cassetto Fiscale, Open Banking, pianificazione fiscale, marginalità multi-dimensionale, assistente numeri | Una alla volta, dopo validazione col cliente | ⬜ |
Expand Down Expand Up @@ -516,6 +516,39 @@ Verificato con i numeri da manuale: 12.000 € al 6% in 12 rate mensili danno la

Corretta anche la grammatica delle note automatiche ("Magazzino in calo…: libera liquidità").

**Fase 7 — Analisi & Simulazioni (§10.8)**

| Pezzo | Dove |
|---|---|
| Motore dello scenario e dettaglio impatti | `src/shared/engine/simulation.ts` |
| Base (12 mesi, patrimonio, liquidità, finanziamenti), scenari, esportazione Excel | `src/main/server/services/simulation.service.ts` |
| Schermata | `src/renderer/src/pages/business/SimulationView.tsx` |
| Scenari salvati | migrazione `006_simulation_scenarios` |

**Il calcolo gira nella schermata.** Il server prepara la base; lo scenario lo calcola lo stesso motore condiviso direttamente nel renderer, così ogni leva mossa aggiorna i risultati all'istante. L'esportazione Excel usa lo stesso motore sul server: schermata e file non possono divergere.

**Uno scenario salvato è solo l'elenco delle variazioni**, in JSON. I risultati si ricalcolano sempre sulla base corrente: uno scenario di marzo riaperto a settembre ragiona sui numeri di settembre. Salvare è del Consulente; simulare ed esportare anche dell'operatore Azienda, perché non cambia nessun dato.

**La base** sono i 12 mesi che terminano col periodo scelto (tutti obbligatori: altrimenti la schermata dice quali mancano) oppure un bilancio annuale. Un mese solo porterebbe dentro la sua stagionalità.

**Il confronto è fra due proiezioni fatte allo stesso modo**: la situazione attuale è lo stesso calcolo con le leve a zero, sui 12 mesi da oggi.

Il modello del consulente non tratta le simulazioni: le regole sono scelte dichiarate nel codice, da validare con lui.

1. **I costi variabili seguono i ricavi** (materie prime e produzione); "prezzo merci" e "costi di produzione" sono variazioni di prezzo sopra l'effetto volume. Rimanenze ferme.
2. **"Altri costi fissi"** = commerciali + generali. Il personale ha la sua leva (dipendenti × costo annuo), gli ammortamenti seguono gli investimenti.
3. **Imposte** all'aliquota effettiva della base; 24% (IRES) se la base non ha utile.
4. **Circolante**: i crediti seguono i ricavi, magazzino e fornitori gli **acquisti**. Il magazzino non segue il costo del venduto perché nel modello questo comprende personale diretto e ammortamenti, e un forno nuovo farebbe crescere le scorte. Un obiettivo di giorni sposta la voce in proporzione. La variazione assorbe (o libera) cassa nei primi tre mesi.
5. **Cash flow annuo** = EBITDA − imposte − variazione del circolante − investimenti + nuovi finanziamenti − rate dei 12 mesi (esistenti e nuove). Straordinari esclusi.
6. **Investimento** pagato subito e ammortizzato a quote costanti; **finanziamento** incassato subito, francese, prima rata dopo un mese.
7. **PFN fra 12 mesi** = debiti finanziari di oggi − capitale rimborsato + nuovo debito residuo − liquidità finale.

Otto indicatori a confronto (ricavi, margine lordo, EBITDA, utile, break-even, cash flow, liquidità e PFN fra 12 mesi), grafico a barre del conto economico, liquidità mese per mese con la soglia minima, dettaglio degli impatti con la nota che dice da dove arriva ogni differenza, riepilogo di investimento e finanziamento, **Esporta scenario** in Excel e **Vai alla Tesoreria**.

**Verifiche**: 80 test (11 sulle simulazioni: +10% di ricavi calcolato a mano, dipendenti, investimento con lo scudo fiscale dell'ammortamento, finanziamento con la PFN che torna al centesimo, giorni obiettivo, leve fuori scala); `npm run verify:schema` a 51 controlli; nell'app: leve mosse, scenario salvato e riaperto, esportazione Excel riletta, base rifiutata con i mesi mancanti elencati.

**Il menu non ha più voci "in arrivo"**: tutte le sette viste di §10 esistono.

---

## 14. Punti aperti
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Un gestionale desktop per consulenti finanziari aziendali: riclassifica il bilan
calcola gli indici che contano, previene le tensioni di cassa prima che arrivino,
e risponde a "cosa succede se" senza toccare un foglio Excel.

![stato](https://img.shields.io/badge/stato-in%20sviluppo%20%C2%B7%20fase%206%2F10-3ddbff)
![stato](https://img.shields.io/badge/stato-in%20sviluppo%20%C2%B7%20fase%207%2F10-3ddbff)
![piattaforma](https://img.shields.io/badge/Windows-x64-3ddbff)
[![licenza](https://img.shields.io/badge/licenza-MIT-5cff9d)](LICENSE)

Expand Down Expand Up @@ -172,8 +172,11 @@ riclassificato con tutti gli indici, e lo mostra in cinque schermate: panoramica
avvisi automatici, conto economico, stato patrimoniale, capitale circolante con le sue
note automatiche, e tesoreria con scadenziario e previsione di cassa a sei mesi.
C'è anche la parte bancaria: fidi, mutui e leasing con il loro piano di ammortamento,
le rate che entrano da sole nella previsione di cassa. Mancano le simulazioni e il
collegamento fra i due programmi.
le rate che entrano da sole nella previsione di cassa. E ci sono le **simulazioni**:
si muovono le leve (ricavi, costi, dipendenti, giorni di incasso, un investimento, un
finanziamento) e si vede subito l'effetto su utile, cassa e debito, scenario salvabile
ed esportabile in Excel. Tutte e sette le viste esistono; manca il collegamento fra i
due programmi.

| | Fase | Stato |
|---|---|---|
Expand All @@ -184,8 +187,8 @@ collegamento fra i due programmi.
| 4 | Le prime tre schermate di analisi | ✅ Fatta |
| 5 | Capitale circolante, tesoreria, scadenziario | ✅ Fatta |
| 6 | Banche e finanziamenti | ✅ Fatta |
| 7 | Analisi e simulazioni | ⬜ Prossima |
| 8-9 | Collegamento fra i due programmi | ⬜ |
| 7 | Analisi e simulazioni | ✅ Fatta |
| 8-9 | Collegamento fra i due programmi, import Excel più tollerante | ⬜ Prossima |
| 10 | Eseguibili: installer, portable, demo | 🟡 Pubblicati a ogni aggiornamento importante; mancano le varianti Consulente/Azienda |

La roadmap completa, con il dettaglio di cosa c'è dentro ogni fase, è in
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "daprodfinanza",
"version": "0.0.5",
"version": "0.0.6",
"description": "Gestionale desktop di controllo di gestione per consulenti finanziari aziendali",
"main": "./out/main/index.js",
"author": "DaProdProduzioni",
Expand Down
13 changes: 13 additions & 0 deletions scripts/verify-schema.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,19 @@ app.whenReady().then(() => {
check('periodicità inventata', () => insertLoan({ frequency: 'weekly' }), 'rifiutato')
check('tasso negativo', () => insertLoan({ rate: -1 }), 'rifiutato')
check('ammortamento inventato', () => insertLoan({ amortization: 'tedesco' }), 'rifiutato')

console.log('\n Scenari di simulazione')
const insertScenario = (name, params) =>
db
.prepare(
`INSERT INTO simulation_scenarios (uuid, company_uuid, name, params,
created_at, updated_at, synced, deleted)
VALUES (?, ?, ?, ?, ?, ?, 0, 0)`
)
.run(randomUUID(), tempCompany, name, params, now, now)
check('scenario', () => insertScenario('Espansione', '{"ricaviPercent":10}'), 'accettato')
check('stesso nome con le maiuscole diverse', () => insertScenario('ESPANSIONE', '{}'), 'rifiutato')
check('parametri che non sono JSON', () => insertScenario('Rotto', 'ricavi +10'), 'rifiutato')
} finally {
db.exec('ROLLBACK')
db.close()
Expand Down
29 changes: 29 additions & 0 deletions src/main/db/migrations/006_simulation_scenarios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Database } from 'better-sqlite3-multiple-ciphers'

/**
* Fase 7 — scenari di simulazione salvati (AGENTS.md §10.8).
*
* Uno scenario è solo un insieme di variazioni con un nome: i risultati non si
* memorizzano, si ricalcolano sempre dalla base corrente. Così uno scenario
* salvato a marzo, riaperto a settembre, ragiona sui numeri di settembre.
* I parametri stanno in JSON perché sono un blocco unico che cambia insieme
* alla schermata, non dati da interrogare.
*/
export function up(db: Database): void {
db.exec(`
CREATE TABLE simulation_scenarios (
uuid TEXT PRIMARY KEY,
company_uuid TEXT NOT NULL REFERENCES companies(uuid),
name TEXT NOT NULL,
params TEXT NOT NULL CHECK (json_valid(params)),
notes TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
synced INTEGER NOT NULL DEFAULT 0 CHECK (synced IN (0, 1)),
deleted INTEGER NOT NULL DEFAULT 0 CHECK (deleted IN (0, 1))
);

CREATE UNIQUE INDEX idx_simulation_scenarios_name
ON simulation_scenarios(company_uuid, name COLLATE NOCASE) WHERE deleted = 0;
`)
}
4 changes: 3 additions & 1 deletion src/main/db/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { up as up002 } from './002_financial_model'
import { up as up003 } from './003_import_documents_no_unique_hash'
import { up as up004 } from './004_treasury'
import { up as up005 } from './005_banks'
import { up as up006 } from './006_simulation_scenarios'

export interface Migration {
version: number
Expand All @@ -21,7 +22,8 @@ export const MIGRATIONS: Migration[] = [
{ version: 2, name: '002_financial_model', up: up002 },
{ version: 3, name: '003_import_documents_no_unique_hash', up: up003 },
{ version: 4, name: '004_treasury', up: up004 },
{ version: 5, name: '005_banks', up: up005 }
{ version: 5, name: '005_banks', up: up005 },
{ version: 6, name: '006_simulation_scenarios', up: up006 }
]

export function runMigrations(db: Database): number {
Expand Down
2 changes: 2 additions & 0 deletions src/main/server/routes/companies.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { requireAuth, requireRole } from '../middleware/auth'
import { analysisRouter } from './analysis.routes'
import { banksRouter } from './banks.routes'
import { simulationRouter } from './simulation.routes'
import { treasuryRouter } from './treasury.routes'

/** Anagrafica Aziende — AGENTS.md §10.1. */
Expand All @@ -21,6 +22,7 @@ companiesRouter.use(requireAuth)
companiesRouter.use('/:uuid', analysisRouter)
companiesRouter.use('/:uuid', treasuryRouter)
companiesRouter.use('/:uuid', banksRouter)
companiesRouter.use('/:uuid', simulationRouter)

function uuidParam(req: Request): string {
const value = req.params['uuid']
Expand Down
69 changes: 69 additions & 0 deletions src/main/server/routes/simulation.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Router, type Request } from 'express'
import type { Scenario } from '@shared/types'
import { requireAuth, requireRole } from '../middleware/auth'
import { HttpError } from '../http-error'
import {
deleteScenario,
exportScenario,
listScenarios,
saveScenario,
simulationBase
} from '../services/simulation.service'

/**
* Analisi & Simulazioni di un'azienda — AGENTS.md §10.8.
*
* Simulare non cambia nessun dato, quindi anche l'operatore Azienda può farlo
* ed esportare il risultato; salvare uno scenario resta al Consulente.
*/
export const simulationRouter: Router = Router({ mergeParams: true })

simulationRouter.use(requireAuth)

function param(req: Request, name: string): string {
const value = req.params[name]
return Array.isArray(value) ? (value[0] ?? '') : (value ?? '')
}

function assertCanRead(req: Request): string {
const companyUuid = param(req, 'uuid')
if (req.auth!.role === 'company' && req.auth!.company_uuid !== companyUuid) {
throw new HttpError(403, 'Operazione non consentita per questo ruolo.')
}
return companyUuid
}

simulationRouter.get('/periods/:periodUuid/simulation-base', (req, res) => {
res.json(
simulationBase(
assertCanRead(req),
param(req, 'periodUuid'),
(req.query.scenario as Scenario) ?? 'actual'
)
)
})

simulationRouter.get('/simulations', (req, res) => {
res.json(listScenarios(assertCanRead(req)))
})

simulationRouter.post('/simulations/export', async (req, res) => {
const companyUuid = assertCanRead(req)
const { filePath, periodUuid, scenario, params, name } = req.body ?? {}
if (!filePath || !periodUuid) throw new HttpError(400, 'Mancano il file o il periodo di base.')
res.status(201).json(
await exportScenario(companyUuid, { filePath, periodUuid, scenario, params, name })
)
})

simulationRouter.post('/simulations', requireRole('consultant'), (req, res) => {
res.status(201).json(saveScenario(param(req, 'uuid'), req.body ?? {}))
})

simulationRouter.put('/simulations/:id', requireRole('consultant'), (req, res) => {
res.json(saveScenario(param(req, 'uuid'), req.body ?? {}, param(req, 'id')))
})

simulationRouter.delete('/simulations/:id', requireRole('consultant'), (req, res) => {
res.json(deleteScenario(param(req, 'uuid'), param(req, 'id')))
})
Loading