diff --git a/CLAUDE.md b/CLAUDE.md
index 123dfbc..632b181 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -46,12 +46,103 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
## Quick Start - Session Recovery
-**Ultima sessione:** 29 Novembre 2025 (sera)
+**Ultima sessione:** 30 Novembre 2025
**Stato progetto:** Migrazione Oracle APEX → .NET + React TypeScript in corso
**Lavoro completato nell'ultima sessione:**
+- **NUOVA FEATURE: Sistema Codici Automatici Configurabili** - COMPLETATO
+ - **Obiettivo:** Sistema admin per configurare la generazione automatica di codici (articoli, magazzini, movimenti, ecc.)
+ - **Backend implementato:**
+ - `AutoCode.cs` - Entity con pattern configurabile, prefisso, sequenza, reset periodico
+ - `AutoCodeService.cs` - Logica business (generazione, preview, reset, validazione pattern)
+ - `AutoCodesController.cs` - API REST complete
+ - Migration EF Core `AddAutoCodeSystem`
+ - Seed automatico configurazioni default per tutte le entità
+ - **Frontend implementato:**
+ - `autoCode.ts` - Types TypeScript
+ - `autoCodeService.ts` - API calls
+ - `AutoCodesAdminPage.tsx` - Pagina admin con tabella configurazioni, dialog modifica, guida pattern
+ - **Pattern supportati:**
+ - `{PREFIX}` - Prefisso configurabile
+ - `{SEQ:n}` - Sequenza numerica con n cifre
+ - `{YYYY}`, `{YY}` - Anno
+ - `{MM}`, `{DD}` - Mese, Giorno
+ - **Funzionalità:**
+ - Configurazione per entità (warehouse_article, stock_movement, cliente, evento, ecc.)
+ - Reset sequenza annuale o mensile automatico
+ - Preview prossimo codice senza incremento
+ - Reset manuale sequenza
+ - Abilitazione/disabilitazione per entità
+ - Raggruppamento per modulo nell'UI
+ - **API Endpoints:**
+ - `GET /api/autocodes` - Lista configurazioni
+ - `GET /api/autocodes/{entityCode}` - Dettaglio
+ - `GET /api/autocodes/{entityCode}/preview` - Anteprima prossimo codice
+ - `POST /api/autocodes/{entityCode}/generate` - Genera nuovo codice
+ - `PUT /api/autocodes/{id}` - Aggiorna configurazione
+ - `POST /api/autocodes/{entityCode}/reset-sequence` - Reset sequenza
+ - `GET /api/autocodes/placeholders` - Lista placeholder disponibili
+ - **File principali:**
+ - `src/Apollinare.Domain/Entities/AutoCode.cs`
+ - `src/Apollinare.API/Services/AutoCodeService.cs`
+ - `src/Apollinare.API/Controllers/AutoCodesController.cs`
+ - `frontend/src/pages/AutoCodesAdminPage.tsx`
+
+**Lavoro completato nelle sessioni precedenti (29 Novembre 2025 notte):**
+
+- **NUOVA FEATURE: Modulo Magazzino (warehouse)** - COMPLETATO
+ - **Backend implementato:**
+ - Entities complete in `/src/Apollinare.Domain/Entities/Warehouse/`:
+ - `WarehouseLocation.cs` - Magazzini fisici/logici con Type enum (Physical, Virtual, Transit)
+ - `WarehouseArticleCategory.cs` - Categorie gerarchiche con Color, Icon, Level, FullPath
+ - `WarehouseArticle.cs` - Articoli con batch/serial management flags, valorizzazione
+ - `ArticleBatch.cs` - Tracciabilità lotti con scadenza
+ - `ArticleSerial.cs` - Tracciabilità numeri seriali
+ - `StockLevel.cs` - Giacenze per articolo/magazzino/batch
+ - `StockMovement.cs` - Movimenti (Inbound/Outbound/Transfer/Adjustment)
+ - `StockMovementLine.cs` - Righe movimento
+ - `MovementReason.cs` - Causali movimento
+ - `ArticleBarcode.cs` - Multi-barcode support
+ - `StockValuation.cs` + `StockValuationLayer.cs` - Valorizzazione periodo e layer FIFO/LIFO
+ - `InventoryCount.cs` + `InventoryCountLine.cs` - Inventari fisici
+ - Service completo `WarehouseService.cs` con:
+ - CRUD articoli, categorie, magazzini
+ - Gestione movimenti (carico/scarico/trasferimento/rettifica)
+ - Conferma movimenti con aggiornamento giacenze
+ - Calcolo valorizzazione (WeightedAverage, FIFO, LIFO, StandardCost)
+ - Gestione partite e seriali
+ - Controllers REST in `/src/Apollinare.API/Modules/Warehouse/Controllers/`:
+ - `WarehouseLocationsController.cs`
+ - `WarehouseArticlesController.cs`
+ - `WarehouseArticleCategoriesController.cs`
+ - `StockMovementsController.cs`
+ - `StockLevelsController.cs`
+ - Seed dati default (magazzino principale + transito, categorie base, causali)
+
+- **CONFIGURAZIONE: EF Core Code First Migrations** - COMPLETATO
+ - **Problema:** Le tabelle venivano create manualmente invece che con migrations EF Core
+ - **Soluzione implementata:**
+ - Sostituito `db.Database.EnsureCreated()` con `db.Database.MigrateAsync()` in `Program.cs`
+ - Creata migration `InitialCreate` con tutte le tabelle (sistema + moduli + warehouse)
+ - Le migrations vengono applicate **automaticamente all'avvio** dell'applicazione
+ - Logging delle migrations pendenti prima dell'applicazione
+ - **Comandi per future migrations:**
+
+ ```bash
+ # Creare nuova migration
+ dotnet ef migrations add NomeMigration \
+ --project src/Apollinare.Infrastructure \
+ --startup-project src/Apollinare.API
+
+ # L'applicazione è AUTOMATICA all'avvio - non serve "dotnet ef database update"
+ ```
+
+ - **File modificati:** `Program.cs`, `src/Apollinare.Infrastructure/Migrations/`
+
+**Lavoro completato nelle sessioni precedenti (29 Novembre 2025 sera):**
+
- **NUOVA FEATURE: Sistema Moduli Applicativi** - COMPLETATO (continuazione)
- **Obiettivo:** Sistema di modularizzazione per gestire licenze, abbonamenti e funzionalità dinamiche
- **Backend implementato:**
@@ -318,11 +409,14 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
**MODULI BUSINESS (PRIORITÀ ALTA):**
-1. [ ] **Implementare modulo Magazzino (warehouse)** - Base per tutti gli altri
-2. [ ] **Implementare modulo Acquisti (purchases)** - Dipende da Magazzino
-3. [ ] **Implementare modulo Vendite (sales)** - Dipende da Magazzino
-4. [ ] **Implementare modulo Produzione (production)** - Dipende da Magazzino
-5. [ ] **Implementare modulo Qualità (quality)** - Indipendente
+1. [x] **Implementare modulo Magazzino (warehouse)** - COMPLETATO (backend)
+ - Backend: Entities, Service, Controllers, API completi
+ - Manca: Frontend (pagine React per gestione articoli, movimenti, giacenze)
+2. [ ] **Frontend modulo Magazzino** - Pagine React per warehouse
+3. [ ] **Implementare modulo Acquisti (purchases)** - Dipende da Magazzino
+4. [ ] **Implementare modulo Vendite (sales)** - Dipende da Magazzino
+5. [ ] **Implementare modulo Produzione (production)** - Dipende da Magazzino
+6. [ ] **Implementare modulo Qualità (quality)** - Indipendente
**Report System (completamento):**
@@ -1855,3 +1949,79 @@ public interface IWarehouseService
- `ModulePurchasePage.tsx`: Rimosso `moduleService` import
- `ModulesAdminPage.tsx`: Rimosso `PowerIcon`, `CheckIcon`, `CancelIcon`
- **File:** Vari componenti frontend
+
+33. **EF Core Code First vs Database First (FIX 29/11/2025):**
+ - **Problema:** Le tabelle venivano create manualmente con SQL invece di usare EF Core migrations
+ - **Causa:** `db.Database.EnsureCreated()` non supporta migrations e crea le tabelle direttamente
+ - **Soluzione:**
+ - Sostituito `EnsureCreated()` con `MigrateAsync()` in `Program.cs`
+ - Rimosso database e migrations esistenti
+ - Creata nuova migration `InitialCreate` con `dotnet ef migrations add`
+ - Le migrations vengono ora applicate automaticamente all'avvio
+ - **File:** `Program.cs`, `src/Apollinare.Infrastructure/Migrations/20251129134709_InitialCreate.cs`
+
+34. **Modulo Warehouse - Struttura Completa (IMPLEMENTATO 29/11/2025):**
+ - **Entities in `/src/Apollinare.Domain/Entities/Warehouse/`:**
+ - `WarehouseLocation.cs` - Magazzini (Physical, Virtual, Transit)
+ - `WarehouseArticle.cs` - Articoli con batch/serial flags
+ - `WarehouseArticleCategory.cs` - Categorie gerarchiche
+ - `ArticleBatch.cs` - Lotti con scadenza
+ - `ArticleSerial.cs` - Numeri seriali
+ - `StockLevel.cs` - Giacenze
+ - `StockMovement.cs` + `StockMovementLine.cs` - Movimenti
+ - `MovementReason.cs` - Causali
+ - `ArticleBarcode.cs` - Multi-barcode
+ - `StockValuation.cs` + `StockValuationLayer.cs` - Valorizzazione
+ - `InventoryCount.cs` + `InventoryCountLine.cs` - Inventari
+ - **Service:** `WarehouseService.cs` con CRUD completo, movimenti, giacenze, valorizzazione
+ - **Controllers:** `WarehouseLocationsController`, `WarehouseArticlesController`, `WarehouseArticleCategoriesController`, `StockMovementsController`, `StockLevelsController`
+ - **API Endpoints principali:**
+ - `GET/POST /api/warehouse/locations` - Magazzini
+ - `GET/POST /api/warehouse/articles` - Articoli
+ - `GET/POST /api/warehouse/categories` - Categorie
+ - `POST /api/warehouse/movements/inbound` - Carichi
+ - `POST /api/warehouse/movements/outbound` - Scarichi
+ - `POST /api/warehouse/movements/{id}/confirm` - Conferma movimento
+ - `GET /api/warehouse/articles/{id}/stock` - Giacenza articolo
+
+35. **Sistema Codici Automatici Configurabili (IMPLEMENTATO 30/11/2025):**
+ - **Obiettivo:** Sistema per generare automaticamente codici univoci per tutte le entità (articoli, magazzini, movimenti, clienti, eventi, ecc.)
+ - **Entity:** `AutoCode.cs` in `/src/Apollinare.Domain/Entities/`
+ - `EntityCode` - Identificativo entità (es. "warehouse_article")
+ - `EntityName` - Nome visualizzato
+ - `Prefix` - Prefisso per {PREFIX}
+ - `Pattern` - Pattern con placeholder (es. "{PREFIX}{YYYY}-{SEQ:5}")
+ - `LastSequence` - Ultimo numero usato
+ - `ResetSequenceYearly` / `ResetSequenceMonthly` - Reset automatico
+ - `IsEnabled` - Abilita generazione
+ - `IsReadOnly` - Codice non modificabile
+ - `ModuleCode` - Raggruppa per modulo
+ - **Service:** `AutoCodeService.cs` in `/src/Apollinare.API/Services/`
+ - `GenerateNextCodeAsync(entityCode)` - Genera e incrementa
+ - `PreviewNextCodeAsync(entityCode)` - Anteprima senza incremento
+ - `IsCodeUniqueAsync(entityCode, code)` - Verifica univocità
+ - `ResetSequenceAsync(entityCode)` - Reset manuale
+ - `SeedDefaultConfigurationsAsync()` - Seed configurazioni default
+ - **Controller:** `AutoCodesController.cs`
+ - **Frontend:**
+ - `AutoCodesAdminPage.tsx` - Pagina admin con accordions per modulo
+ - `autoCodeService.ts` - API calls
+ - `autoCode.ts` - Types
+ - **Pattern supportati:**
+ - `{PREFIX}` - Prefisso configurabile
+ - `{SEQ:n}` - Sequenza con n cifre (es. {SEQ:5} → 00001)
+ - `{YYYY}`, `{YY}` - Anno 4 o 2 cifre
+ - `{MM}`, `{DD}` - Mese e giorno
+ - Testo statico (es. "-", "/")
+ - **Entità preconfigurate:**
+ - Core: cliente, evento, articolo
+ - Warehouse: warehouse_location, warehouse_article, warehouse_category, stock_movement, inventory_count, article_batch
+ - Purchases (future): purchase_order, supplier
+ - Sales (future): sales_order, invoice
+ - **Esempio utilizzo nel codice:**
+ ```csharp
+ // Nel service che crea un articolo
+ var code = await _autoCodeService.GenerateNextCodeAsync("warehouse_article");
+ if (code != null)
+ article.Code = code;
+ ```
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index fbe379b..bb7c2cb 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -19,6 +19,7 @@ import ReportTemplatesPage from "./pages/ReportTemplatesPage";
import ReportEditorPage from "./pages/ReportEditorPage";
import ModulesAdminPage from "./pages/ModulesAdminPage";
import ModulePurchasePage from "./pages/ModulePurchasePage";
+import AutoCodesAdminPage from "./pages/AutoCodesAdminPage";
import WarehouseRoutes from "./modules/warehouse/routes";
import { ModuleGuard } from "./components/ModuleGuard";
import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates";
@@ -90,12 +91,16 @@ function App() {
path="report-editor/:id"
element={}
/>
- {/* Moduli */}
+ {/* Admin */}
} />
}
/>
+ }
+ />
{/* Warehouse Module */}
, path: "/report-templates" },
{ text: "Moduli", icon: , path: "/modules" },
+ { text: "Codici Auto", icon: , path: "/admin/auto-codes" },
];
export default function Layout() {
diff --git a/frontend/src/modules/warehouse/pages/ArticleFormPage.tsx b/frontend/src/modules/warehouse/pages/ArticleFormPage.tsx
index 22aee9b..78050cd 100644
--- a/frontend/src/modules/warehouse/pages/ArticleFormPage.tsx
+++ b/frontend/src/modules/warehouse/pages/ArticleFormPage.tsx
@@ -86,6 +86,7 @@ export default function ArticleFormPage() {
const [tabValue, setTabValue] = useState(0);
const [formData, setFormData] = useState({
code: "",
+ alternativeCode: "",
description: "",
shortDescription: "",
categoryId: undefined as number | undefined,
@@ -138,6 +139,7 @@ export default function ArticleFormPage() {
if (article) {
setFormData({
code: article.code,
+ alternativeCode: article.alternativeCode || "",
description: article.description,
shortDescription: article.shortDescription || "",
categoryId: article.categoryId,
@@ -190,7 +192,8 @@ export default function ArticleFormPage() {
const validate = (): boolean => {
const newErrors: Record = {};
- if (!formData.code.trim()) {
+ // Il codice è generato automaticamente, non richiede validazione in creazione
+ if (!isNew && !formData.code.trim()) {
newErrors.code = "Il codice è obbligatorio";
}
if (!formData.description.trim()) {
@@ -211,9 +214,10 @@ export default function ArticleFormPage() {
let savedId: number;
if (isNew) {
const createData: CreateArticleDto = {
- code: formData.code,
+ // code è generato automaticamente dal backend
description: formData.description,
shortDescription: formData.shortDescription || undefined,
+ alternativeCode: formData.alternativeCode || undefined,
categoryId: formData.categoryId,
unitOfMeasure: formData.unitOfMeasure,
barcode: formData.barcode || undefined,
@@ -234,9 +238,10 @@ export default function ArticleFormPage() {
savedId = result.id;
} else {
const updateData: UpdateArticleDto = {
- code: formData.code,
+ // code non modificabile
description: formData.description,
shortDescription: formData.shortDescription || undefined,
+ alternativeCode: formData.alternativeCode || undefined,
categoryId: formData.categoryId,
unitOfMeasure: formData.unitOfMeasure,
barcode: formData.barcode || undefined,
@@ -327,19 +332,46 @@ export default function ArticleFormPage() {
Informazioni Base
-
+
handleChange("code", e.target.value)}
- error={!!errors.code}
- helperText={errors.code}
- required
- disabled={!isNew}
+ value={
+ isNew ? "(Generato al salvataggio)" : formData.code
+ }
+ disabled
+ helperText={
+ isNew
+ ? "Verrà assegnato automaticamente"
+ : "Generato automaticamente"
+ }
+ InputProps={{
+ readOnly: true,
+ }}
+ sx={
+ isNew
+ ? {
+ "& .MuiInputBase-input.Mui-disabled": {
+ fontStyle: "italic",
+ color: "text.secondary",
+ },
+ }
+ : undefined
+ }
/>
-
+
+
+ handleChange("alternativeCode", e.target.value)
+ }
+ helperText="Opzionale"
+ />
+
+
{
const newErrors: Record = {};
- if (!formData.code.trim()) {
- newErrors.code = "Il codice è obbligatorio";
- }
+ // Il codice è generato automaticamente, non richiede validazione in creazione
if (!formData.name.trim()) {
newErrors.name = "Il nome è obbligatorio";
}
@@ -124,12 +124,16 @@ export default function WarehouseLocationsPage() {
try {
if (editingWarehouse) {
+ // In modifica non inviamo il code (non modificabile)
+ const { code: _code, ...updateData } = formData;
await updateMutation.mutateAsync({
id: editingWarehouse.id,
- data: formData,
+ data: updateData,
});
} else {
- await createMutation.mutateAsync(formData);
+ // In creazione non inviamo il code (generato automaticamente dal backend)
+ const { code: _code, ...createData } = formData;
+ await createMutation.mutateAsync(createData);
}
handleCloseDialog();
} catch (error) {
@@ -364,19 +368,46 @@ export default function WarehouseLocationsPage() {
-
+
handleChange("code", e.target.value)}
- error={!!errors.code}
- helperText={errors.code}
- required
- disabled={!!editingWarehouse}
+ value={
+ editingWarehouse ? formData.code : "(Generato al salvataggio)"
+ }
+ disabled
+ helperText={
+ editingWarehouse
+ ? "Generato automaticamente"
+ : "Verrà assegnato automaticamente"
+ }
+ InputProps={{
+ readOnly: true,
+ }}
+ sx={
+ !editingWarehouse
+ ? {
+ "& .MuiInputBase-input.Mui-disabled": {
+ fontStyle: "italic",
+ color: "text.secondary",
+ },
+ }
+ : undefined
+ }
/>
-
+
+
+ handleChange("alternativeCode", e.target.value)
+ }
+ helperText="Opzionale"
+ />
+
+
>({ attivo: true });
const { data: articoli = [], isLoading } = useQuery({
- queryKey: ['articoli'],
+ queryKey: ["articoli"],
queryFn: () => articoliService.getAll(),
});
const { data: tipiMateriale = [] } = useQuery({
- queryKey: ['lookup', 'tipi-materiale'],
+ queryKey: ["lookup", "tipi-materiale"],
queryFn: () => lookupService.getTipiMateriale(),
});
const { data: categorie = [] } = useQuery({
- queryKey: ['lookup', 'categorie'],
+ queryKey: ["lookup", "categorie"],
queryFn: () => lookupService.getCategorie(),
});
const createMutation = useMutation({
mutationFn: (data: Partial) => articoliService.create(data),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['articoli'] });
+ queryClient.invalidateQueries({ queryKey: ["articoli"] });
handleCloseDialog();
},
});
const updateMutation = useMutation({
- mutationFn: ({ id, data }: { id: number; data: Partial }) => articoliService.update(id, data),
+ mutationFn: ({ id, data }: { id: number; data: Partial }) =>
+ articoliService.update(id, data),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['articoli'] });
+ queryClient.invalidateQueries({ queryKey: ["articoli"] });
handleCloseDialog();
},
});
const deleteMutation = useMutation({
mutationFn: (id: number) => articoliService.delete(id),
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['articoli'] }),
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["articoli"] }),
});
const handleCloseDialog = () => {
@@ -78,35 +83,45 @@ export default function ArticoliPage() {
const handleSubmit = () => {
if (editingId) {
- updateMutation.mutate({ id: editingId, data: formData });
+ // In modifica, non inviamo il codice (non modificabile)
+ const { codice: _codice, ...updateData } = formData;
+ updateMutation.mutate({ id: editingId, data: updateData });
} else {
- createMutation.mutate(formData);
+ // In creazione, non inviamo il codice (generato automaticamente)
+ const { codice: _codice, ...createData } = formData;
+ createMutation.mutate(createData);
}
};
const columns: GridColDef[] = [
- { field: 'codice', headerName: 'Codice', width: 100 },
- { field: 'descrizione', headerName: 'Descrizione', flex: 1, minWidth: 200 },
+ { field: "codice", headerName: "Codice", width: 100 },
+ { field: "codiceAlternativo", headerName: "Cod. Alt.", width: 100 },
+ { field: "descrizione", headerName: "Descrizione", flex: 1, minWidth: 200 },
{
- field: 'tipoMateriale',
- headerName: 'Tipo',
+ field: "tipoMateriale",
+ headerName: "Tipo",
width: 130,
- valueGetter: (value: any) => value?.descrizione || '',
+ valueGetter: (value: any) => value?.descrizione || "",
},
{
- field: 'categoria',
- headerName: 'Categoria',
+ field: "categoria",
+ headerName: "Categoria",
width: 120,
- valueGetter: (value: any) => value?.descrizione || '',
+ valueGetter: (value: any) => value?.descrizione || "",
},
- { field: 'qtaDisponibile', headerName: 'Disponibile', width: 100, type: 'number' },
- { field: 'qtaStdA', headerName: 'Qta A', width: 80, type: 'number' },
- { field: 'qtaStdB', headerName: 'Qta B', width: 80, type: 'number' },
- { field: 'qtaStdS', headerName: 'Qta S', width: 80, type: 'number' },
- { field: 'unitaMisura', headerName: 'UM', width: 60 },
{
- field: 'actions',
- headerName: 'Azioni',
+ field: "qtaDisponibile",
+ headerName: "Disponibile",
+ width: 100,
+ type: "number",
+ },
+ { field: "qtaStdA", headerName: "Qta A", width: 80, type: "number" },
+ { field: "qtaStdB", headerName: "Qta B", width: 80, type: "number" },
+ { field: "qtaStdS", headerName: "Qta S", width: 80, type: "number" },
+ { field: "unitaMisura", headerName: "UM", width: 60 },
+ {
+ field: "actions",
+ headerName: "Azioni",
width: 120,
sortable: false,
renderCell: (params) => (
@@ -118,7 +133,7 @@ export default function ArticoliPage() {
size="small"
color="error"
onClick={() => {
- if (confirm('Eliminare questo articolo?')) {
+ if (confirm("Eliminare questo articolo?")) {
deleteMutation.mutate(params.row.id);
}
}}
@@ -132,14 +147,25 @@ export default function ArticoliPage() {
return (
-
+
Articoli
- } onClick={() => setOpenDialog(true)}>
+ }
+ onClick={() => setOpenDialog(true)}
+ >
Nuovo Articolo
-
+
-
+ }
+ >
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+ Anteprima:
+
+
+ {currentExample}
+
+
+
+
+
+
+ Reset Sequenza
+
+
+
+
+
+
+ setFormData({
+ ...formData,
+ description: e.target.value || null,
+ })
+ }
+ fullWidth
+ size="small"
+ multiline
+ rows={2}
+ />
+
+
+
+
+ setFormData({
+ ...formData,
+ isEnabled: e.target.checked,
+ })
+ }
+ />
+ }
+ label="Generazione attiva"
+ />
+
+
+
+
+ setFormData({
+ ...formData,
+ isReadOnly: e.target.checked,
+ })
+ }
+ />
+ }
+ label="Codice non modificabile"
+ />
+
+
+
+ {error && (
+
+ {error.message || "Errore durante il salvataggio"}
+
+ )}
+
+
+
+ : null
+ }
+ >
+ Salva
+
+
+ >
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/ClientiPage.tsx b/frontend/src/pages/ClientiPage.tsx
index 815ef3d..0f9e92f 100644
--- a/frontend/src/pages/ClientiPage.tsx
+++ b/frontend/src/pages/ClientiPage.tsx
@@ -1,5 +1,5 @@
-import { useState } from 'react';
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Box,
Typography,
@@ -12,11 +12,15 @@ import {
DialogActions,
TextField,
Grid,
-} from '@mui/material';
-import { DataGrid, GridColDef } from '@mui/x-data-grid';
-import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
-import { clientiService } from '../services/lookupService';
-import { Cliente } from '../types';
+} from "@mui/material";
+import { DataGrid, GridColDef } from "@mui/x-data-grid";
+import {
+ Add as AddIcon,
+ Edit as EditIcon,
+ Delete as DeleteIcon,
+} from "@mui/icons-material";
+import { clientiService } from "../services/lookupService";
+import { Cliente } from "../types";
export default function ClientiPage() {
const queryClient = useQueryClient();
@@ -25,29 +29,30 @@ export default function ClientiPage() {
const [formData, setFormData] = useState>({ attivo: true });
const { data: clienti = [], isLoading } = useQuery({
- queryKey: ['clienti'],
+ queryKey: ["clienti"],
queryFn: () => clientiService.getAll(),
});
const createMutation = useMutation({
mutationFn: (data: Partial) => clientiService.create(data),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['clienti'] });
+ queryClient.invalidateQueries({ queryKey: ["clienti"] });
handleCloseDialog();
},
});
const updateMutation = useMutation({
- mutationFn: ({ id, data }: { id: number; data: Partial }) => clientiService.update(id, data),
+ mutationFn: ({ id, data }: { id: number; data: Partial }) =>
+ clientiService.update(id, data),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['clienti'] });
+ queryClient.invalidateQueries({ queryKey: ["clienti"] });
handleCloseDialog();
},
});
const deleteMutation = useMutation({
mutationFn: (id: number) => clientiService.delete(id),
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['clienti'] }),
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["clienti"] }),
});
const handleCloseDialog = () => {
@@ -64,22 +69,33 @@ export default function ClientiPage() {
const handleSubmit = () => {
if (editingId) {
- updateMutation.mutate({ id: editingId, data: formData });
+ // In modifica, non inviamo il codice (non modificabile)
+ const { codice: _codice, ...updateData } = formData;
+ updateMutation.mutate({ id: editingId, data: updateData });
} else {
- createMutation.mutate(formData);
+ // In creazione, non inviamo il codice (generato automaticamente)
+ const { codice: _codice, ...createData } = formData;
+ createMutation.mutate(createData);
}
};
const columns: GridColDef[] = [
- { field: 'ragioneSociale', headerName: 'Ragione Sociale', flex: 1, minWidth: 200 },
- { field: 'citta', headerName: 'Città', width: 150 },
- { field: 'provincia', headerName: 'Prov.', width: 80 },
- { field: 'telefono', headerName: 'Telefono', width: 130 },
- { field: 'email', headerName: 'Email', width: 200 },
- { field: 'partitaIva', headerName: 'P.IVA', width: 130 },
+ { field: "codice", headerName: "Codice", width: 100 },
+ { field: "codiceAlternativo", headerName: "Cod. Alt.", width: 100 },
{
- field: 'actions',
- headerName: 'Azioni',
+ field: "ragioneSociale",
+ headerName: "Ragione Sociale",
+ flex: 1,
+ minWidth: 200,
+ },
+ { field: "citta", headerName: "Città", width: 150 },
+ { field: "provincia", headerName: "Prov.", width: 80 },
+ { field: "telefono", headerName: "Telefono", width: 130 },
+ { field: "email", headerName: "Email", width: 200 },
+ { field: "partitaIva", headerName: "P.IVA", width: 130 },
+ {
+ field: "actions",
+ headerName: "Azioni",
width: 120,
sortable: false,
renderCell: (params) => (
@@ -91,7 +107,7 @@ export default function ClientiPage() {
size="small"
color="error"
onClick={() => {
- if (confirm('Eliminare questo cliente?')) {
+ if (confirm("Eliminare questo cliente?")) {
deleteMutation.mutate(params.row.id);
}
}}
@@ -105,14 +121,25 @@ export default function ClientiPage() {
return (
-
+
Clienti
- } onClick={() => setOpenDialog(true)}>
+ }
+ onClick={() => setOpenDialog(true)}
+ >
Nuovo Cliente
-
+
-
- {editingId ? 'Modifica Cliente' : 'Nuovo Cliente'}
+
+
+ {editingId ? "Modifica Cliente" : "Nuovo Cliente"}
+
-
+
+
+
+
+
+ setFormData({
+ ...formData,
+ codiceAlternativo: e.target.value,
+ })
+ }
+ helperText="Opzionale"
+ />
+
+
setFormData({ ...formData, ragioneSociale: e.target.value })}
+ value={formData.ragioneSociale || ""}
+ onChange={(e) =>
+ setFormData({ ...formData, ragioneSociale: e.target.value })
+ }
/>
setFormData({ ...formData, indirizzo: e.target.value })}
+ value={formData.indirizzo || ""}
+ onChange={(e) =>
+ setFormData({ ...formData, indirizzo: e.target.value })
+ }
/>
setFormData({ ...formData, cap: e.target.value })}
+ value={formData.cap || ""}
+ onChange={(e) =>
+ setFormData({ ...formData, cap: e.target.value })
+ }
/>
setFormData({ ...formData, citta: e.target.value })}
+ value={formData.citta || ""}
+ onChange={(e) =>
+ setFormData({ ...formData, citta: e.target.value })
+ }
/>
setFormData({ ...formData, provincia: e.target.value })}
+ value={formData.provincia || ""}
+ onChange={(e) =>
+ setFormData({ ...formData, provincia: e.target.value })
+ }
/>
setFormData({ ...formData, telefono: e.target.value })}
+ value={formData.telefono || ""}
+ onChange={(e) =>
+ setFormData({ ...formData, telefono: e.target.value })
+ }
/>
@@ -183,40 +273,53 @@ export default function ClientiPage() {
label="Email"
fullWidth
type="email"
- value={formData.email || ''}
- onChange={(e) => setFormData({ ...formData, email: e.target.value })}
+ value={formData.email || ""}
+ onChange={(e) =>
+ setFormData({ ...formData, email: e.target.value })
+ }
/>
setFormData({ ...formData, pec: e.target.value })}
+ value={formData.pec || ""}
+ onChange={(e) =>
+ setFormData({ ...formData, pec: e.target.value })
+ }
/>
setFormData({ ...formData, codiceFiscale: e.target.value })}
+ value={formData.codiceFiscale || ""}
+ onChange={(e) =>
+ setFormData({ ...formData, codiceFiscale: e.target.value })
+ }
/>
setFormData({ ...formData, partitaIva: e.target.value })}
+ value={formData.partitaIva || ""}
+ onChange={(e) =>
+ setFormData({ ...formData, partitaIva: e.target.value })
+ }
/>
setFormData({ ...formData, codiceDestinatario: e.target.value })}
+ value={formData.codiceDestinatario || ""}
+ onChange={(e) =>
+ setFormData({
+ ...formData,
+ codiceDestinatario: e.target.value,
+ })
+ }
/>
@@ -225,8 +328,10 @@ export default function ClientiPage() {
fullWidth
multiline
rows={3}
- value={formData.note || ''}
- onChange={(e) => setFormData({ ...formData, note: e.target.value })}
+ value={formData.note || ""}
+ onChange={(e) =>
+ setFormData({ ...formData, note: e.target.value })
+ }
/>
@@ -234,7 +339,7 @@ export default function ClientiPage() {
diff --git a/frontend/src/services/autoCodeService.ts b/frontend/src/services/autoCodeService.ts
new file mode 100644
index 0000000..05905bd
--- /dev/null
+++ b/frontend/src/services/autoCodeService.ts
@@ -0,0 +1,97 @@
+import api from "./api";
+import type {
+ AutoCodeDto,
+ AutoCodeUpdateDto,
+ GenerateCodeResponse,
+ ResetSequenceRequest,
+ CheckUniqueResponse,
+ PlaceholderInfo,
+} from "../types/autoCode";
+
+/**
+ * Service per la gestione dei codici automatici
+ */
+export const autoCodeService = {
+ /**
+ * Ottiene tutte le configurazioni AutoCode
+ */
+ getAll: async (): Promise => {
+ const response = await api.get("/autocodes");
+ return response.data;
+ },
+
+ /**
+ * Ottiene le configurazioni per un modulo specifico
+ */
+ getByModule: async (moduleCode: string): Promise => {
+ const response = await api.get(`/autocodes/module/${moduleCode}`);
+ return response.data;
+ },
+
+ /**
+ * Ottiene una configurazione specifica per codice entita
+ */
+ getByEntityCode: async (entityCode: string): Promise => {
+ const response = await api.get(`/autocodes/${entityCode}`);
+ return response.data;
+ },
+
+ /**
+ * Genera un nuovo codice per un'entita
+ */
+ generateCode: async (entityCode: string): Promise => {
+ const response = await api.post(`/autocodes/${entityCode}/generate`);
+ return response.data;
+ },
+
+ /**
+ * Ottiene un'anteprima del prossimo codice senza incrementare la sequenza
+ */
+ previewCode: async (entityCode: string): Promise => {
+ const response = await api.get(`/autocodes/${entityCode}/preview`);
+ return response.data;
+ },
+
+ /**
+ * Aggiorna una configurazione AutoCode
+ */
+ update: async (id: number, data: AutoCodeUpdateDto): Promise => {
+ const response = await api.put(`/autocodes/${id}`, data);
+ return response.data;
+ },
+
+ /**
+ * Resetta la sequenza per un'entita
+ */
+ resetSequence: async (
+ entityCode: string,
+ request?: ResetSequenceRequest
+ ): Promise<{ message: string }> => {
+ const response = await api.post(`/autocodes/${entityCode}/reset-sequence`, request || {});
+ return response.data;
+ },
+
+ /**
+ * Verifica se un codice e unico per un'entita
+ */
+ checkUnique: async (
+ entityCode: string,
+ code: string,
+ excludeId?: number
+ ): Promise => {
+ const response = await api.get(`/autocodes/${entityCode}/check-unique`, {
+ params: { code, excludeId },
+ });
+ return response.data;
+ },
+
+ /**
+ * Ottiene i placeholder disponibili per i pattern
+ */
+ getPlaceholders: async (): Promise => {
+ const response = await api.get("/autocodes/placeholders");
+ return response.data;
+ },
+};
+
+export default autoCodeService;
diff --git a/frontend/src/types/autoCode.ts b/frontend/src/types/autoCode.ts
new file mode 100644
index 0000000..9f9a5e8
--- /dev/null
+++ b/frontend/src/types/autoCode.ts
@@ -0,0 +1,94 @@
+/**
+ * Types per il sistema di codici automatici
+ */
+
+export interface AutoCodeDto {
+ id: number;
+ entityCode: string;
+ entityName: string;
+ prefix: string | null;
+ pattern: string;
+ lastSequence: number;
+ resetSequenceYearly: boolean;
+ resetSequenceMonthly: boolean;
+ lastResetYear: number | null;
+ lastResetMonth: number | null;
+ isEnabled: boolean;
+ isReadOnly: boolean;
+ moduleCode: string | null;
+ description: string | null;
+ sortOrder: number;
+ exampleCode: string;
+ createdAt: string | null;
+ updatedAt: string | null;
+}
+
+export interface AutoCodeUpdateDto {
+ prefix?: string | null;
+ pattern?: string | null;
+ resetSequenceYearly?: boolean;
+ resetSequenceMonthly?: boolean;
+ isEnabled?: boolean;
+ isReadOnly?: boolean;
+ description?: string | null;
+}
+
+export interface GenerateCodeResponse {
+ code: string;
+ entityCode: string;
+ isPreview?: boolean;
+}
+
+export interface ResetSequenceRequest {
+ newValue?: number;
+}
+
+export interface CheckUniqueResponse {
+ isUnique: boolean;
+ code: string;
+ entityCode: string;
+}
+
+export interface PlaceholderInfo {
+ placeholder: string;
+ description: string;
+ example: string;
+}
+
+/**
+ * Raggruppa le configurazioni per modulo
+ */
+export function groupByModule(configs: AutoCodeDto[]): Record {
+ return configs.reduce((acc, config) => {
+ const module = config.moduleCode || "core";
+ if (!acc[module]) {
+ acc[module] = [];
+ }
+ acc[module].push(config);
+ return acc;
+ }, {} as Record);
+}
+
+/**
+ * Nomi visualizzati per i moduli
+ */
+export const moduleNames: Record = {
+ core: "Sistema Base",
+ warehouse: "Magazzino",
+ purchases: "Acquisti",
+ sales: "Vendite",
+ production: "Produzione",
+ quality: "Qualità",
+};
+
+/**
+ * Icone per i moduli (nomi MUI icons)
+ */
+export const moduleIcons: Record = {
+ core: "Settings",
+ warehouse: "Warehouse",
+ purchases: "ShoppingCart",
+ sales: "PointOfSale",
+ production: "Factory",
+ quality: "VerifiedUser",
+};
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index e64789c..939c149 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -13,6 +13,8 @@ export interface BaseEntity {
}
export interface Cliente extends BaseEntity {
+ codice: string;
+ codiceAlternativo?: string;
ragioneSociale: string;
indirizzo?: string;
cap?: string;
@@ -89,6 +91,7 @@ export interface Risorsa extends BaseEntity {
export interface Articolo extends BaseEntity {
codice: string;
+ codiceAlternativo?: string;
descrizione: string;
tipoMaterialeId?: number;
tipoMateriale?: TipoMateriale;
diff --git a/src/Apollinare.API/Controllers/ArticoliController.cs b/src/Apollinare.API/Controllers/ArticoliController.cs
index d52b56e..c1d55f1 100644
--- a/src/Apollinare.API/Controllers/ArticoliController.cs
+++ b/src/Apollinare.API/Controllers/ArticoliController.cs
@@ -1,3 +1,4 @@
+using Apollinare.API.Services;
using Apollinare.Domain.Entities;
using Apollinare.Infrastructure.Data;
using Microsoft.AspNetCore.Mvc;
@@ -10,10 +11,12 @@ namespace Apollinare.API.Controllers;
public class ArticoliController : ControllerBase
{
private readonly AppollinareDbContext _context;
+ private readonly AutoCodeService _autoCodeService;
- public ArticoliController(AppollinareDbContext context)
+ public ArticoliController(AppollinareDbContext context, AutoCodeService autoCodeService)
{
_context = context;
+ _autoCodeService = autoCodeService;
}
[HttpGet]
@@ -60,6 +63,13 @@ public class ArticoliController : ControllerBase
[HttpPost]
public async Task> CreateArticolo(Articolo articolo)
{
+ // Genera codice automatico
+ var codice = await _autoCodeService.GenerateNextCodeAsync("articolo");
+ if (!string.IsNullOrEmpty(codice))
+ {
+ articolo.Codice = codice;
+ }
+
articolo.CreatedAt = DateTime.UtcNow;
_context.Articoli.Add(articolo);
await _context.SaveChangesAsync();
diff --git a/src/Apollinare.API/Controllers/AutoCodesController.cs b/src/Apollinare.API/Controllers/AutoCodesController.cs
new file mode 100644
index 0000000..234f5a9
--- /dev/null
+++ b/src/Apollinare.API/Controllers/AutoCodesController.cs
@@ -0,0 +1,240 @@
+using Apollinare.API.Services;
+using Apollinare.Domain.Entities;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Apollinare.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class AutoCodesController : ControllerBase
+{
+ private readonly AutoCodeService _autoCodeService;
+ private readonly ILogger _logger;
+
+ public AutoCodesController(AutoCodeService autoCodeService, ILogger logger)
+ {
+ _autoCodeService = autoCodeService;
+ _logger = logger;
+ }
+
+ ///
+ /// Ottiene tutte le configurazioni AutoCode.
+ ///
+ [HttpGet]
+ public async Task>> GetAll()
+ {
+ var configs = await _autoCodeService.GetAllConfigurationsAsync();
+ return Ok(configs.Select(ToDto).ToList());
+ }
+
+ ///
+ /// Ottiene le configurazioni AutoCode per un modulo specifico.
+ ///
+ [HttpGet("module/{moduleCode}")]
+ public async Task>> GetByModule(string moduleCode)
+ {
+ var configs = await _autoCodeService.GetConfigurationsByModuleAsync(moduleCode);
+ return Ok(configs.Select(ToDto).ToList());
+ }
+
+ ///
+ /// Ottiene una configurazione AutoCode specifica.
+ ///
+ [HttpGet("{entityCode}")]
+ public async Task> Get(string entityCode)
+ {
+ var config = await _autoCodeService.GetConfigurationAsync(entityCode);
+ if (config == null)
+ return NotFound($"Configurazione per '{entityCode}' non trovata");
+
+ return Ok(ToDto(config));
+ }
+
+ ///
+ /// Genera un nuovo codice per un'entità specifica.
+ ///
+ [HttpPost("{entityCode}/generate")]
+ public async Task> GenerateCode(string entityCode)
+ {
+ try
+ {
+ var code = await _autoCodeService.GenerateNextCodeAsync(entityCode);
+ if (code == null)
+ {
+ return BadRequest(new { error = "Generazione automatica disabilitata o configurazione non trovata" });
+ }
+
+ return Ok(new GenerateCodeResponse { Code = code, EntityCode = entityCode });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Errore durante la generazione del codice per {EntityCode}", entityCode);
+ return StatusCode(500, new { error = "Errore durante la generazione del codice" });
+ }
+ }
+
+ ///
+ /// Ottiene un'anteprima del prossimo codice senza incrementare la sequenza.
+ ///
+ [HttpGet("{entityCode}/preview")]
+ public async Task> PreviewCode(string entityCode)
+ {
+ var code = await _autoCodeService.PreviewNextCodeAsync(entityCode);
+ if (code == null)
+ {
+ return BadRequest(new { error = "Generazione automatica disabilitata o configurazione non trovata" });
+ }
+
+ return Ok(new GenerateCodeResponse { Code = code, EntityCode = entityCode, IsPreview = true });
+ }
+
+ ///
+ /// Aggiorna una configurazione AutoCode.
+ ///
+ [HttpPut("{id:int}")]
+ public async Task> Update(int id, [FromBody] AutoCodeUpdateDto dto)
+ {
+ try
+ {
+ var config = await _autoCodeService.UpdateConfigurationAsync(id, dto);
+ return Ok(ToDto(config));
+ }
+ catch (KeyNotFoundException ex)
+ {
+ return NotFound(new { error = ex.Message });
+ }
+ catch (ArgumentException ex)
+ {
+ return BadRequest(new { error = ex.Message });
+ }
+ }
+
+ ///
+ /// Resetta la sequenza per un'entità specifica.
+ ///
+ [HttpPost("{entityCode}/reset-sequence")]
+ public async Task ResetSequence(string entityCode, [FromBody] ResetSequenceRequest? request)
+ {
+ try
+ {
+ await _autoCodeService.ResetSequenceAsync(entityCode, request?.NewValue ?? 0);
+ return Ok(new { message = $"Sequenza resettata per '{entityCode}'" });
+ }
+ catch (KeyNotFoundException ex)
+ {
+ return NotFound(new { error = ex.Message });
+ }
+ }
+
+ ///
+ /// Verifica se un codice è unico per un'entità.
+ ///
+ [HttpGet("{entityCode}/check-unique")]
+ public async Task> CheckUnique(
+ string entityCode,
+ [FromQuery] string code,
+ [FromQuery] int? excludeId = null)
+ {
+ var isUnique = await _autoCodeService.IsCodeUniqueAsync(entityCode, code, excludeId);
+ return Ok(new CheckUniqueResponse { IsUnique = isUnique, Code = code, EntityCode = entityCode });
+ }
+
+ ///
+ /// Ottiene i placeholder disponibili per i pattern.
+ ///
+ [HttpGet("placeholders")]
+ public ActionResult> GetPlaceholders()
+ {
+ var placeholders = new List
+ {
+ new() { Placeholder = "{PREFIX}", Description = "Prefisso configurato", Example = "ART" },
+ new() { Placeholder = "{SEQ:n}", Description = "Sequenza numerica con n cifre", Example = "{SEQ:5} → 00001" },
+ new() { Placeholder = "{YYYY}", Description = "Anno a 4 cifre", Example = "2025" },
+ new() { Placeholder = "{YY}", Description = "Anno a 2 cifre", Example = "25" },
+ new() { Placeholder = "{MM}", Description = "Mese a 2 cifre", Example = "11" },
+ new() { Placeholder = "{DD}", Description = "Giorno a 2 cifre", Example = "29" },
+ new() { Placeholder = "{YEAR}", Description = "Alias per {YYYY}", Example = "2025" },
+ new() { Placeholder = "{MONTH}", Description = "Alias per {MM}", Example = "11" },
+ new() { Placeholder = "{DAY}", Description = "Alias per {DD}", Example = "29" },
+ };
+
+ return Ok(placeholders);
+ }
+
+ private static AutoCodeDto ToDto(AutoCode entity)
+ {
+ return new AutoCodeDto
+ {
+ Id = entity.Id,
+ EntityCode = entity.EntityCode,
+ EntityName = entity.EntityName,
+ Prefix = entity.Prefix,
+ Pattern = entity.Pattern,
+ LastSequence = entity.LastSequence,
+ ResetSequenceYearly = entity.ResetSequenceYearly,
+ ResetSequenceMonthly = entity.ResetSequenceMonthly,
+ LastResetYear = entity.LastResetYear,
+ LastResetMonth = entity.LastResetMonth,
+ IsEnabled = entity.IsEnabled,
+ IsReadOnly = entity.IsReadOnly,
+ ModuleCode = entity.ModuleCode,
+ Description = entity.Description,
+ SortOrder = entity.SortOrder,
+ ExampleCode = entity.GetExampleCode(),
+ CreatedAt = entity.CreatedAt,
+ UpdatedAt = entity.UpdatedAt
+ };
+ }
+}
+
+#region DTOs
+
+public class AutoCodeDto
+{
+ public int Id { get; set; }
+ public string EntityCode { get; set; } = string.Empty;
+ public string EntityName { get; set; } = string.Empty;
+ public string? Prefix { get; set; }
+ public string Pattern { get; set; } = string.Empty;
+ public long LastSequence { get; set; }
+ public bool ResetSequenceYearly { get; set; }
+ public bool ResetSequenceMonthly { get; set; }
+ public int? LastResetYear { get; set; }
+ public int? LastResetMonth { get; set; }
+ public bool IsEnabled { get; set; }
+ public bool IsReadOnly { get; set; }
+ public string? ModuleCode { get; set; }
+ public string? Description { get; set; }
+ public int SortOrder { get; set; }
+ public string ExampleCode { get; set; } = string.Empty;
+ public DateTime? CreatedAt { get; set; }
+ public DateTime? UpdatedAt { get; set; }
+}
+
+public class GenerateCodeResponse
+{
+ public string Code { get; set; } = string.Empty;
+ public string EntityCode { get; set; } = string.Empty;
+ public bool IsPreview { get; set; }
+}
+
+public class ResetSequenceRequest
+{
+ public long NewValue { get; set; } = 0;
+}
+
+public class CheckUniqueResponse
+{
+ public bool IsUnique { get; set; }
+ public string Code { get; set; } = string.Empty;
+ public string EntityCode { get; set; } = string.Empty;
+}
+
+public class PlaceholderInfo
+{
+ public string Placeholder { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ public string Example { get; set; } = string.Empty;
+}
+
+#endregion
diff --git a/src/Apollinare.API/Controllers/ClientiController.cs b/src/Apollinare.API/Controllers/ClientiController.cs
index 5938e65..21f25f4 100644
--- a/src/Apollinare.API/Controllers/ClientiController.cs
+++ b/src/Apollinare.API/Controllers/ClientiController.cs
@@ -1,3 +1,4 @@
+using Apollinare.API.Services;
using Apollinare.Domain.Entities;
using Apollinare.Infrastructure.Data;
using Microsoft.AspNetCore.Mvc;
@@ -10,10 +11,12 @@ namespace Apollinare.API.Controllers;
public class ClientiController : ControllerBase
{
private readonly AppollinareDbContext _context;
+ private readonly AutoCodeService _autoCodeService;
- public ClientiController(AppollinareDbContext context)
+ public ClientiController(AppollinareDbContext context, AutoCodeService autoCodeService)
{
_context = context;
+ _autoCodeService = autoCodeService;
}
[HttpGet]
@@ -47,6 +50,13 @@ public class ClientiController : ControllerBase
[HttpPost]
public async Task> CreateCliente(Cliente cliente)
{
+ // Genera codice automatico
+ var codice = await _autoCodeService.GenerateNextCodeAsync("cliente");
+ if (!string.IsNullOrEmpty(codice))
+ {
+ cliente.Codice = codice;
+ }
+
cliente.CreatedAt = DateTime.UtcNow;
_context.Clienti.Add(cliente);
await _context.SaveChangesAsync();
diff --git a/src/Apollinare.API/Controllers/EventiController.cs b/src/Apollinare.API/Controllers/EventiController.cs
index 7fcf075..0a160e7 100644
--- a/src/Apollinare.API/Controllers/EventiController.cs
+++ b/src/Apollinare.API/Controllers/EventiController.cs
@@ -1,4 +1,5 @@
using Apollinare.API.Hubs;
+using Apollinare.API.Services;
using Apollinare.Domain.Entities;
using Apollinare.Domain.Enums;
using Apollinare.Infrastructure.Data;
@@ -13,11 +14,13 @@ public class EventiController : ControllerBase
{
private readonly AppollinareDbContext _context;
private readonly DataNotificationService _notifier;
+ private readonly AutoCodeService _autoCodeService;
- public EventiController(AppollinareDbContext context, DataNotificationService notifier)
+ public EventiController(AppollinareDbContext context, DataNotificationService notifier, AutoCodeService autoCodeService)
{
_context = context;
_notifier = notifier;
+ _autoCodeService = autoCodeService;
}
[HttpGet]
@@ -343,6 +346,12 @@ public class EventiController : ControllerBase
private async Task GeneraCodiceEvento()
{
+ // Usa AutoCodeService per generare il codice
+ var generatedCode = await _autoCodeService.GenerateNextCodeAsync("evento");
+ if (generatedCode != null)
+ return generatedCode;
+
+ // Fallback: metodo legacy
var anno = DateTime.Now.Year;
var ultimoEvento = await _context.Eventi
.Where(e => e.Codice != null && e.Codice.StartsWith($"EV{anno}"))
diff --git a/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseArticlesController.cs b/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseArticlesController.cs
index 7193a08..9c5ef63 100644
--- a/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseArticlesController.cs
+++ b/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseArticlesController.cs
@@ -239,6 +239,7 @@ public class WarehouseArticlesController : ControllerBase
public record ArticleDto(
int Id,
string Code,
+ string? AlternativeCode,
string Description,
string? ShortDescription,
string? Barcode,
@@ -273,36 +274,36 @@ public class WarehouseArticlesController : ControllerBase
);
public record CreateArticleDto(
- string Code,
string Description,
- string? ShortDescription,
- string? Barcode,
- string? ManufacturerCode,
- int? CategoryId,
string UnitOfMeasure,
- string? SecondaryUnitOfMeasure,
- decimal? UnitConversionFactor,
- StockManagementType StockManagement,
- bool IsBatchManaged,
- bool IsSerialManaged,
- bool HasExpiry,
- int? ExpiryWarningDays,
- decimal? MinimumStock,
- decimal? MaximumStock,
- decimal? ReorderPoint,
- decimal? ReorderQuantity,
- int? LeadTimeDays,
- ValuationMethod? ValuationMethod,
- decimal? StandardCost,
- decimal? BaseSellingPrice,
- decimal? Weight,
- decimal? Volume,
- string? Notes
+ string? AlternativeCode = null,
+ string? ShortDescription = null,
+ string? Barcode = null,
+ string? ManufacturerCode = null,
+ int? CategoryId = null,
+ string? SecondaryUnitOfMeasure = null,
+ decimal? UnitConversionFactor = null,
+ StockManagementType StockManagement = StockManagementType.Standard,
+ bool IsBatchManaged = false,
+ bool IsSerialManaged = false,
+ bool HasExpiry = false,
+ int? ExpiryWarningDays = null,
+ decimal? MinimumStock = null,
+ decimal? MaximumStock = null,
+ decimal? ReorderPoint = null,
+ decimal? ReorderQuantity = null,
+ int? LeadTimeDays = null,
+ ValuationMethod? ValuationMethod = null,
+ decimal? StandardCost = null,
+ decimal? BaseSellingPrice = null,
+ decimal? Weight = null,
+ decimal? Volume = null,
+ string? Notes = null
);
public record UpdateArticleDto(
- string Code,
string Description,
+ string? AlternativeCode,
string? ShortDescription,
string? Barcode,
string? ManufacturerCode,
@@ -363,6 +364,7 @@ public class WarehouseArticlesController : ControllerBase
private static ArticleDto MapToDto(WarehouseArticle article) => new(
article.Id,
article.Code,
+ article.AlternativeCode,
article.Description,
article.ShortDescription,
article.Barcode,
@@ -398,7 +400,8 @@ public class WarehouseArticlesController : ControllerBase
private static WarehouseArticle MapFromDto(CreateArticleDto dto) => new()
{
- Code = dto.Code,
+ // Code viene generato automaticamente da WarehouseService.CreateArticleAsync
+ AlternativeCode = dto.AlternativeCode,
Description = dto.Description,
ShortDescription = dto.ShortDescription,
Barcode = dto.Barcode,
@@ -428,7 +431,8 @@ public class WarehouseArticlesController : ControllerBase
private static void UpdateFromDto(WarehouseArticle article, UpdateArticleDto dto)
{
- article.Code = dto.Code;
+ // Code non viene aggiornato - è generato automaticamente e immutabile
+ article.AlternativeCode = dto.AlternativeCode;
article.Description = dto.Description;
article.ShortDescription = dto.ShortDescription;
article.Barcode = dto.Barcode;
diff --git a/src/Apollinare.API/Modules/Warehouse/Services/WarehouseService.cs b/src/Apollinare.API/Modules/Warehouse/Services/WarehouseService.cs
index be59d14..c076c06 100644
--- a/src/Apollinare.API/Modules/Warehouse/Services/WarehouseService.cs
+++ b/src/Apollinare.API/Modules/Warehouse/Services/WarehouseService.cs
@@ -1,3 +1,4 @@
+using Apollinare.API.Services;
using Apollinare.Domain.Entities.Warehouse;
using Apollinare.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
@@ -13,6 +14,7 @@ public class WarehouseService : IWarehouseService
private readonly AppollinareDbContext _context;
private readonly IMemoryCache _cache;
private readonly ILogger _logger;
+ private readonly AutoCodeService _autoCodeService;
private const string WAREHOUSES_CACHE_KEY = "warehouse_locations";
private const string CATEGORIES_CACHE_KEY = "warehouse_categories";
@@ -22,11 +24,13 @@ public class WarehouseService : IWarehouseService
public WarehouseService(
AppollinareDbContext context,
IMemoryCache cache,
- ILogger logger)
+ ILogger logger,
+ AutoCodeService autoCodeService)
{
_context = context;
_cache = cache;
_logger = logger;
+ _autoCodeService = autoCodeService;
}
#region Articoli
@@ -118,6 +122,16 @@ public class WarehouseService : IWarehouseService
public async Task CreateArticleAsync(WarehouseArticle article)
{
+ // Genera codice automaticamente se non specificato
+ if (string.IsNullOrWhiteSpace(article.Code))
+ {
+ var generatedCode = await _autoCodeService.GenerateNextCodeAsync("warehouse_article");
+ if (generatedCode != null)
+ article.Code = generatedCode;
+ else
+ throw new InvalidOperationException("Impossibile generare codice automatico per l'articolo");
+ }
+
// Verifica unicità codice
if (await _context.WarehouseArticles.AnyAsync(a => a.Code == article.Code))
throw new InvalidOperationException($"Esiste già un articolo con codice '{article.Code}'");
@@ -230,6 +244,16 @@ public class WarehouseService : IWarehouseService
public async Task CreateCategoryAsync(WarehouseArticleCategory category)
{
+ // Genera codice automaticamente se non specificato
+ if (string.IsNullOrWhiteSpace(category.Code))
+ {
+ var generatedCode = await _autoCodeService.GenerateNextCodeAsync("warehouse_category");
+ if (generatedCode != null)
+ category.Code = generatedCode;
+ else
+ throw new InvalidOperationException("Impossibile generare codice automatico per la categoria");
+ }
+
if (await _context.WarehouseArticleCategories.AnyAsync(c => c.Code == category.Code))
throw new InvalidOperationException($"Esiste già una categoria con codice '{category.Code}'");
@@ -336,6 +360,16 @@ public class WarehouseService : IWarehouseService
public async Task CreateWarehouseAsync(WarehouseLocation warehouse)
{
+ // Genera codice automaticamente se non specificato
+ if (string.IsNullOrWhiteSpace(warehouse.Code))
+ {
+ var generatedCode = await _autoCodeService.GenerateNextCodeAsync("warehouse_location");
+ if (generatedCode != null)
+ warehouse.Code = generatedCode;
+ else
+ throw new InvalidOperationException("Impossibile generare codice automatico per il magazzino");
+ }
+
if (await _context.WarehouseLocations.AnyAsync(w => w.Code == warehouse.Code))
throw new InvalidOperationException($"Esiste già un magazzino con codice '{warehouse.Code}'");
@@ -464,6 +498,16 @@ public class WarehouseService : IWarehouseService
if (!article.IsBatchManaged)
throw new InvalidOperationException("L'articolo non è gestito a lotti");
+ // Genera numero lotto automaticamente se non specificato
+ if (string.IsNullOrWhiteSpace(batch.BatchNumber))
+ {
+ var generatedCode = await _autoCodeService.GenerateNextCodeAsync("article_batch");
+ if (generatedCode != null)
+ batch.BatchNumber = generatedCode;
+ else
+ throw new InvalidOperationException("Impossibile generare numero lotto automatico");
+ }
+
// Verifica unicità batch number per articolo
if (await _context.ArticleBatches.AnyAsync(b => b.ArticleId == batch.ArticleId && b.BatchNumber == batch.BatchNumber))
throw new InvalidOperationException($"Esiste già un lotto '{batch.BatchNumber}' per questo articolo");
@@ -809,9 +853,15 @@ public class WarehouseService : IWarehouseService
public async Task CreateMovementAsync(StockMovement movement)
{
- // Genera numero documento se non specificato
+ // Genera numero documento automaticamente se non specificato
if (string.IsNullOrEmpty(movement.DocumentNumber))
- movement.DocumentNumber = await GenerateDocumentNumberAsync(movement.Type);
+ {
+ var generatedCode = await _autoCodeService.GenerateNextCodeAsync("stock_movement");
+ if (generatedCode != null)
+ movement.DocumentNumber = generatedCode;
+ else
+ movement.DocumentNumber = await GenerateDocumentNumberAsync(movement.Type); // Fallback
+ }
// Verifica unicità documento
if (await _context.StockMovements.AnyAsync(m => m.DocumentNumber == movement.DocumentNumber))
@@ -1428,8 +1478,15 @@ public class WarehouseService : IWarehouseService
public async Task CreateInventoryCountAsync(InventoryCount inventory)
{
+ // Genera codice automaticamente se non specificato
if (string.IsNullOrEmpty(inventory.Code))
- inventory.Code = $"INV/{DateTime.UtcNow:yyyyMMdd}/{await GenerateInventorySequenceAsync()}";
+ {
+ var generatedCode = await _autoCodeService.GenerateNextCodeAsync("inventory_count");
+ if (generatedCode != null)
+ inventory.Code = generatedCode;
+ else
+ inventory.Code = $"INV/{DateTime.UtcNow:yyyyMMdd}/{await GenerateInventorySequenceAsync()}"; // Fallback
+ }
inventory.CreatedAt = DateTime.UtcNow;
_context.InventoryCounts.Add(inventory);
diff --git a/src/Apollinare.API/Program.cs b/src/Apollinare.API/Program.cs
index 44c699e..c6478dd 100644
--- a/src/Apollinare.API/Program.cs
+++ b/src/Apollinare.API/Program.cs
@@ -19,6 +19,7 @@ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
builder.Services.AddSingleton();
// Warehouse Module Services
@@ -100,6 +101,10 @@ using (var scope = app.Services.CreateScope())
// Seed warehouse default data
var warehouseService = scope.ServiceProvider.GetRequiredService();
await warehouseService.SeedDefaultDataAsync();
+
+ // Seed AutoCode configurations
+ var autoCodeService = scope.ServiceProvider.GetRequiredService();
+ await autoCodeService.SeedDefaultConfigurationsAsync();
}
if (app.Environment.IsDevelopment())
diff --git a/src/Apollinare.API/Services/AutoCodeService.cs b/src/Apollinare.API/Services/AutoCodeService.cs
new file mode 100644
index 0000000..b9684ac
--- /dev/null
+++ b/src/Apollinare.API/Services/AutoCodeService.cs
@@ -0,0 +1,489 @@
+using System.Text.RegularExpressions;
+using Apollinare.Domain.Entities;
+using Apollinare.Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+
+namespace Apollinare.API.Services;
+
+///
+/// Servizio per la generazione automatica di codici univoci.
+/// Thread-safe grazie all'uso di transazioni database.
+///
+public class AutoCodeService
+{
+ private readonly AppollinareDbContext _db;
+ private readonly ILogger _logger;
+ private static readonly Regex SequencePattern = new(@"\{SEQ:(\d+)\}", RegexOptions.Compiled);
+
+ public AutoCodeService(AppollinareDbContext db, ILogger logger)
+ {
+ _db = db;
+ _logger = logger;
+ }
+
+ ///
+ /// Genera il prossimo codice per un'entità specifica.
+ /// Incrementa automaticamente la sequenza e gestisce i reset periodici.
+ ///
+ /// Codice dell'entità (es. "warehouse_article")
+ /// Il nuovo codice generato, o null se la generazione automatica è disabilitata
+ public async Task GenerateNextCodeAsync(string entityCode)
+ {
+ // Usa una transazione per garantire atomicità
+ await using var transaction = await _db.Database.BeginTransactionAsync();
+
+ try
+ {
+ var config = await _db.AutoCodes
+ .FirstOrDefaultAsync(c => c.EntityCode == entityCode);
+
+ if (config == null)
+ {
+ _logger.LogWarning("Configurazione AutoCode non trovata per entità: {EntityCode}", entityCode);
+ return null;
+ }
+
+ if (!config.IsEnabled)
+ {
+ _logger.LogDebug("Generazione automatica disabilitata per entità: {EntityCode}", entityCode);
+ return null;
+ }
+
+ var now = DateTime.Now;
+
+ // Gestione reset sequenza
+ if (ShouldResetSequence(config, now))
+ {
+ config.LastSequence = 0;
+ config.LastResetYear = now.Year;
+ config.LastResetMonth = now.Month;
+ _logger.LogInformation("Sequenza resettata per entità {EntityCode} (Anno: {Year}, Mese: {Month})",
+ entityCode, now.Year, now.Month);
+ }
+
+ // Incrementa sequenza
+ config.LastSequence++;
+ config.UpdatedAt = now;
+
+ // Genera codice dal pattern
+ var code = GenerateCodeFromPattern(config.Pattern, config.Prefix, config.LastSequence, now);
+
+ await _db.SaveChangesAsync();
+ await transaction.CommitAsync();
+
+ _logger.LogDebug("Codice generato per {EntityCode}: {Code}", entityCode, code);
+ return code;
+ }
+ catch (Exception ex)
+ {
+ await transaction.RollbackAsync();
+ _logger.LogError(ex, "Errore durante la generazione del codice per {EntityCode}", entityCode);
+ throw;
+ }
+ }
+
+ ///
+ /// Genera un codice di anteprima senza incrementare la sequenza.
+ /// Utile per mostrare all'utente cosa verrà generato.
+ ///
+ public async Task PreviewNextCodeAsync(string entityCode)
+ {
+ var config = await _db.AutoCodes
+ .AsNoTracking()
+ .FirstOrDefaultAsync(c => c.EntityCode == entityCode);
+
+ if (config == null || !config.IsEnabled)
+ return null;
+
+ var now = DateTime.Now;
+ var nextSequence = config.LastSequence + 1;
+
+ // Simula reset se necessario
+ if (ShouldResetSequence(config, now))
+ nextSequence = 1;
+
+ return GenerateCodeFromPattern(config.Pattern, config.Prefix, nextSequence, now);
+ }
+
+ ///
+ /// Verifica se un codice è già utilizzato per un'entità.
+ ///
+ public async Task IsCodeUniqueAsync(string entityCode, string code, int? excludeId = null)
+ {
+ return entityCode switch
+ {
+ "warehouse_article" => !await _db.WarehouseArticles
+ .AnyAsync(a => a.Code == code && (excludeId == null || a.Id != excludeId)),
+
+ "warehouse_location" => !await _db.WarehouseLocations
+ .AnyAsync(w => w.Code == code && (excludeId == null || w.Id != excludeId)),
+
+ "warehouse_category" => !await _db.WarehouseArticleCategories
+ .AnyAsync(c => c.Code == code && (excludeId == null || c.Id != excludeId)),
+
+ "stock_movement" => !await _db.StockMovements
+ .AnyAsync(m => m.DocumentNumber == code && (excludeId == null || m.Id != excludeId)),
+
+ "inventory_count" => !await _db.InventoryCounts
+ .AnyAsync(i => i.Code == code && (excludeId == null || i.Id != excludeId)),
+
+ "cliente" => !await _db.Clienti
+ .AnyAsync(c => c.Codice == code && (excludeId == null || c.Id != excludeId)),
+
+ "evento" => !await _db.Eventi
+ .AnyAsync(e => e.Codice == code && (excludeId == null || e.Id != excludeId)),
+
+ "articolo" => !await _db.Articoli
+ .AnyAsync(a => a.Codice == code && (excludeId == null || a.Id != excludeId)),
+
+ _ => true // Entità non gestita, assume codice unico
+ };
+ }
+
+ ///
+ /// Ottiene tutte le configurazioni AutoCode.
+ ///
+ public async Task> GetAllConfigurationsAsync()
+ {
+ return await _db.AutoCodes
+ .OrderBy(c => c.ModuleCode)
+ .ThenBy(c => c.SortOrder)
+ .ThenBy(c => c.EntityName)
+ .ToListAsync();
+ }
+
+ ///
+ /// Ottiene le configurazioni AutoCode per un modulo specifico.
+ ///
+ public async Task> GetConfigurationsByModuleAsync(string moduleCode)
+ {
+ return await _db.AutoCodes
+ .Where(c => c.ModuleCode == moduleCode)
+ .OrderBy(c => c.SortOrder)
+ .ThenBy(c => c.EntityName)
+ .ToListAsync();
+ }
+
+ ///
+ /// Ottiene una configurazione AutoCode per codice entità.
+ ///
+ public async Task GetConfigurationAsync(string entityCode)
+ {
+ return await _db.AutoCodes
+ .FirstOrDefaultAsync(c => c.EntityCode == entityCode);
+ }
+
+ ///
+ /// Aggiorna una configurazione AutoCode.
+ ///
+ public async Task UpdateConfigurationAsync(int id, AutoCodeUpdateDto dto)
+ {
+ var config = await _db.AutoCodes.FindAsync(id)
+ ?? throw new KeyNotFoundException($"Configurazione AutoCode con ID {id} non trovata");
+
+ if (dto.Prefix != null)
+ config.Prefix = dto.Prefix;
+
+ if (dto.Pattern != null)
+ {
+ // Valida il pattern
+ ValidatePattern(dto.Pattern);
+ config.Pattern = dto.Pattern;
+ }
+
+ if (dto.ResetSequenceYearly.HasValue)
+ config.ResetSequenceYearly = dto.ResetSequenceYearly.Value;
+
+ if (dto.ResetSequenceMonthly.HasValue)
+ config.ResetSequenceMonthly = dto.ResetSequenceMonthly.Value;
+
+ if (dto.IsEnabled.HasValue)
+ config.IsEnabled = dto.IsEnabled.Value;
+
+ if (dto.IsReadOnly.HasValue)
+ config.IsReadOnly = dto.IsReadOnly.Value;
+
+ if (dto.Description != null)
+ config.Description = dto.Description;
+
+ config.UpdatedAt = DateTime.Now;
+
+ await _db.SaveChangesAsync();
+ return config;
+ }
+
+ ///
+ /// Resetta manualmente la sequenza per un'entità.
+ ///
+ public async Task ResetSequenceAsync(string entityCode, long newValue = 0)
+ {
+ var config = await _db.AutoCodes
+ .FirstOrDefaultAsync(c => c.EntityCode == entityCode)
+ ?? throw new KeyNotFoundException($"Configurazione AutoCode per '{entityCode}' non trovata");
+
+ config.LastSequence = newValue;
+ config.LastResetYear = DateTime.Now.Year;
+ config.LastResetMonth = DateTime.Now.Month;
+ config.UpdatedAt = DateTime.Now;
+
+ await _db.SaveChangesAsync();
+
+ _logger.LogInformation("Sequenza resettata manualmente per {EntityCode} a {Value}", entityCode, newValue);
+ }
+
+ ///
+ /// Inizializza le configurazioni di default per tutte le entità.
+ /// Chiamato al seed dell'applicazione.
+ ///
+ public async Task SeedDefaultConfigurationsAsync()
+ {
+ var defaults = new List
+ {
+ // Core
+ new()
+ {
+ EntityCode = "cliente",
+ EntityName = "Cliente",
+ Prefix = "CLI",
+ Pattern = "{PREFIX}-{SEQ:5}",
+ ModuleCode = "core",
+ Description = "Codice cliente (es. CLI-00001)",
+ SortOrder = 10
+ },
+ new()
+ {
+ EntityCode = "evento",
+ EntityName = "Evento",
+ Prefix = "EVT",
+ Pattern = "{PREFIX}{YYYY}-{SEQ:5}",
+ ResetSequenceYearly = true,
+ ModuleCode = "core",
+ Description = "Codice evento con anno (es. EVT2025-00001)",
+ SortOrder = 20
+ },
+ new()
+ {
+ EntityCode = "articolo",
+ EntityName = "Articolo (Legacy)",
+ Prefix = "ART",
+ Pattern = "{PREFIX}-{SEQ:5}",
+ ModuleCode = "core",
+ Description = "Codice articolo legacy (es. ART-00001)",
+ SortOrder = 30
+ },
+
+ // Warehouse module
+ new()
+ {
+ EntityCode = "warehouse_location",
+ EntityName = "Magazzino",
+ Prefix = "MAG",
+ Pattern = "{PREFIX}-{SEQ:3}",
+ ModuleCode = "warehouse",
+ Description = "Codice magazzino (es. MAG-001)",
+ SortOrder = 10
+ },
+ new()
+ {
+ EntityCode = "warehouse_article",
+ EntityName = "Articolo Magazzino",
+ Prefix = "WA",
+ Pattern = "{PREFIX}{SEQ:6}",
+ ModuleCode = "warehouse",
+ Description = "Codice articolo magazzino (es. WA000001)",
+ SortOrder = 20
+ },
+ new()
+ {
+ EntityCode = "warehouse_category",
+ EntityName = "Categoria Articolo",
+ Prefix = "CAT",
+ Pattern = "{PREFIX}-{SEQ:4}",
+ ModuleCode = "warehouse",
+ Description = "Codice categoria (es. CAT-0001)",
+ SortOrder = 30
+ },
+ new()
+ {
+ EntityCode = "stock_movement",
+ EntityName = "Movimento Magazzino",
+ Prefix = "MOV",
+ Pattern = "{PREFIX}{YYYY}{MM}-{SEQ:5}",
+ ResetSequenceMonthly = true,
+ ModuleCode = "warehouse",
+ Description = "Numero documento movimento (es. MOV202511-00001)",
+ SortOrder = 40
+ },
+ new()
+ {
+ EntityCode = "inventory_count",
+ EntityName = "Inventario",
+ Prefix = "INV",
+ Pattern = "{PREFIX}{YYYY}-{SEQ:4}",
+ ResetSequenceYearly = true,
+ ModuleCode = "warehouse",
+ Description = "Codice inventario (es. INV2025-0001)",
+ SortOrder = 50
+ },
+ new()
+ {
+ EntityCode = "article_batch",
+ EntityName = "Lotto Articolo",
+ Prefix = "LOT",
+ Pattern = "{PREFIX}{YYYY}{MM}{DD}-{SEQ:4}",
+ ResetSequenceMonthly = true,
+ ModuleCode = "warehouse",
+ Description = "Numero lotto (es. LOT20251129-0001)",
+ SortOrder = 60
+ },
+
+ // Future: Purchases module
+ new()
+ {
+ EntityCode = "purchase_order",
+ EntityName = "Ordine Acquisto",
+ Prefix = "ODA",
+ Pattern = "{PREFIX}{YYYY}-{SEQ:5}",
+ ResetSequenceYearly = true,
+ ModuleCode = "purchases",
+ Description = "Numero ordine acquisto (es. ODA2025-00001)",
+ SortOrder = 10,
+ IsEnabled = false // Disabilitato finché il modulo non è implementato
+ },
+ new()
+ {
+ EntityCode = "supplier",
+ EntityName = "Fornitore",
+ Prefix = "FOR",
+ Pattern = "{PREFIX}-{SEQ:5}",
+ ModuleCode = "purchases",
+ Description = "Codice fornitore (es. FOR-00001)",
+ SortOrder = 20,
+ IsEnabled = false
+ },
+
+ // Future: Sales module
+ new()
+ {
+ EntityCode = "sales_order",
+ EntityName = "Ordine Vendita",
+ Prefix = "ODV",
+ Pattern = "{PREFIX}{YYYY}-{SEQ:5}",
+ ResetSequenceYearly = true,
+ ModuleCode = "sales",
+ Description = "Numero ordine vendita (es. ODV2025-00001)",
+ SortOrder = 10,
+ IsEnabled = false
+ },
+ new()
+ {
+ EntityCode = "invoice",
+ EntityName = "Fattura",
+ Prefix = "FT",
+ Pattern = "{PREFIX}{YYYY}/{SEQ:5}",
+ ResetSequenceYearly = true,
+ ModuleCode = "sales",
+ Description = "Numero fattura (es. FT2025/00001)",
+ SortOrder = 20,
+ IsEnabled = false
+ },
+ };
+
+ foreach (var config in defaults)
+ {
+ var exists = await _db.AutoCodes.AnyAsync(c => c.EntityCode == config.EntityCode);
+ if (!exists)
+ {
+ config.CreatedAt = DateTime.Now;
+ _db.AutoCodes.Add(config);
+ _logger.LogInformation("Configurazione AutoCode creata per {EntityCode}", config.EntityCode);
+ }
+ }
+
+ await _db.SaveChangesAsync();
+ }
+
+ #region Private Methods
+
+ private bool ShouldResetSequence(AutoCode config, DateTime now)
+ {
+ if (config.ResetSequenceMonthly)
+ {
+ return config.LastResetYear != now.Year || config.LastResetMonth != now.Month;
+ }
+
+ if (config.ResetSequenceYearly)
+ {
+ return config.LastResetYear != now.Year;
+ }
+
+ return false;
+ }
+
+ private static string GenerateCodeFromPattern(string pattern, string? prefix, long sequence, DateTime date)
+ {
+ var code = pattern
+ .Replace("{PREFIX}", prefix ?? "")
+ .Replace("{YEAR}", date.Year.ToString())
+ .Replace("{YYYY}", date.Year.ToString())
+ .Replace("{YY}", date.Year.ToString().Substring(2))
+ .Replace("{MONTH}", date.Month.ToString("D2"))
+ .Replace("{MM}", date.Month.ToString("D2"))
+ .Replace("{DAY}", date.Day.ToString("D2"))
+ .Replace("{DD}", date.Day.ToString("D2"));
+
+ // Gestisci {SEQ:n}
+ code = SequencePattern.Replace(code, match =>
+ {
+ var digits = int.Parse(match.Groups[1].Value);
+ return sequence.ToString($"D{digits}");
+ });
+
+ return code;
+ }
+
+ private static void ValidatePattern(string pattern)
+ {
+ // Verifica che ci sia almeno un placeholder sequenza
+ if (!SequencePattern.IsMatch(pattern))
+ {
+ throw new ArgumentException(
+ "Il pattern deve contenere almeno un placeholder {SEQ:n} per la sequenza numerica");
+ }
+
+ // Verifica che i placeholder siano validi
+ var validPlaceholders = new[]
+ {
+ "{PREFIX}", "{YEAR}", "{YYYY}", "{YY}",
+ "{MONTH}", "{MM}", "{DAY}", "{DD}"
+ };
+
+ var placeholderRegex = new Regex(@"\{[^}]+\}");
+ var matches = placeholderRegex.Matches(pattern);
+
+ foreach (Match match in matches)
+ {
+ var placeholder = match.Value;
+ if (!SequencePattern.IsMatch(placeholder) && !validPlaceholders.Contains(placeholder))
+ {
+ throw new ArgumentException($"Placeholder non valido: {placeholder}");
+ }
+ }
+ }
+
+ #endregion
+}
+
+///
+/// DTO per l'aggiornamento di una configurazione AutoCode.
+///
+public class AutoCodeUpdateDto
+{
+ public string? Prefix { get; set; }
+ public string? Pattern { get; set; }
+ public bool? ResetSequenceYearly { get; set; }
+ public bool? ResetSequenceMonthly { get; set; }
+ public bool? IsEnabled { get; set; }
+ public bool? IsReadOnly { get; set; }
+ public string? Description { get; set; }
+}
diff --git a/src/Apollinare.API/apollinare.db-shm b/src/Apollinare.API/apollinare.db-shm
index 05ea21c..4b9e522 100644
Binary files a/src/Apollinare.API/apollinare.db-shm and b/src/Apollinare.API/apollinare.db-shm differ
diff --git a/src/Apollinare.API/apollinare.db-wal b/src/Apollinare.API/apollinare.db-wal
index 42a4026..114b37d 100644
Binary files a/src/Apollinare.API/apollinare.db-wal and b/src/Apollinare.API/apollinare.db-wal differ
diff --git a/src/Apollinare.Domain/Entities/Articolo.cs b/src/Apollinare.Domain/Entities/Articolo.cs
index 37e3a37..c85cc96 100644
--- a/src/Apollinare.Domain/Entities/Articolo.cs
+++ b/src/Apollinare.Domain/Entities/Articolo.cs
@@ -2,7 +2,16 @@ namespace Apollinare.Domain.Entities;
public class Articolo : BaseEntity
{
+ ///
+ /// Codice articolo - generato automaticamente
+ ///
public string Codice { get; set; } = string.Empty;
+
+ ///
+ /// Codice alternativo (opzionale, inserito dall'utente)
+ ///
+ public string? CodiceAlternativo { get; set; }
+
public string Descrizione { get; set; } = string.Empty;
public int? TipoMaterialeId { get; set; }
public int? CategoriaId { get; set; }
diff --git a/src/Apollinare.Domain/Entities/AutoCode.cs b/src/Apollinare.Domain/Entities/AutoCode.cs
new file mode 100644
index 0000000..2659359
--- /dev/null
+++ b/src/Apollinare.Domain/Entities/AutoCode.cs
@@ -0,0 +1,117 @@
+namespace Apollinare.Domain.Entities;
+
+///
+/// Configurazione per la generazione automatica di codici.
+/// Ogni entità può avere la propria configurazione con pattern personalizzabile.
+///
+/// Pattern supportati:
+/// - {SEQ:n} - Sequenza numerica con n cifre (es. {SEQ:5} → 00001)
+/// - {YEAR} o {YYYY} - Anno corrente a 4 cifre
+/// - {YY} - Anno corrente a 2 cifre
+/// - {MONTH} o {MM} - Mese corrente a 2 cifre
+/// - {DAY} o {DD} - Giorno corrente a 2 cifre
+/// - {PREFIX} - Usa il prefisso definito
+/// - Testo statico (es. "ART-", "-", "/")
+///
+/// Esempi di pattern:
+/// - "ART-{YYYY}-{SEQ:5}" → ART-2025-00001
+/// - "{PREFIX}{YY}{MM}{SEQ:4}" → MAG2511-0001
+/// - "CLI/{YYYY}/{SEQ:6}" → CLI/2025/000001
+///
+public class AutoCode : BaseEntity
+{
+ ///
+ /// Codice univoco dell'entità (es. "warehouse_article", "warehouse_location", "cliente")
+ ///
+ public string EntityCode { get; set; } = string.Empty;
+
+ ///
+ /// Nome visualizzato dell'entità (es. "Articolo Magazzino", "Magazzino", "Cliente")
+ ///
+ public string EntityName { get; set; } = string.Empty;
+
+ ///
+ /// Prefisso opzionale da usare nel pattern con {PREFIX}
+ ///
+ public string? Prefix { get; set; }
+
+ ///
+ /// Pattern per la generazione del codice
+ ///
+ public string Pattern { get; set; } = "{PREFIX}{SEQ:5}";
+
+ ///
+ /// Ultimo numero di sequenza utilizzato (per {SEQ:n})
+ ///
+ public long LastSequence { get; set; } = 0;
+
+ ///
+ /// Se true, la sequenza viene resettata ogni anno
+ ///
+ public bool ResetSequenceYearly { get; set; } = false;
+
+ ///
+ /// Se true, la sequenza viene resettata ogni mese
+ ///
+ public bool ResetSequenceMonthly { get; set; } = false;
+
+ ///
+ /// Anno dell'ultimo reset della sequenza
+ ///
+ public int? LastResetYear { get; set; }
+
+ ///
+ /// Mese dell'ultimo reset della sequenza (se ResetSequenceMonthly)
+ ///
+ public int? LastResetMonth { get; set; }
+
+ ///
+ /// Se true, la generazione automatica è abilitata
+ ///
+ public bool IsEnabled { get; set; } = true;
+
+ ///
+ /// Se true, il codice non può essere modificato manualmente
+ ///
+ public bool IsReadOnly { get; set; } = false;
+
+ ///
+ /// Modulo di appartenenza (es. "core", "warehouse", "purchases")
+ ///
+ public string? ModuleCode { get; set; }
+
+ ///
+ /// Descrizione della configurazione
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// Ordine di visualizzazione nel pannello admin
+ ///
+ public int SortOrder { get; set; } = 0;
+
+ ///
+ /// Esempio di codice generato (calcolato, non persistito)
+ ///
+ public string GetExampleCode()
+ {
+ var now = DateTime.Now;
+ return Pattern
+ .Replace("{PREFIX}", Prefix ?? "")
+ .Replace("{YEAR}", now.Year.ToString())
+ .Replace("{YYYY}", now.Year.ToString())
+ .Replace("{YY}", now.Year.ToString().Substring(2))
+ .Replace("{MONTH}", now.Month.ToString("D2"))
+ .Replace("{MM}", now.Month.ToString("D2"))
+ .Replace("{DAY}", now.Day.ToString("D2"))
+ .Replace("{DD}", now.Day.ToString("D2"))
+ .Replace("{SEQ:1}", "X")
+ .Replace("{SEQ:2}", "XX")
+ .Replace("{SEQ:3}", "XXX")
+ .Replace("{SEQ:4}", "XXXX")
+ .Replace("{SEQ:5}", "XXXXX")
+ .Replace("{SEQ:6}", "XXXXXX")
+ .Replace("{SEQ:7}", "XXXXXXX")
+ .Replace("{SEQ:8}", "XXXXXXXX");
+ }
+}
diff --git a/src/Apollinare.Domain/Entities/Cliente.cs b/src/Apollinare.Domain/Entities/Cliente.cs
index 404cc04..c17d301 100644
--- a/src/Apollinare.Domain/Entities/Cliente.cs
+++ b/src/Apollinare.Domain/Entities/Cliente.cs
@@ -2,6 +2,16 @@ namespace Apollinare.Domain.Entities;
public class Cliente : BaseEntity
{
+ ///
+ /// Codice cliente - generato automaticamente
+ ///
+ public string Codice { get; set; } = string.Empty;
+
+ ///
+ /// Codice alternativo (opzionale, inserito dall'utente)
+ ///
+ public string? CodiceAlternativo { get; set; }
+
public string RagioneSociale { get; set; } = string.Empty;
public string? Indirizzo { get; set; }
public string? Cap { get; set; }
diff --git a/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticle.cs b/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticle.cs
index 6244e06..3ef36f1 100644
--- a/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticle.cs
+++ b/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticle.cs
@@ -6,10 +6,15 @@ namespace Apollinare.Domain.Entities.Warehouse;
public class WarehouseArticle : BaseEntity
{
///
- /// Codice univoco articolo (SKU)
+ /// Codice univoco articolo (SKU) - generato automaticamente
///
public string Code { get; set; } = string.Empty;
+ ///
+ /// Codice alternativo (opzionale, inserito dall'utente)
+ ///
+ public string? AlternativeCode { get; set; }
+
///
/// Descrizione articolo
///
diff --git a/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticleCategory.cs b/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticleCategory.cs
index 39d83a1..a730534 100644
--- a/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticleCategory.cs
+++ b/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticleCategory.cs
@@ -6,10 +6,15 @@ namespace Apollinare.Domain.Entities.Warehouse;
public class WarehouseArticleCategory : BaseEntity
{
///
- /// Codice categoria
+ /// Codice categoria - generato automaticamente
///
public string Code { get; set; } = string.Empty;
+ ///
+ /// Codice alternativo (opzionale, inserito dall'utente)
+ ///
+ public string? AlternativeCode { get; set; }
+
///
/// Nome categoria
///
diff --git a/src/Apollinare.Domain/Entities/Warehouse/WarehouseLocation.cs b/src/Apollinare.Domain/Entities/Warehouse/WarehouseLocation.cs
index 093a56a..94f9048 100644
--- a/src/Apollinare.Domain/Entities/Warehouse/WarehouseLocation.cs
+++ b/src/Apollinare.Domain/Entities/Warehouse/WarehouseLocation.cs
@@ -6,10 +6,15 @@ namespace Apollinare.Domain.Entities.Warehouse;
public class WarehouseLocation : BaseEntity
{
///
- /// Codice univoco del magazzino (es. "MAG01", "CENTRALE")
+ /// Codice univoco del magazzino - generato automaticamente
///
public string Code { get; set; } = string.Empty;
+ ///
+ /// Codice alternativo (opzionale, inserito dall'utente)
+ ///
+ public string? AlternativeCode { get; set; }
+
///
/// Nome descrittivo del magazzino
///
diff --git a/src/Apollinare.Infrastructure/Data/AppollinareDbContext.cs b/src/Apollinare.Infrastructure/Data/AppollinareDbContext.cs
index dcfd3a4..0c49e4c 100644
--- a/src/Apollinare.Infrastructure/Data/AppollinareDbContext.cs
+++ b/src/Apollinare.Infrastructure/Data/AppollinareDbContext.cs
@@ -41,6 +41,9 @@ public class AppollinareDbContext : DbContext
public DbSet AppModules => Set();
public DbSet ModuleSubscriptions => Set();
+ // Auto Code system
+ public DbSet AutoCodes => Set();
+
// Warehouse module entities
public DbSet WarehouseLocations => Set();
public DbSet WarehouseArticles => Set();
@@ -274,6 +277,13 @@ public class AppollinareDbContext : DbContext
.OnDelete(DeleteBehavior.Cascade);
});
+ // AutoCode
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasIndex(e => e.EntityCode).IsUnique();
+ entity.HasIndex(e => e.ModuleCode);
+ });
+
// ===============================================
// WAREHOUSE MODULE ENTITIES
// ===============================================
diff --git a/src/Apollinare.Infrastructure/Migrations/20251129135918_AddAutoCodeSystem.Designer.cs b/src/Apollinare.Infrastructure/Migrations/20251129135918_AddAutoCodeSystem.Designer.cs
new file mode 100644
index 0000000..ede0bed
--- /dev/null
+++ b/src/Apollinare.Infrastructure/Migrations/20251129135918_AddAutoCodeSystem.Designer.cs
@@ -0,0 +1,3091 @@
+//
+using System;
+using Apollinare.Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Apollinare.Infrastructure.Migrations
+{
+ [DbContext(typeof(AppollinareDbContext))]
+ [Migration("20251129135918_AddAutoCodeSystem")]
+ partial class AddAutoCodeSystem
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.AppModule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("BasePrice")
+ .HasPrecision(18, 2)
+ .HasColumnType("TEXT");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("Dependencies")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .HasColumnType("TEXT");
+
+ b.Property("Icon")
+ .HasColumnType("TEXT");
+
+ b.Property("IsAvailable")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsCore")
+ .HasColumnType("INTEGER");
+
+ b.Property("MonthlyMultiplier")
+ .HasPrecision(5, 2)
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("RoutePath")
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("SortOrder");
+
+ b.ToTable("AppModules");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.Articolo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Attivo")
+ .HasColumnType("INTEGER");
+
+ b.Property("CategoriaId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Codice")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("Descrizione")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Immagine")
+ .HasColumnType("BLOB");
+
+ b.Property("MimeType")
+ .HasColumnType("TEXT");
+
+ b.Property("Note")
+ .HasColumnType("TEXT");
+
+ b.Property("QtaDisponibile")
+ .HasColumnType("TEXT");
+
+ b.Property("QtaStdA")
+ .HasColumnType("TEXT");
+
+ b.Property("QtaStdB")
+ .HasColumnType("TEXT");
+
+ b.Property("QtaStdS")
+ .HasColumnType("TEXT");
+
+ b.Property("TipoMaterialeId")
+ .HasColumnType("INTEGER");
+
+ b.Property("UnitaMisura")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CategoriaId");
+
+ b.HasIndex("Codice")
+ .IsUnique();
+
+ b.HasIndex("TipoMaterialeId");
+
+ b.ToTable("Articoli");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.AutoCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .HasColumnType("TEXT");
+
+ b.Property("EntityCode")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("EntityName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("IsEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsReadOnly")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastResetMonth")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastResetYear")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastSequence")
+ .HasColumnType("INTEGER");
+
+ b.Property("ModuleCode")
+ .HasColumnType("TEXT");
+
+ b.Property("Pattern")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Prefix")
+ .HasColumnType("TEXT");
+
+ b.Property("ResetSequenceMonthly")
+ .HasColumnType("INTEGER");
+
+ b.Property("ResetSequenceYearly")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EntityCode")
+ .IsUnique();
+
+ b.HasIndex("ModuleCode");
+
+ b.ToTable("AutoCodes");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.Cliente", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Attivo")
+ .HasColumnType("INTEGER");
+
+ b.Property("Cap")
+ .HasColumnType("TEXT");
+
+ b.Property("Citta")
+ .HasColumnType("TEXT");
+
+ b.Property("CodiceDestinatario")
+ .HasColumnType("TEXT");
+
+ b.Property("CodiceFiscale")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasColumnType("TEXT");
+
+ b.Property("Indirizzo")
+ .HasColumnType("TEXT");
+
+ b.Property("Note")
+ .HasColumnType("TEXT");
+
+ b.Property("PartitaIva")
+ .HasColumnType("TEXT");
+
+ b.Property("Pec")
+ .HasColumnType("TEXT");
+
+ b.Property("Provincia")
+ .HasColumnType("TEXT");
+
+ b.Property("RagioneSociale")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Telefono")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PartitaIva");
+
+ b.HasIndex("RagioneSociale");
+
+ b.ToTable("Clienti");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.CodiceCategoria", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Attivo")
+ .HasColumnType("INTEGER");
+
+ b.Property("Codice")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CoeffA")
+ .HasColumnType("TEXT");
+
+ b.Property("CoeffB")
+ .HasColumnType("TEXT");
+
+ b.Property("CoeffS")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("Descrizione")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("CodiciCategoria");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.Configurazione", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Chiave")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("Descrizione")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("Valore")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Chiave")
+ .IsUnique();
+
+ b.ToTable("Configurazioni");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.Evento", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClienteId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Codice")
+ .HasColumnType("TEXT");
+
+ b.Property("Confermato")
+ .HasColumnType("INTEGER");
+
+ b.Property("CostoPersona")
+ .HasColumnType("TEXT");
+
+ b.Property("CostoTotale")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("DataEvento")
+ .HasColumnType("TEXT");
+
+ b.Property("DataScadenzaPreventivo")
+ .HasColumnType("TEXT");
+
+ b.Property("Descrizione")
+ .HasColumnType("TEXT");
+
+ b.Property("LocationId")
+ .HasColumnType("INTEGER");
+
+ b.Property("NoteAllestimento")
+ .HasColumnType("TEXT");
+
+ b.Property("NoteCliente")
+ .HasColumnType("TEXT");
+
+ b.Property("NoteCucina")
+ .HasColumnType("TEXT");
+
+ b.Property("NoteInterne")
+ .HasColumnType("TEXT");
+
+ b.Property("NumeroOspiti")
+ .HasColumnType("INTEGER");
+
+ b.Property("NumeroOspitiAdulti")
+ .HasColumnType("INTEGER");
+
+ b.Property("NumeroOspitiBambini")
+ .HasColumnType("INTEGER");
+
+ b.Property("NumeroOspitiBuffet")
+ .HasColumnType("INTEGER");
+
+ b.Property("NumeroOspitiSeduti")
+ .HasColumnType("INTEGER");
+
+ b.Property("OraFine")
+ .HasColumnType("TEXT");
+
+ b.Property("OraInizio")
+ .HasColumnType("TEXT");
+
+ b.Property("Saldo")
+ .HasColumnType("TEXT");
+
+ b.Property("Stato")
+ .HasColumnType("INTEGER");
+
+ b.Property("TipoEventoId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TotaleAcconti")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClienteId");
+
+ b.HasIndex("Codice");
+
+ b.HasIndex("DataEvento");
+
+ b.HasIndex("LocationId");
+
+ b.HasIndex("Stato");
+
+ b.HasIndex("TipoEventoId");
+
+ b.ToTable("Eventi");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.EventoAcconto", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AConferma")
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("DataPagamento")
+ .HasColumnType("TEXT");
+
+ b.Property("Descrizione")
+ .HasColumnType("TEXT");
+
+ b.Property("EventoId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Importo")
+ .HasColumnType("TEXT");
+
+ b.Property("MetodoPagamento")
+ .HasColumnType("TEXT");
+
+ b.Property("Note")
+ .HasColumnType("TEXT");
+
+ b.Property("Ordine")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EventoId");
+
+ b.ToTable("EventiAcconti");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.EventoAllegato", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Contenuto")
+ .HasColumnType("BLOB");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("EventoId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MimeType")
+ .HasColumnType("TEXT");
+
+ b.Property("NomeFile")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Note")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EventoId");
+
+ b.ToTable("EventiAllegati");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.EventoAltroCosto", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AliquotaIva")
+ .HasColumnType("TEXT");
+
+ b.Property("ApplicaIva")
+ .HasColumnType("INTEGER");
+
+ b.Property("CostoUnitario")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("Descrizione")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("EventoId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Ordine")
+ .HasColumnType("INTEGER");
+
+ b.Property("Quantita")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EventoId");
+
+ b.ToTable("EventiAltriCosti");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.EventoDegustazione", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Completata")
+ .HasColumnType("INTEGER");
+
+ b.Property("CostoDegustazione")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("DataDegustazione")
+ .HasColumnType("TEXT");
+
+ b.Property("Detraibile")
+ .HasColumnType("INTEGER");
+
+ b.Property("EventoId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Luogo")
+ .HasColumnType("TEXT");
+
+ b.Property("Menu")
+ .HasColumnType("TEXT");
+
+ b.Property("Note")
+ .HasColumnType("TEXT");
+
+ b.Property("NumeroPaganti")
+ .HasColumnType("INTEGER");
+
+ b.Property("NumeroPersone")
+ .HasColumnType("INTEGER");
+
+ b.Property("Ora")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EventoId");
+
+ b.ToTable("EventiDegustazioni");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.EventoDettaglioOspiti", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CostoUnitario")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("EventoId")
+ .HasColumnType("INTEGER");
+
+ b.Property