Sistema di Gestione Inventario
Panoramica
Il sistema di gestione inventario include due funzionalità principali:
- Inventario Fisico: Permette di registrare i conteggi fisici del magazzino e confrontarli con le quantità stimate
- Aggiornamento Automatico Giacenze: Aggiorna automaticamente le giacenze stimate in base alle vendite dei prodotti finiti
Inventario Fisico
Come funziona
- Vai su Magazzino → Nuovo Inventario
- Seleziona i prodotti da conteggiare dalla lista a sinistra
- Inserisci la quantità fisica contata per ogni prodotto
- Il sistema calcola automaticamente le varianze rispetto alle quantità stimate
- Salva lo snapshot dell’inventario
Cosa viene salvato
- Data e ora del conteggio
- Chi ha effettuato il conteggio (opzionale)
- Note (opzionale)
- Per ogni prodotto:
- Quantità fisica contata
- Quantità stimata dal sistema
- Varianza (differenza)
- Varianza percentuale
- Valore della varianza in €
Visualizzazione Storico
Vai su Magazzino → Storico Inventari per vedere tutti gli inventari effettuati.
Ogni snapshot mostra:
- Data e operatore
- Numero di prodotti contati
- Varianza totale in valore (€)
- Dettaglio per ogni prodotto (espandibile)
Aggiornamento Automatico Giacenze da Vendite
Come funziona
Quando vengono venduti prodotti finiti attraverso Cassa in Cloud, il sistema:
- Identifica il prodotto finito venduto tramite il
cassaincloudmapping - Legge la ricetta del prodotto finito
- Per ogni ingrediente nella ricetta:
- Calcola la quantità consumata (quantità per porzione × quantità vendute)
- Converte l’unità di misura se necessario
- Calcola quante “unità di acquisto” sono state consumate
- Aggiorna la giacenza stimata sottraendo la quantità consumata
Esempio
Prodotto Finito: Carbonara (vendute 10 porzioni)
Ricetta:
- 100g guanciale per porzione
- 50g pecorino per porzione
- 2 uova per porzione
Prodotti Acquistati:
- Guanciale: comprato 1000g per confezione, 5 confezioni in magazzino
- Pecorino: comprato 500g per confezione, 3 confezioni in magazzino
- Uova: comprato 12 pz per confezione, 2 confezioni in magazzino
Calcolo Consumo:
- Guanciale: 100g × 10 = 1000g = 1 confezione → giacenza stimata: 5 - 1 = 4 confezioni
- Pecorino: 50g × 10 = 500g = 1 confezione → giacenza stimata: 3 - 1 = 2 confezioni
- Uova: 2 × 10 = 20 pz = 1.67 confezioni → giacenza stimata: 2 - 1.67 = 0.33 confezioni
Integrazione con Cassa in Cloud (Cassanova)
Il sistema è già configurato per integrarsi con l’API di Cassa in Cloud (https://api.cassanova.com ).
Configurazione Esistente:
- API Client:
/src/store/api/cassaincloudApi.ts - API Proxy:
/src/app/api/cassa-in-cloud/route.ts - API Key:
NEXT_PUBLIC_CASSAINCLOUD_API_KEY(variabile d’ambiente)
Opzione 1: Webhook (Consigliato)
Configura un webhook in Cassa in Cloud per chiamare il nostro endpoint ogni volta che viene effettuata una vendita:
Endpoint: POST /api/inventory/update-from-sales
Payload:
{
"sales": [
{
"product_id": "CASSAINCLOUD_MAPPING_ID",
"quantity": 10
}
]
}Configurazione Webhook su Cassa in Cloud:
- Accedi a MyCassa in Cloud → Impostazioni → Webhooks
- Crea un nuovo webhook per l’entità “Receipt” o “Bill”
- URL:
https://tuodominio.com/api/inventory/update-from-sales - Eventi: create, edit
Opzione 2: Sincronizzazione Automatica Giornaliera con BullMQ (✅ IMPLEMENTATO)
Il sistema include un job BullMQ che sincronizza automaticamente le vendite una volta al giorno alle 2:00 AM.
Attivazione:
-
Tramite UI: Vai su Magazzino → Sincronizza Vendite → Attiva Sync Automatica
-
Tramite API:
// POST /api/inventory/sync-sales
{
"action": "schedule"
}- Tramite Codice:
import { scheduleDailySalesSync } from "@/jobs/syncSalesJob";
await scheduleDailySalesSync(tenantId);Come funziona:
- Il job viene eseguito ogni giorno alle 2:00 AM
- Recupera le vendite del giorno precedente da Cassa in Cloud
- Aggiorna automaticamente le giacenze stimate per ogni ingrediente
- Registra i risultati per monitoraggio
Sincronizzazione Manuale:
Puoi anche triggerare una sincronizzazione manuale:
-
Tramite UI: Magazzino → Sincronizza Vendite → Sincronizza Ora (Ieri)
-
Tramite API:
// POST /api/inventory/sync-sales
{
"action": "trigger",
"date": "2025-11-25" // opzionale, default: ieri
}Monitoraggio:
Controlla lo stato della sincronizzazione:
// GET /api/inventory/sync-sales
// Ritorna:
// - scheduled: job schedulati
// - active: job in esecuzione
// - recent_completed: ultimi job completati
// - recent_failed: ultimi job fallitiOpzione 3: Sincronizzazione Manuale
Crea un pulsante nella dashboard che permette di sincronizzare manualmente le vendite:
const handleSyncSales = async () => {
// Ottieni le vendite dalle ultime 24 ore da Cassa in Cloud
const sales = await fetchRecentSales();
await fetch("/api/inventory/update-from-sales", {
method: "POST",
body: JSON.stringify({ sales }),
});
};API Endpoints
POST /api/inventory/snapshots
Crea un nuovo snapshot dell’inventario fisico.
Body:
{
"items": [
{
"product_id": "PRODUCT_ID",
"physical_count": 5.5
}
],
"notes": "Inventario mensile",
"performed_by": "Mario Rossi"
}Response:
{
"success": true,
"snapshot": {
"_id": "SNAPSHOT_ID",
"snapshot_date": "2025-11-26T10:00:00.000Z",
"total_variance_value": -150.50,
"items": [...]
}
}GET /api/inventory/snapshots
Recupera lo storico degli snapshot.
Query params:
limit: numero massimo di risultati (default: 10)skip: numero di risultati da saltare (default: 0)
Response:
{
"snapshots": [...],
"total": 25,
"limit": 10,
"skip": 0
}POST /api/inventory/update-from-sales
Aggiorna le giacenze stimate in base alle vendite.
Body:
{
"sales": [
{
"product_id": "CASSAINCLOUD_MAPPING_ID",
"quantity": 10
}
]
}Response:
{
"success": true,
"processed": 1,
"results": [
{
"product_id": "CASSAINCLOUD_MAPPING_ID",
"product_name": "Carbonara",
"quantity_sold": 10,
"success": true,
"ingredient_updates": [
{
"product_id": "INGREDIENT_ID",
"product_name": "Guanciale",
"consumed_units": 1.0,
"old_estimate": 5,
"new_estimate": 4
}
]
}
]
}Best Practices
- Inventario Fisico Regolare: Esegui un inventario fisico almeno una volta al mese
- Confronta le Varianze: Analizza le varianze per identificare:
- Sprechi non registrati
- Errori nelle ricette
- Furti o perdite
- Errori nei conteggi di acquisto
- Mantieni le Ricette Aggiornate: Assicurati che le ricette riflettano le quantità effettivamente utilizzate
- Monitora le Giacenze Stimate: Se le giacenze stimate diventano negative o molto diverse dalla realtà, potrebbe indicare problemi nelle ricette o nei consumi
File Importanti
/src/jobs/syncSalesJob.ts
Contiene il worker BullMQ per la sincronizzazione automatica delle vendite:
Funzioni principali:
scheduleDailySalesSync(tenantId): Schedula la sincronizzazione automatica giornalieratriggerManualSalesSync(tenantId, date?): Triggera una sincronizzazione manualesyncSalesQueue: La queue BullMQ per i job di sincronizzazionesyncSalesWorker: Il worker che processa i job
Workflow del Worker:
- Riceve un job con
tenantIdedate(opzionale) - Calcola la data target (default: ieri)
- Chiama
/api/cassa-in-cloud/sales?date=YYYY-MM-DD - Ottiene le vendite da Cassa in Cloud
- Chiama
/api/inventory/update-from-salescon le vendite - Aggiorna le giacenze stimate
- Ritorna il risultato con statistiche
Configurazione:
- Schedule: Ogni giorno alle 2:00 AM (
0 2 * * *) - Retry: 3 tentativi con backoff esponenziale
- Concurrency: 1 (per evitare conflitti)
/src/app/api/inventory/sync-sales/route.ts
Endpoint API per gestire la sincronizzazione:
Actions:
schedule: Attiva la sincronizzazione automatica giornalieratrigger: Esegue una sincronizzazione manuale immediatastatus: Ottiene lo stato di tutti i jobcancel: Cancella la sincronizzazione schedulata
/src/app/api/cassa-in-cloud/sales/route.ts
Endpoint che recupera le vendite dall’API reale di Cassa in Cloud.
Integrazione con API Cassanova:
- Utilizza il report “Sold by Product” (
/reports/sold/products) - Chiama l’API proxy locale che gestisce l’autenticazione OAuth2
- Trasforma i dati dal formato Cassa in Cloud al formato interno
- Filtra per data e punto vendita
Parametri:
date: Data in formato YYYY-MM-DD (richiesto)idsSalesPoint: IDs dei punti vendita separati da virgola (opzionale)
Flusso:
- Valida i parametri della richiesta
- Costruisce la query per il report “Sold by Product” di Cassa in Cloud
- Chiama
/api/cassa-in-cloud?endpoint=reports/sold/products&... - Il proxy
/api/cassa-in-cloud/route.tsgestisce:- Autenticazione con API key (
POST /apikey/token) - Caching del token di accesso
- Chiamata all’API Cassanova con Bearer token
- Autenticazione con API key (
- Trasforma la risposta nel formato atteso dall’inventario
- Restituisce le vendite con totali
Variabili d’Ambiente Richieste:
NEXT_PUBLIC_CASSAINCLOUD_API_KEY: API key fornita da Cassa in CloudNEXT_PUBLIC_BASE_URL: URL base dell’applicazione (per chiamate interne)
Risposta di Esempio:
{
"success": true,
"date": "2025-11-25",
"sales": [
{
"product_mapping_id": "CASSACLOUD_PRODUCT_ID",
"product_name": "Carbonara",
"quantity_sold": 15,
"total_amount": 180.0,
"sales_point_id": 1,
"timestamp": "2025-11-25"
}
],
"total_sales": 35,
"total_amount": 408.0,
"total_quantity": 35
}Modelli Database
InventorySnapshot
{
snapshot_date: Date,
items: [{
product_id: ObjectId,
product_name: string,
physical_count: number,
estimated_count: number,
variance: number,
variance_percentage: number,
unit: string,
value_at_count: number
}],
total_variance_value: number,
notes: string,
performed_by: string,
tenant_id: ObjectId
}Product (campi rilevanti per inventario)
{
quantity: number, // Quantità acquistata per unità
buyUnit: string, // Unità di acquisto (es: "kg", "pz")
stockEstimate: number, // Giacenza stimata (numero di unità di acquisto)
wastePercentage: number, // Percentuale di scarto
// ... altri campi
}