From c7dbcde5dd66340344d62a2d5160f7ca1352b3cd Mon Sep 17 00:00:00 2001 From: dnviti Date: Sat, 29 Nov 2025 14:52:39 +0100 Subject: [PATCH] - --- frontend/src/App.tsx | 11 + frontend/src/components/Layout.tsx | 17 +- .../warehouse/contexts/WarehouseContext.tsx | 722 ++++ frontend/src/modules/warehouse/hooks/index.ts | 76 + .../warehouse/hooks/useStockCalculations.ts | 280 ++ .../warehouse/hooks/useWarehouseNavigation.ts | 160 + .../warehouse/pages/ArticleFormPage.tsx | 900 +++++ .../modules/warehouse/pages/ArticlesPage.tsx | 523 +++ .../warehouse/pages/InboundMovementPage.tsx | 454 +++ .../modules/warehouse/pages/MovementsPage.tsx | 650 ++++ .../warehouse/pages/OutboundMovementPage.tsx | 481 +++ .../warehouse/pages/StockLevelsPage.tsx | 355 ++ .../warehouse/pages/TransferMovementPage.tsx | 447 +++ .../warehouse/pages/WarehouseDashboard.tsx | 539 +++ .../pages/WarehouseLocationsPage.tsx | 496 +++ frontend/src/modules/warehouse/pages/index.ts | 18 + frontend/src/modules/warehouse/routes.tsx | 52 + .../warehouse/services/warehouseService.ts | 571 ++++ frontend/src/modules/warehouse/types/index.ts | 921 +++++ .../Controllers/BatchesController.cs | 288 ++ .../Controllers/InventoryController.cs | 428 +++ .../Controllers/SerialsController.cs | 366 ++ .../Controllers/StockLevelsController.cs | 360 ++ .../Controllers/StockMovementsController.cs | 564 +++ .../WarehouseArticlesController.cs | 460 +++ .../WarehouseCategoriesController.cs | 240 ++ .../WarehouseLocationsController.cs | 249 ++ .../Warehouse/Services/IWarehouseService.cs | 172 + .../Warehouse/Services/WarehouseService.cs | 1897 +++++++++++ src/Apollinare.API/Program.cs | 42 +- src/Apollinare.API/apollinare.db-shm | Bin 0 -> 32768 bytes src/Apollinare.API/apollinare.db-wal | Bin 0 -> 976472 bytes .../Entities/Warehouse/ArticleBarcode.cs | 96 + .../Entities/Warehouse/ArticleBatch.cs | 145 + .../Entities/Warehouse/ArticleSerial.cs | 129 + .../Entities/Warehouse/InventoryCount.cs | 236 ++ .../Entities/Warehouse/MovementReason.cs | 65 + .../Entities/Warehouse/StockLevel.cs | 72 + .../Entities/Warehouse/StockMovement.cs | 201 ++ .../Entities/Warehouse/StockMovementLine.cs | 78 + .../Entities/Warehouse/StockValuation.cs | 153 + .../Entities/Warehouse/WarehouseArticle.cs | 237 ++ .../Warehouse/WarehouseArticleCategory.cs | 72 + .../Entities/Warehouse/WarehouseLocation.cs | 113 + .../Data/AppollinareDbContext.cs | 351 ++ .../Data/warehouse_tables.sql | 421 +++ .../20251129134709_InitialCreate.Designer.cs | 3018 +++++++++++++++++ .../20251129134709_InitialCreate.cs | 1952 +++++++++++ .../AppollinareDbContextModelSnapshot.cs | 3015 ++++++++++++++++ 49 files changed, 23088 insertions(+), 5 deletions(-) create mode 100644 frontend/src/modules/warehouse/contexts/WarehouseContext.tsx create mode 100644 frontend/src/modules/warehouse/hooks/index.ts create mode 100644 frontend/src/modules/warehouse/hooks/useStockCalculations.ts create mode 100644 frontend/src/modules/warehouse/hooks/useWarehouseNavigation.ts create mode 100644 frontend/src/modules/warehouse/pages/ArticleFormPage.tsx create mode 100644 frontend/src/modules/warehouse/pages/ArticlesPage.tsx create mode 100644 frontend/src/modules/warehouse/pages/InboundMovementPage.tsx create mode 100644 frontend/src/modules/warehouse/pages/MovementsPage.tsx create mode 100644 frontend/src/modules/warehouse/pages/OutboundMovementPage.tsx create mode 100644 frontend/src/modules/warehouse/pages/StockLevelsPage.tsx create mode 100644 frontend/src/modules/warehouse/pages/TransferMovementPage.tsx create mode 100644 frontend/src/modules/warehouse/pages/WarehouseDashboard.tsx create mode 100644 frontend/src/modules/warehouse/pages/WarehouseLocationsPage.tsx create mode 100644 frontend/src/modules/warehouse/pages/index.ts create mode 100644 frontend/src/modules/warehouse/routes.tsx create mode 100644 frontend/src/modules/warehouse/services/warehouseService.ts create mode 100644 frontend/src/modules/warehouse/types/index.ts create mode 100644 src/Apollinare.API/Modules/Warehouse/Controllers/BatchesController.cs create mode 100644 src/Apollinare.API/Modules/Warehouse/Controllers/InventoryController.cs create mode 100644 src/Apollinare.API/Modules/Warehouse/Controllers/SerialsController.cs create mode 100644 src/Apollinare.API/Modules/Warehouse/Controllers/StockLevelsController.cs create mode 100644 src/Apollinare.API/Modules/Warehouse/Controllers/StockMovementsController.cs create mode 100644 src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseArticlesController.cs create mode 100644 src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseCategoriesController.cs create mode 100644 src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseLocationsController.cs create mode 100644 src/Apollinare.API/Modules/Warehouse/Services/IWarehouseService.cs create mode 100644 src/Apollinare.API/Modules/Warehouse/Services/WarehouseService.cs create mode 100644 src/Apollinare.API/apollinare.db-shm create mode 100644 src/Apollinare.API/apollinare.db-wal create mode 100644 src/Apollinare.Domain/Entities/Warehouse/ArticleBarcode.cs create mode 100644 src/Apollinare.Domain/Entities/Warehouse/ArticleBatch.cs create mode 100644 src/Apollinare.Domain/Entities/Warehouse/ArticleSerial.cs create mode 100644 src/Apollinare.Domain/Entities/Warehouse/InventoryCount.cs create mode 100644 src/Apollinare.Domain/Entities/Warehouse/MovementReason.cs create mode 100644 src/Apollinare.Domain/Entities/Warehouse/StockLevel.cs create mode 100644 src/Apollinare.Domain/Entities/Warehouse/StockMovement.cs create mode 100644 src/Apollinare.Domain/Entities/Warehouse/StockMovementLine.cs create mode 100644 src/Apollinare.Domain/Entities/Warehouse/StockValuation.cs create mode 100644 src/Apollinare.Domain/Entities/Warehouse/WarehouseArticle.cs create mode 100644 src/Apollinare.Domain/Entities/Warehouse/WarehouseArticleCategory.cs create mode 100644 src/Apollinare.Domain/Entities/Warehouse/WarehouseLocation.cs create mode 100644 src/Apollinare.Infrastructure/Data/warehouse_tables.sql create mode 100644 src/Apollinare.Infrastructure/Migrations/20251129134709_InitialCreate.Designer.cs create mode 100644 src/Apollinare.Infrastructure/Migrations/20251129134709_InitialCreate.cs create mode 100644 src/Apollinare.Infrastructure/Migrations/AppollinareDbContextModelSnapshot.cs diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index adc9e09..fbe379b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,6 +19,8 @@ import ReportTemplatesPage from "./pages/ReportTemplatesPage"; import ReportEditorPage from "./pages/ReportEditorPage"; import ModulesAdminPage from "./pages/ModulesAdminPage"; import ModulePurchasePage from "./pages/ModulePurchasePage"; +import WarehouseRoutes from "./modules/warehouse/routes"; +import { ModuleGuard } from "./components/ModuleGuard"; import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates"; import { CollaborationProvider } from "./contexts/CollaborationContext"; import { ModuleProvider } from "./contexts/ModuleContext"; @@ -94,6 +96,15 @@ function App() { path="modules/purchase/:code" element={} /> + {/* Warehouse Module */} + + + + } + /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 9372299..49c7700 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -28,8 +28,10 @@ import { Print as PrintIcon, Close as CloseIcon, Extension as ModulesIcon, + Warehouse as WarehouseIcon, } from "@mui/icons-material"; import CollaborationIndicator from "./collaboration/CollaborationIndicator"; +import { useModules } from "../contexts/ModuleContext"; const DRAWER_WIDTH = 240; const DRAWER_WIDTH_COLLAPSED = 64; @@ -42,6 +44,12 @@ const menuItems = [ { text: "Location", icon: , path: "/location" }, { text: "Articoli", icon: , path: "/articoli" }, { text: "Risorse", icon: , path: "/risorse" }, + { + text: "Magazzino", + icon: , + path: "/warehouse", + moduleCode: "warehouse", + }, { text: "Report", icon: , path: "/report-templates" }, { text: "Moduli", icon: , path: "/modules" }, ]; @@ -51,6 +59,7 @@ export default function Layout() { const navigate = useNavigate(); const location = useLocation(); const theme = useTheme(); + const { activeModules } = useModules(); // Breakpoints const isMobile = useMediaQuery(theme.breakpoints.down("sm")); // < 600px @@ -59,6 +68,12 @@ export default function Layout() { // Drawer width based on screen size const drawerWidth = isTablet ? DRAWER_WIDTH_COLLAPSED : DRAWER_WIDTH; + // Filter menu items based on active modules + const activeModuleCodes = activeModules.map((m) => m.code); + const filteredMenuItems = menuItems.filter( + (item) => !item.moduleCode || activeModuleCodes.includes(item.moduleCode), + ); + const handleDrawerToggle = () => { setMobileOpen(!mobileOpen); }; @@ -89,7 +104,7 @@ export default function Layout() { - {menuItems.map((item) => ( + {filteredMenuItems.map((item) => ( ["warehouse", "articles", id] as const, + articleStock: (id: number) => ["warehouse", "articles", id, "stock"] as const, + locations: ["warehouse", "locations"] as const, + location: (id: number) => ["warehouse", "locations", id] as const, + categories: ["warehouse", "categories"] as const, + categoryTree: ["warehouse", "categories", "tree"] as const, + movements: ["warehouse", "movements"] as const, + movement: (id: number) => ["warehouse", "movements", id] as const, + batches: ["warehouse", "batches"] as const, + batch: (id: number) => ["warehouse", "batches", id] as const, + serials: ["warehouse", "serials"] as const, + serial: (id: number) => ["warehouse", "serials", id] as const, + stockLevels: ["warehouse", "stock-levels"] as const, + inventories: ["warehouse", "inventories"] as const, + inventory: (id: number) => ["warehouse", "inventories", id] as const, +}; + +// Context state interface +interface WarehouseContextState { + selectedArticle: ArticleDto | null; + selectedWarehouse: WarehouseLocationDto | null; + setSelectedArticle: (article: ArticleDto | null) => void; + setSelectedWarehouse: (warehouse: WarehouseLocationDto | null) => void; +} + +const WarehouseContext = createContext( + undefined, +); + +export function WarehouseProvider({ children }: { children: ReactNode }) { + const [selectedArticle, setSelectedArticle] = useState( + null, + ); + const [selectedWarehouse, setSelectedWarehouse] = + useState(null); + + const value: WarehouseContextState = { + selectedArticle, + selectedWarehouse, + setSelectedArticle, + setSelectedWarehouse, + }; + + return ( + + {children} + + ); +} + +export function useWarehouseContext() { + const context = useContext(WarehouseContext); + if (!context) { + throw new Error( + "useWarehouseContext must be used within a WarehouseProvider", + ); + } + return context; +} + +// ============================================ +// ARTICLES HOOKS +// ============================================ + +export function useArticles(filter?: ArticleFilterDto) { + return useQuery({ + queryKey: [...warehouseQueryKeys.articles, filter], + queryFn: () => articleService.getAll(filter), + }); +} + +export function useArticle(id: number | undefined) { + return useQuery({ + queryKey: warehouseQueryKeys.article(id!), + queryFn: () => articleService.getById(id!), + enabled: !!id, + }); +} + +export function useArticleStock(id: number | undefined) { + return useQuery({ + queryKey: warehouseQueryKeys.articleStock(id!), + queryFn: () => articleService.getStock(id!), + enabled: !!id, + }); +} + +export function useCreateArticle() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateArticleDto) => articleService.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.articles }); + }, + }); +} + +export function useUpdateArticle() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: UpdateArticleDto }) => + articleService.update(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.articles }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.article(id), + }); + }, + }); +} + +export function useDeleteArticle() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => articleService.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.articles }); + }, + }); +} + +export function useUploadArticleImage() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, file }: { id: number; file: File }) => + articleService.uploadImage(id, file), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.article(id), + }); + }, + }); +} + +// ============================================ +// WAREHOUSE LOCATIONS HOOKS +// ============================================ + +export function useWarehouses(params?: { active?: boolean }) { + return useQuery({ + queryKey: [...warehouseQueryKeys.locations, params], + queryFn: () => warehouseLocationService.getAll(params?.active), + }); +} + +export function useWarehouse(id: number | undefined) { + return useQuery({ + queryKey: warehouseQueryKeys.location(id!), + queryFn: () => warehouseLocationService.getById(id!), + enabled: !!id, + }); +} + +export function useCreateWarehouse() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateWarehouseDto) => + warehouseLocationService.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.locations }); + }, + }); +} + +export function useUpdateWarehouse() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: UpdateWarehouseDto }) => + warehouseLocationService.update(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.locations }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.location(id), + }); + }, + }); +} + +export function useDeleteWarehouse() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => warehouseLocationService.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.locations }); + }, + }); +} + +export function useSetDefaultWarehouse() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => warehouseLocationService.setDefault(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.locations }); + }, + }); +} + +// ============================================ +// CATEGORIES HOOKS +// ============================================ + +export function useCategories() { + return useQuery({ + queryKey: warehouseQueryKeys.categories, + queryFn: () => categoryService.getAll(), + }); +} + +export function useCategoryTree() { + return useQuery({ + queryKey: warehouseQueryKeys.categoryTree, + queryFn: () => categoryService.getTree(), + }); +} + +export function useCreateCategory() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateCategoryDto) => categoryService.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.categories, + }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.categoryTree, + }); + }, + }); +} + +export function useUpdateCategory() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: UpdateCategoryDto }) => + categoryService.update(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.categories, + }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.categoryTree, + }); + }, + }); +} + +export function useDeleteCategory() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => categoryService.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.categories, + }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.categoryTree, + }); + }, + }); +} + +// ============================================ +// STOCK MOVEMENTS HOOKS +// ============================================ + +export function useMovements(filter?: MovementFilterDto) { + return useQuery({ + queryKey: [...warehouseQueryKeys.movements, filter], + queryFn: () => movementService.getAll(filter), + }); +} + +export function useMovement(id: number | undefined) { + return useQuery({ + queryKey: warehouseQueryKeys.movement(id!), + queryFn: () => movementService.getById(id!), + enabled: !!id, + }); +} + +export function useCreateInboundMovement() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateMovementDto) => + movementService.createInbound(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.movements }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.stockLevels, + }); + }, + }); +} + +export function useCreateOutboundMovement() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateMovementDto) => + movementService.createOutbound(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.movements }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.stockLevels, + }); + }, + }); +} + +export function useCreateTransferMovement() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateTransferDto) => + movementService.createTransfer(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.movements }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.stockLevels, + }); + }, + }); +} + +export function useCreateAdjustmentMovement() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateMovementDto) => + movementService.createAdjustment(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.movements }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.stockLevels, + }); + }, + }); +} + +export function useConfirmMovement() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => movementService.confirm(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.movements }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.movement(id), + }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.stockLevels, + }); + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.batches }); + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.serials }); + }, + }); +} + +export function useCancelMovement() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => movementService.cancel(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.movements }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.movement(id), + }); + }, + }); +} + +export function useDeleteMovement() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => movementService.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.movements }); + }, + }); +} + +// ============================================ +// BATCHES HOOKS +// ============================================ + +export function useBatches(articleId?: number, status?: BatchStatus) { + return useQuery({ + queryKey: [...warehouseQueryKeys.batches, articleId, status], + queryFn: () => batchService.getAll(articleId, status), + }); +} + +export function useBatch(id: number | undefined) { + return useQuery({ + queryKey: warehouseQueryKeys.batch(id!), + queryFn: () => batchService.getById(id!), + enabled: !!id, + }); +} + +export function useArticleBatches(articleId: number | undefined) { + return useQuery({ + queryKey: [...warehouseQueryKeys.batches, "article", articleId], + queryFn: () => batchService.getAll(articleId), + enabled: !!articleId, + }); +} + +export function useExpiringBatches(days: number = 30) { + return useQuery({ + queryKey: [...warehouseQueryKeys.batches, "expiring", days], + queryFn: () => batchService.getExpiring(days), + }); +} + +export function useCreateBatch() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateBatchDto) => batchService.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.batches }); + }, + }); +} + +export function useUpdateBatchStatus() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, status }: { id: number; status: BatchStatus }) => + batchService.updateStatus(id, status), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.batches }); + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.batch(id) }); + }, + }); +} + +// ============================================ +// SERIALS HOOKS +// ============================================ + +export function useSerials(articleId?: number, status?: SerialStatus) { + return useQuery({ + queryKey: [...warehouseQueryKeys.serials, articleId, status], + queryFn: () => serialService.getAll(articleId, status), + }); +} + +export function useSerial(id: number | undefined) { + return useQuery({ + queryKey: warehouseQueryKeys.serial(id!), + queryFn: () => serialService.getById(id!), + enabled: !!id, + }); +} + +export function useArticleSerials(articleId: number | undefined) { + return useQuery({ + queryKey: [...warehouseQueryKeys.serials, "article", articleId], + queryFn: () => serialService.getAll(articleId), + enabled: !!articleId, + }); +} + +export function useCreateSerial() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateSerialDto) => serialService.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.serials }); + }, + }); +} + +export function useCreateSerialsBulk() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateSerialsBulkDto) => serialService.createBulk(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.serials }); + }, + }); +} + +export function useRegisterSerialSale() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ + id, + customerId, + saleReference, + }: { + id: number; + customerId?: number; + saleReference?: string; + }) => serialService.registerSale(id, customerId, saleReference), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.serials }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.serial(id), + }); + }, + }); +} + +export function useRegisterSerialReturn() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ + id, + warehouseId, + isDefective, + }: { + id: number; + warehouseId: number; + isDefective: boolean; + }) => serialService.registerReturn(id, warehouseId, isDefective), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.serials }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.serial(id), + }); + }, + }); +} + +// ============================================ +// STOCK LEVELS HOOKS +// ============================================ + +export function useStockLevels(filter?: StockLevelFilterDto) { + return useQuery({ + queryKey: [...warehouseQueryKeys.stockLevels, filter], + queryFn: () => stockService.getAll(filter), + }); +} + +export function useArticleStockLevels(articleId: number | undefined) { + return useQuery({ + queryKey: [...warehouseQueryKeys.stockLevels, "article", articleId], + queryFn: () => stockService.getAll({ articleId }), + enabled: !!articleId, + }); +} + +export function useWarehouseStockLevels(warehouseId: number | undefined) { + return useQuery({ + queryKey: [...warehouseQueryKeys.stockLevels, "warehouse", warehouseId], + queryFn: () => stockService.getAll({ warehouseId }), + enabled: !!warehouseId, + }); +} + +export function useStockValuation(warehouseId?: number) { + return useQuery({ + queryKey: [...warehouseQueryKeys.stockLevels, "valuation", warehouseId], + queryFn: () => stockService.getValuation(warehouseId || 0), + }); +} + +export function useClosePeriod() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (period: number) => stockService.closePeriod(period), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.stockLevels, + }); + }, + }); +} + +// ============================================ +// INVENTORY HOOKS +// ============================================ + +export function useInventories(status?: InventoryStatus) { + return useQuery({ + queryKey: [...warehouseQueryKeys.inventories, status], + queryFn: () => inventoryService.getAll(status), + }); +} + +export function useInventory(id: number | undefined) { + return useQuery({ + queryKey: warehouseQueryKeys.inventory(id!), + queryFn: () => inventoryService.getById(id!), + enabled: !!id, + }); +} + +export function useCreateInventory() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateInventoryCountDto) => + inventoryService.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.inventories, + }); + }, + }); +} + +export function useStartInventory() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => inventoryService.start(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.inventories, + }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.inventory(id), + }); + }, + }); +} + +export function useUpdateInventoryLine() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ + inventoryId: _inventoryId, + lineId, + countedQty, + }: { + inventoryId: number; + lineId: number; + countedQty: number; + }) => inventoryService.updateLine(lineId, countedQty), + onSuccess: (_, { inventoryId: invId }) => { + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.inventory(invId), + }); + }, + }); +} + +export function useCompleteInventory() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => inventoryService.complete(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.inventories, + }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.inventory(id), + }); + }, + }); +} + +export function useConfirmInventory() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => inventoryService.confirm(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.inventories, + }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.inventory(id), + }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.stockLevels, + }); + queryClient.invalidateQueries({ queryKey: warehouseQueryKeys.movements }); + }, + }); +} + +export function useCancelInventory() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => inventoryService.cancel(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.inventories, + }); + queryClient.invalidateQueries({ + queryKey: warehouseQueryKeys.inventory(id), + }); + }, + }); +} diff --git a/frontend/src/modules/warehouse/hooks/index.ts b/frontend/src/modules/warehouse/hooks/index.ts new file mode 100644 index 0000000..0bf8f32 --- /dev/null +++ b/frontend/src/modules/warehouse/hooks/index.ts @@ -0,0 +1,76 @@ +// Re-export all hooks from context +export { + useWarehouseContext, + warehouseQueryKeys, + // Articles + useArticles, + useArticle, + useArticleStock, + useCreateArticle, + useUpdateArticle, + useDeleteArticle, + useUploadArticleImage, + // Warehouses + useWarehouses, + useWarehouse, + useCreateWarehouse, + useUpdateWarehouse, + useDeleteWarehouse, + useSetDefaultWarehouse, + // Categories + useCategories, + useCategoryTree, + useCreateCategory, + useUpdateCategory, + useDeleteCategory, + // Movements + useMovements, + useMovement, + useCreateInboundMovement, + useCreateOutboundMovement, + useCreateTransferMovement, + useCreateAdjustmentMovement, + useConfirmMovement, + useCancelMovement, + useDeleteMovement, + // Batches + useBatches, + useBatch, + useArticleBatches, + useExpiringBatches, + useCreateBatch, + useUpdateBatchStatus, + // Serials + useSerials, + useSerial, + useArticleSerials, + useCreateSerial, + useCreateSerialsBulk, + useRegisterSerialSale, + useRegisterSerialReturn, + // Stock Levels + useStockLevels, + useArticleStockLevels, + useWarehouseStockLevels, + useStockValuation, + useClosePeriod, + // Inventory + useInventories, + useInventory, + useCreateInventory, + useStartInventory, + useUpdateInventoryLine, + useCompleteInventory, + useConfirmInventory, + useCancelInventory, +} from "../contexts/WarehouseContext"; + +// Export navigation hook +export { useWarehouseNavigation } from "./useWarehouseNavigation"; + +// Export calculation hooks +export { + useStockCalculations, + useArticleAvailability, + calculateValuation, +} from "./useStockCalculations"; diff --git a/frontend/src/modules/warehouse/hooks/useStockCalculations.ts b/frontend/src/modules/warehouse/hooks/useStockCalculations.ts new file mode 100644 index 0000000..64183ea --- /dev/null +++ b/frontend/src/modules/warehouse/hooks/useStockCalculations.ts @@ -0,0 +1,280 @@ +import { useMemo } from "react"; +import { StockLevelDto, ArticleDto, ValuationMethod } from "../types"; + +interface StockSummary { + totalQuantity: number; + totalValue: number; + averageCost: number; + articleCount: number; + lowStockCount: number; + outOfStockCount: number; +} + +interface ArticleStockInfo { + totalQuantity: number; + totalValue: number; + availableQuantity: number; + reservedQuantity: number; + warehouseBreakdown: { + warehouseId: number; + warehouseName: string; + quantity: number; + value: number; + }[]; +} + +/** + * Hook for stock calculations and aggregations + */ +export function useStockCalculations(stockLevels: StockLevelDto[] | undefined) { + const summary = useMemo(() => { + if (!stockLevels || stockLevels.length === 0) { + return { + totalQuantity: 0, + totalValue: 0, + averageCost: 0, + articleCount: 0, + lowStockCount: 0, + outOfStockCount: 0, + }; + } + + const articleIds = new Set(); + let totalQuantity = 0; + let totalValue = 0; + let lowStockCount = 0; + let outOfStockCount = 0; + + for (const level of stockLevels) { + articleIds.add(level.articleId); + totalQuantity += level.quantity; + totalValue += level.stockValue || 0; + + if (level.quantity <= 0) { + outOfStockCount++; + } else if (level.isLowStock) { + lowStockCount++; + } + } + + return { + totalQuantity, + totalValue, + averageCost: totalQuantity > 0 ? totalValue / totalQuantity : 0, + articleCount: articleIds.size, + lowStockCount, + outOfStockCount, + }; + }, [stockLevels]); + + const getArticleStock = useMemo(() => { + return (articleId: number): ArticleStockInfo => { + if (!stockLevels) { + return { + totalQuantity: 0, + totalValue: 0, + availableQuantity: 0, + reservedQuantity: 0, + warehouseBreakdown: [], + }; + } + + const articleLevels = stockLevels.filter( + (l) => l.articleId === articleId, + ); + + let totalQuantity = 0; + let totalValue = 0; + let reservedQuantity = 0; + const warehouseBreakdown: ArticleStockInfo["warehouseBreakdown"] = []; + + for (const level of articleLevels) { + totalQuantity += level.quantity; + totalValue += level.stockValue || 0; + reservedQuantity += level.reservedQuantity; + + warehouseBreakdown.push({ + warehouseId: level.warehouseId, + warehouseName: + level.warehouseName || `Magazzino ${level.warehouseId}`, + quantity: level.quantity, + value: level.stockValue || 0, + }); + } + + return { + totalQuantity, + totalValue, + availableQuantity: totalQuantity - reservedQuantity, + reservedQuantity, + warehouseBreakdown, + }; + }; + }, [stockLevels]); + + const groupByWarehouse = useMemo(() => { + if (!stockLevels) return new Map(); + + const grouped = new Map(); + for (const level of stockLevels) { + const existing = grouped.get(level.warehouseId) || []; + existing.push(level); + grouped.set(level.warehouseId, existing); + } + return grouped; + }, [stockLevels]); + + return { + summary, + getArticleStock, + groupByWarehouse, + }; +} + +/** + * Calculate valuation based on method + */ +export function calculateValuation( + movements: { quantity: number; unitCost: number; date: string }[], + method: ValuationMethod, + targetQuantity: number, +): { totalCost: number; averageCost: number } { + if (targetQuantity <= 0 || movements.length === 0) { + return { totalCost: 0, averageCost: 0 }; + } + + // Sort movements by date + const sorted = [...movements].sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + ); + + switch (method) { + case ValuationMethod.WeightedAverage: { + let totalQty = 0; + let totalCost = 0; + for (const m of sorted) { + totalQty += m.quantity; + totalCost += m.quantity * m.unitCost; + } + const avgCost = totalQty > 0 ? totalCost / totalQty : 0; + return { + totalCost: targetQuantity * avgCost, + averageCost: avgCost, + }; + } + + case ValuationMethod.FIFO: { + // First In, First Out + let remaining = targetQuantity; + let totalCost = 0; + for (const m of sorted) { + if (remaining <= 0) break; + const take = Math.min(remaining, m.quantity); + totalCost += take * m.unitCost; + remaining -= take; + } + return { + totalCost, + averageCost: targetQuantity > 0 ? totalCost / targetQuantity : 0, + }; + } + + case ValuationMethod.LIFO: { + // Last In, First Out + const reversed = [...sorted].reverse(); + let remaining = targetQuantity; + let totalCost = 0; + for (const m of reversed) { + if (remaining <= 0) break; + const take = Math.min(remaining, m.quantity); + totalCost += take * m.unitCost; + remaining -= take; + } + return { + totalCost, + averageCost: targetQuantity > 0 ? totalCost / targetQuantity : 0, + }; + } + + case ValuationMethod.StandardCost: { + // Use the most recent cost as standard + const lastMovement = sorted[sorted.length - 1]; + const standardCost = lastMovement?.unitCost || 0; + return { + totalCost: targetQuantity * standardCost, + averageCost: standardCost, + }; + } + + case ValuationMethod.SpecificCost: { + // Specific cost requires batch/serial tracking + // For now, fall back to weighted average + let totalQty = 0; + let totalCost = 0; + for (const m of sorted) { + totalQty += m.quantity; + totalCost += m.quantity * m.unitCost; + } + const avgCost = totalQty > 0 ? totalCost / totalQty : 0; + return { + totalCost: targetQuantity * avgCost, + averageCost: avgCost, + }; + } + + default: + return { totalCost: 0, averageCost: 0 }; + } +} + +/** + * Hook for article availability check + */ +export function useArticleAvailability( + article: ArticleDto | undefined, + stockLevels: StockLevelDto[] | undefined, + requestedQuantity: number, + warehouseId?: number, +) { + return useMemo(() => { + if (!article || !stockLevels) { + return { + isAvailable: false, + availableQuantity: 0, + shortageQuantity: requestedQuantity, + message: "Dati non disponibili", + }; + } + + const relevantLevels = warehouseId + ? stockLevels.filter( + (l) => l.articleId === article.id && l.warehouseId === warehouseId, + ) + : stockLevels.filter((l) => l.articleId === article.id); + + const totalAvailable = relevantLevels.reduce( + (sum, l) => + sum + (l.availableQuantity || l.quantity - l.reservedQuantity), + 0, + ); + + const isAvailable = totalAvailable >= requestedQuantity; + const shortageQuantity = Math.max(0, requestedQuantity - totalAvailable); + + let message: string; + if (isAvailable) { + message = `Disponibile: ${totalAvailable} ${article.unitOfMeasure}`; + } else if (totalAvailable > 0) { + message = `Disponibile parzialmente: ${totalAvailable} ${article.unitOfMeasure} (mancano ${shortageQuantity})`; + } else { + message = "Non disponibile"; + } + + return { + isAvailable, + availableQuantity: totalAvailable, + shortageQuantity, + message, + }; + }, [article, stockLevels, requestedQuantity, warehouseId]); +} diff --git a/frontend/src/modules/warehouse/hooks/useWarehouseNavigation.ts b/frontend/src/modules/warehouse/hooks/useWarehouseNavigation.ts new file mode 100644 index 0000000..86f5692 --- /dev/null +++ b/frontend/src/modules/warehouse/hooks/useWarehouseNavigation.ts @@ -0,0 +1,160 @@ +import { useNavigate } from 'react-router-dom'; +import { useCallback } from 'react'; + +/** + * Hook for navigating within the warehouse module + */ +export function useWarehouseNavigation() { + const navigate = useNavigate(); + + // Articles + const goToArticles = useCallback(() => { + navigate('/warehouse/articles'); + }, [navigate]); + + const goToArticle = useCallback((id: number) => { + navigate(`/warehouse/articles/${id}`); + }, [navigate]); + + const goToNewArticle = useCallback(() => { + navigate('/warehouse/articles/new'); + }, [navigate]); + + const goToEditArticle = useCallback((id: number) => { + navigate(`/warehouse/articles/${id}/edit`); + }, [navigate]); + + // Warehouses + const goToWarehouses = useCallback(() => { + navigate('/warehouse/locations'); + }, [navigate]); + + const goToWarehouse = useCallback((id: number) => { + navigate(`/warehouse/locations/${id}`); + }, [navigate]); + + const goToNewWarehouse = useCallback(() => { + navigate('/warehouse/locations/new'); + }, [navigate]); + + // Categories + const goToCategories = useCallback(() => { + navigate('/warehouse/categories'); + }, [navigate]); + + // Movements + const goToMovements = useCallback(() => { + navigate('/warehouse/movements'); + }, [navigate]); + + const goToMovement = useCallback((id: number) => { + navigate(`/warehouse/movements/${id}`); + }, [navigate]); + + const goToNewInbound = useCallback(() => { + navigate('/warehouse/movements/inbound/new'); + }, [navigate]); + + const goToNewOutbound = useCallback(() => { + navigate('/warehouse/movements/outbound/new'); + }, [navigate]); + + const goToNewTransfer = useCallback(() => { + navigate('/warehouse/movements/transfer/new'); + }, [navigate]); + + const goToNewAdjustment = useCallback(() => { + navigate('/warehouse/movements/adjustment/new'); + }, [navigate]); + + // Batches + const goToBatches = useCallback(() => { + navigate('/warehouse/batches'); + }, [navigate]); + + const goToBatch = useCallback((id: number) => { + navigate(`/warehouse/batches/${id}`); + }, [navigate]); + + const goToNewBatch = useCallback(() => { + navigate('/warehouse/batches/new'); + }, [navigate]); + + // Serials + const goToSerials = useCallback(() => { + navigate('/warehouse/serials'); + }, [navigate]); + + const goToSerial = useCallback((id: number) => { + navigate(`/warehouse/serials/${id}`); + }, [navigate]); + + const goToNewSerial = useCallback(() => { + navigate('/warehouse/serials/new'); + }, [navigate]); + + // Stock + const goToStockLevels = useCallback(() => { + navigate('/warehouse/stock'); + }, [navigate]); + + const goToValuation = useCallback(() => { + navigate('/warehouse/valuation'); + }, [navigate]); + + // Inventory + const goToInventories = useCallback(() => { + navigate('/warehouse/inventories'); + }, [navigate]); + + const goToInventory = useCallback((id: number) => { + navigate(`/warehouse/inventories/${id}`); + }, [navigate]); + + const goToNewInventory = useCallback(() => { + navigate('/warehouse/inventories/new'); + }, [navigate]); + + // Dashboard + const goToDashboard = useCallback(() => { + navigate('/warehouse'); + }, [navigate]); + + return { + // Articles + goToArticles, + goToArticle, + goToNewArticle, + goToEditArticle, + // Warehouses + goToWarehouses, + goToWarehouse, + goToNewWarehouse, + // Categories + goToCategories, + // Movements + goToMovements, + goToMovement, + goToNewInbound, + goToNewOutbound, + goToNewTransfer, + goToNewAdjustment, + // Batches + goToBatches, + goToBatch, + goToNewBatch, + // Serials + goToSerials, + goToSerial, + goToNewSerial, + // Stock + goToStockLevels, + goToValuation, + // Inventory + goToInventories, + goToInventory, + goToNewInventory, + // Dashboard + goToDashboard, + }; +} diff --git a/frontend/src/modules/warehouse/pages/ArticleFormPage.tsx b/frontend/src/modules/warehouse/pages/ArticleFormPage.tsx new file mode 100644 index 0000000..22aee9b --- /dev/null +++ b/frontend/src/modules/warehouse/pages/ArticleFormPage.tsx @@ -0,0 +1,900 @@ +import React, { useState, useEffect } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { + Box, + Paper, + Typography, + TextField, + Button, + Grid, + FormControl, + InputLabel, + Select, + MenuItem, + FormControlLabel, + Switch, + Divider, + Alert, + CircularProgress, + IconButton, + Card, + CardMedia, + Tabs, + Tab, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Chip, + InputAdornment, +} from "@mui/material"; +import { + ArrowBack as ArrowBackIcon, + Save as SaveIcon, + Upload as UploadIcon, + Delete as DeleteIcon, + Image as ImageIcon, +} from "@mui/icons-material"; +import { + useArticle, + useCreateArticle, + useUpdateArticle, + useUploadArticleImage, + useCategoryTree, + useArticleStockLevels, + useArticleBatches, + useArticleSerials, +} from "../hooks"; +import { + ValuationMethod, + StockManagementType, + valuationMethodLabels, + stockManagementTypeLabels, + formatCurrency, + formatQuantity, + formatDate, + CreateArticleDto, + UpdateArticleDto, + StockLevelDto, + BatchDto, + SerialDto, +} from "../types"; + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + return ( + + ); +} + +export default function ArticleFormPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const isNew = !id || id === "new"; + const articleId = isNew ? undefined : parseInt(id, 10); + + const [tabValue, setTabValue] = useState(0); + const [formData, setFormData] = useState({ + code: "", + description: "", + shortDescription: "", + categoryId: undefined as number | undefined, + unitOfMeasure: "PZ", + barcode: "", + minimumStock: 0, + maximumStock: 0, + reorderPoint: 0, + reorderQuantity: 0, + standardCost: 0, + stockManagement: StockManagementType.Standard, + valuationMethod: ValuationMethod.WeightedAverage, + isBatchManaged: false, + isSerialManaged: false, + hasExpiry: false, + expiryWarningDays: 30, + isActive: true, + notes: "", + }); + const [imageFile, setImageFile] = useState(null); + const [imagePreview, setImagePreview] = useState(null); + const [errors, setErrors] = useState>({}); + + const { data: article, isLoading: loadingArticle } = useArticle(articleId); + const { data: categoryTree } = useCategoryTree(); + const { data: stockLevels } = useArticleStockLevels(articleId); + const { data: batches } = useArticleBatches(articleId); + const { data: serials } = useArticleSerials(articleId); + + const createMutation = useCreateArticle(); + const updateMutation = useUpdateArticle(); + const uploadImageMutation = useUploadArticleImage(); + + // Flatten category tree + const flatCategories = React.useMemo(() => { + const result: { id: number; name: string; level: number }[] = []; + const flatten = (categories: typeof categoryTree, level = 0) => { + if (!categories) return; + for (const cat of categories) { + result.push({ id: cat.id, name: cat.name, level }); + if (cat.children) flatten(cat.children, level + 1); + } + }; + flatten(categoryTree); + return result; + }, [categoryTree]); + + // Load article data + useEffect(() => { + if (article) { + setFormData({ + code: article.code, + description: article.description, + shortDescription: article.shortDescription || "", + categoryId: article.categoryId, + unitOfMeasure: article.unitOfMeasure, + barcode: article.barcode || "", + minimumStock: article.minimumStock || 0, + maximumStock: article.maximumStock || 0, + reorderPoint: article.reorderPoint || 0, + reorderQuantity: article.reorderQuantity || 0, + standardCost: article.standardCost || 0, + stockManagement: article.stockManagement, + valuationMethod: + article.valuationMethod || ValuationMethod.WeightedAverage, + isBatchManaged: article.isBatchManaged, + isSerialManaged: article.isSerialManaged, + hasExpiry: article.hasExpiry, + expiryWarningDays: article.expiryWarningDays || 30, + isActive: article.isActive, + notes: article.notes || "", + }); + if (article.hasImage) { + setImagePreview(`/api/warehouse/articles/${article.id}/image`); + } + } + }, [article]); + + const handleChange = (field: string, value: unknown) => { + setFormData((prev) => ({ ...prev, [field]: value })); + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: "" })); + } + }; + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + setImageFile(file); + const reader = new FileReader(); + reader.onloadend = () => { + setImagePreview(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + + const handleRemoveImage = () => { + setImageFile(null); + setImagePreview(null); + }; + + const validate = (): boolean => { + const newErrors: Record = {}; + if (!formData.code.trim()) { + newErrors.code = "Il codice è obbligatorio"; + } + if (!formData.description.trim()) { + newErrors.description = "La descrizione è obbligatoria"; + } + if (!formData.unitOfMeasure.trim()) { + newErrors.unitOfMeasure = "L'unità di misura è obbligatoria"; + } + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!validate()) return; + + try { + let savedId: number; + if (isNew) { + const createData: CreateArticleDto = { + code: formData.code, + description: formData.description, + shortDescription: formData.shortDescription || undefined, + categoryId: formData.categoryId, + unitOfMeasure: formData.unitOfMeasure, + barcode: formData.barcode || undefined, + minimumStock: formData.minimumStock, + maximumStock: formData.maximumStock, + reorderPoint: formData.reorderPoint, + reorderQuantity: formData.reorderQuantity, + standardCost: formData.standardCost, + stockManagement: formData.stockManagement, + valuationMethod: formData.valuationMethod, + isBatchManaged: formData.isBatchManaged, + isSerialManaged: formData.isSerialManaged, + hasExpiry: formData.hasExpiry, + expiryWarningDays: formData.expiryWarningDays, + notes: formData.notes || undefined, + }; + const result = await createMutation.mutateAsync(createData); + savedId = result.id; + } else { + const updateData: UpdateArticleDto = { + code: formData.code, + description: formData.description, + shortDescription: formData.shortDescription || undefined, + categoryId: formData.categoryId, + unitOfMeasure: formData.unitOfMeasure, + barcode: formData.barcode || undefined, + minimumStock: formData.minimumStock, + maximumStock: formData.maximumStock, + reorderPoint: formData.reorderPoint, + reorderQuantity: formData.reorderQuantity, + standardCost: formData.standardCost, + stockManagement: formData.stockManagement, + valuationMethod: formData.valuationMethod, + isBatchManaged: formData.isBatchManaged, + isSerialManaged: formData.isSerialManaged, + hasExpiry: formData.hasExpiry, + expiryWarningDays: formData.expiryWarningDays, + isActive: formData.isActive, + notes: formData.notes || undefined, + }; + await updateMutation.mutateAsync({ id: articleId!, data: updateData }); + savedId = articleId!; + } + + // Upload image if selected + if (imageFile) { + await uploadImageMutation.mutateAsync({ id: savedId, file: imageFile }); + } + + navigate(`/warehouse/articles/${savedId}`); + } catch (error) { + console.error("Errore salvataggio:", error); + } + }; + + const isPending = + createMutation.isPending || + updateMutation.isPending || + uploadImageMutation.isPending; + + if (!isNew && loadingArticle) { + return ( + + + + ); + } + + return ( + + {/* Header */} + + navigate(-1)}> + + + + {isNew ? "Nuovo Articolo" : `Articolo: ${article?.code}`} + + + + {(createMutation.error || updateMutation.error) && ( + + Errore durante il salvataggio:{" "} + {((createMutation.error || updateMutation.error) as Error).message} + + )} + + {!isNew && ( + + setTabValue(v)}> + + + {article?.isBatchManaged && } + {article?.isSerialManaged && } + + + )} + + +
+ + {/* Left Column - Form */} + + + + Informazioni Base + + + + handleChange("code", e.target.value)} + error={!!errors.code} + helperText={errors.code} + required + disabled={!isNew} + /> + + + + handleChange("description", e.target.value) + } + error={!!errors.description} + helperText={errors.description} + required + /> + + + + handleChange("shortDescription", e.target.value) + } + /> + + + + Categoria + + + + + + handleChange("unitOfMeasure", e.target.value) + } + error={!!errors.unitOfMeasure} + helperText={errors.unitOfMeasure} + required + /> + + + handleChange("barcode", e.target.value)} + /> + + + + + + + Livelli di Scorta + + + + + handleChange( + "minimumStock", + parseFloat(e.target.value) || 0, + ) + } + InputProps={{ inputProps: { min: 0, step: 0.01 } }} + /> + + + + handleChange( + "maximumStock", + parseFloat(e.target.value) || 0, + ) + } + InputProps={{ inputProps: { min: 0, step: 0.01 } }} + /> + + + + handleChange( + "reorderPoint", + parseFloat(e.target.value) || 0, + ) + } + InputProps={{ inputProps: { min: 0, step: 0.01 } }} + /> + + + + handleChange( + "reorderQuantity", + parseFloat(e.target.value) || 0, + ) + } + InputProps={{ inputProps: { min: 0, step: 0.01 } }} + /> + + + + + + + Costi e Valorizzazione + + + + + handleChange( + "standardCost", + parseFloat(e.target.value) || 0, + ) + } + InputProps={{ + startAdornment: ( + + ), + inputProps: { min: 0, step: 0.01 }, + }} + /> + + + + Gestione Stock + + + + + + Metodo di Valorizzazione + + + + + + + + + Tracciabilità + + + + + handleChange("isBatchManaged", e.target.checked) + } + disabled={!isNew && article?.isBatchManaged} + /> + } + label="Gestione Lotti" + /> + + + + handleChange("isSerialManaged", e.target.checked) + } + disabled={!isNew && article?.isSerialManaged} + /> + } + label="Gestione Matricole" + /> + + + + handleChange("hasExpiry", e.target.checked) + } + /> + } + label="Gestione Scadenza" + /> + + + + handleChange("isActive", e.target.checked) + } + /> + } + label="Articolo Attivo" + /> + + + + + + handleChange("notes", e.target.value)} + multiline + rows={3} + /> + + + + {/* Right Column - Image */} + + + + Immagine + + {imagePreview ? ( + + + + ) : ( + + + + )} + + + {imagePreview && ( + + + + )} + + + + {/* Summary Card (only for existing articles) */} + {!isNew && article && ( + + + Riepilogo + + + + + Costo Medio: + + + {formatCurrency(article.weightedAverageCost || 0)} + + + + + Ultimo Acquisto: + + + {formatCurrency(article.lastPurchaseCost || 0)} + + + + + )} + + + {/* Submit Button */} + + + + + + + +
+
+ + {/* Stock Levels Tab */} + + + + Giacenze per Magazzino + + + + + + Magazzino + Quantità + Riservata + Disponibile + Valore + + + + {!stockLevels || stockLevels.length === 0 ? ( + + + + Nessuna giacenza + + + + ) : ( + stockLevels.map((level: StockLevelDto) => ( + + {level.warehouseName} + + {formatQuantity(level.quantity)}{" "} + {article?.unitOfMeasure} + + + {formatQuantity(level.reservedQuantity)}{" "} + {article?.unitOfMeasure} + + + {formatQuantity(level.availableQuantity)}{" "} + {article?.unitOfMeasure} + + + {formatCurrency(level.stockValue)} + + + )) + )} + +
+
+
+
+ + {/* Batches Tab */} + {article?.isBatchManaged && ( + + + + Lotti + + + + + + Numero Lotto + Quantità + Data Scadenza + Stato + + + + {!batches || batches.length === 0 ? ( + + + + Nessun lotto + + + + ) : ( + batches.map((batch: BatchDto) => ( + + {batch.batchNumber} + + {formatQuantity(batch.currentQuantity)}{" "} + {article?.unitOfMeasure} + + + {batch.expiryDate + ? formatDate(batch.expiryDate) + : "-"} + + + + + + )) + )} + +
+
+
+
+ )} + + {/* Serials Tab */} + {article?.isSerialManaged && ( + + + + Matricole + + + + + + Matricola + Magazzino + Lotto + Stato + + + + {!serials || serials.length === 0 ? ( + + + + Nessuna matricola + + + + ) : ( + serials.map((serial: SerialDto) => ( + + {serial.serialNumber} + + {serial.currentWarehouseName || "-"} + + {serial.batchNumber || "-"} + + + + + )) + )} + +
+
+
+
+ )} +
+ ); +} diff --git a/frontend/src/modules/warehouse/pages/ArticlesPage.tsx b/frontend/src/modules/warehouse/pages/ArticlesPage.tsx new file mode 100644 index 0000000..631169e --- /dev/null +++ b/frontend/src/modules/warehouse/pages/ArticlesPage.tsx @@ -0,0 +1,523 @@ +import React, { useState, useMemo } from "react"; +import { + Box, + Paper, + Typography, + Button, + TextField, + InputAdornment, + IconButton, + Chip, + Menu, + MenuItem, + ListItemIcon, + ListItemText, + Tooltip, + Card, + CardContent, + CardMedia, + CardActions, + Grid, + ToggleButton, + ToggleButtonGroup, + FormControl, + InputLabel, + Select, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Alert, + Skeleton, +} from "@mui/material"; +import { + Add as AddIcon, + Search as SearchIcon, + Clear as ClearIcon, + ViewList as ViewListIcon, + ViewModule as ViewModuleIcon, + MoreVert as MoreVertIcon, + Edit as EditIcon, + Delete as DeleteIcon, + Inventory as InventoryIcon, + FilterList as FilterListIcon, + Image as ImageIcon, +} from "@mui/icons-material"; +import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; +import { useArticles, useDeleteArticle, useCategoryTree } from "../hooks"; +import { useWarehouseNavigation } from "../hooks/useWarehouseNavigation"; +import { ArticleDto, formatCurrency } from "../types"; + +type ViewMode = "list" | "grid"; + +export default function ArticlesPage() { + const [viewMode, setViewMode] = useState("list"); + const [search, setSearch] = useState(""); + const [categoryId, setCategoryId] = useState(""); + const [showInactive, setShowInactive] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [articleToDelete, setArticleToDelete] = useState( + null, + ); + const [menuAnchor, setMenuAnchor] = useState(null); + const [menuArticle, setMenuArticle] = useState(null); + + const nav = useWarehouseNavigation(); + + const { + data: articles, + isLoading, + error, + } = useArticles({ + categoryId: categoryId || undefined, + search: search || undefined, + isActive: showInactive ? undefined : true, + }); + + const { data: categoryTree } = useCategoryTree(); + + const deleteMutation = useDeleteArticle(); + + // Flatten category tree for select + const flatCategories = useMemo(() => { + const result: { id: number; name: string; level: number }[] = []; + const flatten = (categories: typeof categoryTree, level = 0) => { + if (!categories) return; + for (const cat of categories) { + result.push({ id: cat.id, name: cat.name, level }); + if (cat.children) flatten(cat.children, level + 1); + } + }; + flatten(categoryTree); + return result; + }, [categoryTree]); + + const handleMenuOpen = ( + event: React.MouseEvent, + article: ArticleDto, + ) => { + event.stopPropagation(); + setMenuAnchor(event.currentTarget); + setMenuArticle(article); + }; + + const handleMenuClose = () => { + setMenuAnchor(null); + setMenuArticle(null); + }; + + const handleEdit = () => { + if (menuArticle) { + nav.goToEditArticle(menuArticle.id); + } + handleMenuClose(); + }; + + const handleDeleteClick = () => { + setArticleToDelete(menuArticle); + setDeleteDialogOpen(true); + handleMenuClose(); + }; + + const handleDeleteConfirm = async () => { + if (articleToDelete) { + await deleteMutation.mutateAsync(articleToDelete.id); + setDeleteDialogOpen(false); + setArticleToDelete(null); + } + }; + + const handleViewStock = () => { + if (menuArticle) { + nav.goToArticle(menuArticle.id); + } + handleMenuClose(); + }; + + const columns: GridColDef[] = [ + { + field: "hasImage", + headerName: "", + width: 60, + sortable: false, + renderCell: (params: GridRenderCellParams) => ( + + {params.row.hasImage ? ( + {params.row.description} + ) : ( + + )} + + ), + }, + { + field: "code", + headerName: "Codice", + width: 120, + renderCell: (params: GridRenderCellParams) => ( + + {params.value} + + ), + }, + { + field: "description", + headerName: "Descrizione", + flex: 1, + minWidth: 200, + }, + { + field: "categoryName", + headerName: "Categoria", + width: 150, + }, + { + field: "unitOfMeasure", + headerName: "U.M.", + width: 80, + align: "center", + }, + { + field: "weightedAverageCost", + headerName: "Costo Medio", + width: 120, + align: "right", + renderCell: (params: GridRenderCellParams) => + formatCurrency(params.value || 0), + }, + { + field: "isActive", + headerName: "Stato", + width: 100, + renderCell: (params: GridRenderCellParams) => ( + + ), + }, + { + field: "actions", + headerName: "", + width: 60, + sortable: false, + renderCell: (params: GridRenderCellParams) => ( + handleMenuOpen(e, params.row)}> + + + ), + }, + ]; + + if (error) { + return ( + + + Errore nel caricamento degli articoli: {(error as Error).message} + + + ); + } + + return ( + + {/* Header */} + + + Anagrafica Articoli + + + + + {/* Filters */} + + + + setSearch(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: search && ( + + setSearch("")}> + + + + ), + }} + /> + + + + Categoria + + + + + + + + value && setViewMode(value)} + size="small" + > + + + + + + + + + + + + + + + + {/* Content */} + {viewMode === "list" ? ( + + nav.goToArticle(params.row.id)} + disableRowSelectionOnClick + sx={{ + "& .MuiDataGrid-row:hover": { + cursor: "pointer", + }, + }} + /> + + ) : ( + + {isLoading ? ( + Array.from({ length: 8 }).map((_, i) => ( + + + + + + + + + + )) + ) : articles?.length === 0 ? ( + + + + + Nessun articolo trovato + + + + ) : ( + articles?.map((article) => ( + + nav.goToArticle(article.id)} + > + {article.hasImage ? ( + + ) : ( + + + + )} + + + {article.code} + + + {article.description} + + + {article.categoryName && ( + + )} + + + + + handleMenuOpen(e, article)} + sx={{ ml: "auto" }} + > + + + + + + )) + )} + + )} + + {/* Context Menu */} + + + + + + Modifica + + + + + + Visualizza Giacenze + + + + + + Elimina + + + + {/* Delete Confirmation Dialog */} + setDeleteDialogOpen(false)} + > + Conferma Eliminazione + + + Sei sicuro di voler eliminare l'articolo{" "} + + {articleToDelete?.code} - {articleToDelete?.description} + + ? + + + Questa azione non può essere annullata. + + + + + + + + + ); +} diff --git a/frontend/src/modules/warehouse/pages/InboundMovementPage.tsx b/frontend/src/modules/warehouse/pages/InboundMovementPage.tsx new file mode 100644 index 0000000..91272da --- /dev/null +++ b/frontend/src/modules/warehouse/pages/InboundMovementPage.tsx @@ -0,0 +1,454 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + Box, + Paper, + Typography, + TextField, + Button, + Grid, + FormControl, + InputLabel, + Select, + MenuItem, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Alert, + CircularProgress, + Autocomplete, + InputAdornment, + Divider, + Chip, +} from "@mui/material"; +import { + ArrowBack as ArrowBackIcon, + Save as SaveIcon, + Add as AddIcon, + Delete as DeleteIcon, + Check as ConfirmIcon, +} from "@mui/icons-material"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import dayjs, { Dayjs } from "dayjs"; +import "dayjs/locale/it"; +import { + useWarehouses, + useArticles, + useCreateInboundMovement, + useConfirmMovement, +} from "../hooks"; +import { ArticleDto, CreateMovementDto, formatCurrency } from "../types"; + +interface MovementLine { + id: string; + article: ArticleDto | null; + quantity: number; + unitCost: number; + notes?: string; +} + +export default function InboundMovementPage() { + const navigate = useNavigate(); + const [movementDate, setMovementDate] = useState(dayjs()); + const [warehouseId, setWarehouseId] = useState(""); + const [documentNumber, setDocumentNumber] = useState(""); + const [externalReference, setExternalReference] = useState(""); + const [notes, setNotes] = useState(""); + const [lines, setLines] = useState([ + { id: crypto.randomUUID(), article: null, quantity: 1, unitCost: 0 }, + ]); + const [errors, setErrors] = useState>({}); + + const { data: warehouses } = useWarehouses({ active: true }); + const { data: articles } = useArticles({ isActive: true }); + const createMutation = useCreateInboundMovement(); + const confirmMutation = useConfirmMovement(); + + // Set default warehouse + React.useEffect(() => { + if (warehouses && warehouseId === "") { + const defaultWarehouse = warehouses.find((w) => w.isDefault); + if (defaultWarehouse) { + setWarehouseId(defaultWarehouse.id); + } + } + }, [warehouses, warehouseId]); + + const handleAddLine = () => { + setLines([ + ...lines, + { id: crypto.randomUUID(), article: null, quantity: 1, unitCost: 0 }, + ]); + }; + + const handleRemoveLine = (id: string) => { + if (lines.length > 1) { + setLines(lines.filter((l) => l.id !== id)); + } + }; + + const handleLineChange = ( + id: string, + field: keyof MovementLine, + value: unknown, + ) => { + setLines( + lines.map((l) => { + if (l.id === id) { + const updated = { ...l, [field]: value }; + // Auto-fill unit cost from article + if (field === "article" && value) { + const article = value as ArticleDto; + updated.unitCost = + article.lastPurchaseCost || article.weightedAverageCost || 0; + } + return updated; + } + return l; + }), + ); + }; + + const totalQuantity = lines.reduce((sum, l) => sum + (l.quantity || 0), 0); + const totalValue = lines.reduce( + (sum, l) => sum + (l.quantity || 0) * (l.unitCost || 0), + 0, + ); + + const validate = (): boolean => { + const newErrors: Record = {}; + if (!warehouseId) { + newErrors.warehouseId = "Seleziona un magazzino"; + } + if (!movementDate) { + newErrors.movementDate = "Inserisci la data"; + } + const validLines = lines.filter((l) => l.article && l.quantity > 0); + if (validLines.length === 0) { + newErrors.lines = "Inserisci almeno una riga con articolo e quantità"; + } + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (andConfirm: boolean = false) => { + if (!validate()) return; + + const data: CreateMovementDto = { + warehouseId: warehouseId as number, + movementDate: movementDate!.format("YYYY-MM-DD"), + documentNumber: documentNumber || undefined, + externalReference: externalReference || undefined, + notes: notes || undefined, + lines: lines + .filter((l) => l.article && l.quantity > 0) + .map((l) => ({ + articleId: l.article!.id, + quantity: l.quantity, + unitCost: l.unitCost, + notes: l.notes, + })), + }; + + try { + const result = await createMutation.mutateAsync(data); + if (andConfirm) { + await confirmMutation.mutateAsync(result.id); + } + navigate(`/warehouse/movements/${result.id}`); + } catch (error) { + console.error("Errore salvataggio:", error); + } + }; + + const isPending = createMutation.isPending || confirmMutation.isPending; + + return ( + + + {/* Header */} + + navigate(-1)}> + + + + + Nuovo Carico + + + Movimento di entrata merce in magazzino + + + + + {(createMutation.error || confirmMutation.error) && ( + + Errore:{" "} + {((createMutation.error || confirmMutation.error) as Error).message} + + )} + + {/* Form Header */} + + + Dati Movimento + + + + + + + + Magazzino + + {errors.warehouseId && ( + + {errors.warehouseId} + + )} + + + + setDocumentNumber(e.target.value)} + placeholder="DDT, Fattura, etc." + /> + + + setExternalReference(e.target.value)} + placeholder="Ordine, Fornitore, etc." + /> + + + setNotes(e.target.value)} + multiline + rows={2} + /> + + + + + {/* Lines */} + + + Righe Movimento + + + + {errors.lines && ( + + {errors.lines} + + )} + + + + + + Articolo + + Quantità + + + Costo Unitario + + + Totale + + + + + + {lines.map((line) => ( + + + + handleLineChange(line.id, "article", value) + } + options={articles || []} + getOptionLabel={(option) => + `${option.code} - ${option.description}` + } + renderInput={(params) => ( + + )} + isOptionEqualToValue={(option, value) => + option.id === value.id + } + /> + + + + handleLineChange( + line.id, + "quantity", + parseFloat(e.target.value) || 0, + ) + } + slotProps={{ + htmlInput: { min: 0, step: 0.01 }, + input: { + endAdornment: line.article && ( + + {line.article.unitOfMeasure} + + ), + }, + }} + fullWidth + /> + + + + handleLineChange( + line.id, + "unitCost", + parseFloat(e.target.value) || 0, + ) + } + slotProps={{ + htmlInput: { min: 0, step: 0.01 }, + input: { + startAdornment: ( + + € + + ), + }, + }} + fullWidth + /> + + + + {formatCurrency(line.quantity * line.unitCost)} + + + + handleRemoveLine(line.id)} + disabled={lines.length === 1} + > + + + + + ))} + +
+
+ + + + {/* Totals */} + + + + Totale Quantità + + {totalQuantity.toFixed(2)} + + + + Totale Valore + + {formatCurrency(totalValue)} + + +
+ + {/* Actions */} + + + + + +
+
+ ); +} diff --git a/frontend/src/modules/warehouse/pages/MovementsPage.tsx b/frontend/src/modules/warehouse/pages/MovementsPage.tsx new file mode 100644 index 0000000..be66c5e --- /dev/null +++ b/frontend/src/modules/warehouse/pages/MovementsPage.tsx @@ -0,0 +1,650 @@ +import React, { useState } from "react"; +import { + Box, + Paper, + Typography, + Button, + TextField, + InputAdornment, + IconButton, + Chip, + Menu, + MenuItem, + ListItemIcon, + ListItemText, + FormControl, + InputLabel, + Select, + Grid, + Alert, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + SpeedDial, + SpeedDialAction, + SpeedDialIcon, +} from "@mui/material"; +import { + Search as SearchIcon, + Clear as ClearIcon, + MoreVert as MoreVertIcon, + Visibility as ViewIcon, + Check as ConfirmIcon, + Close as CancelIcon, + Delete as DeleteIcon, + Add as AddIcon, + Download as DownloadIcon, + Upload as UploadIcon, + SwapHoriz as TransferIcon, + Build as AdjustmentIcon, + FilterList as FilterIcon, +} from "@mui/icons-material"; +import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { Dayjs } from "dayjs"; +import "dayjs/locale/it"; +import { + useMovements, + useWarehouses, + useConfirmMovement, + useCancelMovement, + useDeleteMovement, +} from "../hooks"; +import { useWarehouseNavigation } from "../hooks/useWarehouseNavigation"; +import { + MovementDto, + MovementType, + MovementStatus, + movementTypeLabels, + movementStatusLabels, + getMovementTypeColor, + getMovementStatusColor, + formatDate, +} from "../types"; + +export default function MovementsPage() { + const [search, setSearch] = useState(""); + const [warehouseId, setWarehouseId] = useState(""); + const [movementType, setMovementType] = useState(""); + const [status, setStatus] = useState(""); + const [dateFrom, setDateFrom] = useState(null); + const [dateTo, setDateTo] = useState(null); + const [menuAnchor, setMenuAnchor] = useState(null); + const [menuMovement, setMenuMovement] = useState(null); + const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); + const [cancelDialogOpen, setCancelDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [speedDialOpen, setSpeedDialOpen] = useState(false); + + const nav = useWarehouseNavigation(); + + const { + data: movements, + isLoading, + error, + } = useMovements({ + warehouseId: warehouseId || undefined, + type: movementType !== "" ? movementType : undefined, + status: status !== "" ? status : undefined, + dateFrom: dateFrom?.format("YYYY-MM-DD"), + dateTo: dateTo?.format("YYYY-MM-DD"), + }); + + const { data: warehouses } = useWarehouses(); + const confirmMutation = useConfirmMovement(); + const cancelMutation = useCancelMovement(); + const deleteMutation = useDeleteMovement(); + + // Filter movements by search + const filteredMovements = React.useMemo(() => { + if (!movements) return []; + if (!search) return movements; + const lower = search.toLowerCase(); + return movements.filter( + (m) => + m.documentNumber?.toLowerCase().includes(lower) || + m.externalReference?.toLowerCase().includes(lower) || + m.notes?.toLowerCase().includes(lower), + ); + }, [movements, search]); + + const handleMenuOpen = ( + event: React.MouseEvent, + movement: MovementDto, + ) => { + event.stopPropagation(); + setMenuAnchor(event.currentTarget); + setMenuMovement(movement); + }; + + const handleMenuClose = () => { + setMenuAnchor(null); + setMenuMovement(null); + }; + + const handleView = () => { + if (menuMovement) { + nav.goToMovement(menuMovement.id); + } + handleMenuClose(); + }; + + const handleConfirmClick = () => { + setConfirmDialogOpen(true); + setMenuAnchor(null); + }; + + const handleCancelClick = () => { + setCancelDialogOpen(true); + setMenuAnchor(null); + }; + + const handleDeleteClick = () => { + setDeleteDialogOpen(true); + setMenuAnchor(null); + }; + + const handleConfirmMovement = async () => { + if (menuMovement) { + await confirmMutation.mutateAsync(menuMovement.id); + setConfirmDialogOpen(false); + setMenuMovement(null); + } + }; + + const handleCancelMovement = async () => { + if (menuMovement) { + await cancelMutation.mutateAsync(menuMovement.id); + setCancelDialogOpen(false); + setMenuMovement(null); + } + }; + + const handleDeleteMovement = async () => { + if (menuMovement) { + await deleteMutation.mutateAsync(menuMovement.id); + setDeleteDialogOpen(false); + setMenuMovement(null); + } + }; + + const clearFilters = () => { + setSearch(""); + setWarehouseId(""); + setMovementType(""); + setStatus(""); + setDateFrom(null); + setDateTo(null); + }; + + const columns: GridColDef[] = [ + { + field: "documentNumber", + headerName: "Documento", + width: 140, + renderCell: (params: GridRenderCellParams) => ( + + {params.value || "-"} + + ), + }, + { + field: "movementDate", + headerName: "Data", + width: 110, + renderCell: (params: GridRenderCellParams) => + formatDate(params.value), + }, + { + field: "type", + headerName: "Tipo", + width: 130, + renderCell: (params: GridRenderCellParams) => ( + + ), + }, + { + field: "status", + headerName: "Stato", + width: 120, + renderCell: (params: GridRenderCellParams) => ( + + ), + }, + { + field: "sourceWarehouseName", + headerName: "Magazzino", + width: 150, + renderCell: (params: GridRenderCellParams) => + params.row.sourceWarehouseName || + params.row.destinationWarehouseName || + "-", + }, + { + field: "destinationWarehouseName", + headerName: "Destinazione", + width: 150, + renderCell: (params: GridRenderCellParams) => { + // Show destination only for transfers + if (params.row.type === MovementType.Transfer) { + return params.row.destinationWarehouseName || "-"; + } + return "-"; + }, + }, + { + field: "reasonDescription", + headerName: "Causale", + width: 150, + renderCell: (params: GridRenderCellParams) => + params.value || "-", + }, + { + field: "lineCount", + headerName: "Righe", + width: 80, + align: "center", + }, + { + field: "totalValue", + headerName: "Valore", + width: 100, + align: "right", + renderCell: (params: GridRenderCellParams) => + params.value != null + ? new Intl.NumberFormat("it-IT", { + style: "currency", + currency: "EUR", + }).format(params.value) + : "-", + }, + { + field: "externalReference", + headerName: "Riferimento", + width: 140, + renderCell: (params: GridRenderCellParams) => + params.value || "-", + }, + { + field: "actions", + headerName: "", + width: 60, + sortable: false, + renderCell: (params: GridRenderCellParams) => ( + handleMenuOpen(e, params.row)}> + + + ), + }, + ]; + + const speedDialActions = [ + { icon: , name: "Carico", action: nav.goToNewInbound }, + { icon: , name: "Scarico", action: nav.goToNewOutbound }, + { + icon: , + name: "Trasferimento", + action: nav.goToNewTransfer, + }, + { + icon: , + name: "Rettifica", + action: nav.goToNewAdjustment, + }, + ]; + + if (error) { + return ( + + + Errore nel caricamento dei movimenti: {(error as Error).message} + + + ); + } + + return ( + + + {/* Header */} + + + Movimenti di Magazzino + + + + {/* Filters */} + + + + setSearch(e.target.value)} + slotProps={{ + input: { + startAdornment: ( + + + + ), + endAdornment: search && ( + + setSearch("")}> + + + + ), + }, + }} + /> + + + + Magazzino + + + + + + Tipo + + + + + + Stato + + + + + + + + + + {(search || + warehouseId || + movementType !== "" || + status !== "" || + dateFrom || + dateTo) && ( + + + + )} + + + + {/* Data Grid */} + + nav.goToMovement(params.row.id)} + disableRowSelectionOnClick + sx={{ + "& .MuiDataGrid-row:hover": { + cursor: "pointer", + }, + }} + /> + + + {/* Speed Dial for New Movements */} + } />} + open={speedDialOpen} + onOpen={() => setSpeedDialOpen(true)} + onClose={() => setSpeedDialOpen(false)} + > + {speedDialActions.map((action) => ( + { + setSpeedDialOpen(false); + action.action(); + }} + /> + ))} + + + {/* Context Menu */} + + + + + + Visualizza + + {menuMovement?.status === MovementStatus.Draft && ( + <> + + + + + Conferma + + + + + + Annulla + + + + + + Elimina + + + )} + + + {/* Confirm Movement Dialog */} + setConfirmDialogOpen(false)} + > + Conferma Movimento + + + Confermare il movimento{" "} + {menuMovement?.documentNumber}? + + + Le giacenze verranno aggiornate e il movimento non potrà più + essere modificato. + + + + + + + + + {/* Cancel Movement Dialog */} + setCancelDialogOpen(false)} + > + Annulla Movimento + + + Annullare il movimento{" "} + {menuMovement?.documentNumber}? + + + Il movimento verrà marcato come annullato ma non eliminato. + + + + + + + + + {/* Delete Movement Dialog */} + setDeleteDialogOpen(false)} + > + Elimina Movimento + + + Eliminare definitivamente il movimento{" "} + {menuMovement?.documentNumber}? + + + Questa azione non può essere annullata. + + + + + + + + + + ); +} diff --git a/frontend/src/modules/warehouse/pages/OutboundMovementPage.tsx b/frontend/src/modules/warehouse/pages/OutboundMovementPage.tsx new file mode 100644 index 0000000..b39de61 --- /dev/null +++ b/frontend/src/modules/warehouse/pages/OutboundMovementPage.tsx @@ -0,0 +1,481 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + Box, + Paper, + Typography, + TextField, + Button, + Grid, + FormControl, + InputLabel, + Select, + MenuItem, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Alert, + CircularProgress, + Autocomplete, + InputAdornment, + Divider, + Chip, + Tooltip, +} from "@mui/material"; +import { + ArrowBack as ArrowBackIcon, + Save as SaveIcon, + Add as AddIcon, + Delete as DeleteIcon, + Check as ConfirmIcon, + Warning as WarningIcon, +} from "@mui/icons-material"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import dayjs, { Dayjs } from "dayjs"; +import "dayjs/locale/it"; +import { + useWarehouses, + useArticles, + useStockLevels, + useCreateOutboundMovement, + useConfirmMovement, +} from "../hooks"; +import { ArticleDto, CreateMovementDto, formatQuantity } from "../types"; + +interface MovementLine { + id: string; + article: ArticleDto | null; + quantity: number; + notes?: string; + availableQty?: number; +} + +export default function OutboundMovementPage() { + const navigate = useNavigate(); + const [movementDate, setMovementDate] = useState(dayjs()); + const [warehouseId, setWarehouseId] = useState(""); + const [documentNumber, setDocumentNumber] = useState(""); + const [externalReference, setExternalReference] = useState(""); + const [notes, setNotes] = useState(""); + const [lines, setLines] = useState([ + { id: crypto.randomUUID(), article: null, quantity: 1 }, + ]); + const [errors, setErrors] = useState>({}); + + const { data: warehouses } = useWarehouses({ active: true }); + const { data: articles } = useArticles({ isActive: true }); + const { data: stockLevels } = useStockLevels({ + warehouseId: warehouseId || undefined, + }); + const createMutation = useCreateOutboundMovement(); + const confirmMutation = useConfirmMovement(); + + // Set default warehouse + React.useEffect(() => { + if (warehouses && warehouseId === "") { + const defaultWarehouse = warehouses.find((w) => w.isDefault); + if (defaultWarehouse) { + setWarehouseId(defaultWarehouse.id); + } + } + }, [warehouses, warehouseId]); + + // Get available quantity for an article + const getAvailableQty = (articleId: number): number => { + if (!stockLevels) return 0; + const level = stockLevels.find((l) => l.articleId === articleId); + return level ? level.availableQuantity : 0; + }; + + const handleAddLine = () => { + setLines([ + ...lines, + { id: crypto.randomUUID(), article: null, quantity: 1 }, + ]); + }; + + const handleRemoveLine = (id: string) => { + if (lines.length > 1) { + setLines(lines.filter((l) => l.id !== id)); + } + }; + + const handleLineChange = ( + id: string, + field: keyof MovementLine, + value: unknown, + ) => { + setLines( + lines.map((l) => { + if (l.id === id) { + const updated = { ...l, [field]: value }; + // Update available qty when article changes + if (field === "article" && value) { + const article = value as ArticleDto; + updated.availableQty = getAvailableQty(article.id); + } + return updated; + } + return l; + }), + ); + }; + + // Check for stock issues + const hasStockIssues = lines.some( + (l) => + l.article && l.availableQty !== undefined && l.quantity > l.availableQty, + ); + + const totalQuantity = lines.reduce((sum, l) => sum + (l.quantity || 0), 0); + + const validate = (): boolean => { + const newErrors: Record = {}; + if (!warehouseId) { + newErrors.warehouseId = "Seleziona un magazzino"; + } + if (!movementDate) { + newErrors.movementDate = "Inserisci la data"; + } + const validLines = lines.filter((l) => l.article && l.quantity > 0); + if (validLines.length === 0) { + newErrors.lines = "Inserisci almeno una riga con articolo e quantità"; + } + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (andConfirm: boolean = false) => { + if (!validate()) return; + + const data: CreateMovementDto = { + warehouseId: warehouseId as number, + movementDate: movementDate!.format("YYYY-MM-DD"), + documentNumber: documentNumber || undefined, + externalReference: externalReference || undefined, + notes: notes || undefined, + lines: lines + .filter((l) => l.article && l.quantity > 0) + .map((l) => ({ + articleId: l.article!.id, + quantity: l.quantity, + notes: l.notes, + })), + }; + + try { + const result = await createMutation.mutateAsync(data); + if (andConfirm) { + await confirmMutation.mutateAsync(result.id); + } + navigate(`/warehouse/movements/${result.id}`); + } catch (error) { + console.error("Errore salvataggio:", error); + } + }; + + const isPending = createMutation.isPending || confirmMutation.isPending; + + return ( + + + {/* Header */} + + navigate(-1)}> + + + + + Nuovo Scarico + + + Movimento di uscita merce da magazzino + + + + + {(createMutation.error || confirmMutation.error) && ( + + Errore:{" "} + {((createMutation.error || confirmMutation.error) as Error).message} + + )} + + {hasStockIssues && ( + }> + Attenzione: alcune righe superano la disponibilità in magazzino + + )} + + {/* Form Header */} + + + Dati Movimento + + + + + + + + Magazzino + + {errors.warehouseId && ( + + {errors.warehouseId} + + )} + + + + setDocumentNumber(e.target.value)} + placeholder="DDT, Bolla, etc." + /> + + + setExternalReference(e.target.value)} + placeholder="Ordine, Cliente, etc." + /> + + + setNotes(e.target.value)} + multiline + rows={2} + /> + + + + + {/* Lines */} + + + Righe Movimento + + + + {errors.lines && ( + + {errors.lines} + + )} + + + + + + Articolo + + Disponibile + + + Quantità + + Note + + + + + {lines.map((line) => { + const isOverStock = + line.article && + line.availableQty !== undefined && + line.quantity > line.availableQty; + return ( + + + + handleLineChange(line.id, "article", value) + } + options={articles || []} + getOptionLabel={(option) => + `${option.code} - ${option.description}` + } + renderInput={(params) => ( + + )} + isOptionEqualToValue={(option, value) => + option.id === value.id + } + /> + + + {line.article ? ( + + ) : ( + "-" + )} + + + + + handleLineChange( + line.id, + "quantity", + parseFloat(e.target.value) || 0, + ) + } + slotProps={{ + htmlInput: { min: 0, step: 0.01 }, + input: { + endAdornment: line.article && ( + + {line.article.unitOfMeasure} + + ), + }, + }} + error={isOverStock ?? undefined} + fullWidth + /> + {isOverStock && ( + + + + )} + + + + + handleLineChange(line.id, "notes", e.target.value) + } + fullWidth + /> + + + handleRemoveLine(line.id)} + disabled={lines.length === 1} + > + + + + + ); + })} + +
+
+ + + + {/* Totals */} + + + + Totale Quantità + + {totalQuantity.toFixed(2)} + + +
+ + {/* Actions */} + + + + + +
+
+ ); +} diff --git a/frontend/src/modules/warehouse/pages/StockLevelsPage.tsx b/frontend/src/modules/warehouse/pages/StockLevelsPage.tsx new file mode 100644 index 0000000..9cfb205 --- /dev/null +++ b/frontend/src/modules/warehouse/pages/StockLevelsPage.tsx @@ -0,0 +1,355 @@ +import React, { useState } from "react"; +import { + Box, + Paper, + Typography, + TextField, + InputAdornment, + IconButton, + FormControl, + InputLabel, + Select, + MenuItem, + Grid, + Alert, + Chip, + FormControlLabel, + Switch, + Button, + Card, + CardContent, +} from "@mui/material"; +import { + Search as SearchIcon, + Clear as ClearIcon, + Warning as WarningIcon, + TrendingUp as TrendingUpIcon, +} from "@mui/icons-material"; +import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; +import { useStockLevels, useWarehouses, useCategoryTree } from "../hooks"; +import { useStockCalculations } from "../hooks/useStockCalculations"; +import { useWarehouseNavigation } from "../hooks/useWarehouseNavigation"; +import { StockLevelDto, formatCurrency, formatQuantity } from "../types"; + +export default function StockLevelsPage() { + const [search, setSearch] = useState(""); + const [warehouseId, setWarehouseId] = useState(""); + const [categoryId, setCategoryId] = useState(""); + const [lowStockOnly, setLowStockOnly] = useState(false); + + const nav = useWarehouseNavigation(); + + const { + data: stockLevels, + isLoading, + error, + } = useStockLevels({ + warehouseId: warehouseId || undefined, + categoryId: categoryId || undefined, + onlyLowStock: lowStockOnly || undefined, + }); + + const { data: warehouses } = useWarehouses(); + const { data: categoryTree } = useCategoryTree(); + const { summary } = useStockCalculations(stockLevels); + + // Flatten categories + const flatCategories = React.useMemo(() => { + const result: { id: number; name: string; level: number }[] = []; + const flatten = (cats: typeof categoryTree, level = 0) => { + if (!cats) return; + for (const cat of cats) { + result.push({ id: cat.id, name: cat.name, level }); + if (cat.children) flatten(cat.children, level + 1); + } + }; + flatten(categoryTree); + return result; + }, [categoryTree]); + + // Filter by search + const filteredLevels = React.useMemo(() => { + if (!stockLevels) return []; + if (!search) return stockLevels; + const lower = search.toLowerCase(); + return stockLevels.filter( + (l) => + l.articleCode?.toLowerCase().includes(lower) || + l.articleDescription?.toLowerCase().includes(lower), + ); + }, [stockLevels, search]); + + const columns: GridColDef[] = [ + { + field: "articleCode", + headerName: "Codice", + width: 120, + renderCell: (params: GridRenderCellParams) => ( + + {params.value} + + ), + }, + { + field: "articleDescription", + headerName: "Articolo", + flex: 1, + minWidth: 200, + }, + { + field: "warehouseName", + headerName: "Magazzino", + width: 150, + }, + { + field: "categoryName", + headerName: "Categoria", + width: 140, + }, + { + field: "quantity", + headerName: "Giacenza", + width: 120, + align: "right", + renderCell: (params: GridRenderCellParams) => { + const qty = params.value || 0; + const isLow = params.row.isLowStock; + return ( + + {isLow && } + + + ); + }, + }, + { + field: "reservedQuantity", + headerName: "Riservata", + width: 100, + align: "right", + renderCell: (params: GridRenderCellParams) => + formatQuantity(params.value || 0), + }, + { + field: "availableQuantity", + headerName: "Disponibile", + width: 110, + align: "right", + renderCell: (params: GridRenderCellParams) => { + const available = + params.row.availableQuantity || + params.row.quantity - params.row.reservedQuantity; + return ( + + {formatQuantity(available)} + + ); + }, + }, + { + field: "unitCost", + headerName: "Costo Medio", + width: 120, + align: "right", + renderCell: (params: GridRenderCellParams) => + formatCurrency(params.value || 0), + }, + { + field: "stockValue", + headerName: "Valore", + width: 130, + align: "right", + renderCell: (params: GridRenderCellParams) => ( + + {formatCurrency(params.value || 0)} + + ), + }, + ]; + + if (error) { + return ( + + Errore: {(error as Error).message} + + ); + } + + return ( + + {/* Header */} + + + Giacenze di Magazzino + + + + + {/* Summary Cards */} + + + + + + Articoli + + + {summary.articleCount} + + + + + + + + + Quantità Totale + + + {formatQuantity(summary.totalQuantity)} + + + + + + + + + Valore Totale + + + {formatCurrency(summary.totalValue)} + + + + + + + + + Sotto Scorta + + + {summary.lowStockCount + summary.outOfStockCount} + + + + + + + {/* Filters */} + + + + setSearch(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: search && ( + + setSearch("")}> + + + + ), + }} + /> + + + + Magazzino + + + + + + Categoria + + + + + setLowStockOnly(e.target.checked)} + /> + } + label="Solo sotto scorta" + /> + + + + + {/* Data Grid */} + + nav.goToArticle(params.row.articleId)} + disableRowSelectionOnClick + /> + + + ); +} diff --git a/frontend/src/modules/warehouse/pages/TransferMovementPage.tsx b/frontend/src/modules/warehouse/pages/TransferMovementPage.tsx new file mode 100644 index 0000000..61a1b05 --- /dev/null +++ b/frontend/src/modules/warehouse/pages/TransferMovementPage.tsx @@ -0,0 +1,447 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + Box, + Paper, + Typography, + TextField, + Button, + Grid, + FormControl, + InputLabel, + Select, + MenuItem, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Alert, + CircularProgress, + Autocomplete, + InputAdornment, + Divider, + Chip, +} from "@mui/material"; +import { + ArrowBack as ArrowBackIcon, + Save as SaveIcon, + Add as AddIcon, + Delete as DeleteIcon, + Check as ConfirmIcon, + SwapHoriz as TransferIcon, +} from "@mui/icons-material"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import dayjs, { Dayjs } from "dayjs"; +import "dayjs/locale/it"; +import { + useWarehouses, + useArticles, + useStockLevels, + useCreateTransferMovement, + useConfirmMovement, +} from "../hooks"; +import { ArticleDto, CreateTransferDto, formatQuantity } from "../types"; + +interface MovementLine { + id: string; + article: ArticleDto | null; + quantity: number; + notes?: string; + availableQty?: number; +} + +export default function TransferMovementPage() { + const navigate = useNavigate(); + const [movementDate, setMovementDate] = useState(dayjs()); + const [sourceWarehouseId, setSourceWarehouseId] = useState(""); + const [destWarehouseId, setDestWarehouseId] = useState(""); + const [documentNumber, setDocumentNumber] = useState(""); + const [externalReference, setExternalReference] = useState(""); + const [notes, setNotes] = useState(""); + const [lines, setLines] = useState([ + { id: crypto.randomUUID(), article: null, quantity: 1 }, + ]); + const [errors, setErrors] = useState>({}); + + const { data: warehouses } = useWarehouses({ active: true }); + const { data: articles } = useArticles({ isActive: true }); + const { data: stockLevels } = useStockLevels({ + warehouseId: sourceWarehouseId || undefined, + }); + const createMutation = useCreateTransferMovement(); + const confirmMutation = useConfirmMovement(); + + const getAvailableQty = (articleId: number): number => { + if (!stockLevels) return 0; + const level = stockLevels.find((l) => l.articleId === articleId); + return level ? level.availableQuantity : 0; + }; + + const handleAddLine = () => { + setLines([ + ...lines, + { id: crypto.randomUUID(), article: null, quantity: 1 }, + ]); + }; + + const handleRemoveLine = (id: string) => { + if (lines.length > 1) { + setLines(lines.filter((l) => l.id !== id)); + } + }; + + const handleLineChange = ( + id: string, + field: keyof MovementLine, + value: unknown, + ) => { + setLines( + lines.map((l) => { + if (l.id === id) { + const updated = { ...l, [field]: value }; + if (field === "article" && value) { + const article = value as ArticleDto; + updated.availableQty = getAvailableQty(article.id); + } + return updated; + } + return l; + }), + ); + }; + + const totalQuantity = lines.reduce((sum, l) => sum + (l.quantity || 0), 0); + + const validate = (): boolean => { + const newErrors: Record = {}; + if (!sourceWarehouseId) { + newErrors.sourceWarehouseId = "Seleziona magazzino origine"; + } + if (!destWarehouseId) { + newErrors.destWarehouseId = "Seleziona magazzino destinazione"; + } + if ( + sourceWarehouseId && + destWarehouseId && + sourceWarehouseId === destWarehouseId + ) { + newErrors.destWarehouseId = + "Origine e destinazione devono essere diversi"; + } + if (!movementDate) { + newErrors.movementDate = "Inserisci la data"; + } + const validLines = lines.filter((l) => l.article && l.quantity > 0); + if (validLines.length === 0) { + newErrors.lines = "Inserisci almeno una riga"; + } + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (andConfirm: boolean = false) => { + if (!validate()) return; + + const data: CreateTransferDto = { + sourceWarehouseId: sourceWarehouseId as number, + destinationWarehouseId: destWarehouseId as number, + movementDate: movementDate!.format("YYYY-MM-DD"), + documentNumber: documentNumber || undefined, + externalReference: externalReference || undefined, + notes: notes || undefined, + lines: lines + .filter((l) => l.article && l.quantity > 0) + .map((l) => ({ + articleId: l.article!.id, + quantity: l.quantity, + notes: l.notes, + })), + }; + + try { + const result = await createMutation.mutateAsync(data); + if (andConfirm) { + await confirmMutation.mutateAsync(result.id); + } + navigate(`/warehouse/movements/${result.id}`); + } catch (error) { + console.error("Errore:", error); + } + }; + + const isPending = createMutation.isPending || confirmMutation.isPending; + + return ( + + + {/* Header */} + + navigate(-1)}> + + + + + + + Trasferimento tra Magazzini + + + Sposta merce da un magazzino all'altro + + + + + + {(createMutation.error || confirmMutation.error) && ( + + Errore:{" "} + {((createMutation.error || confirmMutation.error) as Error).message} + + )} + + {/* Form */} + + + Dati Trasferimento + + + + + + + + Magazzino Origine + + + + + + Magazzino Destinazione + + {errors.destWarehouseId && ( + + {errors.destWarehouseId} + + )} + + + + setDocumentNumber(e.target.value)} + /> + + + setExternalReference(e.target.value)} + /> + + + setNotes(e.target.value)} + /> + + + + + {/* Lines */} + + + Articoli da Trasferire + + + + {errors.lines && ( + + {errors.lines} + + )} + + + + + + Articolo + Disponibile + Quantità + Note + + + + + {lines.map((line) => ( + + + + handleLineChange(line.id, "article", v) + } + options={articles || []} + getOptionLabel={(o) => `${o.code} - ${o.description}`} + renderInput={(params) => ( + + )} + isOptionEqualToValue={(o, v) => o.id === v.id} + /> + + + {line.article && ( + + )} + + + + handleLineChange( + line.id, + "quantity", + parseFloat(e.target.value) || 0, + ) + } + slotProps={{ + htmlInput: { min: 0, step: 0.01 }, + input: { + endAdornment: line.article && ( + + {line.article.unitOfMeasure} + + ), + }, + }} + fullWidth + /> + + + + handleLineChange(line.id, "notes", e.target.value) + } + placeholder="Note" + fullWidth + /> + + + handleRemoveLine(line.id)} + disabled={lines.length === 1} + > + + + + + ))} + +
+
+ + + + + Totale: {formatQuantity(totalQuantity)} + + +
+ + {/* Actions */} + + + + + +
+
+ ); +} diff --git a/frontend/src/modules/warehouse/pages/WarehouseDashboard.tsx b/frontend/src/modules/warehouse/pages/WarehouseDashboard.tsx new file mode 100644 index 0000000..65947a1 --- /dev/null +++ b/frontend/src/modules/warehouse/pages/WarehouseDashboard.tsx @@ -0,0 +1,539 @@ +import React from "react"; +import { + Box, + Paper, + Typography, + Grid, + Card, + CardContent, + Button, + List, + ListItem, + ListItemIcon, + ListItemText, + Chip, + Divider, + Skeleton, + Alert, +} from "@mui/material"; +import { + Inventory as InventoryIcon, + Warehouse as WarehouseIcon, + TrendingUp as TrendingUpIcon, + TrendingDown as TrendingDownIcon, + Warning as WarningIcon, + Schedule as ScheduleIcon, + ArrowForward as ArrowForwardIcon, + Add as AddIcon, + Assessment as AssessmentIcon, +} from "@mui/icons-material"; +import { + useArticles, + useWarehouses, + useMovements, + useStockLevels, + useExpiringBatches, +} from "../hooks"; +import { useWarehouseNavigation } from "../hooks/useWarehouseNavigation"; +import { + MovementStatus, + MovementType, + formatCurrency, + formatQuantity, + formatDate, + movementTypeLabels, + getMovementTypeColor, +} from "../types"; + +interface StatCardProps { + title: string; + value: string | number; + subtitle?: string; + icon: React.ReactNode; + color?: string; + loading?: boolean; +} + +function StatCard({ + title, + value, + subtitle, + icon, + color = "primary.main", + loading, +}: StatCardProps) { + return ( + + + + + + {title} + + {loading ? ( + + ) : ( + + {value} + + )} + {subtitle && ( + + {subtitle} + + )} + + + {icon} + + + + + ); +} + +export default function WarehouseDashboard() { + const nav = useWarehouseNavigation(); + + const { data: articles, isLoading: loadingArticles } = useArticles({ + isActive: true, + }); + const { data: warehouses, isLoading: loadingWarehouses } = useWarehouses({ + active: true, + }); + const { data: stockLevels, isLoading: loadingStock } = useStockLevels(); + const { data: recentMovements, isLoading: loadingMovements } = useMovements(); + const { data: expiringBatches } = useExpiringBatches(30); + + // Calculate statistics + const totalArticles = articles?.length || 0; + const totalWarehouses = warehouses?.length || 0; + + const stockStats = React.useMemo(() => { + if (!stockLevels) return { totalValue: 0, lowStock: 0, outOfStock: 0 }; + + let totalValue = 0; + let lowStock = 0; + let outOfStock = 0; + + for (const level of stockLevels) { + totalValue += level.stockValue || 0; + if (level.quantity <= 0) outOfStock++; + else if (level.isLowStock) lowStock++; + } + + return { totalValue, lowStock, outOfStock }; + }, [stockLevels]); + + // Recent movements (last 10) + const lastMovements = React.useMemo(() => { + if (!recentMovements) return []; + return recentMovements + .filter((m) => m.status === MovementStatus.Confirmed) + .slice(0, 10); + }, [recentMovements]); + + // Pending movements (drafts) + const pendingMovements = React.useMemo(() => { + if (!recentMovements) return []; + return recentMovements.filter((m) => m.status === MovementStatus.Draft); + }, [recentMovements]); + + // Low stock articles + const lowStockArticles = React.useMemo(() => { + if (!stockLevels || !articles) return []; + const lowIds = new Set( + stockLevels + .filter((l) => l.isLowStock || l.quantity <= 0) + .map((l) => l.articleId), + ); + return articles.filter((a) => lowIds.has(a.id)).slice(0, 5); + }, [stockLevels, articles]); + + // Get stock quantity for an article + const getArticleStock = (articleId: number) => { + if (!stockLevels) return 0; + return stockLevels + .filter((l) => l.articleId === articleId) + .reduce((sum, l) => sum + l.quantity, 0); + }; + + return ( + + {/* Header */} + + + + Magazzino + + + Dashboard e panoramica giacenze + + + + + + + + + {/* Stats Cards */} + + + } + loading={loadingArticles} + /> + + + } + loading={loadingWarehouses} + /> + + + } + color="success.main" + loading={loadingStock} + /> + + + } + color="warning.main" + loading={loadingStock} + /> + + + + {/* Main Content */} + + {/* Recent Movements */} + + + + Ultimi Movimenti + + + {loadingMovements ? ( + + {[1, 2, 3].map((i) => ( + + ))} + + ) : lastMovements.length === 0 ? ( + + Nessun movimento recente + + ) : ( + + {lastMovements.map((movement, index) => ( + + nav.goToMovement(movement.id)} + > + + {movement.type === MovementType.Inbound ? ( + + ) : movement.type === MovementType.Outbound ? ( + + ) : ( + + )} + + + + {movement.documentNumber || `MOV-${movement.id}`} + + + + } + secondary={`${movement.sourceWarehouseName || movement.destinationWarehouseName || "-"} - ${formatDate(movement.movementDate)}`} + /> + + {formatQuantity(movement.lineCount)} righe + +
+ {index < lastMovements.length - 1 && } + + ))} +
+ )} + + + + {/* Alerts Section */} + + + {/* Pending Movements */} + {pendingMovements.length > 0 && ( + + } + action={ + + } + > + {pendingMovements.length} movimenti in bozza + da confermare + + + )} + + {/* Expiring Batches */} + {expiringBatches && expiringBatches.length > 0 && ( + + } + action={ + + } + > + {expiringBatches.length} lotti in scadenza + nei prossimi 30 giorni + + + )} + + {/* Low Stock Articles */} + + + + Articoli Sotto Scorta + + + {lowStockArticles.length === 0 ? ( + + Nessun articolo sotto scorta + + ) : ( + + {lowStockArticles.map((article, index) => { + const currentStock = getArticleStock(article.id); + return ( + + nav.goToArticle(article.id)} + > + + + + + + + {index < lowStockArticles.length - 1 && } + + ); + })} + + )} + + + + + + {/* Quick Actions */} + + + + Azioni Rapide + + + + + + + Carico + + + + + + + + Scarico + + + + + + + + Trasferimento + + + + + + + + Nuovo Articolo + + + + + + + + Inventario + + + + + + + + Valorizzazione + + + + + + + + + ); +} diff --git a/frontend/src/modules/warehouse/pages/WarehouseLocationsPage.tsx b/frontend/src/modules/warehouse/pages/WarehouseLocationsPage.tsx new file mode 100644 index 0000000..f2ab7d5 --- /dev/null +++ b/frontend/src/modules/warehouse/pages/WarehouseLocationsPage.tsx @@ -0,0 +1,496 @@ +import { useState } from "react"; +import { + Box, + Paper, + Typography, + Button, + IconButton, + Chip, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + FormControl, + InputLabel, + Select, + MenuItem, + FormControlLabel, + Switch, + Grid, + Alert, + Card, + CardContent, + CardActions, + Tooltip, +} from "@mui/material"; +import { + Add as AddIcon, + Edit as EditIcon, + Delete as DeleteIcon, + Star as StarIcon, + StarBorder as StarBorderIcon, + Warehouse as WarehouseIcon, +} from "@mui/icons-material"; +import { + useWarehouses, + useCreateWarehouse, + useUpdateWarehouse, + useDeleteWarehouse, + useSetDefaultWarehouse, +} from "../hooks"; +import { + WarehouseLocationDto, + WarehouseType, + warehouseTypeLabels, +} from "../types"; + +const initialFormData = { + code: "", + name: "", + description: "", + type: WarehouseType.Physical, + address: "", + isDefault: false, + isActive: true, + sortOrder: 0, +}; + +export default function WarehouseLocationsPage() { + const [dialogOpen, setDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [editingWarehouse, setEditingWarehouse] = + useState(null); + const [warehouseToDelete, setWarehouseToDelete] = + useState(null); + const [formData, setFormData] = useState(initialFormData); + const [errors, setErrors] = useState>({}); + + const { data: warehouses, isLoading, error } = useWarehouses(); + const createMutation = useCreateWarehouse(); + const updateMutation = useUpdateWarehouse(); + const deleteMutation = useDeleteWarehouse(); + const setDefaultMutation = useSetDefaultWarehouse(); + + const handleOpenDialog = (warehouse?: WarehouseLocationDto) => { + if (warehouse) { + setEditingWarehouse(warehouse); + setFormData({ + code: warehouse.code, + name: warehouse.name, + description: warehouse.description || "", + type: warehouse.type, + address: warehouse.address || "", + isDefault: warehouse.isDefault, + isActive: warehouse.isActive, + sortOrder: warehouse.sortOrder, + }); + } else { + setEditingWarehouse(null); + setFormData(initialFormData); + } + setErrors({}); + setDialogOpen(true); + }; + + const handleCloseDialog = () => { + setDialogOpen(false); + setEditingWarehouse(null); + setFormData(initialFormData); + setErrors({}); + }; + + const handleChange = (field: string, value: unknown) => { + setFormData((prev) => ({ ...prev, [field]: value })); + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: "" })); + } + }; + + const validate = (): boolean => { + const newErrors: Record = {}; + if (!formData.code.trim()) { + newErrors.code = "Il codice è obbligatorio"; + } + if (!formData.name.trim()) { + newErrors.name = "Il nome è obbligatorio"; + } + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + if (!validate()) return; + + try { + if (editingWarehouse) { + await updateMutation.mutateAsync({ + id: editingWarehouse.id, + data: formData, + }); + } else { + await createMutation.mutateAsync(formData); + } + handleCloseDialog(); + } catch (error) { + console.error("Errore salvataggio:", error); + } + }; + + const handleDeleteClick = (warehouse: WarehouseLocationDto) => { + setWarehouseToDelete(warehouse); + setDeleteDialogOpen(true); + }; + + const handleDeleteConfirm = async () => { + if (warehouseToDelete) { + await deleteMutation.mutateAsync(warehouseToDelete.id); + setDeleteDialogOpen(false); + setWarehouseToDelete(null); + } + }; + + const handleSetDefault = async (warehouse: WarehouseLocationDto) => { + if (!warehouse.isDefault) { + await setDefaultMutation.mutateAsync(warehouse.id); + } + }; + + const getTypeColor = ( + type: WarehouseType, + ): "primary" | "warning" | "error" | "secondary" | "info" | "default" => { + switch (type) { + case WarehouseType.Physical: + return "primary"; + case WarehouseType.Transit: + return "warning"; + case WarehouseType.Returns: + return "error"; + case WarehouseType.Defective: + return "secondary"; + case WarehouseType.Subcontract: + return "info"; + default: + return "default"; + } + }; + + if (error) { + return ( + + + Errore nel caricamento dei magazzini: {(error as Error).message} + + + ); + } + + return ( + + {/* Header */} + + + Gestione Magazzini + + + + + {/* Warehouse Cards */} + + {isLoading ? ( + Array.from({ length: 4 }).map((_, i) => ( + + + + + + + + )) + ) : warehouses?.length === 0 ? ( + + + + + Nessun magazzino configurato + + + + + ) : ( + warehouses?.map((warehouse) => ( + + + {warehouse.isDefault && ( + + + + )} + + + + + {warehouse.code} + + + + {warehouse.name} + + {warehouse.description && ( + + {warehouse.description} + + )} + + + {!warehouse.isActive && ( + + )} + + {warehouse.address && ( + + {warehouse.address} + + )} + + + + handleSetDefault(warehouse)} + disabled={ + warehouse.isDefault || setDefaultMutation.isPending + } + > + {warehouse.isDefault ? ( + + ) : ( + + )} + + + + handleOpenDialog(warehouse)} + > + + + + + handleDeleteClick(warehouse)} + disabled={warehouse.isDefault} + > + + + + + + + )) + )} + + + {/* Create/Edit Dialog */} + + + {editingWarehouse ? "Modifica Magazzino" : "Nuovo Magazzino"} + + + + + handleChange("code", e.target.value)} + error={!!errors.code} + helperText={errors.code} + required + disabled={!!editingWarehouse} + /> + + + handleChange("name", e.target.value)} + error={!!errors.name} + helperText={errors.name} + required + /> + + + handleChange("description", e.target.value)} + multiline + rows={2} + /> + + + + Tipo + + + + + handleChange("address", e.target.value)} + /> + + + + handleChange("isDefault", e.target.checked) + } + /> + } + label="Magazzino Predefinito" + /> + + + handleChange("isActive", e.target.checked)} + /> + } + label="Attivo" + /> + + + + + + + + + + {/* Delete Confirmation Dialog */} + setDeleteDialogOpen(false)} + > + Conferma Eliminazione + + + Sei sicuro di voler eliminare il magazzino{" "} + + {warehouseToDelete?.code} - {warehouseToDelete?.name} + + ? + + + Questa azione non può essere annullata. + + + + + + + + + ); +} diff --git a/frontend/src/modules/warehouse/pages/index.ts b/frontend/src/modules/warehouse/pages/index.ts new file mode 100644 index 0000000..203c1aa --- /dev/null +++ b/frontend/src/modules/warehouse/pages/index.ts @@ -0,0 +1,18 @@ +// Dashboard +export { default as WarehouseDashboard } from './WarehouseDashboard'; + +// Articles +export { default as ArticlesPage } from './ArticlesPage'; +export { default as ArticleFormPage } from './ArticleFormPage'; + +// Warehouse Locations +export { default as WarehouseLocationsPage } from './WarehouseLocationsPage'; + +// Movements +export { default as MovementsPage } from './MovementsPage'; +export { default as InboundMovementPage } from './InboundMovementPage'; +export { default as OutboundMovementPage } from './OutboundMovementPage'; +export { default as TransferMovementPage } from './TransferMovementPage'; + +// Stock +export { default as StockLevelsPage } from './StockLevelsPage'; diff --git a/frontend/src/modules/warehouse/routes.tsx b/frontend/src/modules/warehouse/routes.tsx new file mode 100644 index 0000000..25dcb01 --- /dev/null +++ b/frontend/src/modules/warehouse/routes.tsx @@ -0,0 +1,52 @@ +import { Routes, Route } from "react-router-dom"; +import { WarehouseProvider } from "./contexts/WarehouseContext"; +import { + WarehouseDashboard, + ArticlesPage, + ArticleFormPage, + WarehouseLocationsPage, + MovementsPage, + InboundMovementPage, + OutboundMovementPage, + TransferMovementPage, + StockLevelsPage, +} from "./pages"; + +export default function WarehouseRoutes() { + return ( + + + {/* Dashboard */} + } /> + + {/* Articles */} + } /> + } /> + } /> + } /> + + {/* Warehouse Locations */} + } /> + + {/* Movements */} + } /> + } /> + } /> + } + /> + } + /> + + {/* Stock */} + } /> + + {/* Fallback */} + } /> + + + ); +} diff --git a/frontend/src/modules/warehouse/services/warehouseService.ts b/frontend/src/modules/warehouse/services/warehouseService.ts new file mode 100644 index 0000000..95834da --- /dev/null +++ b/frontend/src/modules/warehouse/services/warehouseService.ts @@ -0,0 +1,571 @@ +import api from "../../../services/api"; +import { + ArticleDto, + ArticleFilterDto, + ArticleStockDto, + ArticleValuationDto, + BatchDto, + BatchStatus, + CategoryDto, + CategoryTreeDto, + CreateArticleDto, + CreateBatchDto, + CreateCategoryDto, + CreateInventoryCountDto, + CreateMovementDto, + CreateSerialDto, + CreateSerialsBulkDto, + CreateTransferDto, + CreateAdjustmentDto, + CreateWarehouseDto, + InventoryCountDetailDto, + InventoryCountDto, + InventoryCountLineDto, + InventoryStatus, + MovementDetailDto, + MovementDto, + MovementFilterDto, + MovementReasonDto, + MovementType, + PeriodValuationDto, + QualityStatus, + SerialDto, + SerialStatus, + StockLevelDto, + StockLevelFilterDto, + StockSummaryDto, + UpdateArticleDto, + UpdateBatchDto, + UpdateCategoryDto, + UpdateWarehouseDto, + ValuationMethod, + WarehouseLocationDto, +} from "../types"; + +const BASE_URL = "/warehouse"; + +// =============================================== +// WAREHOUSE LOCATIONS +// =============================================== + +export const warehouseLocationService = { + getAll: async (includeInactive = false): Promise => { + const response = await api.get(`${BASE_URL}/locations`, { + params: { includeInactive }, + }); + return response.data; + }, + + getById: async (id: number): Promise => { + const response = await api.get(`${BASE_URL}/locations/${id}`); + return response.data; + }, + + getDefault: async (): Promise => { + const response = await api.get(`${BASE_URL}/locations/default`); + return response.data; + }, + + create: async (data: CreateWarehouseDto): Promise => { + const response = await api.post(`${BASE_URL}/locations`, data); + return response.data; + }, + + update: async ( + id: number, + data: UpdateWarehouseDto, + ): Promise => { + const response = await api.put(`${BASE_URL}/locations/${id}`, data); + return response.data; + }, + + delete: async (id: number): Promise => { + await api.delete(`${BASE_URL}/locations/${id}`); + }, + + setDefault: async (id: number): Promise => { + await api.put(`${BASE_URL}/locations/${id}/set-default`); + }, +}; + +// =============================================== +// ARTICLE CATEGORIES +// =============================================== + +export const categoryService = { + getAll: async (includeInactive = false): Promise => { + const response = await api.get(`${BASE_URL}/categories`, { + params: { includeInactive }, + }); + return response.data; + }, + + getTree: async (): Promise => { + const response = await api.get(`${BASE_URL}/categories/tree`); + return response.data; + }, + + getById: async (id: number): Promise => { + const response = await api.get(`${BASE_URL}/categories/${id}`); + return response.data; + }, + + create: async (data: CreateCategoryDto): Promise => { + const response = await api.post(`${BASE_URL}/categories`, data); + return response.data; + }, + + update: async (id: number, data: UpdateCategoryDto): Promise => { + const response = await api.put(`${BASE_URL}/categories/${id}`, data); + return response.data; + }, + + delete: async (id: number): Promise => { + await api.delete(`${BASE_URL}/categories/${id}`); + }, +}; + +// =============================================== +// ARTICLES +// =============================================== + +export const articleService = { + getAll: async (filter?: ArticleFilterDto): Promise => { + const response = await api.get(`${BASE_URL}/articles`, { params: filter }); + return response.data; + }, + + getById: async (id: number): Promise => { + const response = await api.get(`${BASE_URL}/articles/${id}`); + return response.data; + }, + + getByCode: async (code: string): Promise => { + const response = await api.get(`${BASE_URL}/articles/by-code/${code}`); + return response.data; + }, + + getByBarcode: async (barcode: string): Promise => { + const response = await api.get( + `${BASE_URL}/articles/by-barcode/${barcode}`, + ); + return response.data; + }, + + create: async (data: CreateArticleDto): Promise => { + const response = await api.post(`${BASE_URL}/articles`, data); + return response.data; + }, + + update: async (id: number, data: UpdateArticleDto): Promise => { + const response = await api.put(`${BASE_URL}/articles/${id}`, data); + return response.data; + }, + + delete: async (id: number): Promise => { + await api.delete(`${BASE_URL}/articles/${id}`); + }, + + uploadImage: async (id: number, file: File): Promise => { + const formData = new FormData(); + formData.append("file", file); + await api.post(`${BASE_URL}/articles/${id}/image`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + }, + + getImageUrl: (id: number): string => { + return `${api.defaults.baseURL}${BASE_URL}/articles/${id}/image`; + }, + + getStock: async (id: number): Promise => { + const response = await api.get(`${BASE_URL}/articles/${id}/stock`); + return response.data; + }, +}; + +// =============================================== +// BATCHES +// =============================================== + +export const batchService = { + getAll: async ( + articleId?: number, + status?: BatchStatus, + ): Promise => { + const response = await api.get(`${BASE_URL}/batches`, { + params: { articleId, status }, + }); + return response.data; + }, + + getById: async (id: number): Promise => { + const response = await api.get(`${BASE_URL}/batches/${id}`); + return response.data; + }, + + getByNumber: async ( + articleId: number, + batchNumber: string, + ): Promise => { + const response = await api.get( + `${BASE_URL}/batches/by-number/${articleId}/${batchNumber}`, + ); + return response.data; + }, + + create: async (data: CreateBatchDto): Promise => { + const response = await api.post(`${BASE_URL}/batches`, data); + return response.data; + }, + + update: async (id: number, data: UpdateBatchDto): Promise => { + const response = await api.put(`${BASE_URL}/batches/${id}`, data); + return response.data; + }, + + updateStatus: async (id: number, status: BatchStatus): Promise => { + await api.put(`${BASE_URL}/batches/${id}/status`, { status }); + }, + + getExpiring: async (daysThreshold = 30): Promise => { + const response = await api.get(`${BASE_URL}/batches/expiring`, { + params: { daysThreshold }, + }); + return response.data; + }, + + recordQualityCheck: async ( + id: number, + qualityStatus: QualityStatus, + notes?: string, + ): Promise => { + const response = await api.post(`${BASE_URL}/batches/${id}/quality-check`, { + qualityStatus, + notes, + }); + return response.data; + }, +}; + +// =============================================== +// SERIALS +// =============================================== + +export const serialService = { + getAll: async ( + articleId?: number, + status?: SerialStatus, + ): Promise => { + const response = await api.get(`${BASE_URL}/serials`, { + params: { articleId, status }, + }); + return response.data; + }, + + getById: async (id: number): Promise => { + const response = await api.get(`${BASE_URL}/serials/${id}`); + return response.data; + }, + + getByNumber: async ( + articleId: number, + serialNumber: string, + ): Promise => { + const response = await api.get( + `${BASE_URL}/serials/by-number/${articleId}/${serialNumber}`, + ); + return response.data; + }, + + create: async (data: CreateSerialDto): Promise => { + const response = await api.post(`${BASE_URL}/serials`, data); + return response.data; + }, + + createBulk: async (data: CreateSerialsBulkDto): Promise => { + const response = await api.post(`${BASE_URL}/serials/bulk`, data); + return response.data; + }, + + updateStatus: async (id: number, status: SerialStatus): Promise => { + await api.put(`${BASE_URL}/serials/${id}/status`, { status }); + }, + + registerSale: async ( + id: number, + customerId?: number, + salesReference?: string, + ): Promise => { + const response = await api.post(`${BASE_URL}/serials/${id}/sell`, { + customerId, + salesReference, + }); + return response.data; + }, + + registerReturn: async ( + id: number, + warehouseId: number, + isDefective: boolean, + ): Promise => { + const response = await api.post(`${BASE_URL}/serials/${id}/return`, { + warehouseId, + isDefective, + }); + return response.data; + }, +}; + +// =============================================== +// STOCK LEVELS +// =============================================== + +export const stockService = { + getAll: async (filter?: StockLevelFilterDto): Promise => { + const response = await api.get(`${BASE_URL}/stock`, { params: filter }); + return response.data; + }, + + get: async ( + articleId: number, + warehouseId: number, + batchId?: number, + ): Promise => { + const response = await api.get( + `${BASE_URL}/stock/${articleId}/${warehouseId}`, + { + params: { batchId }, + }, + ); + return response.data; + }, + + getLowStock: async (): Promise => { + const response = await api.get(`${BASE_URL}/stock/low-stock`); + return response.data; + }, + + getSummary: async (articleId: number): Promise => { + const response = await api.get(`${BASE_URL}/stock/summary/${articleId}`); + return response.data; + }, + + getValuation: async ( + articleId: number, + method?: ValuationMethod, + ): Promise => { + const response = await api.get(`${BASE_URL}/stock/valuation/${articleId}`, { + params: { method }, + }); + return response.data; + }, + + getPeriodValuation: async ( + period: number, + warehouseId?: number, + ): Promise => { + const response = await api.get( + `${BASE_URL}/stock/valuation/period/${period}`, + { + params: { warehouseId }, + }, + ); + return response.data; + }, + + calculatePeriodValuation: async ( + articleId: number, + period: number, + warehouseId?: number, + ): Promise => { + const response = await api.post(`${BASE_URL}/stock/valuation/calculate`, { + articleId, + period, + warehouseId, + }); + return response.data; + }, + + closePeriod: async (period: number): Promise => { + await api.post(`${BASE_URL}/stock/valuation/close-period/${period}`); + }, + + recalculateAverageCost: async ( + articleId: number, + ): Promise<{ articleId: number; weightedAverageCost: number }> => { + const response = await api.post( + `${BASE_URL}/stock/recalculate-average/${articleId}`, + ); + return response.data; + }, +}; + +// =============================================== +// MOVEMENTS +// =============================================== + +export const movementService = { + getAll: async (filter?: MovementFilterDto): Promise => { + const response = await api.get(`${BASE_URL}/movements`, { params: filter }); + return response.data; + }, + + getById: async (id: number): Promise => { + const response = await api.get(`${BASE_URL}/movements/${id}`); + return response.data; + }, + + getByDocumentNumber: async ( + documentNumber: string, + ): Promise => { + const response = await api.get( + `${BASE_URL}/movements/by-document/${documentNumber}`, + ); + return response.data; + }, + + createInbound: async ( + data: CreateMovementDto, + ): Promise => { + const response = await api.post(`${BASE_URL}/movements/inbound`, data); + return response.data; + }, + + createOutbound: async ( + data: CreateMovementDto, + ): Promise => { + const response = await api.post(`${BASE_URL}/movements/outbound`, data); + return response.data; + }, + + createTransfer: async ( + data: CreateTransferDto, + ): Promise => { + const response = await api.post(`${BASE_URL}/movements/transfer`, data); + return response.data; + }, + + createAdjustment: async ( + data: CreateAdjustmentDto, + ): Promise => { + const response = await api.post(`${BASE_URL}/movements/adjustment`, data); + return response.data; + }, + + confirm: async (id: number): Promise => { + const response = await api.post(`${BASE_URL}/movements/${id}/confirm`); + return response.data; + }, + + cancel: async (id: number): Promise => { + const response = await api.post(`${BASE_URL}/movements/${id}/cancel`); + return response.data; + }, + + delete: async (id: number): Promise => { + await api.delete(`${BASE_URL}/movements/${id}`); + }, + + generateDocumentNumber: async (type: MovementType): Promise => { + const response = await api.get( + `${BASE_URL}/movements/generate-number/${type}`, + ); + return response.data.documentNumber; + }, + + getReasons: async ( + type?: MovementType, + includeInactive = false, + ): Promise => { + const response = await api.get(`${BASE_URL}/movements/reasons`, { + params: { type, includeInactive }, + }); + return response.data; + }, +}; + +// =============================================== +// INVENTORY +// =============================================== + +export const inventoryService = { + getAll: async (status?: InventoryStatus): Promise => { + const response = await api.get(`${BASE_URL}/inventory`, { + params: { status }, + }); + return response.data; + }, + + getById: async (id: number): Promise => { + const response = await api.get(`${BASE_URL}/inventory/${id}`); + return response.data; + }, + + create: async (data: CreateInventoryCountDto): Promise => { + const response = await api.post(`${BASE_URL}/inventory`, data); + return response.data; + }, + + start: async (id: number): Promise => { + const response = await api.post(`${BASE_URL}/inventory/${id}/start`); + return response.data; + }, + + complete: async (id: number): Promise => { + const response = await api.post(`${BASE_URL}/inventory/${id}/complete`); + return response.data; + }, + + confirm: async (id: number): Promise => { + const response = await api.post(`${BASE_URL}/inventory/${id}/confirm`); + return response.data; + }, + + cancel: async (id: number): Promise => { + const response = await api.post(`${BASE_URL}/inventory/${id}/cancel`); + return response.data; + }, + + updateLine: async ( + lineId: number, + countedQuantity: number, + countedBy?: string, + ): Promise => { + const response = await api.put( + `${BASE_URL}/inventory/lines/${lineId}/count`, + { + countedQuantity, + countedBy, + }, + ); + return response.data; + }, + + updateLinesBatch: async ( + inventoryId: number, + lines: { lineId: number; countedQuantity: number }[], + countedBy?: string, + ): Promise => { + const response = await api.put( + `${BASE_URL}/inventory/${inventoryId}/count-batch`, + { + countedBy, + lines, + }, + ); + return response.data; + }, +}; + +// Export all services +export default { + locations: warehouseLocationService, + categories: categoryService, + articles: articleService, + batches: batchService, + serials: serialService, + stock: stockService, + movements: movementService, + inventory: inventoryService, +}; diff --git a/frontend/src/modules/warehouse/types/index.ts b/frontend/src/modules/warehouse/types/index.ts new file mode 100644 index 0000000..bf63518 --- /dev/null +++ b/frontend/src/modules/warehouse/types/index.ts @@ -0,0 +1,921 @@ +// ===================================================== +// WAREHOUSE MODULE - TYPESCRIPT TYPES +// ===================================================== + +// =============================================== +// ENUMS +// =============================================== + +export enum WarehouseType { + Physical = 0, + Logical = 1, + Transit = 2, + Returns = 3, + Defective = 4, + Subcontract = 5, +} + +export enum StockManagementType { + Standard = 0, + NotManaged = 1, + VariableWeight = 2, + Kit = 3, +} + +export enum ValuationMethod { + WeightedAverage = 0, + FIFO = 1, + LIFO = 2, + StandardCost = 3, + SpecificCost = 4, +} + +export enum BatchStatus { + Available = 0, + Quarantine = 1, + Blocked = 2, + Expired = 3, + Depleted = 4, +} + +export enum QualityStatus { + NotChecked = 0, + Approved = 1, + Rejected = 2, + ConditionallyApproved = 3, +} + +export enum SerialStatus { + Available = 0, + Reserved = 1, + Sold = 2, + InRepair = 3, + Defective = 4, + Returned = 5, + Disposed = 6, +} + +export enum MovementType { + Inbound = 0, + Outbound = 1, + Transfer = 2, + Adjustment = 3, + Production = 4, + Consumption = 5, + SupplierReturn = 6, + CustomerReturn = 7, +} + +export enum MovementStatus { + Draft = 0, + Confirmed = 1, + Cancelled = 2, +} + +export enum ExternalDocumentType { + PurchaseOrder = 0, + InboundDeliveryNote = 1, + PurchaseInvoice = 2, + SalesOrder = 3, + OutboundDeliveryNote = 4, + SalesInvoice = 5, + ProductionOrder = 6, + InventoryDocument = 7, +} + +export enum BarcodeType { + EAN13 = 0, + EAN8 = 1, + UPCA = 2, + UPCE = 3, + Code128 = 4, + Code39 = 5, + QRCode = 6, + DataMatrix = 7, + Internal = 8, +} + +export enum InventoryType { + Full = 0, + Partial = 1, + Cyclic = 2, + Sample = 3, +} + +export enum InventoryStatus { + Draft = 0, + InProgress = 1, + Completed = 2, + Confirmed = 3, + Cancelled = 4, +} + +// =============================================== +// WAREHOUSE LOCATION +// =============================================== + +export interface WarehouseLocationDto { + id: number; + code: string; + name: string; + description?: string; + address?: string; + city?: string; + province?: string; + postalCode?: string; + country?: string; + type: WarehouseType; + isDefault: boolean; + isActive: boolean; + sortOrder: number; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateWarehouseDto { + code: string; + name: string; + description?: string; + address?: string; + city?: string; + province?: string; + postalCode?: string; + country?: string; + type: WarehouseType; + isDefault: boolean; + sortOrder: number; + notes?: string; +} + +export interface UpdateWarehouseDto extends CreateWarehouseDto { + isActive: boolean; +} + +// =============================================== +// ARTICLE CATEGORY +// =============================================== + +export interface CategoryDto { + id: number; + code: string; + name: string; + description?: string; + parentCategoryId?: number; + parentCategoryName?: string; + level: number; + fullPath?: string; + icon?: string; + color?: string; + defaultValuationMethod?: ValuationMethod; + sortOrder: number; + isActive: boolean; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CategoryTreeDto { + id: number; + code: string; + name: string; + description?: string; + level: number; + fullPath?: string; + icon?: string; + color?: string; + isActive: boolean; + children: CategoryTreeDto[]; +} + +export interface CreateCategoryDto { + code: string; + name: string; + description?: string; + parentCategoryId?: number; + icon?: string; + color?: string; + defaultValuationMethod?: ValuationMethod; + sortOrder: number; + notes?: string; +} + +export interface UpdateCategoryDto { + code: string; + name: string; + description?: string; + icon?: string; + color?: string; + defaultValuationMethod?: ValuationMethod; + sortOrder: number; + isActive: boolean; + notes?: string; +} + +// =============================================== +// ARTICLE +// =============================================== + +export interface ArticleDto { + id: number; + code: string; + description: string; + shortDescription?: string; + barcode?: string; + manufacturerCode?: string; + categoryId?: number; + categoryName?: string; + unitOfMeasure: string; + secondaryUnitOfMeasure?: string; + unitConversionFactor?: number; + stockManagement: StockManagementType; + isBatchManaged: boolean; + isSerialManaged: boolean; + hasExpiry: boolean; + expiryWarningDays?: number; + minimumStock?: number; + maximumStock?: number; + reorderPoint?: number; + reorderQuantity?: number; + leadTimeDays?: number; + valuationMethod?: ValuationMethod; + standardCost?: number; + lastPurchaseCost?: number; + weightedAverageCost?: number; + baseSellingPrice?: number; + weight?: number; + volume?: number; + isActive: boolean; + notes?: string; + hasImage: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateArticleDto { + code: string; + description: string; + shortDescription?: string; + barcode?: string; + manufacturerCode?: string; + categoryId?: number; + unitOfMeasure: string; + secondaryUnitOfMeasure?: string; + unitConversionFactor?: number; + stockManagement: StockManagementType; + isBatchManaged: boolean; + isSerialManaged: boolean; + hasExpiry: boolean; + expiryWarningDays?: number; + minimumStock?: number; + maximumStock?: number; + reorderPoint?: number; + reorderQuantity?: number; + leadTimeDays?: number; + valuationMethod?: ValuationMethod; + standardCost?: number; + baseSellingPrice?: number; + weight?: number; + volume?: number; + notes?: string; +} + +export interface UpdateArticleDto extends CreateArticleDto { + isActive: boolean; +} + +export interface ArticleFilterDto { + search?: string; + categoryId?: number; + isActive?: boolean; + isBatchManaged?: boolean; + isSerialManaged?: boolean; + skip?: number; + take?: number; + orderBy?: string; + orderDescending?: boolean; +} + +export interface ArticleStockDto { + articleId: number; + articleCode: string; + articleDescription: string; + totalStock: number; + availableStock: number; + unitOfMeasure: string; + minimumStock?: number; + maximumStock?: number; + reorderPoint?: number; + isLowStock: boolean; + stockByWarehouse: WarehouseStockDto[]; +} + +export interface WarehouseStockDto { + warehouseId: number; + warehouseCode: string; + warehouseName: string; + quantity: number; + reservedQuantity: number; + availableQuantity: number; + unitCost?: number; + stockValue?: number; + batchId?: number; + batchNumber?: string; +} + +// =============================================== +// BATCH +// =============================================== + +export interface BatchDto { + id: number; + articleId: number; + articleCode?: string; + articleDescription?: string; + batchNumber: string; + productionDate?: string; + expiryDate?: string; + supplierBatch?: string; + supplierId?: number; + unitCost?: number; + initialQuantity: number; + currentQuantity: number; + reservedQuantity: number; + availableQuantity: number; + status: BatchStatus; + qualityStatus?: QualityStatus; + lastQualityCheckDate?: string; + certifications?: string; + notes?: string; + isExpired: boolean; + daysToExpiry?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateBatchDto { + articleId: number; + batchNumber: string; + productionDate?: string; + expiryDate?: string; + supplierBatch?: string; + supplierId?: number; + unitCost?: number; + initialQuantity: number; + certifications?: string; + notes?: string; +} + +export interface UpdateBatchDto { + productionDate?: string; + expiryDate?: string; + supplierBatch?: string; + unitCost?: number; + certifications?: string; + notes?: string; +} + +// =============================================== +// SERIAL +// =============================================== + +export interface SerialDto { + id: number; + articleId: number; + articleCode?: string; + articleDescription?: string; + batchId?: number; + batchNumber?: string; + serialNumber: string; + manufacturerSerial?: string; + productionDate?: string; + warrantyExpiryDate?: string; + currentWarehouseId?: number; + currentWarehouseCode?: string; + currentWarehouseName?: string; + status: SerialStatus; + unitCost?: number; + supplierId?: number; + customerId?: number; + soldDate?: string; + salesReference?: string; + attributes?: string; + notes?: string; + isWarrantyValid: boolean; + daysToWarrantyExpiry?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateSerialDto { + articleId: number; + batchId?: number; + serialNumber: string; + manufacturerSerial?: string; + productionDate?: string; + warrantyExpiryDate?: string; + warehouseId?: number; + unitCost?: number; + supplierId?: number; + attributes?: string; + notes?: string; +} + +export interface CreateSerialsBulkDto { + articleId: number; + batchId?: number; + serialNumbers: string[]; + productionDate?: string; + warrantyExpiryDate?: string; + warehouseId?: number; + unitCost?: number; + supplierId?: number; +} + +// =============================================== +// STOCK LEVEL +// =============================================== + +export interface StockLevelDto { + id: number; + articleId: number; + articleCode: string; + articleDescription: string; + categoryName?: string; + warehouseId: number; + warehouseCode: string; + warehouseName: string; + batchId?: number; + batchNumber?: string; + batchExpiryDate?: string; + quantity: number; + reservedQuantity: number; + availableQuantity: number; + onOrderQuantity: number; + unitCost?: number; + stockValue?: number; + locationCode?: string; + lastMovementDate?: string; + lastInventoryDate?: string; + minimumStock?: number; + isLowStock: boolean; +} + +export interface StockLevelFilterDto { + articleId?: number; + warehouseId?: number; + batchId?: number; + categoryId?: number; + onlyWithStock?: boolean; + onlyLowStock?: boolean; + skip?: number; + take?: number; +} + +export interface StockSummaryDto { + articleId: number; + articleCode: string; + articleDescription: string; + unitOfMeasure: string; + totalStock: number; + availableStock: number; + minimumStock?: number; + maximumStock?: number; + reorderPoint?: number; + isLowStock: boolean; + totalValue: number; + warehouseCount: number; + stockByWarehouse: StockLevelDto[]; +} + +// =============================================== +// MOVEMENT +// =============================================== + +export interface MovementDto { + id: number; + documentNumber: string; + movementDate: string; + type: MovementType; + status: MovementStatus; + sourceWarehouseId?: number; + sourceWarehouseCode?: string; + sourceWarehouseName?: string; + destinationWarehouseId?: number; + destinationWarehouseCode?: string; + destinationWarehouseName?: string; + reasonId?: number; + reasonDescription?: string; + externalReference?: string; + totalValue?: number; + lineCount: number; + confirmedDate?: string; + notes?: string; + createdAt?: string; +} + +export interface MovementDetailDto extends MovementDto { + externalDocumentType?: ExternalDocumentType; + supplierId?: number; + customerId?: number; + confirmedBy?: string; + updatedAt?: string; + lines: MovementLineDto[]; +} + +export interface MovementLineDto { + id: number; + lineNumber: number; + articleId: number; + articleCode: string; + articleDescription: string; + batchId?: number; + batchNumber?: string; + serialId?: number; + serialNumber?: string; + quantity: number; + unitOfMeasure: string; + unitCost?: number; + lineValue?: number; + sourceLocationCode?: string; + destinationLocationCode?: string; + notes?: string; +} + +export interface CreateMovementDto { + documentNumber?: string; + movementDate?: string; + reasonId?: number; + warehouseId: number; + externalReference?: string; + externalDocumentType?: ExternalDocumentType; + supplierId?: number; + customerId?: number; + notes?: string; + lines: CreateMovementLineDto[]; +} + +export interface CreateTransferDto { + documentNumber?: string; + movementDate?: string; + reasonId?: number; + sourceWarehouseId: number; + destinationWarehouseId: number; + externalReference?: string; + notes?: string; + lines: CreateMovementLineDto[]; +} + +export interface CreateAdjustmentDto { + documentNumber?: string; + movementDate?: string; + reasonId?: number; + warehouseId: number; + externalReference?: string; + notes?: string; + lines: CreateMovementLineDto[]; +} + +export interface CreateMovementLineDto { + articleId: number; + batchId?: number; + serialId?: number; + quantity: number; + unitOfMeasure?: string; + unitCost?: number; + sourceLocationCode?: string; + destinationLocationCode?: string; + externalLineReference?: string; + notes?: string; +} + +export interface MovementFilterDto { + dateFrom?: string; + dateTo?: string; + type?: MovementType; + status?: MovementStatus; + warehouseId?: number; + articleId?: number; + reasonId?: number; + externalReference?: string; + skip?: number; + take?: number; + orderBy?: string; + orderDescending?: boolean; +} + +export interface MovementReasonDto { + id: number; + code: string; + description: string; + movementType: MovementType; + stockSign: number; + requiresExternalReference: boolean; + requiresValuation: boolean; + updatesAverageCost: boolean; + isSystem: boolean; + isActive: boolean; +} + +// =============================================== +// VALUATION +// =============================================== + +export interface ArticleValuationDto { + articleId: number; + articleCode: string; + articleDescription: string; + method: ValuationMethod; + totalQuantity: number; + unitOfMeasure: string; + weightedAverageCost: number; + standardCost?: number; + lastPurchaseCost?: number; + totalValue: number; +} + +export interface PeriodValuationDto { + id: number; + period: number; + valuationDate: string; + articleId: number; + articleCode: string; + articleDescription: string; + warehouseId?: number; + warehouseCode?: string; + warehouseName?: string; + quantity: number; + method: ValuationMethod; + unitCost: number; + totalValue: number; + inboundQuantity: number; + inboundValue: number; + outboundQuantity: number; + outboundValue: number; + isClosed: boolean; +} + +// =============================================== +// INVENTORY +// =============================================== + +export interface InventoryCountDto { + id: number; + code: string; + description: string; + inventoryDate: string; + warehouseId?: number; + warehouseCode?: string; + warehouseName?: string; + categoryId?: number; + categoryName?: string; + type: InventoryType; + status: InventoryStatus; + startDate?: string; + endDate?: string; + confirmedDate?: string; + adjustmentMovementId?: number; + positiveDifferenceValue?: number; + negativeDifferenceValue?: number; + lineCount: number; + countedLineCount: number; + notes?: string; + createdAt?: string; +} + +export interface InventoryCountDetailDto extends InventoryCountDto { + confirmedBy?: string; + updatedAt?: string; + lines: InventoryCountLineDto[]; +} + +export interface InventoryCountLineDto { + id: number; + articleId: number; + articleCode: string; + articleDescription: string; + warehouseId: number; + warehouseCode: string; + batchId?: number; + batchNumber?: string; + locationCode?: string; + theoreticalQuantity: number; + countedQuantity?: number; + difference?: number; + unitCost?: number; + differenceValue?: number; + countedAt?: string; + countedBy?: string; + secondCountQuantity?: number; + secondCountBy?: string; + notes?: string; +} + +export interface CreateInventoryCountDto { + code?: string; + description: string; + inventoryDate?: string; + warehouseId?: number; + categoryId?: number; + type: InventoryType; + notes?: string; +} + +// =============================================== +// HELPER FUNCTIONS +// =============================================== + +export const warehouseTypeLabels: Record = { + [WarehouseType.Physical]: "Fisico", + [WarehouseType.Logical]: "Logico", + [WarehouseType.Transit]: "Transito", + [WarehouseType.Returns]: "Resi", + [WarehouseType.Defective]: "Difettosi", + [WarehouseType.Subcontract]: "Conto Lavoro", +}; + +export const stockManagementTypeLabels: Record = { + [StockManagementType.Standard]: "Standard", + [StockManagementType.NotManaged]: "Non Gestito", + [StockManagementType.VariableWeight]: "Peso Variabile", + [StockManagementType.Kit]: "Kit", +}; + +export const valuationMethodLabels: Record = { + [ValuationMethod.WeightedAverage]: "Costo Medio Ponderato", + [ValuationMethod.FIFO]: "FIFO", + [ValuationMethod.LIFO]: "LIFO", + [ValuationMethod.StandardCost]: "Costo Standard", + [ValuationMethod.SpecificCost]: "Costo Specifico", +}; + +export const batchStatusLabels: Record = { + [BatchStatus.Available]: "Disponibile", + [BatchStatus.Quarantine]: "Quarantena", + [BatchStatus.Blocked]: "Bloccato", + [BatchStatus.Expired]: "Scaduto", + [BatchStatus.Depleted]: "Esaurito", +}; + +export const qualityStatusLabels: Record = { + [QualityStatus.NotChecked]: "Non Controllato", + [QualityStatus.Approved]: "Approvato", + [QualityStatus.Rejected]: "Respinto", + [QualityStatus.ConditionallyApproved]: "Approvato con Riserva", +}; + +export const serialStatusLabels: Record = { + [SerialStatus.Available]: "Disponibile", + [SerialStatus.Reserved]: "Riservato", + [SerialStatus.Sold]: "Venduto", + [SerialStatus.InRepair]: "In Riparazione", + [SerialStatus.Defective]: "Difettoso", + [SerialStatus.Returned]: "Reso", + [SerialStatus.Disposed]: "Dismesso", +}; + +export const movementTypeLabels: Record = { + [MovementType.Inbound]: "Carico", + [MovementType.Outbound]: "Scarico", + [MovementType.Transfer]: "Trasferimento", + [MovementType.Adjustment]: "Rettifica", + [MovementType.Production]: "Produzione", + [MovementType.Consumption]: "Consumo", + [MovementType.SupplierReturn]: "Reso a Fornitore", + [MovementType.CustomerReturn]: "Reso da Cliente", +}; + +export const movementStatusLabels: Record = { + [MovementStatus.Draft]: "Bozza", + [MovementStatus.Confirmed]: "Confermato", + [MovementStatus.Cancelled]: "Annullato", +}; + +export const inventoryTypeLabels: Record = { + [InventoryType.Full]: "Completo", + [InventoryType.Partial]: "Parziale", + [InventoryType.Cyclic]: "Ciclico", + [InventoryType.Sample]: "A Campione", +}; + +export const inventoryStatusLabels: Record = { + [InventoryStatus.Draft]: "Bozza", + [InventoryStatus.InProgress]: "In Corso", + [InventoryStatus.Completed]: "Completato", + [InventoryStatus.Confirmed]: "Confermato", + [InventoryStatus.Cancelled]: "Annullato", +}; + +export function formatCurrency(value: number | undefined | null): string { + if (value === undefined || value === null) return "-"; + return new Intl.NumberFormat("it-IT", { + style: "currency", + currency: "EUR", + }).format(value); +} + +export function formatQuantity( + value: number | undefined | null, + decimals: number = 2 +): string { + if (value === undefined || value === null) return "-"; + return new Intl.NumberFormat("it-IT", { + minimumFractionDigits: 0, + maximumFractionDigits: decimals, + }).format(value); +} + +export function formatDate(dateString: string | undefined | null): string { + if (!dateString) return "-"; + return new Date(dateString).toLocaleDateString("it-IT"); +} + +export function formatDateTime(dateString: string | undefined | null): string { + if (!dateString) return "-"; + return new Date(dateString).toLocaleString("it-IT"); +} + +export function getMovementTypeColor(type: MovementType): string { + switch (type) { + case MovementType.Inbound: + case MovementType.Production: + case MovementType.CustomerReturn: + return "success"; + case MovementType.Outbound: + case MovementType.Consumption: + case MovementType.SupplierReturn: + return "error"; + case MovementType.Transfer: + return "info"; + case MovementType.Adjustment: + return "warning"; + default: + return "default"; + } +} + +export function getMovementStatusColor(status: MovementStatus): string { + switch (status) { + case MovementStatus.Draft: + return "warning"; + case MovementStatus.Confirmed: + return "success"; + case MovementStatus.Cancelled: + return "error"; + default: + return "default"; + } +} + +export function getBatchStatusColor(status: BatchStatus): string { + switch (status) { + case BatchStatus.Available: + return "success"; + case BatchStatus.Quarantine: + return "warning"; + case BatchStatus.Blocked: + case BatchStatus.Expired: + return "error"; + case BatchStatus.Depleted: + return "default"; + default: + return "default"; + } +} + +export function getSerialStatusColor(status: SerialStatus): string { + switch (status) { + case SerialStatus.Available: + return "success"; + case SerialStatus.Reserved: + return "info"; + case SerialStatus.Sold: + return "primary"; + case SerialStatus.Returned: + return "warning"; + case SerialStatus.InRepair: + case SerialStatus.Defective: + return "error"; + case SerialStatus.Disposed: + return "default"; + default: + return "default"; + } +} + +export function getInventoryStatusColor(status: InventoryStatus): string { + switch (status) { + case InventoryStatus.Draft: + return "default"; + case InventoryStatus.InProgress: + return "info"; + case InventoryStatus.Completed: + return "warning"; + case InventoryStatus.Confirmed: + return "success"; + case InventoryStatus.Cancelled: + return "error"; + default: + return "default"; + } +} diff --git a/src/Apollinare.API/Modules/Warehouse/Controllers/BatchesController.cs b/src/Apollinare.API/Modules/Warehouse/Controllers/BatchesController.cs new file mode 100644 index 0000000..a7d78ac --- /dev/null +++ b/src/Apollinare.API/Modules/Warehouse/Controllers/BatchesController.cs @@ -0,0 +1,288 @@ +using Apollinare.API.Modules.Warehouse.Services; +using Apollinare.Domain.Entities.Warehouse; +using Microsoft.AspNetCore.Mvc; + +namespace Apollinare.API.Modules.Warehouse.Controllers; + +/// +/// Controller per la gestione delle partite/lotti +/// +[ApiController] +[Route("api/warehouse/batches")] +public class BatchesController : ControllerBase +{ + private readonly IWarehouseService _warehouseService; + private readonly ILogger _logger; + + public BatchesController( + IWarehouseService warehouseService, + ILogger logger) + { + _warehouseService = warehouseService; + _logger = logger; + } + + /// + /// Ottiene la lista delle partite con filtri opzionali + /// + [HttpGet] + public async Task>> GetBatches( + [FromQuery] int? articleId = null, + [FromQuery] BatchStatus? status = null) + { + var batches = await _warehouseService.GetBatchesAsync(articleId, status); + return Ok(batches.Select(MapToDto)); + } + + /// + /// Ottiene una partita per ID + /// + [HttpGet("{id}")] + public async Task> GetBatch(int id) + { + var batch = await _warehouseService.GetBatchByIdAsync(id); + if (batch == null) + return NotFound(); + + return Ok(MapToDto(batch)); + } + + /// + /// Ottiene una partita per articolo e numero lotto + /// + [HttpGet("by-number/{articleId}/{batchNumber}")] + public async Task> GetBatchByNumber(int articleId, string batchNumber) + { + var batch = await _warehouseService.GetBatchByNumberAsync(articleId, batchNumber); + if (batch == null) + return NotFound(); + + return Ok(MapToDto(batch)); + } + + /// + /// Crea una nuova partita + /// + [HttpPost] + public async Task> CreateBatch([FromBody] CreateBatchDto dto) + { + try + { + var batch = new ArticleBatch + { + ArticleId = dto.ArticleId, + BatchNumber = dto.BatchNumber, + ProductionDate = dto.ProductionDate, + ExpiryDate = dto.ExpiryDate, + SupplierBatch = dto.SupplierBatch, + SupplierId = dto.SupplierId, + UnitCost = dto.UnitCost, + InitialQuantity = dto.InitialQuantity, + CurrentQuantity = dto.InitialQuantity, + Status = BatchStatus.Available, + Certifications = dto.Certifications, + Notes = dto.Notes + }; + + var created = await _warehouseService.CreateBatchAsync(batch); + return CreatedAtAction(nameof(GetBatch), new { id = created.Id }, MapToDto(created)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Aggiorna una partita esistente + /// + [HttpPut("{id}")] + public async Task> UpdateBatch(int id, [FromBody] UpdateBatchDto dto) + { + try + { + var existing = await _warehouseService.GetBatchByIdAsync(id); + if (existing == null) + return NotFound(); + + if (dto.ProductionDate.HasValue) + existing.ProductionDate = dto.ProductionDate; + if (dto.ExpiryDate.HasValue) + existing.ExpiryDate = dto.ExpiryDate; + if (dto.SupplierBatch != null) + existing.SupplierBatch = dto.SupplierBatch; + if (dto.UnitCost.HasValue) + existing.UnitCost = dto.UnitCost; + if (dto.Certifications != null) + existing.Certifications = dto.Certifications; + if (dto.Notes != null) + existing.Notes = dto.Notes; + + var updated = await _warehouseService.UpdateBatchAsync(existing); + return Ok(MapToDto(updated)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Aggiorna lo stato di una partita + /// + [HttpPut("{id}/status")] + public async Task UpdateBatchStatus(int id, [FromBody] UpdateBatchStatusDto dto) + { + try + { + await _warehouseService.UpdateBatchStatusAsync(id, dto.Status); + return Ok(); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// Ottiene le partite in scadenza + /// + [HttpGet("expiring")] + public async Task>> GetExpiringBatches([FromQuery] int daysThreshold = 30) + { + var batches = await _warehouseService.GetExpiringBatchesAsync(daysThreshold); + return Ok(batches.Select(MapToDto)); + } + + /// + /// Registra un controllo qualità sulla partita + /// + [HttpPost("{id}/quality-check")] + public async Task> RecordQualityCheck(int id, [FromBody] QualityCheckDto dto) + { + try + { + var batch = await _warehouseService.GetBatchByIdAsync(id); + if (batch == null) + return NotFound(); + + batch.QualityStatus = dto.QualityStatus; + batch.LastQualityCheckDate = DateTime.UtcNow; + + // Aggiorna lo stato del lotto in base al risultato + if (dto.QualityStatus == QualityStatus.Rejected) + { + batch.Status = BatchStatus.Blocked; + } + else if (dto.QualityStatus == QualityStatus.Approved && batch.Status == BatchStatus.Quarantine) + { + batch.Status = BatchStatus.Available; + } + + var updated = await _warehouseService.UpdateBatchAsync(batch); + return Ok(MapToDto(updated)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + #region DTOs + + public record BatchDto( + int Id, + int ArticleId, + string? ArticleCode, + string? ArticleDescription, + string BatchNumber, + DateTime? ProductionDate, + DateTime? ExpiryDate, + string? SupplierBatch, + int? SupplierId, + decimal? UnitCost, + decimal InitialQuantity, + decimal CurrentQuantity, + decimal ReservedQuantity, + decimal AvailableQuantity, + BatchStatus Status, + QualityStatus? QualityStatus, + DateTime? LastQualityCheckDate, + string? Certifications, + string? Notes, + bool IsExpired, + int? DaysToExpiry, + DateTime? CreatedAt, + DateTime? UpdatedAt + ); + + public record CreateBatchDto( + int ArticleId, + string BatchNumber, + DateTime? ProductionDate, + DateTime? ExpiryDate, + string? SupplierBatch, + int? SupplierId, + decimal? UnitCost, + decimal InitialQuantity, + string? Certifications, + string? Notes + ); + + public record UpdateBatchDto( + DateTime? ProductionDate, + DateTime? ExpiryDate, + string? SupplierBatch, + decimal? UnitCost, + string? Certifications, + string? Notes + ); + + public record UpdateBatchStatusDto(BatchStatus Status); + + public record QualityCheckDto(QualityStatus QualityStatus, string? Notes); + + #endregion + + #region Mapping + + private static BatchDto MapToDto(ArticleBatch batch) + { + var isExpired = batch.ExpiryDate.HasValue && batch.ExpiryDate.Value < DateTime.UtcNow; + var daysToExpiry = batch.ExpiryDate.HasValue + ? (int?)Math.Max(0, (batch.ExpiryDate.Value - DateTime.UtcNow).Days) + : null; + + return new BatchDto( + batch.Id, + batch.ArticleId, + batch.Article?.Code, + batch.Article?.Description, + batch.BatchNumber, + batch.ProductionDate, + batch.ExpiryDate, + batch.SupplierBatch, + batch.SupplierId, + batch.UnitCost, + batch.InitialQuantity, + batch.CurrentQuantity, + batch.ReservedQuantity, + batch.CurrentQuantity - batch.ReservedQuantity, + batch.Status, + batch.QualityStatus, + batch.LastQualityCheckDate, + batch.Certifications, + batch.Notes, + isExpired, + daysToExpiry, + batch.CreatedAt, + batch.UpdatedAt + ); + } + + #endregion +} diff --git a/src/Apollinare.API/Modules/Warehouse/Controllers/InventoryController.cs b/src/Apollinare.API/Modules/Warehouse/Controllers/InventoryController.cs new file mode 100644 index 0000000..9189e52 --- /dev/null +++ b/src/Apollinare.API/Modules/Warehouse/Controllers/InventoryController.cs @@ -0,0 +1,428 @@ +using Apollinare.API.Modules.Warehouse.Services; +using Apollinare.Domain.Entities.Warehouse; +using Microsoft.AspNetCore.Mvc; + +namespace Apollinare.API.Modules.Warehouse.Controllers; + +/// +/// Controller per la gestione degli inventari fisici +/// +[ApiController] +[Route("api/warehouse/inventory")] +public class InventoryController : ControllerBase +{ + private readonly IWarehouseService _warehouseService; + private readonly ILogger _logger; + + public InventoryController( + IWarehouseService warehouseService, + ILogger logger) + { + _warehouseService = warehouseService; + _logger = logger; + } + + /// + /// Ottiene la lista degli inventari + /// + [HttpGet] + public async Task>> GetInventoryCounts([FromQuery] InventoryStatus? status = null) + { + var inventories = await _warehouseService.GetInventoryCountsAsync(status); + return Ok(inventories.Select(MapToDto)); + } + + /// + /// Ottiene un inventario per ID + /// + [HttpGet("{id}")] + public async Task> GetInventoryCount(int id) + { + var inventory = await _warehouseService.GetInventoryCountByIdAsync(id); + if (inventory == null) + return NotFound(); + + return Ok(MapToDetailDto(inventory)); + } + + /// + /// Crea un nuovo inventario + /// + [HttpPost] + public async Task> CreateInventoryCount([FromBody] CreateInventoryCountDto dto) + { + try + { + var inventory = new InventoryCount + { + Code = dto.Code ?? "", + Description = dto.Description, + InventoryDate = dto.InventoryDate ?? DateTime.UtcNow.Date, + WarehouseId = dto.WarehouseId, + CategoryId = dto.CategoryId, + Type = dto.Type, + Notes = dto.Notes, + Status = InventoryStatus.Draft + }; + + var created = await _warehouseService.CreateInventoryCountAsync(inventory); + return CreatedAtAction(nameof(GetInventoryCount), new { id = created.Id }, MapToDto(created)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Aggiorna un inventario esistente (solo bozze) + /// + [HttpPut("{id}")] + public async Task> UpdateInventoryCount(int id, [FromBody] UpdateInventoryCountDto dto) + { + try + { + var existing = await _warehouseService.GetInventoryCountByIdAsync(id); + if (existing == null) + return NotFound(); + + if (dto.Description != null) + existing.Description = dto.Description; + if (dto.InventoryDate.HasValue) + existing.InventoryDate = dto.InventoryDate.Value; + if (dto.WarehouseId.HasValue) + existing.WarehouseId = dto.WarehouseId; + if (dto.CategoryId.HasValue) + existing.CategoryId = dto.CategoryId; + if (dto.Type.HasValue) + existing.Type = dto.Type.Value; + if (dto.Notes != null) + existing.Notes = dto.Notes; + + var updated = await _warehouseService.UpdateInventoryCountAsync(existing); + return Ok(MapToDto(updated)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Avvia un inventario (genera righe da contare) + /// + [HttpPost("{id}/start")] + public async Task> StartInventoryCount(int id) + { + try + { + var inventory = await _warehouseService.StartInventoryCountAsync(id); + return Ok(MapToDetailDto(inventory)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Completa un inventario (tutti i conteggi effettuati) + /// + [HttpPost("{id}/complete")] + public async Task> CompleteInventoryCount(int id) + { + try + { + var inventory = await _warehouseService.CompleteInventoryCountAsync(id); + return Ok(MapToDetailDto(inventory)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Conferma un inventario (applica rettifiche) + /// + [HttpPost("{id}/confirm")] + public async Task> ConfirmInventoryCount(int id) + { + try + { + var inventory = await _warehouseService.ConfirmInventoryCountAsync(id); + return Ok(MapToDetailDto(inventory)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Annulla un inventario + /// + [HttpPost("{id}/cancel")] + public async Task> CancelInventoryCount(int id) + { + try + { + var inventory = await _warehouseService.CancelInventoryCountAsync(id); + return Ok(MapToDto(inventory)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Registra il conteggio di una riga + /// + [HttpPut("lines/{lineId}/count")] + public async Task> UpdateCountLine(int lineId, [FromBody] UpdateCountLineDto dto) + { + try + { + var line = await _warehouseService.UpdateCountLineAsync(lineId, dto.CountedQuantity, dto.CountedBy); + return Ok(MapLineToDto(line)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Registra conteggi multipli in batch + /// + [HttpPut("{id}/count-batch")] + public async Task UpdateCountLinesBatch(int id, [FromBody] UpdateCountLinesBatchDto dto) + { + try + { + var results = new List(); + foreach (var lineUpdate in dto.Lines) + { + var line = await _warehouseService.UpdateCountLineAsync( + lineUpdate.LineId, + lineUpdate.CountedQuantity, + dto.CountedBy); + results.Add(MapLineToDto(line)); + } + return Ok(results); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + #region DTOs + + public record InventoryCountDto( + int Id, + string Code, + string Description, + DateTime InventoryDate, + int? WarehouseId, + string? WarehouseCode, + string? WarehouseName, + int? CategoryId, + string? CategoryName, + InventoryType Type, + InventoryStatus Status, + DateTime? StartDate, + DateTime? EndDate, + DateTime? ConfirmedDate, + int? AdjustmentMovementId, + decimal? PositiveDifferenceValue, + decimal? NegativeDifferenceValue, + int LineCount, + int CountedLineCount, + string? Notes, + DateTime? CreatedAt + ); + + public record InventoryCountDetailDto( + int Id, + string Code, + string Description, + DateTime InventoryDate, + int? WarehouseId, + string? WarehouseCode, + string? WarehouseName, + int? CategoryId, + string? CategoryName, + InventoryType Type, + InventoryStatus Status, + DateTime? StartDate, + DateTime? EndDate, + DateTime? ConfirmedDate, + string? ConfirmedBy, + int? AdjustmentMovementId, + decimal? PositiveDifferenceValue, + decimal? NegativeDifferenceValue, + string? Notes, + DateTime? CreatedAt, + DateTime? UpdatedAt, + List Lines + ); + + public record InventoryCountLineDto( + int Id, + int ArticleId, + string ArticleCode, + string ArticleDescription, + int WarehouseId, + string WarehouseCode, + int? BatchId, + string? BatchNumber, + string? LocationCode, + decimal TheoreticalQuantity, + decimal? CountedQuantity, + decimal? Difference, + decimal? UnitCost, + decimal? DifferenceValue, + DateTime? CountedAt, + string? CountedBy, + decimal? SecondCountQuantity, + string? SecondCountBy, + string? Notes + ); + + public record CreateInventoryCountDto( + string? Code, + string Description, + DateTime? InventoryDate, + int? WarehouseId, + int? CategoryId, + InventoryType Type, + string? Notes + ); + + public record UpdateInventoryCountDto( + string? Description, + DateTime? InventoryDate, + int? WarehouseId, + int? CategoryId, + InventoryType? Type, + string? Notes + ); + + public record UpdateCountLineDto( + decimal CountedQuantity, + string? CountedBy + ); + + public record UpdateCountLinesBatchDto( + string? CountedBy, + List Lines + ); + + public record CountLineUpdate( + int LineId, + decimal CountedQuantity + ); + + #endregion + + #region Mapping + + private static InventoryCountDto MapToDto(InventoryCount inventory) => new( + inventory.Id, + inventory.Code, + inventory.Description, + inventory.InventoryDate, + inventory.WarehouseId, + inventory.Warehouse?.Code, + inventory.Warehouse?.Name, + inventory.CategoryId, + inventory.Category?.Name, + inventory.Type, + inventory.Status, + inventory.StartDate, + inventory.EndDate, + inventory.ConfirmedDate, + inventory.AdjustmentMovementId, + inventory.PositiveDifferenceValue, + inventory.NegativeDifferenceValue, + inventory.Lines.Count, + inventory.Lines.Count(l => l.CountedQuantity.HasValue), + inventory.Notes, + inventory.CreatedAt + ); + + private static InventoryCountDetailDto MapToDetailDto(InventoryCount inventory) => new( + inventory.Id, + inventory.Code, + inventory.Description, + inventory.InventoryDate, + inventory.WarehouseId, + inventory.Warehouse?.Code, + inventory.Warehouse?.Name, + inventory.CategoryId, + inventory.Category?.Name, + inventory.Type, + inventory.Status, + inventory.StartDate, + inventory.EndDate, + inventory.ConfirmedDate, + inventory.ConfirmedBy, + inventory.AdjustmentMovementId, + inventory.PositiveDifferenceValue, + inventory.NegativeDifferenceValue, + inventory.Notes, + inventory.CreatedAt, + inventory.UpdatedAt, + inventory.Lines.Select(MapLineToDto).ToList() + ); + + private static InventoryCountLineDto MapLineToDto(InventoryCountLine line) => new( + line.Id, + line.ArticleId, + line.Article?.Code ?? "", + line.Article?.Description ?? "", + line.WarehouseId, + line.Warehouse?.Code ?? "", + line.BatchId, + line.Batch?.BatchNumber, + line.LocationCode, + line.TheoreticalQuantity, + line.CountedQuantity, + line.Difference, + line.UnitCost, + line.DifferenceValue, + line.CountedAt, + line.CountedBy, + line.SecondCountQuantity, + line.SecondCountBy, + line.Notes + ); + + #endregion +} diff --git a/src/Apollinare.API/Modules/Warehouse/Controllers/SerialsController.cs b/src/Apollinare.API/Modules/Warehouse/Controllers/SerialsController.cs new file mode 100644 index 0000000..b90f0eb --- /dev/null +++ b/src/Apollinare.API/Modules/Warehouse/Controllers/SerialsController.cs @@ -0,0 +1,366 @@ +using Apollinare.API.Modules.Warehouse.Services; +using Apollinare.Domain.Entities.Warehouse; +using Microsoft.AspNetCore.Mvc; + +namespace Apollinare.API.Modules.Warehouse.Controllers; + +/// +/// Controller per la gestione dei seriali/matricole +/// +[ApiController] +[Route("api/warehouse/serials")] +public class SerialsController : ControllerBase +{ + private readonly IWarehouseService _warehouseService; + private readonly ILogger _logger; + + public SerialsController( + IWarehouseService warehouseService, + ILogger logger) + { + _warehouseService = warehouseService; + _logger = logger; + } + + /// + /// Ottiene la lista dei seriali con filtri opzionali + /// + [HttpGet] + public async Task>> GetSerials( + [FromQuery] int? articleId = null, + [FromQuery] SerialStatus? status = null) + { + var serials = await _warehouseService.GetSerialsAsync(articleId, status); + return Ok(serials.Select(MapToDto)); + } + + /// + /// Ottiene un seriale per ID + /// + [HttpGet("{id}")] + public async Task> GetSerial(int id) + { + var serial = await _warehouseService.GetSerialByIdAsync(id); + if (serial == null) + return NotFound(); + + return Ok(MapToDto(serial)); + } + + /// + /// Ottiene un seriale per articolo e numero seriale + /// + [HttpGet("by-number/{articleId}/{serialNumber}")] + public async Task> GetSerialByNumber(int articleId, string serialNumber) + { + var serial = await _warehouseService.GetSerialByNumberAsync(articleId, serialNumber); + if (serial == null) + return NotFound(); + + return Ok(MapToDto(serial)); + } + + /// + /// Crea un nuovo seriale + /// + [HttpPost] + public async Task> CreateSerial([FromBody] CreateSerialDto dto) + { + try + { + var serial = new ArticleSerial + { + ArticleId = dto.ArticleId, + BatchId = dto.BatchId, + SerialNumber = dto.SerialNumber, + ManufacturerSerial = dto.ManufacturerSerial, + ProductionDate = dto.ProductionDate, + WarrantyExpiryDate = dto.WarrantyExpiryDate, + CurrentWarehouseId = dto.WarehouseId, + UnitCost = dto.UnitCost, + SupplierId = dto.SupplierId, + Attributes = dto.Attributes, + Notes = dto.Notes, + Status = SerialStatus.Available + }; + + var created = await _warehouseService.CreateSerialAsync(serial); + return CreatedAtAction(nameof(GetSerial), new { id = created.Id }, MapToDto(created)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Crea più seriali in batch + /// + [HttpPost("bulk")] + public async Task>> CreateSerialsBulk([FromBody] CreateSerialsBulkDto dto) + { + try + { + var createdSerials = new List(); + + foreach (var serialNumber in dto.SerialNumbers) + { + var serial = new ArticleSerial + { + ArticleId = dto.ArticleId, + BatchId = dto.BatchId, + SerialNumber = serialNumber, + ProductionDate = dto.ProductionDate, + WarrantyExpiryDate = dto.WarrantyExpiryDate, + CurrentWarehouseId = dto.WarehouseId, + UnitCost = dto.UnitCost, + SupplierId = dto.SupplierId, + Status = SerialStatus.Available + }; + + var created = await _warehouseService.CreateSerialAsync(serial); + createdSerials.Add(created); + } + + return Ok(createdSerials.Select(MapToDto)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Aggiorna un seriale esistente + /// + [HttpPut("{id}")] + public async Task> UpdateSerial(int id, [FromBody] UpdateSerialDto dto) + { + try + { + var existing = await _warehouseService.GetSerialByIdAsync(id); + if (existing == null) + return NotFound(); + + if (dto.ManufacturerSerial != null) + existing.ManufacturerSerial = dto.ManufacturerSerial; + if (dto.ProductionDate.HasValue) + existing.ProductionDate = dto.ProductionDate; + if (dto.WarrantyExpiryDate.HasValue) + existing.WarrantyExpiryDate = dto.WarrantyExpiryDate; + if (dto.UnitCost.HasValue) + existing.UnitCost = dto.UnitCost; + if (dto.Attributes != null) + existing.Attributes = dto.Attributes; + if (dto.Notes != null) + existing.Notes = dto.Notes; + + var updated = await _warehouseService.UpdateSerialAsync(existing); + return Ok(MapToDto(updated)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Aggiorna lo stato di un seriale + /// + [HttpPut("{id}/status")] + public async Task UpdateSerialStatus(int id, [FromBody] UpdateSerialStatusDto dto) + { + try + { + await _warehouseService.UpdateSerialStatusAsync(id, dto.Status); + return Ok(); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// Registra la vendita di un seriale + /// + [HttpPost("{id}/sell")] + public async Task> RegisterSale(int id, [FromBody] RegisterSaleDto dto) + { + try + { + var serial = await _warehouseService.GetSerialByIdAsync(id); + if (serial == null) + return NotFound(); + + if (serial.Status != SerialStatus.Available && serial.Status != SerialStatus.Reserved) + return BadRequest(new { error = "Il seriale non è disponibile per la vendita" }); + + serial.Status = SerialStatus.Sold; + serial.CustomerId = dto.CustomerId; + serial.SoldDate = DateTime.UtcNow; + serial.SalesReference = dto.SalesReference; + serial.CurrentWarehouseId = null; + + var updated = await _warehouseService.UpdateSerialAsync(serial); + return Ok(MapToDto(updated)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// Registra un reso di un seriale + /// + [HttpPost("{id}/return")] + public async Task> RegisterReturn(int id, [FromBody] RegisterReturnDto dto) + { + try + { + var serial = await _warehouseService.GetSerialByIdAsync(id); + if (serial == null) + return NotFound(); + + if (serial.Status != SerialStatus.Sold) + return BadRequest(new { error = "Solo i seriali venduti possono essere resi" }); + + serial.Status = dto.IsDefective ? SerialStatus.Defective : SerialStatus.Returned; + serial.CurrentWarehouseId = dto.WarehouseId; + + var updated = await _warehouseService.UpdateSerialAsync(serial); + return Ok(MapToDto(updated)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + #region DTOs + + public record SerialDto( + int Id, + int ArticleId, + string? ArticleCode, + string? ArticleDescription, + int? BatchId, + string? BatchNumber, + string SerialNumber, + string? ManufacturerSerial, + DateTime? ProductionDate, + DateTime? WarrantyExpiryDate, + int? CurrentWarehouseId, + string? CurrentWarehouseCode, + string? CurrentWarehouseName, + SerialStatus Status, + decimal? UnitCost, + int? SupplierId, + int? CustomerId, + DateTime? SoldDate, + string? SalesReference, + string? Attributes, + string? Notes, + bool IsWarrantyValid, + int? DaysToWarrantyExpiry, + DateTime? CreatedAt, + DateTime? UpdatedAt + ); + + public record CreateSerialDto( + int ArticleId, + int? BatchId, + string SerialNumber, + string? ManufacturerSerial, + DateTime? ProductionDate, + DateTime? WarrantyExpiryDate, + int? WarehouseId, + decimal? UnitCost, + int? SupplierId, + string? Attributes, + string? Notes + ); + + public record CreateSerialsBulkDto( + int ArticleId, + int? BatchId, + List SerialNumbers, + DateTime? ProductionDate, + DateTime? WarrantyExpiryDate, + int? WarehouseId, + decimal? UnitCost, + int? SupplierId + ); + + public record UpdateSerialDto( + string? ManufacturerSerial, + DateTime? ProductionDate, + DateTime? WarrantyExpiryDate, + decimal? UnitCost, + string? Attributes, + string? Notes + ); + + public record UpdateSerialStatusDto(SerialStatus Status); + + public record RegisterSaleDto( + int? CustomerId, + string? SalesReference + ); + + public record RegisterReturnDto( + int WarehouseId, + bool IsDefective + ); + + #endregion + + #region Mapping + + private static SerialDto MapToDto(ArticleSerial serial) + { + var isWarrantyValid = serial.WarrantyExpiryDate.HasValue && serial.WarrantyExpiryDate.Value > DateTime.UtcNow; + var daysToWarrantyExpiry = serial.WarrantyExpiryDate.HasValue + ? (int?)Math.Max(0, (serial.WarrantyExpiryDate.Value - DateTime.UtcNow).Days) + : null; + + return new SerialDto( + serial.Id, + serial.ArticleId, + serial.Article?.Code, + serial.Article?.Description, + serial.BatchId, + serial.Batch?.BatchNumber, + serial.SerialNumber, + serial.ManufacturerSerial, + serial.ProductionDate, + serial.WarrantyExpiryDate, + serial.CurrentWarehouseId, + serial.CurrentWarehouse?.Code, + serial.CurrentWarehouse?.Name, + serial.Status, + serial.UnitCost, + serial.SupplierId, + serial.CustomerId, + serial.SoldDate, + serial.SalesReference, + serial.Attributes, + serial.Notes, + isWarrantyValid, + daysToWarrantyExpiry, + serial.CreatedAt, + serial.UpdatedAt + ); + } + + #endregion +} diff --git a/src/Apollinare.API/Modules/Warehouse/Controllers/StockLevelsController.cs b/src/Apollinare.API/Modules/Warehouse/Controllers/StockLevelsController.cs new file mode 100644 index 0000000..de51c68 --- /dev/null +++ b/src/Apollinare.API/Modules/Warehouse/Controllers/StockLevelsController.cs @@ -0,0 +1,360 @@ +using Apollinare.API.Modules.Warehouse.Services; +using Apollinare.Domain.Entities.Warehouse; +using Microsoft.AspNetCore.Mvc; + +namespace Apollinare.API.Modules.Warehouse.Controllers; + +/// +/// Controller per la gestione delle giacenze e valorizzazione +/// +[ApiController] +[Route("api/warehouse/stock")] +public class StockLevelsController : ControllerBase +{ + private readonly IWarehouseService _warehouseService; + private readonly ILogger _logger; + + public StockLevelsController( + IWarehouseService warehouseService, + ILogger logger) + { + _warehouseService = warehouseService; + _logger = logger; + } + + /// + /// Ottiene le giacenze con filtri opzionali + /// + [HttpGet] + public async Task>> GetStockLevels([FromQuery] StockLevelFilterDto? filter) + { + var stockFilter = filter != null ? new StockLevelFilter + { + ArticleId = filter.ArticleId, + WarehouseId = filter.WarehouseId, + BatchId = filter.BatchId, + CategoryId = filter.CategoryId, + OnlyWithStock = filter.OnlyWithStock, + OnlyLowStock = filter.OnlyLowStock, + Skip = filter.Skip, + Take = filter.Take + } : null; + + var stockLevels = await _warehouseService.GetStockLevelsAsync(stockFilter); + return Ok(stockLevels.Select(MapToDto)); + } + + /// + /// Ottiene la giacenza per articolo/magazzino/batch + /// + [HttpGet("{articleId}/{warehouseId}")] + public async Task> GetStockLevel(int articleId, int warehouseId, [FromQuery] int? batchId = null) + { + var stockLevel = await _warehouseService.GetStockLevelAsync(articleId, warehouseId, batchId); + if (stockLevel == null) + return NotFound(); + + return Ok(MapToDto(stockLevel)); + } + + /// + /// Ottiene gli articoli sotto scorta + /// + [HttpGet("low-stock")] + public async Task>> GetLowStockArticles() + { + var lowStock = await _warehouseService.GetLowStockArticlesAsync(); + return Ok(lowStock.Select(MapToDto)); + } + + /// + /// Ottiene il riepilogo giacenze per articolo + /// + [HttpGet("summary/{articleId}")] + public async Task> GetStockSummary(int articleId) + { + var article = await _warehouseService.GetArticleByIdAsync(articleId); + if (article == null) + return NotFound(); + + var totalStock = await _warehouseService.GetTotalStockAsync(articleId); + var availableStock = await _warehouseService.GetAvailableStockAsync(articleId); + var stockLevels = await _warehouseService.GetStockLevelsAsync(new StockLevelFilter { ArticleId = articleId }); + + return Ok(new StockSummaryDto( + articleId, + article.Code, + article.Description, + article.UnitOfMeasure, + totalStock, + availableStock, + article.MinimumStock, + article.MaximumStock, + article.ReorderPoint, + article.MinimumStock.HasValue && totalStock <= article.MinimumStock.Value, + stockLevels.Sum(s => s.StockValue ?? 0), + stockLevels.Count, + stockLevels.Select(MapToDto).ToList() + )); + } + + /// + /// Calcola la valorizzazione di un articolo + /// + [HttpGet("valuation/{articleId}")] + public async Task> GetArticleValuation(int articleId, [FromQuery] ValuationMethod? method = null) + { + try + { + var article = await _warehouseService.GetArticleByIdAsync(articleId); + if (article == null) + return NotFound(); + + var effectiveMethod = method ?? article.ValuationMethod ?? ValuationMethod.WeightedAverage; + var totalValue = await _warehouseService.CalculateArticleValueAsync(articleId, effectiveMethod); + var totalStock = await _warehouseService.GetTotalStockAsync(articleId); + var avgCost = await _warehouseService.GetWeightedAverageCostAsync(articleId); + + return Ok(new ArticleValuationDto( + articleId, + article.Code, + article.Description, + effectiveMethod, + totalStock, + article.UnitOfMeasure, + avgCost, + article.StandardCost, + article.LastPurchaseCost, + totalValue + )); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// Calcola la valorizzazione di periodo + /// + [HttpGet("valuation/period/{period}")] + public async Task>> GetPeriodValuation(int period, [FromQuery] int? warehouseId = null) + { + var valuations = await _warehouseService.GetValuationsAsync(period, warehouseId); + return Ok(valuations.Select(v => new PeriodValuationDto( + v.Id, + v.Period, + v.ValuationDate, + v.ArticleId, + v.Article?.Code ?? "", + v.Article?.Description ?? "", + v.WarehouseId, + v.Warehouse?.Code, + v.Warehouse?.Name, + v.Quantity, + v.Method, + v.UnitCost, + v.TotalValue, + v.InboundQuantity, + v.InboundValue, + v.OutboundQuantity, + v.OutboundValue, + v.IsClosed + ))); + } + + /// + /// Genera la valorizzazione per un articolo e periodo + /// + [HttpPost("valuation/calculate")] + public async Task> CalculatePeriodValuation([FromBody] CalculateValuationDto dto) + { + try + { + var valuation = await _warehouseService.CalculatePeriodValuationAsync(dto.ArticleId, dto.Period, dto.WarehouseId); + return Ok(new PeriodValuationDto( + valuation.Id, + valuation.Period, + valuation.ValuationDate, + valuation.ArticleId, + valuation.Article?.Code ?? "", + valuation.Article?.Description ?? "", + valuation.WarehouseId, + valuation.Warehouse?.Code, + valuation.Warehouse?.Name, + valuation.Quantity, + valuation.Method, + valuation.UnitCost, + valuation.TotalValue, + valuation.InboundQuantity, + valuation.InboundValue, + valuation.OutboundQuantity, + valuation.OutboundValue, + valuation.IsClosed + )); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// Chiude un periodo (blocca modifiche) + /// + [HttpPost("valuation/close-period/{period}")] + public async Task ClosePeriod(int period) + { + await _warehouseService.ClosePeriodAsync(period); + return Ok(new { message = $"Periodo {period} chiuso correttamente" }); + } + + /// + /// Ricalcola il costo medio ponderato di un articolo + /// + [HttpPost("recalculate-average/{articleId}")] + public async Task RecalculateAverageCost(int articleId) + { + try + { + await _warehouseService.UpdateWeightedAverageCostAsync(articleId); + var avgCost = await _warehouseService.GetWeightedAverageCostAsync(articleId); + return Ok(new { articleId, weightedAverageCost = avgCost }); + } + catch (Exception ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + #region DTOs + + public record StockLevelFilterDto( + int? ArticleId, + int? WarehouseId, + int? BatchId, + int? CategoryId, + bool? OnlyWithStock, + bool? OnlyLowStock, + int Skip = 0, + int Take = 100 + ); + + public record StockLevelDto( + int Id, + int ArticleId, + string ArticleCode, + string ArticleDescription, + string? CategoryName, + int WarehouseId, + string WarehouseCode, + string WarehouseName, + int? BatchId, + string? BatchNumber, + DateTime? BatchExpiryDate, + decimal Quantity, + decimal ReservedQuantity, + decimal AvailableQuantity, + decimal OnOrderQuantity, + decimal? UnitCost, + decimal? StockValue, + string? LocationCode, + DateTime? LastMovementDate, + DateTime? LastInventoryDate, + decimal? MinimumStock, + bool IsLowStock + ); + + public record StockSummaryDto( + int ArticleId, + string ArticleCode, + string ArticleDescription, + string UnitOfMeasure, + decimal TotalStock, + decimal AvailableStock, + decimal? MinimumStock, + decimal? MaximumStock, + decimal? ReorderPoint, + bool IsLowStock, + decimal TotalValue, + int WarehouseCount, + List StockByWarehouse + ); + + public record ArticleValuationDto( + int ArticleId, + string ArticleCode, + string ArticleDescription, + ValuationMethod Method, + decimal TotalQuantity, + string UnitOfMeasure, + decimal WeightedAverageCost, + decimal? StandardCost, + decimal? LastPurchaseCost, + decimal TotalValue + ); + + public record PeriodValuationDto( + int Id, + int Period, + DateTime ValuationDate, + int ArticleId, + string ArticleCode, + string ArticleDescription, + int? WarehouseId, + string? WarehouseCode, + string? WarehouseName, + decimal Quantity, + ValuationMethod Method, + decimal UnitCost, + decimal TotalValue, + decimal InboundQuantity, + decimal InboundValue, + decimal OutboundQuantity, + decimal OutboundValue, + bool IsClosed + ); + + public record CalculateValuationDto( + int ArticleId, + int Period, + int? WarehouseId + ); + + #endregion + + #region Mapping + + private static StockLevelDto MapToDto(StockLevel stock) + { + var isLowStock = stock.Article?.MinimumStock.HasValue == true && + stock.Quantity <= stock.Article.MinimumStock.Value; + + return new StockLevelDto( + stock.Id, + stock.ArticleId, + stock.Article?.Code ?? "", + stock.Article?.Description ?? "", + stock.Article?.Category?.Name, + stock.WarehouseId, + stock.Warehouse?.Code ?? "", + stock.Warehouse?.Name ?? "", + stock.BatchId, + stock.Batch?.BatchNumber, + stock.Batch?.ExpiryDate, + stock.Quantity, + stock.ReservedQuantity, + stock.AvailableQuantity, + stock.OnOrderQuantity, + stock.UnitCost, + stock.StockValue, + stock.LocationCode, + stock.LastMovementDate, + stock.LastInventoryDate, + stock.Article?.MinimumStock, + isLowStock + ); + } + + #endregion +} diff --git a/src/Apollinare.API/Modules/Warehouse/Controllers/StockMovementsController.cs b/src/Apollinare.API/Modules/Warehouse/Controllers/StockMovementsController.cs new file mode 100644 index 0000000..5252fe6 --- /dev/null +++ b/src/Apollinare.API/Modules/Warehouse/Controllers/StockMovementsController.cs @@ -0,0 +1,564 @@ +using Apollinare.API.Modules.Warehouse.Services; +using Apollinare.Domain.Entities.Warehouse; +using Microsoft.AspNetCore.Mvc; + +namespace Apollinare.API.Modules.Warehouse.Controllers; + +/// +/// Controller per la gestione dei movimenti di magazzino +/// +[ApiController] +[Route("api/warehouse/movements")] +public class StockMovementsController : ControllerBase +{ + private readonly IWarehouseService _warehouseService; + private readonly ILogger _logger; + + public StockMovementsController( + IWarehouseService warehouseService, + ILogger logger) + { + _warehouseService = warehouseService; + _logger = logger; + } + + /// + /// Ottiene la lista dei movimenti con filtri opzionali + /// + [HttpGet] + public async Task>> GetMovements([FromQuery] MovementFilterDto? filter) + { + var movementFilter = filter != null ? new MovementFilter + { + DateFrom = filter.DateFrom, + DateTo = filter.DateTo, + Type = filter.Type, + Status = filter.Status, + WarehouseId = filter.WarehouseId, + ArticleId = filter.ArticleId, + ReasonId = filter.ReasonId, + ExternalReference = filter.ExternalReference, + Skip = filter.Skip, + Take = filter.Take, + OrderBy = filter.OrderBy, + OrderDescending = filter.OrderDescending + } : null; + + var movements = await _warehouseService.GetMovementsAsync(movementFilter); + return Ok(movements.Select(MapToDto)); + } + + /// + /// Ottiene un movimento per ID + /// + [HttpGet("{id}")] + public async Task> GetMovement(int id) + { + var movement = await _warehouseService.GetMovementByIdAsync(id); + if (movement == null) + return NotFound(); + + return Ok(MapToDetailDto(movement)); + } + + /// + /// Ottiene un movimento per numero documento + /// + [HttpGet("by-document/{documentNumber}")] + public async Task> GetMovementByDocumentNumber(string documentNumber) + { + var movement = await _warehouseService.GetMovementByDocumentNumberAsync(documentNumber); + if (movement == null) + return NotFound(); + + return Ok(MapToDetailDto(movement)); + } + + /// + /// Crea un nuovo movimento (carico) + /// + [HttpPost("inbound")] + public async Task> CreateInboundMovement([FromBody] CreateMovementDto dto) + { + return await CreateMovement(dto, MovementType.Inbound); + } + + /// + /// Crea un nuovo movimento (scarico) + /// + [HttpPost("outbound")] + public async Task> CreateOutboundMovement([FromBody] CreateMovementDto dto) + { + return await CreateMovement(dto, MovementType.Outbound); + } + + /// + /// Crea un nuovo movimento (trasferimento) + /// + [HttpPost("transfer")] + public async Task> CreateTransferMovement([FromBody] CreateTransferDto dto) + { + try + { + var movement = new StockMovement + { + DocumentNumber = dto.DocumentNumber ?? "", + MovementDate = dto.MovementDate ?? DateTime.UtcNow, + Type = MovementType.Transfer, + ReasonId = dto.ReasonId, + SourceWarehouseId = dto.SourceWarehouseId, + DestinationWarehouseId = dto.DestinationWarehouseId, + ExternalReference = dto.ExternalReference, + Notes = dto.Notes, + Status = MovementStatus.Draft, + Lines = dto.Lines.Select((l, i) => new StockMovementLine + { + LineNumber = i + 1, + ArticleId = l.ArticleId, + BatchId = l.BatchId, + SerialId = l.SerialId, + Quantity = l.Quantity, + UnitOfMeasure = l.UnitOfMeasure ?? "PZ", + Notes = l.Notes + }).ToList() + }; + + var created = await _warehouseService.CreateMovementAsync(movement); + return CreatedAtAction(nameof(GetMovement), new { id = created.Id }, MapToDetailDto(created)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Crea un nuovo movimento (rettifica) + /// + [HttpPost("adjustment")] + public async Task> CreateAdjustmentMovement([FromBody] CreateAdjustmentDto dto) + { + try + { + var movement = new StockMovement + { + DocumentNumber = dto.DocumentNumber ?? "", + MovementDate = dto.MovementDate ?? DateTime.UtcNow, + Type = MovementType.Adjustment, + ReasonId = dto.ReasonId, + DestinationWarehouseId = dto.WarehouseId, // Per rettifiche positive + SourceWarehouseId = dto.WarehouseId, // Per rettifiche negative + ExternalReference = dto.ExternalReference, + Notes = dto.Notes, + Status = MovementStatus.Draft, + Lines = dto.Lines.Select((l, i) => new StockMovementLine + { + LineNumber = i + 1, + ArticleId = l.ArticleId, + BatchId = l.BatchId, + SerialId = l.SerialId, + Quantity = l.Quantity, // Positiva o negativa + UnitOfMeasure = l.UnitOfMeasure ?? "PZ", + UnitCost = l.UnitCost, + LineValue = l.Quantity * (l.UnitCost ?? 0), + Notes = l.Notes + }).ToList() + }; + + var created = await _warehouseService.CreateMovementAsync(movement); + return CreatedAtAction(nameof(GetMovement), new { id = created.Id }, MapToDetailDto(created)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + private async Task> CreateMovement(CreateMovementDto dto, MovementType type) + { + try + { + var movement = new StockMovement + { + DocumentNumber = dto.DocumentNumber ?? "", + MovementDate = dto.MovementDate ?? DateTime.UtcNow, + Type = type, + ReasonId = dto.ReasonId, + SourceWarehouseId = type == MovementType.Outbound ? dto.WarehouseId : null, + DestinationWarehouseId = type == MovementType.Inbound ? dto.WarehouseId : null, + ExternalReference = dto.ExternalReference, + ExternalDocumentType = dto.ExternalDocumentType, + SupplierId = dto.SupplierId, + CustomerId = dto.CustomerId, + Notes = dto.Notes, + Status = MovementStatus.Draft, + Lines = dto.Lines.Select((l, i) => new StockMovementLine + { + LineNumber = i + 1, + ArticleId = l.ArticleId, + BatchId = l.BatchId, + SerialId = l.SerialId, + Quantity = l.Quantity, + UnitOfMeasure = l.UnitOfMeasure ?? "PZ", + UnitCost = l.UnitCost, + LineValue = l.Quantity * (l.UnitCost ?? 0), + SourceLocationCode = l.SourceLocationCode, + DestinationLocationCode = l.DestinationLocationCode, + ExternalLineReference = l.ExternalLineReference, + Notes = l.Notes + }).ToList() + }; + + var created = await _warehouseService.CreateMovementAsync(movement); + return CreatedAtAction(nameof(GetMovement), new { id = created.Id }, MapToDetailDto(created)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Aggiorna un movimento esistente (solo bozze) + /// + [HttpPut("{id}")] + public async Task> UpdateMovement(int id, [FromBody] UpdateMovementDto dto) + { + try + { + var existing = await _warehouseService.GetMovementByIdAsync(id); + if (existing == null) + return NotFound(); + + existing.MovementDate = dto.MovementDate ?? existing.MovementDate; + existing.ReasonId = dto.ReasonId ?? existing.ReasonId; + existing.ExternalReference = dto.ExternalReference ?? existing.ExternalReference; + existing.Notes = dto.Notes ?? existing.Notes; + + if (dto.Lines != null) + { + existing.Lines = dto.Lines.Select((l, i) => new StockMovementLine + { + MovementId = id, + LineNumber = i + 1, + ArticleId = l.ArticleId, + BatchId = l.BatchId, + SerialId = l.SerialId, + Quantity = l.Quantity, + UnitOfMeasure = l.UnitOfMeasure ?? "PZ", + UnitCost = l.UnitCost, + LineValue = l.Quantity * (l.UnitCost ?? 0), + SourceLocationCode = l.SourceLocationCode, + DestinationLocationCode = l.DestinationLocationCode, + ExternalLineReference = l.ExternalLineReference, + Notes = l.Notes + }).ToList(); + } + + var updated = await _warehouseService.UpdateMovementAsync(existing); + return Ok(MapToDetailDto(updated)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Conferma un movimento (applica alle giacenze) + /// + [HttpPost("{id}/confirm")] + public async Task> ConfirmMovement(int id) + { + try + { + var movement = await _warehouseService.ConfirmMovementAsync(id); + return Ok(MapToDetailDto(movement)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Annulla un movimento + /// + [HttpPost("{id}/cancel")] + public async Task> CancelMovement(int id) + { + try + { + var movement = await _warehouseService.CancelMovementAsync(id); + return Ok(MapToDetailDto(movement)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Genera un nuovo numero documento + /// + [HttpGet("generate-number/{type}")] + public async Task> GenerateDocumentNumber(MovementType type) + { + var number = await _warehouseService.GenerateDocumentNumberAsync(type); + return Ok(new { documentNumber = number }); + } + + /// + /// Ottiene le causali movimento + /// + [HttpGet("reasons")] + public async Task>> GetMovementReasons([FromQuery] MovementType? type = null, [FromQuery] bool includeInactive = false) + { + var reasons = await _warehouseService.GetMovementReasonsAsync(type, includeInactive); + return Ok(reasons.Select(r => new MovementReasonDto( + r.Id, + r.Code, + r.Description, + r.MovementType, + r.StockSign, + r.RequiresExternalReference, + r.RequiresValuation, + r.UpdatesAverageCost, + r.IsSystem, + r.IsActive + ))); + } + + #region DTOs + + public record MovementFilterDto( + DateTime? DateFrom, + DateTime? DateTo, + MovementType? Type, + MovementStatus? Status, + int? WarehouseId, + int? ArticleId, + int? ReasonId, + string? ExternalReference, + int Skip = 0, + int Take = 100, + string? OrderBy = null, + bool OrderDescending = true + ); + + public record MovementDto( + int Id, + string DocumentNumber, + DateTime MovementDate, + MovementType Type, + MovementStatus Status, + int? SourceWarehouseId, + string? SourceWarehouseCode, + string? SourceWarehouseName, + int? DestinationWarehouseId, + string? DestinationWarehouseCode, + string? DestinationWarehouseName, + int? ReasonId, + string? ReasonDescription, + string? ExternalReference, + decimal? TotalValue, + int LineCount, + DateTime? ConfirmedDate, + string? Notes, + DateTime? CreatedAt + ); + + public record MovementDetailDto( + int Id, + string DocumentNumber, + DateTime MovementDate, + MovementType Type, + MovementStatus Status, + int? SourceWarehouseId, + string? SourceWarehouseCode, + string? SourceWarehouseName, + int? DestinationWarehouseId, + string? DestinationWarehouseCode, + string? DestinationWarehouseName, + int? ReasonId, + string? ReasonDescription, + string? ExternalReference, + ExternalDocumentType? ExternalDocumentType, + int? SupplierId, + int? CustomerId, + decimal? TotalValue, + DateTime? ConfirmedDate, + string? ConfirmedBy, + string? Notes, + DateTime? CreatedAt, + DateTime? UpdatedAt, + List Lines + ); + + public record MovementLineDto( + int Id, + int LineNumber, + int ArticleId, + string ArticleCode, + string ArticleDescription, + int? BatchId, + string? BatchNumber, + int? SerialId, + string? SerialNumber, + decimal Quantity, + string UnitOfMeasure, + decimal? UnitCost, + decimal? LineValue, + string? SourceLocationCode, + string? DestinationLocationCode, + string? Notes + ); + + public record CreateMovementDto( + string? DocumentNumber, + DateTime? MovementDate, + int? ReasonId, + int WarehouseId, + string? ExternalReference, + ExternalDocumentType? ExternalDocumentType, + int? SupplierId, + int? CustomerId, + string? Notes, + List Lines + ); + + public record CreateTransferDto( + string? DocumentNumber, + DateTime? MovementDate, + int? ReasonId, + int SourceWarehouseId, + int DestinationWarehouseId, + string? ExternalReference, + string? Notes, + List Lines + ); + + public record CreateAdjustmentDto( + string? DocumentNumber, + DateTime? MovementDate, + int? ReasonId, + int WarehouseId, + string? ExternalReference, + string? Notes, + List Lines + ); + + public record CreateMovementLineDto( + int ArticleId, + int? BatchId, + int? SerialId, + decimal Quantity, + string? UnitOfMeasure, + decimal? UnitCost, + string? SourceLocationCode, + string? DestinationLocationCode, + string? ExternalLineReference, + string? Notes + ); + + public record UpdateMovementDto( + DateTime? MovementDate, + int? ReasonId, + string? ExternalReference, + string? Notes, + List? Lines + ); + + public record MovementReasonDto( + int Id, + string Code, + string Description, + MovementType MovementType, + int StockSign, + bool RequiresExternalReference, + bool RequiresValuation, + bool UpdatesAverageCost, + bool IsSystem, + bool IsActive + ); + + #endregion + + #region Mapping + + private static MovementDto MapToDto(StockMovement movement) => new( + movement.Id, + movement.DocumentNumber, + movement.MovementDate, + movement.Type, + movement.Status, + movement.SourceWarehouseId, + movement.SourceWarehouse?.Code, + movement.SourceWarehouse?.Name, + movement.DestinationWarehouseId, + movement.DestinationWarehouse?.Code, + movement.DestinationWarehouse?.Name, + movement.ReasonId, + movement.Reason?.Description, + movement.ExternalReference, + movement.TotalValue, + movement.Lines.Count, + movement.ConfirmedDate, + movement.Notes, + movement.CreatedAt + ); + + private static MovementDetailDto MapToDetailDto(StockMovement movement) => new( + movement.Id, + movement.DocumentNumber, + movement.MovementDate, + movement.Type, + movement.Status, + movement.SourceWarehouseId, + movement.SourceWarehouse?.Code, + movement.SourceWarehouse?.Name, + movement.DestinationWarehouseId, + movement.DestinationWarehouse?.Code, + movement.DestinationWarehouse?.Name, + movement.ReasonId, + movement.Reason?.Description, + movement.ExternalReference, + movement.ExternalDocumentType, + movement.SupplierId, + movement.CustomerId, + movement.TotalValue, + movement.ConfirmedDate, + movement.ConfirmedBy, + movement.Notes, + movement.CreatedAt, + movement.UpdatedAt, + movement.Lines.Select(l => new MovementLineDto( + l.Id, + l.LineNumber, + l.ArticleId, + l.Article?.Code ?? "", + l.Article?.Description ?? "", + l.BatchId, + l.Batch?.BatchNumber, + l.SerialId, + l.Serial?.SerialNumber, + l.Quantity, + l.UnitOfMeasure, + l.UnitCost, + l.LineValue, + l.SourceLocationCode, + l.DestinationLocationCode, + l.Notes + )).ToList() + ); + + #endregion +} diff --git a/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseArticlesController.cs b/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseArticlesController.cs new file mode 100644 index 0000000..7193a08 --- /dev/null +++ b/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseArticlesController.cs @@ -0,0 +1,460 @@ +using Apollinare.API.Modules.Warehouse.Services; +using Apollinare.Domain.Entities.Warehouse; +using Microsoft.AspNetCore.Mvc; + +namespace Apollinare.API.Modules.Warehouse.Controllers; + +/// +/// Controller per la gestione degli articoli di magazzino +/// +[ApiController] +[Route("api/warehouse/articles")] +public class WarehouseArticlesController : ControllerBase +{ + private readonly IWarehouseService _warehouseService; + private readonly ILogger _logger; + + public WarehouseArticlesController( + IWarehouseService warehouseService, + ILogger logger) + { + _warehouseService = warehouseService; + _logger = logger; + } + + /// + /// Ottiene la lista degli articoli con filtri opzionali + /// + [HttpGet] + public async Task>> GetArticles([FromQuery] ArticleFilterDto? filter) + { + var articleFilter = filter != null ? new ArticleFilter + { + SearchText = filter.Search, + CategoryId = filter.CategoryId, + IsActive = filter.IsActive, + IsBatchManaged = filter.IsBatchManaged, + IsSerialManaged = filter.IsSerialManaged, + Skip = filter.Skip, + Take = filter.Take, + OrderBy = filter.OrderBy, + OrderDescending = filter.OrderDescending + } : null; + + var articles = await _warehouseService.GetArticlesAsync(articleFilter); + return Ok(articles.Select(MapToDto)); + } + + /// + /// Ottiene un articolo per ID + /// + [HttpGet("{id}")] + public async Task> GetArticle(int id) + { + var article = await _warehouseService.GetArticleByIdAsync(id); + if (article == null) + return NotFound(); + + return Ok(MapToDto(article)); + } + + /// + /// Ottiene un articolo per codice + /// + [HttpGet("by-code/{code}")] + public async Task> GetArticleByCode(string code) + { + var article = await _warehouseService.GetArticleByCodeAsync(code); + if (article == null) + return NotFound(); + + return Ok(MapToDto(article)); + } + + /// + /// Ottiene un articolo per barcode + /// + [HttpGet("by-barcode/{barcode}")] + public async Task> GetArticleByBarcode(string barcode) + { + var article = await _warehouseService.GetArticleByBarcodeAsync(barcode); + if (article == null) + return NotFound(); + + return Ok(MapToDto(article)); + } + + /// + /// Crea un nuovo articolo + /// + [HttpPost] + public async Task> CreateArticle([FromBody] CreateArticleDto dto) + { + try + { + var article = MapFromDto(dto); + var created = await _warehouseService.CreateArticleAsync(article); + return CreatedAtAction(nameof(GetArticle), new { id = created.Id }, MapToDto(created)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Aggiorna un articolo esistente + /// + [HttpPut("{id}")] + public async Task> UpdateArticle(int id, [FromBody] UpdateArticleDto dto) + { + try + { + var existing = await _warehouseService.GetArticleByIdAsync(id); + if (existing == null) + return NotFound(); + + UpdateFromDto(existing, dto); + var updated = await _warehouseService.UpdateArticleAsync(existing); + return Ok(MapToDto(updated)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Elimina un articolo + /// + [HttpDelete("{id}")] + public async Task DeleteArticle(int id) + { + try + { + await _warehouseService.DeleteArticleAsync(id); + return NoContent(); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Carica l'immagine di un articolo + /// + [HttpPost("{id}/image")] + public async Task UploadImage(int id, IFormFile file) + { + var article = await _warehouseService.GetArticleByIdAsync(id); + if (article == null) + return NotFound(); + + if (file.Length > 5 * 1024 * 1024) // 5MB max + return BadRequest(new { error = "Il file è troppo grande (max 5MB)" }); + + using var memoryStream = new MemoryStream(); + await file.CopyToAsync(memoryStream); + + article.Image = memoryStream.ToArray(); + article.ImageMimeType = file.ContentType; + + await _warehouseService.UpdateArticleAsync(article); + + return Ok(); + } + + /// + /// Ottiene l'immagine di un articolo + /// + [HttpGet("{id}/image")] + public async Task GetImage(int id) + { + var article = await _warehouseService.GetArticleByIdAsync(id); + if (article == null || article.Image == null) + return NotFound(); + + return File(article.Image, article.ImageMimeType ?? "image/jpeg"); + } + + /// + /// Ottiene la giacenza totale di un articolo + /// + [HttpGet("{id}/stock")] + public async Task> GetArticleStock(int id) + { + var article = await _warehouseService.GetArticleByIdAsync(id); + if (article == null) + return NotFound(); + + var totalStock = await _warehouseService.GetTotalStockAsync(id); + var availableStock = await _warehouseService.GetAvailableStockAsync(id); + var stockLevels = await _warehouseService.GetStockLevelsAsync(new StockLevelFilter { ArticleId = id }); + + return Ok(new ArticleStockDto( + ArticleId: id, + ArticleCode: article.Code, + ArticleDescription: article.Description, + TotalStock: totalStock, + AvailableStock: availableStock, + UnitOfMeasure: article.UnitOfMeasure, + MinimumStock: article.MinimumStock, + MaximumStock: article.MaximumStock, + ReorderPoint: article.ReorderPoint, + IsLowStock: article.MinimumStock.HasValue && totalStock <= article.MinimumStock.Value, + StockByWarehouse: stockLevels.Select(s => new WarehouseStockDto( + WarehouseId: s.WarehouseId, + WarehouseCode: s.Warehouse?.Code ?? "", + WarehouseName: s.Warehouse?.Name ?? "", + Quantity: s.Quantity, + ReservedQuantity: s.ReservedQuantity, + AvailableQuantity: s.AvailableQuantity, + UnitCost: s.UnitCost, + StockValue: s.StockValue, + BatchId: s.BatchId, + BatchNumber: s.Batch?.BatchNumber + )).ToList() + )); + } + + #region DTOs + + public record ArticleFilterDto( + string? Search, + int? CategoryId, + bool? IsActive, + bool? IsBatchManaged, + bool? IsSerialManaged, + int Skip = 0, + int Take = 100, + string? OrderBy = null, + bool OrderDescending = false + ); + + public record ArticleDto( + int Id, + string Code, + string Description, + string? ShortDescription, + string? Barcode, + string? ManufacturerCode, + int? CategoryId, + string? CategoryName, + 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? LastPurchaseCost, + decimal? WeightedAverageCost, + decimal? BaseSellingPrice, + decimal? Weight, + decimal? Volume, + bool IsActive, + string? Notes, + bool HasImage, + DateTime? CreatedAt, + DateTime? UpdatedAt + ); + + 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 + ); + + public record UpdateArticleDto( + 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, + bool IsActive, + string? Notes + ); + + public record ArticleStockDto( + int ArticleId, + string ArticleCode, + string ArticleDescription, + decimal TotalStock, + decimal AvailableStock, + string UnitOfMeasure, + decimal? MinimumStock, + decimal? MaximumStock, + decimal? ReorderPoint, + bool IsLowStock, + List StockByWarehouse + ); + + public record WarehouseStockDto( + int WarehouseId, + string WarehouseCode, + string WarehouseName, + decimal Quantity, + decimal ReservedQuantity, + decimal AvailableQuantity, + decimal? UnitCost, + decimal? StockValue, + int? BatchId, + string? BatchNumber + ); + + #endregion + + #region Mapping + + private static ArticleDto MapToDto(WarehouseArticle article) => new( + article.Id, + article.Code, + article.Description, + article.ShortDescription, + article.Barcode, + article.ManufacturerCode, + article.CategoryId, + article.Category?.Name, + article.UnitOfMeasure, + article.SecondaryUnitOfMeasure, + article.UnitConversionFactor, + article.StockManagement, + article.IsBatchManaged, + article.IsSerialManaged, + article.HasExpiry, + article.ExpiryWarningDays, + article.MinimumStock, + article.MaximumStock, + article.ReorderPoint, + article.ReorderQuantity, + article.LeadTimeDays, + article.ValuationMethod, + article.StandardCost, + article.LastPurchaseCost, + article.WeightedAverageCost, + article.BaseSellingPrice, + article.Weight, + article.Volume, + article.IsActive, + article.Notes, + article.Image != null, + article.CreatedAt, + article.UpdatedAt + ); + + private static WarehouseArticle MapFromDto(CreateArticleDto dto) => new() + { + Code = dto.Code, + Description = dto.Description, + ShortDescription = dto.ShortDescription, + Barcode = dto.Barcode, + ManufacturerCode = dto.ManufacturerCode, + CategoryId = dto.CategoryId, + UnitOfMeasure = dto.UnitOfMeasure, + SecondaryUnitOfMeasure = dto.SecondaryUnitOfMeasure, + UnitConversionFactor = dto.UnitConversionFactor, + StockManagement = dto.StockManagement, + IsBatchManaged = dto.IsBatchManaged, + IsSerialManaged = dto.IsSerialManaged, + HasExpiry = dto.HasExpiry, + ExpiryWarningDays = dto.ExpiryWarningDays, + MinimumStock = dto.MinimumStock, + MaximumStock = dto.MaximumStock, + ReorderPoint = dto.ReorderPoint, + ReorderQuantity = dto.ReorderQuantity, + LeadTimeDays = dto.LeadTimeDays, + ValuationMethod = dto.ValuationMethod, + StandardCost = dto.StandardCost, + BaseSellingPrice = dto.BaseSellingPrice, + Weight = dto.Weight, + Volume = dto.Volume, + Notes = dto.Notes, + IsActive = true + }; + + private static void UpdateFromDto(WarehouseArticle article, UpdateArticleDto dto) + { + article.Code = dto.Code; + article.Description = dto.Description; + article.ShortDescription = dto.ShortDescription; + article.Barcode = dto.Barcode; + article.ManufacturerCode = dto.ManufacturerCode; + article.CategoryId = dto.CategoryId; + article.UnitOfMeasure = dto.UnitOfMeasure; + article.SecondaryUnitOfMeasure = dto.SecondaryUnitOfMeasure; + article.UnitConversionFactor = dto.UnitConversionFactor; + article.StockManagement = dto.StockManagement; + article.IsBatchManaged = dto.IsBatchManaged; + article.IsSerialManaged = dto.IsSerialManaged; + article.HasExpiry = dto.HasExpiry; + article.ExpiryWarningDays = dto.ExpiryWarningDays; + article.MinimumStock = dto.MinimumStock; + article.MaximumStock = dto.MaximumStock; + article.ReorderPoint = dto.ReorderPoint; + article.ReorderQuantity = dto.ReorderQuantity; + article.LeadTimeDays = dto.LeadTimeDays; + article.ValuationMethod = dto.ValuationMethod; + article.StandardCost = dto.StandardCost; + article.BaseSellingPrice = dto.BaseSellingPrice; + article.Weight = dto.Weight; + article.Volume = dto.Volume; + article.IsActive = dto.IsActive; + article.Notes = dto.Notes; + } + + #endregion +} diff --git a/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseCategoriesController.cs b/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseCategoriesController.cs new file mode 100644 index 0000000..ae67400 --- /dev/null +++ b/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseCategoriesController.cs @@ -0,0 +1,240 @@ +using Apollinare.API.Modules.Warehouse.Services; +using Apollinare.Domain.Entities.Warehouse; +using Microsoft.AspNetCore.Mvc; + +namespace Apollinare.API.Modules.Warehouse.Controllers; + +/// +/// Controller per la gestione delle categorie articoli +/// +[ApiController] +[Route("api/warehouse/categories")] +public class WarehouseCategoriesController : ControllerBase +{ + private readonly IWarehouseService _warehouseService; + private readonly ILogger _logger; + + public WarehouseCategoriesController( + IWarehouseService warehouseService, + ILogger logger) + { + _warehouseService = warehouseService; + _logger = logger; + } + + /// + /// Ottiene la lista delle categorie + /// + [HttpGet] + public async Task>> GetCategories([FromQuery] bool includeInactive = false) + { + var categories = await _warehouseService.GetCategoriesAsync(includeInactive); + return Ok(categories.Select(MapToDto)); + } + + /// + /// Ottiene le categorie in formato albero + /// + [HttpGet("tree")] + public async Task>> GetCategoryTree() + { + var categories = await _warehouseService.GetCategoryTreeAsync(); + return Ok(categories.Select(MapToTreeDto)); + } + + /// + /// Ottiene una categoria per ID + /// + [HttpGet("{id}")] + public async Task> GetCategory(int id) + { + var category = await _warehouseService.GetCategoryByIdAsync(id); + if (category == null) + return NotFound(); + + return Ok(MapToDto(category)); + } + + /// + /// Crea una nuova categoria + /// + [HttpPost] + public async Task> CreateCategory([FromBody] CreateCategoryDto dto) + { + try + { + var category = new WarehouseArticleCategory + { + Code = dto.Code, + Name = dto.Name, + Description = dto.Description, + ParentCategoryId = dto.ParentCategoryId, + Icon = dto.Icon, + Color = dto.Color, + DefaultValuationMethod = dto.DefaultValuationMethod, + SortOrder = dto.SortOrder, + Notes = dto.Notes, + IsActive = true + }; + + var created = await _warehouseService.CreateCategoryAsync(category); + return CreatedAtAction(nameof(GetCategory), new { id = created.Id }, MapToDto(created)); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Aggiorna una categoria esistente + /// + [HttpPut("{id}")] + public async Task> UpdateCategory(int id, [FromBody] UpdateCategoryDto dto) + { + try + { + var existing = await _warehouseService.GetCategoryByIdAsync(id); + if (existing == null) + return NotFound(); + + existing.Code = dto.Code; + existing.Name = dto.Name; + existing.Description = dto.Description; + existing.Icon = dto.Icon; + existing.Color = dto.Color; + existing.DefaultValuationMethod = dto.DefaultValuationMethod; + existing.SortOrder = dto.SortOrder; + existing.IsActive = dto.IsActive; + existing.Notes = dto.Notes; + + var updated = await _warehouseService.UpdateCategoryAsync(existing); + return Ok(MapToDto(updated)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Elimina una categoria + /// + [HttpDelete("{id}")] + public async Task DeleteCategory(int id) + { + try + { + await _warehouseService.DeleteCategoryAsync(id); + return NoContent(); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + #region DTOs + + public record CategoryDto( + int Id, + string Code, + string Name, + string? Description, + int? ParentCategoryId, + string? ParentCategoryName, + int Level, + string? FullPath, + string? Icon, + string? Color, + ValuationMethod? DefaultValuationMethod, + int SortOrder, + bool IsActive, + string? Notes, + DateTime? CreatedAt, + DateTime? UpdatedAt + ); + + public record CategoryTreeDto( + int Id, + string Code, + string Name, + string? Description, + int Level, + string? FullPath, + string? Icon, + string? Color, + bool IsActive, + List Children + ); + + public record CreateCategoryDto( + string Code, + string Name, + string? Description, + int? ParentCategoryId, + string? Icon, + string? Color, + ValuationMethod? DefaultValuationMethod, + int SortOrder, + string? Notes + ); + + public record UpdateCategoryDto( + string Code, + string Name, + string? Description, + string? Icon, + string? Color, + ValuationMethod? DefaultValuationMethod, + int SortOrder, + bool IsActive, + string? Notes + ); + + #endregion + + #region Mapping + + private static CategoryDto MapToDto(WarehouseArticleCategory category) => new( + category.Id, + category.Code, + category.Name, + category.Description, + category.ParentCategoryId, + category.ParentCategory?.Name, + category.Level, + category.FullPath, + category.Icon, + category.Color, + category.DefaultValuationMethod, + category.SortOrder, + category.IsActive, + category.Notes, + category.CreatedAt, + category.UpdatedAt + ); + + private static CategoryTreeDto MapToTreeDto(WarehouseArticleCategory category) => new( + category.Id, + category.Code, + category.Name, + category.Description, + category.Level, + category.FullPath, + category.Icon, + category.Color, + category.IsActive, + category.ChildCategories.Select(MapToTreeDto).ToList() + ); + + #endregion +} diff --git a/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseLocationsController.cs b/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseLocationsController.cs new file mode 100644 index 0000000..e4c55d4 --- /dev/null +++ b/src/Apollinare.API/Modules/Warehouse/Controllers/WarehouseLocationsController.cs @@ -0,0 +1,249 @@ +using Apollinare.API.Modules.Warehouse.Services; +using Apollinare.Domain.Entities.Warehouse; +using Microsoft.AspNetCore.Mvc; + +namespace Apollinare.API.Modules.Warehouse.Controllers; + +/// +/// Controller per la gestione dei magazzini +/// +[ApiController] +[Route("api/warehouse/locations")] +public class WarehouseLocationsController : ControllerBase +{ + private readonly IWarehouseService _warehouseService; + private readonly ILogger _logger; + + public WarehouseLocationsController( + IWarehouseService warehouseService, + ILogger logger) + { + _warehouseService = warehouseService; + _logger = logger; + } + + /// + /// Ottiene la lista dei magazzini + /// + [HttpGet] + public async Task>> GetWarehouses([FromQuery] bool includeInactive = false) + { + var warehouses = await _warehouseService.GetWarehousesAsync(includeInactive); + return Ok(warehouses.Select(MapToDto)); + } + + /// + /// Ottiene un magazzino per ID + /// + [HttpGet("{id}")] + public async Task> GetWarehouse(int id) + { + var warehouse = await _warehouseService.GetWarehouseByIdAsync(id); + if (warehouse == null) + return NotFound(); + + return Ok(MapToDto(warehouse)); + } + + /// + /// Ottiene il magazzino predefinito + /// + [HttpGet("default")] + public async Task> GetDefaultWarehouse() + { + var warehouse = await _warehouseService.GetDefaultWarehouseAsync(); + if (warehouse == null) + return NotFound(new { error = "Nessun magazzino predefinito configurato" }); + + return Ok(MapToDto(warehouse)); + } + + /// + /// Crea un nuovo magazzino + /// + [HttpPost] + public async Task> CreateWarehouse([FromBody] CreateWarehouseDto dto) + { + try + { + var warehouse = MapFromDto(dto); + var created = await _warehouseService.CreateWarehouseAsync(warehouse); + return CreatedAtAction(nameof(GetWarehouse), new { id = created.Id }, MapToDto(created)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Aggiorna un magazzino esistente + /// + [HttpPut("{id}")] + public async Task> UpdateWarehouse(int id, [FromBody] UpdateWarehouseDto dto) + { + try + { + var existing = await _warehouseService.GetWarehouseByIdAsync(id); + if (existing == null) + return NotFound(); + + UpdateFromDto(existing, dto); + var updated = await _warehouseService.UpdateWarehouseAsync(existing); + return Ok(MapToDto(updated)); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Elimina un magazzino + /// + [HttpDelete("{id}")] + public async Task DeleteWarehouse(int id) + { + try + { + await _warehouseService.DeleteWarehouseAsync(id); + return NoContent(); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Imposta un magazzino come predefinito + /// + [HttpPut("{id}/set-default")] + public async Task SetDefaultWarehouse(int id) + { + try + { + await _warehouseService.SetDefaultWarehouseAsync(id); + return Ok(); + } + catch (ArgumentException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + #region DTOs + + public record WarehouseLocationDto( + int Id, + string Code, + string Name, + string? Description, + string? Address, + string? City, + string? Province, + string? PostalCode, + string? Country, + WarehouseType Type, + bool IsDefault, + bool IsActive, + int SortOrder, + string? Notes, + DateTime? CreatedAt, + DateTime? UpdatedAt + ); + + public record CreateWarehouseDto( + string Code, + string Name, + string? Description, + string? Address, + string? City, + string? Province, + string? PostalCode, + string? Country, + WarehouseType Type, + bool IsDefault, + int SortOrder, + string? Notes + ); + + public record UpdateWarehouseDto( + string Code, + string Name, + string? Description, + string? Address, + string? City, + string? Province, + string? PostalCode, + string? Country, + WarehouseType Type, + bool IsDefault, + bool IsActive, + int SortOrder, + string? Notes + ); + + #endregion + + #region Mapping + + private static WarehouseLocationDto MapToDto(WarehouseLocation warehouse) => new( + warehouse.Id, + warehouse.Code, + warehouse.Name, + warehouse.Description, + warehouse.Address, + warehouse.City, + warehouse.Province, + warehouse.PostalCode, + warehouse.Country, + warehouse.Type, + warehouse.IsDefault, + warehouse.IsActive, + warehouse.SortOrder, + warehouse.Notes, + warehouse.CreatedAt, + warehouse.UpdatedAt + ); + + private static WarehouseLocation MapFromDto(CreateWarehouseDto dto) => new() + { + Code = dto.Code, + Name = dto.Name, + Description = dto.Description, + Address = dto.Address, + City = dto.City, + Province = dto.Province, + PostalCode = dto.PostalCode, + Country = dto.Country ?? "Italia", + Type = dto.Type, + IsDefault = dto.IsDefault, + SortOrder = dto.SortOrder, + Notes = dto.Notes, + IsActive = true + }; + + private static void UpdateFromDto(WarehouseLocation warehouse, UpdateWarehouseDto dto) + { + warehouse.Code = dto.Code; + warehouse.Name = dto.Name; + warehouse.Description = dto.Description; + warehouse.Address = dto.Address; + warehouse.City = dto.City; + warehouse.Province = dto.Province; + warehouse.PostalCode = dto.PostalCode; + warehouse.Country = dto.Country; + warehouse.Type = dto.Type; + warehouse.IsDefault = dto.IsDefault; + warehouse.IsActive = dto.IsActive; + warehouse.SortOrder = dto.SortOrder; + warehouse.Notes = dto.Notes; + } + + #endregion +} diff --git a/src/Apollinare.API/Modules/Warehouse/Services/IWarehouseService.cs b/src/Apollinare.API/Modules/Warehouse/Services/IWarehouseService.cs new file mode 100644 index 0000000..b23a632 --- /dev/null +++ b/src/Apollinare.API/Modules/Warehouse/Services/IWarehouseService.cs @@ -0,0 +1,172 @@ +using Apollinare.Domain.Entities.Warehouse; + +namespace Apollinare.API.Modules.Warehouse.Services; + +/// +/// Interfaccia servizio principale per il modulo Magazzino +/// +public interface IWarehouseService +{ + // =============================================== + // ARTICOLI + // =============================================== + Task> GetArticlesAsync(ArticleFilter? filter = null); + Task GetArticleByIdAsync(int id); + Task GetArticleByCodeAsync(string code); + Task GetArticleByBarcodeAsync(string barcode); + Task CreateArticleAsync(WarehouseArticle article); + Task UpdateArticleAsync(WarehouseArticle article); + Task DeleteArticleAsync(int id); + + // =============================================== + // CATEGORIE + // =============================================== + Task> GetCategoriesAsync(bool includeInactive = false); + Task> GetCategoryTreeAsync(); + Task GetCategoryByIdAsync(int id); + Task CreateCategoryAsync(WarehouseArticleCategory category); + Task UpdateCategoryAsync(WarehouseArticleCategory category); + Task DeleteCategoryAsync(int id); + + // =============================================== + // MAGAZZINI + // =============================================== + Task> GetWarehousesAsync(bool includeInactive = false); + Task GetWarehouseByIdAsync(int id); + Task GetDefaultWarehouseAsync(); + Task CreateWarehouseAsync(WarehouseLocation warehouse); + Task UpdateWarehouseAsync(WarehouseLocation warehouse); + Task DeleteWarehouseAsync(int id); + Task SetDefaultWarehouseAsync(int id); + + // =============================================== + // PARTITE (BATCH) + // =============================================== + Task> GetBatchesAsync(int? articleId = null, BatchStatus? status = null); + Task GetBatchByIdAsync(int id); + Task GetBatchByNumberAsync(int articleId, string batchNumber); + Task CreateBatchAsync(ArticleBatch batch); + Task UpdateBatchAsync(ArticleBatch batch); + Task> GetExpiringBatchesAsync(int daysThreshold = 30); + Task UpdateBatchStatusAsync(int id, BatchStatus status); + + // =============================================== + // SERIALI + // =============================================== + Task> GetSerialsAsync(int? articleId = null, SerialStatus? status = null); + Task GetSerialByIdAsync(int id); + Task GetSerialByNumberAsync(int articleId, string serialNumber); + Task CreateSerialAsync(ArticleSerial serial); + Task UpdateSerialAsync(ArticleSerial serial); + Task UpdateSerialStatusAsync(int id, SerialStatus status); + + // =============================================== + // GIACENZE + // =============================================== + Task> GetStockLevelsAsync(StockLevelFilter? filter = null); + Task GetStockLevelAsync(int articleId, int warehouseId, int? batchId = null); + Task GetTotalStockAsync(int articleId); + Task GetAvailableStockAsync(int articleId, int? warehouseId = null); + Task> GetLowStockArticlesAsync(); + Task UpdateStockLevelAsync(int articleId, int warehouseId, decimal quantity, int? batchId = null, decimal? unitCost = null); + + // =============================================== + // MOVIMENTI + // =============================================== + Task> GetMovementsAsync(MovementFilter? filter = null); + Task GetMovementByIdAsync(int id); + Task GetMovementByDocumentNumberAsync(string documentNumber); + Task CreateMovementAsync(StockMovement movement); + Task UpdateMovementAsync(StockMovement movement); + Task ConfirmMovementAsync(int id); + Task CancelMovementAsync(int id); + Task GenerateDocumentNumberAsync(MovementType type); + + // =============================================== + // CAUSALI + // =============================================== + Task> GetMovementReasonsAsync(MovementType? type = null, bool includeInactive = false); + Task GetMovementReasonByIdAsync(int id); + Task CreateMovementReasonAsync(MovementReason reason); + Task UpdateMovementReasonAsync(MovementReason reason); + + // =============================================== + // VALORIZZAZIONE + // =============================================== + Task CalculateArticleValueAsync(int articleId, ValuationMethod? method = null); + Task CalculatePeriodValuationAsync(int articleId, int period, int? warehouseId = null); + Task> GetValuationsAsync(int period, int? warehouseId = null); + Task ClosePeriodAsync(int period); + Task GetWeightedAverageCostAsync(int articleId); + Task UpdateWeightedAverageCostAsync(int articleId); + + // =============================================== + // INVENTARIO + // =============================================== + Task> GetInventoryCountsAsync(InventoryStatus? status = null); + Task GetInventoryCountByIdAsync(int id); + Task CreateInventoryCountAsync(InventoryCount inventory); + Task UpdateInventoryCountAsync(InventoryCount inventory); + Task StartInventoryCountAsync(int id); + Task CompleteInventoryCountAsync(int id); + Task ConfirmInventoryCountAsync(int id); + Task CancelInventoryCountAsync(int id); + Task UpdateCountLineAsync(int lineId, decimal countedQuantity, string? countedBy = null); + + // =============================================== + // SEED DATA + // =============================================== + Task SeedDefaultDataAsync(); +} + +/// +/// Filtro per ricerca articoli +/// +public class ArticleFilter +{ + public string? SearchText { get; set; } + public int? CategoryId { get; set; } + public bool? IsActive { get; set; } + public bool? IsBatchManaged { get; set; } + public bool? IsSerialManaged { get; set; } + public StockManagementType? StockManagement { get; set; } + public bool? HasLowStock { get; set; } + public int Skip { get; set; } = 0; + public int Take { get; set; } = 100; + public string? OrderBy { get; set; } + public bool OrderDescending { get; set; } +} + +/// +/// Filtro per ricerca giacenze +/// +public class StockLevelFilter +{ + public int? ArticleId { get; set; } + public int? WarehouseId { get; set; } + public int? BatchId { get; set; } + public int? CategoryId { get; set; } + public bool? OnlyWithStock { get; set; } + public bool? OnlyLowStock { get; set; } + public int Skip { get; set; } = 0; + public int Take { get; set; } = 100; +} + +/// +/// Filtro per ricerca movimenti +/// +public class MovementFilter +{ + public DateTime? DateFrom { get; set; } + public DateTime? DateTo { get; set; } + public MovementType? Type { get; set; } + public MovementStatus? Status { get; set; } + public int? WarehouseId { get; set; } + public int? ArticleId { get; set; } + public int? ReasonId { get; set; } + public string? ExternalReference { get; set; } + public int Skip { get; set; } = 0; + public int Take { get; set; } = 100; + public string? OrderBy { get; set; } + public bool OrderDescending { get; set; } = true; +} diff --git a/src/Apollinare.API/Modules/Warehouse/Services/WarehouseService.cs b/src/Apollinare.API/Modules/Warehouse/Services/WarehouseService.cs new file mode 100644 index 0000000..be59d14 --- /dev/null +++ b/src/Apollinare.API/Modules/Warehouse/Services/WarehouseService.cs @@ -0,0 +1,1897 @@ +using Apollinare.Domain.Entities.Warehouse; +using Apollinare.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; + +namespace Apollinare.API.Modules.Warehouse.Services; + +/// +/// Implementazione del servizio principale per il modulo Magazzino +/// +public class WarehouseService : IWarehouseService +{ + private readonly AppollinareDbContext _context; + private readonly IMemoryCache _cache; + private readonly ILogger _logger; + + private const string WAREHOUSES_CACHE_KEY = "warehouse_locations"; + private const string CATEGORIES_CACHE_KEY = "warehouse_categories"; + private const string REASONS_CACHE_KEY = "movement_reasons"; + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); + + public WarehouseService( + AppollinareDbContext context, + IMemoryCache cache, + ILogger logger) + { + _context = context; + _cache = cache; + _logger = logger; + } + + #region Articoli + + public async Task> GetArticlesAsync(ArticleFilter? filter = null) + { + var query = _context.WarehouseArticles + .Include(a => a.Category) + .AsQueryable(); + + if (filter != null) + { + if (!string.IsNullOrWhiteSpace(filter.SearchText)) + { + var search = filter.SearchText.ToLower(); + query = query.Where(a => + a.Code.ToLower().Contains(search) || + a.Description.ToLower().Contains(search) || + (a.Barcode != null && a.Barcode.Contains(search))); + } + + if (filter.CategoryId.HasValue) + query = query.Where(a => a.CategoryId == filter.CategoryId); + + if (filter.IsActive.HasValue) + query = query.Where(a => a.IsActive == filter.IsActive); + + if (filter.IsBatchManaged.HasValue) + query = query.Where(a => a.IsBatchManaged == filter.IsBatchManaged); + + if (filter.IsSerialManaged.HasValue) + query = query.Where(a => a.IsSerialManaged == filter.IsSerialManaged); + + if (filter.StockManagement.HasValue) + query = query.Where(a => a.StockManagement == filter.StockManagement); + + // Ordinamento + query = filter.OrderBy?.ToLower() switch + { + "code" => filter.OrderDescending ? query.OrderByDescending(a => a.Code) : query.OrderBy(a => a.Code), + "description" => filter.OrderDescending ? query.OrderByDescending(a => a.Description) : query.OrderBy(a => a.Description), + "category" => filter.OrderDescending ? query.OrderByDescending(a => a.Category!.Name) : query.OrderBy(a => a.Category!.Name), + _ => query.OrderBy(a => a.Code) + }; + + query = query.Skip(filter.Skip).Take(filter.Take); + } + else + { + query = query.OrderBy(a => a.Code).Take(100); + } + + return await query.ToListAsync(); + } + + public async Task GetArticleByIdAsync(int id) + { + return await _context.WarehouseArticles + .Include(a => a.Category) + .Include(a => a.Barcodes) + .FirstOrDefaultAsync(a => a.Id == id); + } + + public async Task GetArticleByCodeAsync(string code) + { + return await _context.WarehouseArticles + .Include(a => a.Category) + .FirstOrDefaultAsync(a => a.Code == code); + } + + public async Task GetArticleByBarcodeAsync(string barcode) + { + // Cerca nel barcode principale + var article = await _context.WarehouseArticles + .Include(a => a.Category) + .FirstOrDefaultAsync(a => a.Barcode == barcode); + + if (article != null) + return article; + + // Cerca nei barcode aggiuntivi + var articleBarcode = await _context.ArticleBarcodes + .Include(b => b.Article) + .ThenInclude(a => a!.Category) + .FirstOrDefaultAsync(b => b.Barcode == barcode && b.IsActive); + + return articleBarcode?.Article; + } + + public async Task CreateArticleAsync(WarehouseArticle article) + { + // Verifica unicità codice + if (await _context.WarehouseArticles.AnyAsync(a => a.Code == article.Code)) + throw new InvalidOperationException($"Esiste già un articolo con codice '{article.Code}'"); + + article.CreatedAt = DateTime.UtcNow; + _context.WarehouseArticles.Add(article); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Creato articolo {Code} - {Description}", article.Code, article.Description); + return article; + } + + public async Task UpdateArticleAsync(WarehouseArticle article) + { + var existing = await _context.WarehouseArticles.FindAsync(article.Id); + if (existing == null) + throw new ArgumentException($"Articolo con ID {article.Id} non trovato"); + + // Verifica unicità codice se cambiato + if (existing.Code != article.Code && + await _context.WarehouseArticles.AnyAsync(a => a.Code == article.Code && a.Id != article.Id)) + throw new InvalidOperationException($"Esiste già un articolo con codice '{article.Code}'"); + + _context.Entry(existing).CurrentValues.SetValues(article); + existing.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return existing; + } + + public async Task DeleteArticleAsync(int id) + { + var article = await _context.WarehouseArticles.FindAsync(id); + if (article == null) + throw new ArgumentException($"Articolo con ID {id} non trovato"); + + // Verifica che non ci siano giacenze + if (await _context.StockLevels.AnyAsync(s => s.ArticleId == id && s.Quantity > 0)) + throw new InvalidOperationException("Impossibile eliminare un articolo con giacenze attive"); + + // Verifica che non ci siano movimenti + if (await _context.StockMovementLines.AnyAsync(l => l.ArticleId == id)) + throw new InvalidOperationException("Impossibile eliminare un articolo con movimenti storici"); + + _context.WarehouseArticles.Remove(article); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Eliminato articolo {Code}", article.Code); + } + + #endregion + + #region Categorie + + public async Task> GetCategoriesAsync(bool includeInactive = false) + { + var cacheKey = $"{CATEGORIES_CACHE_KEY}_{includeInactive}"; + + return await _cache.GetOrCreateAsync(cacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; + + var query = _context.WarehouseArticleCategories.AsQueryable(); + + if (!includeInactive) + query = query.Where(c => c.IsActive); + + return await query + .OrderBy(c => c.SortOrder) + .ThenBy(c => c.Name) + .ToListAsync(); + }) ?? new List(); + } + + public async Task> GetCategoryTreeAsync() + { + var categories = await _context.WarehouseArticleCategories + .Where(c => c.IsActive) + .OrderBy(c => c.SortOrder) + .ThenBy(c => c.Name) + .ToListAsync(); + + // Costruisci l'albero + var rootCategories = categories.Where(c => c.ParentCategoryId == null).ToList(); + foreach (var root in rootCategories) + { + BuildCategoryTree(root, categories); + } + + return rootCategories; + } + + private void BuildCategoryTree(WarehouseArticleCategory parent, List allCategories) + { + var children = allCategories.Where(c => c.ParentCategoryId == parent.Id).ToList(); + parent.ChildCategories = children; + + foreach (var child in children) + { + BuildCategoryTree(child, allCategories); + } + } + + public async Task GetCategoryByIdAsync(int id) + { + return await _context.WarehouseArticleCategories + .Include(c => c.ParentCategory) + .FirstOrDefaultAsync(c => c.Id == id); + } + + public async Task CreateCategoryAsync(WarehouseArticleCategory category) + { + if (await _context.WarehouseArticleCategories.AnyAsync(c => c.Code == category.Code)) + throw new InvalidOperationException($"Esiste già una categoria con codice '{category.Code}'"); + + // Calcola livello e path + if (category.ParentCategoryId.HasValue) + { + var parent = await _context.WarehouseArticleCategories.FindAsync(category.ParentCategoryId); + if (parent == null) + throw new ArgumentException("Categoria padre non trovata"); + + category.Level = parent.Level + 1; + category.FullPath = string.IsNullOrEmpty(parent.FullPath) + ? $"{parent.Code}.{category.Code}" + : $"{parent.FullPath}.{category.Code}"; + } + else + { + category.Level = 0; + category.FullPath = category.Code; + } + + category.CreatedAt = DateTime.UtcNow; + _context.WarehouseArticleCategories.Add(category); + await _context.SaveChangesAsync(); + + InvalidateCategoriesCache(); + return category; + } + + public async Task UpdateCategoryAsync(WarehouseArticleCategory category) + { + var existing = await _context.WarehouseArticleCategories.FindAsync(category.Id); + if (existing == null) + throw new ArgumentException($"Categoria con ID {category.Id} non trovata"); + + _context.Entry(existing).CurrentValues.SetValues(category); + existing.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + InvalidateCategoriesCache(); + return existing; + } + + public async Task DeleteCategoryAsync(int id) + { + var category = await _context.WarehouseArticleCategories.FindAsync(id); + if (category == null) + throw new ArgumentException($"Categoria con ID {id} non trovata"); + + // Verifica che non abbia figli + if (await _context.WarehouseArticleCategories.AnyAsync(c => c.ParentCategoryId == id)) + throw new InvalidOperationException("Impossibile eliminare una categoria con sottocategorie"); + + // Verifica che non abbia articoli + if (await _context.WarehouseArticles.AnyAsync(a => a.CategoryId == id)) + throw new InvalidOperationException("Impossibile eliminare una categoria con articoli associati"); + + _context.WarehouseArticleCategories.Remove(category); + await _context.SaveChangesAsync(); + + InvalidateCategoriesCache(); + } + + private void InvalidateCategoriesCache() + { + _cache.Remove($"{CATEGORIES_CACHE_KEY}_true"); + _cache.Remove($"{CATEGORIES_CACHE_KEY}_false"); + } + + #endregion + + #region Magazzini + + public async Task> GetWarehousesAsync(bool includeInactive = false) + { + var cacheKey = $"{WAREHOUSES_CACHE_KEY}_{includeInactive}"; + + return await _cache.GetOrCreateAsync(cacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; + + var query = _context.WarehouseLocations.AsQueryable(); + + if (!includeInactive) + query = query.Where(w => w.IsActive); + + return await query + .OrderBy(w => w.SortOrder) + .ThenBy(w => w.Name) + .ToListAsync(); + }) ?? new List(); + } + + public async Task GetWarehouseByIdAsync(int id) + { + return await _context.WarehouseLocations.FindAsync(id); + } + + public async Task GetDefaultWarehouseAsync() + { + return await _context.WarehouseLocations + .FirstOrDefaultAsync(w => w.IsDefault && w.IsActive); + } + + public async Task CreateWarehouseAsync(WarehouseLocation warehouse) + { + if (await _context.WarehouseLocations.AnyAsync(w => w.Code == warehouse.Code)) + throw new InvalidOperationException($"Esiste già un magazzino con codice '{warehouse.Code}'"); + + // Se è impostato come default, rimuovi il flag dagli altri + if (warehouse.IsDefault) + { + await _context.WarehouseLocations + .Where(w => w.IsDefault) + .ExecuteUpdateAsync(s => s.SetProperty(w => w.IsDefault, false)); + } + + warehouse.CreatedAt = DateTime.UtcNow; + _context.WarehouseLocations.Add(warehouse); + await _context.SaveChangesAsync(); + + InvalidateWarehousesCache(); + _logger.LogInformation("Creato magazzino {Code} - {Name}", warehouse.Code, warehouse.Name); + return warehouse; + } + + public async Task UpdateWarehouseAsync(WarehouseLocation warehouse) + { + var existing = await _context.WarehouseLocations.FindAsync(warehouse.Id); + if (existing == null) + throw new ArgumentException($"Magazzino con ID {warehouse.Id} non trovato"); + + // Se è impostato come default, rimuovi il flag dagli altri + if (warehouse.IsDefault && !existing.IsDefault) + { + await _context.WarehouseLocations + .Where(w => w.IsDefault && w.Id != warehouse.Id) + .ExecuteUpdateAsync(s => s.SetProperty(w => w.IsDefault, false)); + } + + _context.Entry(existing).CurrentValues.SetValues(warehouse); + existing.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + InvalidateWarehousesCache(); + return existing; + } + + public async Task DeleteWarehouseAsync(int id) + { + var warehouse = await _context.WarehouseLocations.FindAsync(id); + if (warehouse == null) + throw new ArgumentException($"Magazzino con ID {id} non trovato"); + + // Verifica che non ci siano giacenze + if (await _context.StockLevels.AnyAsync(s => s.WarehouseId == id && s.Quantity > 0)) + throw new InvalidOperationException("Impossibile eliminare un magazzino con giacenze attive"); + + _context.WarehouseLocations.Remove(warehouse); + await _context.SaveChangesAsync(); + + InvalidateWarehousesCache(); + } + + public async Task SetDefaultWarehouseAsync(int id) + { + var warehouse = await _context.WarehouseLocations.FindAsync(id); + if (warehouse == null) + throw new ArgumentException($"Magazzino con ID {id} non trovato"); + + // Rimuovi default da tutti + await _context.WarehouseLocations + .Where(w => w.IsDefault) + .ExecuteUpdateAsync(s => s.SetProperty(w => w.IsDefault, false)); + + // Imposta il nuovo default + warehouse.IsDefault = true; + warehouse.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + InvalidateWarehousesCache(); + } + + private void InvalidateWarehousesCache() + { + _cache.Remove($"{WAREHOUSES_CACHE_KEY}_true"); + _cache.Remove($"{WAREHOUSES_CACHE_KEY}_false"); + } + + #endregion + + #region Partite (Batch) + + public async Task> GetBatchesAsync(int? articleId = null, BatchStatus? status = null) + { + var query = _context.ArticleBatches + .Include(b => b.Article) + .AsQueryable(); + + if (articleId.HasValue) + query = query.Where(b => b.ArticleId == articleId); + + if (status.HasValue) + query = query.Where(b => b.Status == status); + + return await query + .OrderByDescending(b => b.CreatedAt) + .ToListAsync(); + } + + public async Task GetBatchByIdAsync(int id) + { + return await _context.ArticleBatches + .Include(b => b.Article) + .FirstOrDefaultAsync(b => b.Id == id); + } + + public async Task GetBatchByNumberAsync(int articleId, string batchNumber) + { + return await _context.ArticleBatches + .Include(b => b.Article) + .FirstOrDefaultAsync(b => b.ArticleId == articleId && b.BatchNumber == batchNumber); + } + + public async Task CreateBatchAsync(ArticleBatch batch) + { + // Verifica che l'articolo esista e sia gestito a lotti + var article = await _context.WarehouseArticles.FindAsync(batch.ArticleId); + if (article == null) + throw new ArgumentException("Articolo non trovato"); + + if (!article.IsBatchManaged) + throw new InvalidOperationException("L'articolo non è gestito a lotti"); + + // 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"); + + batch.CreatedAt = DateTime.UtcNow; + _context.ArticleBatches.Add(batch); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Creato lotto {BatchNumber} per articolo {ArticleId}", batch.BatchNumber, batch.ArticleId); + return batch; + } + + public async Task UpdateBatchAsync(ArticleBatch batch) + { + var existing = await _context.ArticleBatches.FindAsync(batch.Id); + if (existing == null) + throw new ArgumentException($"Lotto con ID {batch.Id} non trovato"); + + _context.Entry(existing).CurrentValues.SetValues(batch); + existing.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return existing; + } + + public async Task> GetExpiringBatchesAsync(int daysThreshold = 30) + { + var thresholdDate = DateTime.UtcNow.AddDays(daysThreshold); + + return await _context.ArticleBatches + .Include(b => b.Article) + .Where(b => b.ExpiryDate.HasValue && + b.ExpiryDate.Value <= thresholdDate && + b.Status == BatchStatus.Available && + b.CurrentQuantity > 0) + .OrderBy(b => b.ExpiryDate) + .ToListAsync(); + } + + public async Task UpdateBatchStatusAsync(int id, BatchStatus status) + { + var batch = await _context.ArticleBatches.FindAsync(id); + if (batch == null) + throw new ArgumentException($"Lotto con ID {id} non trovato"); + + batch.Status = status; + batch.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + _logger.LogInformation("Aggiornato stato lotto {Id} a {Status}", id, status); + } + + #endregion + + #region Seriali + + public async Task> GetSerialsAsync(int? articleId = null, SerialStatus? status = null) + { + var query = _context.ArticleSerials + .Include(s => s.Article) + .Include(s => s.Batch) + .Include(s => s.CurrentWarehouse) + .AsQueryable(); + + if (articleId.HasValue) + query = query.Where(s => s.ArticleId == articleId); + + if (status.HasValue) + query = query.Where(s => s.Status == status); + + return await query + .OrderByDescending(s => s.CreatedAt) + .ToListAsync(); + } + + public async Task GetSerialByIdAsync(int id) + { + return await _context.ArticleSerials + .Include(s => s.Article) + .Include(s => s.Batch) + .Include(s => s.CurrentWarehouse) + .FirstOrDefaultAsync(s => s.Id == id); + } + + public async Task GetSerialByNumberAsync(int articleId, string serialNumber) + { + return await _context.ArticleSerials + .Include(s => s.Article) + .FirstOrDefaultAsync(s => s.ArticleId == articleId && s.SerialNumber == serialNumber); + } + + public async Task CreateSerialAsync(ArticleSerial serial) + { + // Verifica che l'articolo esista e sia gestito a seriali + var article = await _context.WarehouseArticles.FindAsync(serial.ArticleId); + if (article == null) + throw new ArgumentException("Articolo non trovato"); + + if (!article.IsSerialManaged) + throw new InvalidOperationException("L'articolo non è gestito a seriali"); + + // Verifica unicità serial number per articolo + if (await _context.ArticleSerials.AnyAsync(s => s.ArticleId == serial.ArticleId && s.SerialNumber == serial.SerialNumber)) + throw new InvalidOperationException($"Esiste già un seriale '{serial.SerialNumber}' per questo articolo"); + + serial.CreatedAt = DateTime.UtcNow; + _context.ArticleSerials.Add(serial); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Creato seriale {SerialNumber} per articolo {ArticleId}", serial.SerialNumber, serial.ArticleId); + return serial; + } + + public async Task UpdateSerialAsync(ArticleSerial serial) + { + var existing = await _context.ArticleSerials.FindAsync(serial.Id); + if (existing == null) + throw new ArgumentException($"Seriale con ID {serial.Id} non trovato"); + + _context.Entry(existing).CurrentValues.SetValues(serial); + existing.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return existing; + } + + public async Task UpdateSerialStatusAsync(int id, SerialStatus status) + { + var serial = await _context.ArticleSerials.FindAsync(id); + if (serial == null) + throw new ArgumentException($"Seriale con ID {id} non trovato"); + + serial.Status = status; + serial.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + _logger.LogInformation("Aggiornato stato seriale {Id} a {Status}", id, status); + } + + #endregion + + #region Giacenze + + public async Task> GetStockLevelsAsync(StockLevelFilter? filter = null) + { + var query = _context.StockLevels + .Include(s => s.Article) + .ThenInclude(a => a!.Category) + .Include(s => s.Warehouse) + .Include(s => s.Batch) + .AsQueryable(); + + if (filter != null) + { + if (filter.ArticleId.HasValue) + query = query.Where(s => s.ArticleId == filter.ArticleId); + + if (filter.WarehouseId.HasValue) + query = query.Where(s => s.WarehouseId == filter.WarehouseId); + + if (filter.BatchId.HasValue) + query = query.Where(s => s.BatchId == filter.BatchId); + + if (filter.CategoryId.HasValue) + query = query.Where(s => s.Article!.CategoryId == filter.CategoryId); + + if (filter.OnlyWithStock == true) + query = query.Where(s => s.Quantity > 0); + + if (filter.OnlyLowStock == true) + query = query.Where(s => + s.Article!.MinimumStock.HasValue && + s.Quantity <= s.Article.MinimumStock.Value); + + query = query.Skip(filter.Skip).Take(filter.Take); + } + + return await query + .OrderBy(s => s.Article!.Code) + .ThenBy(s => s.Warehouse!.Code) + .ToListAsync(); + } + + public async Task GetStockLevelAsync(int articleId, int warehouseId, int? batchId = null) + { + return await _context.StockLevels + .Include(s => s.Article) + .Include(s => s.Warehouse) + .Include(s => s.Batch) + .FirstOrDefaultAsync(s => + s.ArticleId == articleId && + s.WarehouseId == warehouseId && + s.BatchId == batchId); + } + + public async Task GetTotalStockAsync(int articleId) + { + return await _context.StockLevels + .Where(s => s.ArticleId == articleId) + .SumAsync(s => s.Quantity); + } + + public async Task GetAvailableStockAsync(int articleId, int? warehouseId = null) + { + var query = _context.StockLevels.Where(s => s.ArticleId == articleId); + + if (warehouseId.HasValue) + query = query.Where(s => s.WarehouseId == warehouseId); + + return await query.SumAsync(s => s.Quantity - s.ReservedQuantity); + } + + public async Task> GetLowStockArticlesAsync() + { + return await _context.StockLevels + .Include(s => s.Article) + .ThenInclude(a => a!.Category) + .Include(s => s.Warehouse) + .Where(s => + s.Article!.MinimumStock.HasValue && + s.Quantity <= s.Article.MinimumStock.Value && + s.Article.IsActive) + .OrderBy(s => s.Quantity) + .ToListAsync(); + } + + public async Task UpdateStockLevelAsync(int articleId, int warehouseId, decimal quantity, int? batchId = null, decimal? unitCost = null) + { + var stockLevel = await _context.StockLevels + .FirstOrDefaultAsync(s => + s.ArticleId == articleId && + s.WarehouseId == warehouseId && + s.BatchId == batchId); + + if (stockLevel == null) + { + stockLevel = new StockLevel + { + ArticleId = articleId, + WarehouseId = warehouseId, + BatchId = batchId, + Quantity = quantity, + UnitCost = unitCost, + CreatedAt = DateTime.UtcNow + }; + _context.StockLevels.Add(stockLevel); + } + else + { + stockLevel.Quantity = quantity; + if (unitCost.HasValue) + stockLevel.UnitCost = unitCost; + stockLevel.UpdatedAt = DateTime.UtcNow; + } + + stockLevel.LastMovementDate = DateTime.UtcNow; + stockLevel.StockValue = stockLevel.Quantity * (stockLevel.UnitCost ?? 0); + + await _context.SaveChangesAsync(); + } + + #endregion + + #region Movimenti + + public async Task> GetMovementsAsync(MovementFilter? filter = null) + { + var query = _context.StockMovements + .Include(m => m.SourceWarehouse) + .Include(m => m.DestinationWarehouse) + .Include(m => m.Reason) + .Include(m => m.Lines) + .ThenInclude(l => l.Article) + .AsQueryable(); + + if (filter != null) + { + if (filter.DateFrom.HasValue) + query = query.Where(m => m.MovementDate >= filter.DateFrom); + + if (filter.DateTo.HasValue) + query = query.Where(m => m.MovementDate <= filter.DateTo); + + if (filter.Type.HasValue) + query = query.Where(m => m.Type == filter.Type); + + if (filter.Status.HasValue) + query = query.Where(m => m.Status == filter.Status); + + if (filter.WarehouseId.HasValue) + query = query.Where(m => + m.SourceWarehouseId == filter.WarehouseId || + m.DestinationWarehouseId == filter.WarehouseId); + + if (filter.ArticleId.HasValue) + query = query.Where(m => m.Lines.Any(l => l.ArticleId == filter.ArticleId)); + + if (filter.ReasonId.HasValue) + query = query.Where(m => m.ReasonId == filter.ReasonId); + + if (!string.IsNullOrWhiteSpace(filter.ExternalReference)) + query = query.Where(m => m.ExternalReference != null && m.ExternalReference.Contains(filter.ExternalReference)); + + // Ordinamento + query = filter.OrderBy?.ToLower() switch + { + "documentnumber" => filter.OrderDescending ? query.OrderByDescending(m => m.DocumentNumber) : query.OrderBy(m => m.DocumentNumber), + "type" => filter.OrderDescending ? query.OrderByDescending(m => m.Type) : query.OrderBy(m => m.Type), + _ => filter.OrderDescending ? query.OrderByDescending(m => m.MovementDate) : query.OrderBy(m => m.MovementDate) + }; + + query = query.Skip(filter.Skip).Take(filter.Take); + } + else + { + query = query.OrderByDescending(m => m.MovementDate).Take(100); + } + + return await query.ToListAsync(); + } + + public async Task GetMovementByIdAsync(int id) + { + return await _context.StockMovements + .Include(m => m.SourceWarehouse) + .Include(m => m.DestinationWarehouse) + .Include(m => m.Reason) + .Include(m => m.Lines) + .ThenInclude(l => l.Article) + .Include(m => m.Lines) + .ThenInclude(l => l.Batch) + .Include(m => m.Lines) + .ThenInclude(l => l.Serial) + .FirstOrDefaultAsync(m => m.Id == id); + } + + public async Task GetMovementByDocumentNumberAsync(string documentNumber) + { + return await _context.StockMovements + .Include(m => m.Lines) + .FirstOrDefaultAsync(m => m.DocumentNumber == documentNumber); + } + + public async Task CreateMovementAsync(StockMovement movement) + { + // Genera numero documento se non specificato + if (string.IsNullOrEmpty(movement.DocumentNumber)) + movement.DocumentNumber = await GenerateDocumentNumberAsync(movement.Type); + + // Verifica unicità documento + if (await _context.StockMovements.AnyAsync(m => m.DocumentNumber == movement.DocumentNumber)) + throw new InvalidOperationException($"Esiste già un movimento con numero documento '{movement.DocumentNumber}'"); + + // Numera le righe + var lineNumber = 1; + foreach (var line in movement.Lines) + { + line.LineNumber = lineNumber++; + } + + movement.CreatedAt = DateTime.UtcNow; + _context.StockMovements.Add(movement); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Creato movimento {DocumentNumber} tipo {Type}", movement.DocumentNumber, movement.Type); + return movement; + } + + public async Task UpdateMovementAsync(StockMovement movement) + { + var existing = await _context.StockMovements + .Include(m => m.Lines) + .FirstOrDefaultAsync(m => m.Id == movement.Id); + + if (existing == null) + throw new ArgumentException($"Movimento con ID {movement.Id} non trovato"); + + if (existing.Status != MovementStatus.Draft) + throw new InvalidOperationException("È possibile modificare solo movimenti in bozza"); + + _context.Entry(existing).CurrentValues.SetValues(movement); + existing.UpdatedAt = DateTime.UtcNow; + + // Aggiorna le righe + _context.StockMovementLines.RemoveRange(existing.Lines); + var lineNumber = 1; + foreach (var line in movement.Lines) + { + line.MovementId = movement.Id; + line.LineNumber = lineNumber++; + _context.StockMovementLines.Add(line); + } + + await _context.SaveChangesAsync(); + return existing; + } + + public async Task ConfirmMovementAsync(int id) + { + var movement = await GetMovementByIdAsync(id); + if (movement == null) + throw new ArgumentException($"Movimento con ID {id} non trovato"); + + if (movement.Status != MovementStatus.Draft) + throw new InvalidOperationException("Solo i movimenti in bozza possono essere confermati"); + + if (!movement.Lines.Any()) + throw new InvalidOperationException("Il movimento non contiene righe"); + + // Applica il movimento alle giacenze + await ApplyMovementToStockAsync(movement); + + movement.Status = MovementStatus.Confirmed; + movement.ConfirmedDate = DateTime.UtcNow; + movement.UpdatedAt = DateTime.UtcNow; + + // Calcola valore totale + movement.TotalValue = movement.Lines.Sum(l => l.LineValue ?? 0); + + await _context.SaveChangesAsync(); + + _logger.LogInformation("Confermato movimento {DocumentNumber}", movement.DocumentNumber); + return movement; + } + + public async Task CancelMovementAsync(int id) + { + var movement = await GetMovementByIdAsync(id); + if (movement == null) + throw new ArgumentException($"Movimento con ID {id} non trovato"); + + if (movement.Status == MovementStatus.Cancelled) + throw new InvalidOperationException("Il movimento è già annullato"); + + // Se era confermato, storna le giacenze + if (movement.Status == MovementStatus.Confirmed) + { + await ReverseMovementFromStockAsync(movement); + } + + movement.Status = MovementStatus.Cancelled; + movement.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + _logger.LogWarning("Annullato movimento {DocumentNumber}", movement.DocumentNumber); + return movement; + } + + public async Task GenerateDocumentNumberAsync(MovementType type) + { + var prefix = type switch + { + MovementType.Inbound => "CAR", + MovementType.Outbound => "SCA", + MovementType.Transfer => "TRA", + MovementType.Adjustment => "RET", + MovementType.Production => "PRO", + MovementType.Consumption => "CON", + MovementType.SupplierReturn => "RFO", + MovementType.CustomerReturn => "RCL", + _ => "MOV" + }; + + var year = DateTime.UtcNow.Year; + var lastNumber = await _context.StockMovements + .Where(m => m.DocumentNumber.StartsWith($"{prefix}/{year}/")) + .OrderByDescending(m => m.DocumentNumber) + .Select(m => m.DocumentNumber) + .FirstOrDefaultAsync(); + + var nextNumber = 1; + if (lastNumber != null) + { + var parts = lastNumber.Split('/'); + if (parts.Length == 3 && int.TryParse(parts[2], out var num)) + nextNumber = num + 1; + } + + return $"{prefix}/{year}/{nextNumber:D6}"; + } + + private async Task ApplyMovementToStockAsync(StockMovement movement) + { + foreach (var line in movement.Lines) + { + var stockSign = GetStockSign(movement.Type); + + if (movement.Type == MovementType.Transfer) + { + // Trasferimento: scarica da origine e carica su destinazione + if (movement.SourceWarehouseId.HasValue) + { + await UpdateStockFromMovementLineAsync( + line, + movement.SourceWarehouseId.Value, + -line.Quantity); + } + + if (movement.DestinationWarehouseId.HasValue) + { + await UpdateStockFromMovementLineAsync( + line, + movement.DestinationWarehouseId.Value, + line.Quantity); + } + } + else + { + // Altri movimenti: usa il magazzino appropriato + var warehouseId = stockSign > 0 + ? movement.DestinationWarehouseId + : movement.SourceWarehouseId; + + if (warehouseId.HasValue) + { + await UpdateStockFromMovementLineAsync( + line, + warehouseId.Value, + line.Quantity * stockSign); + } + } + + // Aggiorna stato seriale se presente + if (line.SerialId.HasValue) + { + await UpdateSerialFromMovementAsync(line, movement.Type, stockSign > 0 + ? movement.DestinationWarehouseId + : null); + } + + // Aggiorna quantità batch se presente + if (line.BatchId.HasValue) + { + await UpdateBatchQuantityAsync(line.BatchId.Value, line.Quantity * stockSign); + } + } + + // Aggiorna costo medio ponderato per gli articoli interessati + var articleIds = movement.Lines.Select(l => l.ArticleId).Distinct(); + foreach (var articleId in articleIds) + { + await UpdateWeightedAverageCostAsync(articleId); + } + } + + private async Task ReverseMovementFromStockAsync(StockMovement movement) + { + foreach (var line in movement.Lines) + { + var stockSign = GetStockSign(movement.Type); + + if (movement.Type == MovementType.Transfer) + { + // Storna trasferimento + if (movement.SourceWarehouseId.HasValue) + { + await UpdateStockFromMovementLineAsync( + line, + movement.SourceWarehouseId.Value, + line.Quantity); // Ricarica su origine + } + + if (movement.DestinationWarehouseId.HasValue) + { + await UpdateStockFromMovementLineAsync( + line, + movement.DestinationWarehouseId.Value, + -line.Quantity); // Scarica da destinazione + } + } + else + { + var warehouseId = stockSign > 0 + ? movement.DestinationWarehouseId + : movement.SourceWarehouseId; + + if (warehouseId.HasValue) + { + await UpdateStockFromMovementLineAsync( + line, + warehouseId.Value, + -line.Quantity * stockSign); // Inverti il segno + } + } + + // Ripristina batch quantity + if (line.BatchId.HasValue) + { + await UpdateBatchQuantityAsync(line.BatchId.Value, -line.Quantity * stockSign); + } + } + } + + private async Task UpdateStockFromMovementLineAsync(StockMovementLine line, int warehouseId, decimal quantityChange) + { + var stockLevel = await _context.StockLevels + .FirstOrDefaultAsync(s => + s.ArticleId == line.ArticleId && + s.WarehouseId == warehouseId && + s.BatchId == line.BatchId); + + if (stockLevel == null) + { + stockLevel = new StockLevel + { + ArticleId = line.ArticleId, + WarehouseId = warehouseId, + BatchId = line.BatchId, + Quantity = 0, + CreatedAt = DateTime.UtcNow + }; + _context.StockLevels.Add(stockLevel); + } + + stockLevel.Quantity += quantityChange; + stockLevel.LastMovementDate = DateTime.UtcNow; + + // Aggiorna costo unitario e valore se è un carico con costo + if (quantityChange > 0 && line.UnitCost.HasValue) + { + // Media ponderata del costo + var oldValue = (stockLevel.Quantity - quantityChange) * (stockLevel.UnitCost ?? 0); + var newValue = quantityChange * line.UnitCost.Value; + stockLevel.UnitCost = stockLevel.Quantity > 0 + ? (oldValue + newValue) / stockLevel.Quantity + : line.UnitCost; + } + + stockLevel.StockValue = stockLevel.Quantity * (stockLevel.UnitCost ?? 0); + stockLevel.UpdatedAt = DateTime.UtcNow; + } + + private async Task UpdateSerialFromMovementAsync(StockMovementLine line, MovementType movementType, int? destinationWarehouseId) + { + if (!line.SerialId.HasValue) return; + + var serial = await _context.ArticleSerials.FindAsync(line.SerialId); + if (serial == null) return; + + serial.CurrentWarehouseId = destinationWarehouseId; + + serial.Status = movementType switch + { + MovementType.Outbound => SerialStatus.Sold, + MovementType.CustomerReturn => SerialStatus.Returned, + MovementType.Inbound or MovementType.Transfer => SerialStatus.Available, + _ => serial.Status + }; + + serial.UpdatedAt = DateTime.UtcNow; + } + + private async Task UpdateBatchQuantityAsync(int batchId, decimal quantityChange) + { + var batch = await _context.ArticleBatches.FindAsync(batchId); + if (batch == null) return; + + batch.CurrentQuantity += quantityChange; + + if (batch.CurrentQuantity <= 0) + { + batch.Status = BatchStatus.Depleted; + batch.CurrentQuantity = 0; + } + + batch.UpdatedAt = DateTime.UtcNow; + } + + private int GetStockSign(MovementType type) + { + return type switch + { + MovementType.Inbound => 1, + MovementType.Outbound => -1, + MovementType.Production => 1, + MovementType.Consumption => -1, + MovementType.CustomerReturn => 1, + MovementType.SupplierReturn => -1, + MovementType.Adjustment => 1, // Il segno dipende dalla quantità (positiva o negativa) + MovementType.Transfer => 0, // Gestito separatamente + _ => 0 + }; + } + + #endregion + + #region Causali + + public async Task> GetMovementReasonsAsync(MovementType? type = null, bool includeInactive = false) + { + var query = _context.MovementReasons.AsQueryable(); + + if (type.HasValue) + query = query.Where(r => r.MovementType == type); + + if (!includeInactive) + query = query.Where(r => r.IsActive); + + return await query + .OrderBy(r => r.SortOrder) + .ThenBy(r => r.Description) + .ToListAsync(); + } + + public async Task GetMovementReasonByIdAsync(int id) + { + return await _context.MovementReasons.FindAsync(id); + } + + public async Task CreateMovementReasonAsync(MovementReason reason) + { + if (await _context.MovementReasons.AnyAsync(r => r.Code == reason.Code)) + throw new InvalidOperationException($"Esiste già una causale con codice '{reason.Code}'"); + + reason.CreatedAt = DateTime.UtcNow; + _context.MovementReasons.Add(reason); + await _context.SaveChangesAsync(); + + return reason; + } + + public async Task UpdateMovementReasonAsync(MovementReason reason) + { + var existing = await _context.MovementReasons.FindAsync(reason.Id); + if (existing == null) + throw new ArgumentException($"Causale con ID {reason.Id} non trovata"); + + if (existing.IsSystem) + throw new InvalidOperationException("Le causali di sistema non possono essere modificate"); + + _context.Entry(existing).CurrentValues.SetValues(reason); + existing.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return existing; + } + + #endregion + + #region Valorizzazione + + public async Task CalculateArticleValueAsync(int articleId, ValuationMethod? method = null) + { + var article = await _context.WarehouseArticles.FindAsync(articleId); + if (article == null) + throw new ArgumentException("Articolo non trovato"); + + var valuationMethod = method ?? article.ValuationMethod ?? ValuationMethod.WeightedAverage; + + return valuationMethod switch + { + ValuationMethod.WeightedAverage => await CalculateWeightedAverageValueAsync(articleId), + ValuationMethod.FIFO => await CalculateFIFOValueAsync(articleId), + ValuationMethod.LIFO => await CalculateLIFOValueAsync(articleId), + ValuationMethod.StandardCost => await CalculateStandardCostValueAsync(articleId), + ValuationMethod.SpecificCost => await CalculateSpecificCostValueAsync(articleId), + _ => 0 + }; + } + + private async Task CalculateWeightedAverageValueAsync(int articleId) + { + var stockLevels = await _context.StockLevels + .Where(s => s.ArticleId == articleId && s.Quantity > 0) + .ToListAsync(); + + return stockLevels.Sum(s => s.Quantity * (s.UnitCost ?? 0)); + } + + private async Task CalculateFIFOValueAsync(int articleId) + { + var layers = await _context.StockValuationLayers + .Where(l => l.ArticleId == articleId && !l.IsExhausted && l.RemainingQuantity > 0) + .OrderBy(l => l.LayerDate) + .ToListAsync(); + + return layers.Sum(l => l.RemainingValue); + } + + private async Task CalculateLIFOValueAsync(int articleId) + { + var layers = await _context.StockValuationLayers + .Where(l => l.ArticleId == articleId && !l.IsExhausted && l.RemainingQuantity > 0) + .OrderByDescending(l => l.LayerDate) + .ToListAsync(); + + return layers.Sum(l => l.RemainingValue); + } + + private async Task CalculateStandardCostValueAsync(int articleId) + { + var article = await _context.WarehouseArticles.FindAsync(articleId); + if (article?.StandardCost == null) + return 0; + + var totalQty = await GetTotalStockAsync(articleId); + return totalQty * article.StandardCost.Value; + } + + private async Task CalculateSpecificCostValueAsync(int articleId) + { + // Per costo specifico, somma il valore di tutti i batch + var batches = await _context.ArticleBatches + .Where(b => b.ArticleId == articleId && b.CurrentQuantity > 0) + .ToListAsync(); + + return batches.Sum(b => b.CurrentQuantity * (b.UnitCost ?? 0)); + } + + public async Task CalculatePeriodValuationAsync(int articleId, int period, int? warehouseId = null) + { + var article = await _context.WarehouseArticles.FindAsync(articleId); + if (article == null) + throw new ArgumentException("Articolo non trovato"); + + var method = article.ValuationMethod ?? ValuationMethod.WeightedAverage; + + // Calcola quantità a fine periodo + var stockQuery = _context.StockLevels.Where(s => s.ArticleId == articleId); + if (warehouseId.HasValue) + stockQuery = stockQuery.Where(s => s.WarehouseId == warehouseId); + + var quantity = await stockQuery.SumAsync(s => s.Quantity); + + // Calcola movimenti del periodo + var periodStart = new DateTime(period / 100, period % 100, 1); + var periodEnd = periodStart.AddMonths(1); + + var movements = await _context.StockMovementLines + .Include(l => l.Movement) + .Where(l => l.ArticleId == articleId && + l.Movement!.Status == MovementStatus.Confirmed && + l.Movement.MovementDate >= periodStart && + l.Movement.MovementDate < periodEnd) + .ToListAsync(); + + var inboundQty = movements + .Where(l => GetStockSign(l.Movement!.Type) > 0) + .Sum(l => l.Quantity); + + var inboundValue = movements + .Where(l => GetStockSign(l.Movement!.Type) > 0) + .Sum(l => l.LineValue ?? 0); + + var outboundQty = movements + .Where(l => GetStockSign(l.Movement!.Type) < 0) + .Sum(l => l.Quantity); + + var outboundValue = movements + .Where(l => GetStockSign(l.Movement!.Type) < 0) + .Sum(l => l.LineValue ?? 0); + + var unitCost = await GetWeightedAverageCostAsync(articleId); + + var valuation = new StockValuation + { + ArticleId = articleId, + WarehouseId = warehouseId, + Period = period, + ValuationDate = DateTime.UtcNow, + Method = method, + Quantity = quantity, + UnitCost = unitCost, + TotalValue = quantity * unitCost, + InboundQuantity = inboundQty, + InboundValue = inboundValue, + OutboundQuantity = outboundQty, + OutboundValue = outboundValue, + CreatedAt = DateTime.UtcNow + }; + + return valuation; + } + + public async Task> GetValuationsAsync(int period, int? warehouseId = null) + { + var query = _context.StockValuations + .Include(v => v.Article) + .Include(v => v.Warehouse) + .Where(v => v.Period == period); + + if (warehouseId.HasValue) + query = query.Where(v => v.WarehouseId == warehouseId); + + return await query + .OrderBy(v => v.Article!.Code) + .ToListAsync(); + } + + public async Task ClosePeriodAsync(int period) + { + var valuations = await _context.StockValuations + .Where(v => v.Period == period && !v.IsClosed) + .ToListAsync(); + + foreach (var valuation in valuations) + { + valuation.IsClosed = true; + valuation.ClosedDate = DateTime.UtcNow; + } + + await _context.SaveChangesAsync(); + _logger.LogInformation("Chiuso periodo {Period} con {Count} valorizzazioni", period, valuations.Count); + } + + public async Task GetWeightedAverageCostAsync(int articleId) + { + var article = await _context.WarehouseArticles.FindAsync(articleId); + return article?.WeightedAverageCost ?? 0; + } + + public async Task UpdateWeightedAverageCostAsync(int articleId) + { + var article = await _context.WarehouseArticles.FindAsync(articleId); + if (article == null) return; + + var stockLevels = await _context.StockLevels + .Where(s => s.ArticleId == articleId && s.Quantity > 0) + .ToListAsync(); + + var totalQty = stockLevels.Sum(s => s.Quantity); + var totalValue = stockLevels.Sum(s => s.Quantity * (s.UnitCost ?? 0)); + + article.WeightedAverageCost = totalQty > 0 ? totalValue / totalQty : 0; + article.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + } + + #endregion + + #region Inventario + + public async Task> GetInventoryCountsAsync(InventoryStatus? status = null) + { + var query = _context.InventoryCounts + .Include(i => i.Warehouse) + .Include(i => i.Category) + .AsQueryable(); + + if (status.HasValue) + query = query.Where(i => i.Status == status); + + return await query + .OrderByDescending(i => i.InventoryDate) + .ToListAsync(); + } + + public async Task GetInventoryCountByIdAsync(int id) + { + return await _context.InventoryCounts + .Include(i => i.Warehouse) + .Include(i => i.Category) + .Include(i => i.Lines) + .ThenInclude(l => l.Article) + .Include(i => i.Lines) + .ThenInclude(l => l.Warehouse) + .Include(i => i.Lines) + .ThenInclude(l => l.Batch) + .FirstOrDefaultAsync(i => i.Id == id); + } + + public async Task CreateInventoryCountAsync(InventoryCount inventory) + { + if (string.IsNullOrEmpty(inventory.Code)) + inventory.Code = $"INV/{DateTime.UtcNow:yyyyMMdd}/{await GenerateInventorySequenceAsync()}"; + + inventory.CreatedAt = DateTime.UtcNow; + _context.InventoryCounts.Add(inventory); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Creato inventario {Code}", inventory.Code); + return inventory; + } + + public async Task UpdateInventoryCountAsync(InventoryCount inventory) + { + var existing = await _context.InventoryCounts.FindAsync(inventory.Id); + if (existing == null) + throw new ArgumentException($"Inventario con ID {inventory.Id} non trovato"); + + if (existing.Status != InventoryStatus.Draft) + throw new InvalidOperationException("È possibile modificare solo inventari in bozza"); + + _context.Entry(existing).CurrentValues.SetValues(inventory); + existing.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return existing; + } + + public async Task StartInventoryCountAsync(int id) + { + var inventory = await GetInventoryCountByIdAsync(id); + if (inventory == null) + throw new ArgumentException($"Inventario con ID {id} non trovato"); + + if (inventory.Status != InventoryStatus.Draft) + throw new InvalidOperationException("Solo gli inventari in bozza possono essere avviati"); + + // Genera le righe da contare + await GenerateInventoryLinesAsync(inventory); + + inventory.Status = InventoryStatus.InProgress; + inventory.StartDate = DateTime.UtcNow; + inventory.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + _logger.LogInformation("Avviato inventario {Code} con {Count} righe", inventory.Code, inventory.Lines.Count); + return inventory; + } + + public async Task CompleteInventoryCountAsync(int id) + { + var inventory = await GetInventoryCountByIdAsync(id); + if (inventory == null) + throw new ArgumentException($"Inventario con ID {id} non trovato"); + + if (inventory.Status != InventoryStatus.InProgress) + throw new InvalidOperationException("Solo gli inventari in corso possono essere completati"); + + // Verifica che tutte le righe siano state contate + if (inventory.Lines.Any(l => !l.CountedQuantity.HasValue)) + throw new InvalidOperationException("Alcune righe non sono state ancora contate"); + + // Calcola le differenze + inventory.PositiveDifferenceValue = inventory.Lines + .Where(l => l.Difference > 0) + .Sum(l => l.DifferenceValue ?? 0); + + inventory.NegativeDifferenceValue = inventory.Lines + .Where(l => l.Difference < 0) + .Sum(l => Math.Abs(l.DifferenceValue ?? 0)); + + inventory.Status = InventoryStatus.Completed; + inventory.EndDate = DateTime.UtcNow; + inventory.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + _logger.LogInformation("Completato inventario {Code}", inventory.Code); + return inventory; + } + + public async Task ConfirmInventoryCountAsync(int id) + { + var inventory = await GetInventoryCountByIdAsync(id); + if (inventory == null) + throw new ArgumentException($"Inventario con ID {id} non trovato"); + + if (inventory.Status != InventoryStatus.Completed) + throw new InvalidOperationException("Solo gli inventari completati possono essere confermati"); + + // Crea movimento di rettifica + var adjustmentMovement = new StockMovement + { + DocumentNumber = await GenerateDocumentNumberAsync(MovementType.Adjustment), + MovementDate = DateTime.UtcNow, + Type = MovementType.Adjustment, + Status = MovementStatus.Draft, + Notes = $"Rettifica da inventario {inventory.Code}", + CreatedAt = DateTime.UtcNow + }; + + var lineNumber = 1; + foreach (var line in inventory.Lines.Where(l => l.Difference != 0)) + { + var movementLine = new StockMovementLine + { + LineNumber = lineNumber++, + ArticleId = line.ArticleId, + BatchId = line.BatchId, + Quantity = line.Difference!.Value, + UnitOfMeasure = line.Article?.UnitOfMeasure ?? "PZ", + UnitCost = line.UnitCost, + LineValue = line.DifferenceValue + }; + adjustmentMovement.Lines.Add(movementLine); + } + + if (adjustmentMovement.Lines.Any()) + { + // Determina il magazzino per il movimento + var warehouseId = inventory.WarehouseId ?? + inventory.Lines.FirstOrDefault()?.WarehouseId; + + if (warehouseId.HasValue) + { + // Per le rettifiche, usa il magazzino come destinazione per qta positive, origine per negative + adjustmentMovement.DestinationWarehouseId = warehouseId; + } + + _context.StockMovements.Add(adjustmentMovement); + await _context.SaveChangesAsync(); + + // Conferma il movimento per applicare le rettifiche + await ConfirmMovementAsync(adjustmentMovement.Id); + + inventory.AdjustmentMovementId = adjustmentMovement.Id; + } + + inventory.Status = InventoryStatus.Confirmed; + inventory.ConfirmedDate = DateTime.UtcNow; + inventory.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + _logger.LogInformation("Confermato inventario {Code}, movimento rettifica: {MovementId}", + inventory.Code, inventory.AdjustmentMovementId); + + return inventory; + } + + public async Task CancelInventoryCountAsync(int id) + { + var inventory = await _context.InventoryCounts.FindAsync(id); + if (inventory == null) + throw new ArgumentException($"Inventario con ID {id} non trovato"); + + if (inventory.Status == InventoryStatus.Confirmed) + throw new InvalidOperationException("Gli inventari confermati non possono essere annullati"); + + inventory.Status = InventoryStatus.Cancelled; + inventory.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + _logger.LogWarning("Annullato inventario {Code}", inventory.Code); + return inventory; + } + + public async Task UpdateCountLineAsync(int lineId, decimal countedQuantity, string? countedBy = null) + { + var line = await _context.InventoryCountLines + .Include(l => l.InventoryCount) + .FirstOrDefaultAsync(l => l.Id == lineId); + + if (line == null) + throw new ArgumentException($"Riga inventario con ID {lineId} non trovata"); + + if (line.InventoryCount?.Status != InventoryStatus.InProgress) + throw new InvalidOperationException("È possibile contare solo inventari in corso"); + + line.CountedQuantity = countedQuantity; + line.CountedAt = DateTime.UtcNow; + line.CountedBy = countedBy; + line.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + return line; + } + + private async Task GenerateInventoryLinesAsync(InventoryCount inventory) + { + var stockQuery = _context.StockLevels + .Include(s => s.Article) + .AsQueryable(); + + if (inventory.WarehouseId.HasValue) + stockQuery = stockQuery.Where(s => s.WarehouseId == inventory.WarehouseId); + + if (inventory.CategoryId.HasValue) + stockQuery = stockQuery.Where(s => s.Article!.CategoryId == inventory.CategoryId); + + var stockLevels = await stockQuery.ToListAsync(); + + foreach (var stock in stockLevels) + { + var line = new InventoryCountLine + { + InventoryCountId = inventory.Id, + ArticleId = stock.ArticleId, + WarehouseId = stock.WarehouseId, + BatchId = stock.BatchId, + TheoreticalQuantity = stock.Quantity, + UnitCost = stock.UnitCost ?? stock.Article?.WeightedAverageCost ?? 0, + CreatedAt = DateTime.UtcNow + }; + _context.InventoryCountLines.Add(line); + } + + await _context.SaveChangesAsync(); + } + + private async Task GenerateInventorySequenceAsync() + { + var today = DateTime.UtcNow.Date; + var count = await _context.InventoryCounts + .CountAsync(i => i.CreatedAt >= today); + return count + 1; + } + + #endregion + + #region Seed Data + + public async Task SeedDefaultDataAsync() + { + // Seed magazzini + if (!await _context.WarehouseLocations.AnyAsync()) + { + var warehouses = new List + { + new WarehouseLocation + { + Code = "MAG01", + Name = "Magazzino Principale", + Description = "Magazzino centrale", + Type = WarehouseType.Physical, + IsDefault = true, + IsActive = true, + SortOrder = 10, + CreatedAt = DateTime.UtcNow + }, + new WarehouseLocation + { + Code = "TRANS", + Name = "Magazzino Transito", + Description = "Merce in transito", + Type = WarehouseType.Transit, + IsDefault = false, + IsActive = true, + SortOrder = 20, + CreatedAt = DateTime.UtcNow + } + }; + _context.WarehouseLocations.AddRange(warehouses); + _logger.LogInformation("Seed {Count} magazzini di default", warehouses.Count); + } + + // Seed categorie + if (!await _context.WarehouseArticleCategories.AnyAsync()) + { + var categories = new List + { + new WarehouseArticleCategory + { + Code = "MAT", + Name = "Materie Prime", + Level = 0, + FullPath = "MAT", + Icon = "Inventory2", + IsActive = true, + SortOrder = 10, + CreatedAt = DateTime.UtcNow + }, + new WarehouseArticleCategory + { + Code = "SEM", + Name = "Semilavorati", + Level = 0, + FullPath = "SEM", + Icon = "Build", + IsActive = true, + SortOrder = 20, + CreatedAt = DateTime.UtcNow + }, + new WarehouseArticleCategory + { + Code = "PF", + Name = "Prodotti Finiti", + Level = 0, + FullPath = "PF", + Icon = "CheckBox", + IsActive = true, + SortOrder = 30, + CreatedAt = DateTime.UtcNow + }, + new WarehouseArticleCategory + { + Code = "CONS", + Name = "Consumabili", + Level = 0, + FullPath = "CONS", + Icon = "LocalOffer", + IsActive = true, + SortOrder = 40, + CreatedAt = DateTime.UtcNow + } + }; + _context.WarehouseArticleCategories.AddRange(categories); + _logger.LogInformation("Seed {Count} categorie di default", categories.Count); + } + + // Seed causali movimento + if (!await _context.MovementReasons.AnyAsync()) + { + var reasons = new List + { + // Causali carico + new MovementReason + { + Code = "ACQ", + Description = "Acquisto da fornitore", + MovementType = MovementType.Inbound, + StockSign = 1, + RequiresExternalReference = true, + RequiresValuation = true, + UpdatesAverageCost = true, + IsSystem = true, + IsActive = true, + SortOrder = 10, + CreatedAt = DateTime.UtcNow + }, + new MovementReason + { + Code = "RCLI", + Description = "Reso da cliente", + MovementType = MovementType.CustomerReturn, + StockSign = 1, + RequiresExternalReference = true, + RequiresValuation = true, + UpdatesAverageCost = false, + IsSystem = true, + IsActive = true, + SortOrder = 20, + CreatedAt = DateTime.UtcNow + }, + new MovementReason + { + Code = "PROD", + Description = "Ingresso da produzione", + MovementType = MovementType.Production, + StockSign = 1, + RequiresExternalReference = true, + RequiresValuation = true, + UpdatesAverageCost = true, + IsSystem = true, + IsActive = true, + SortOrder = 30, + CreatedAt = DateTime.UtcNow + }, + // Causali scarico + new MovementReason + { + Code = "VEN", + Description = "Vendita a cliente", + MovementType = MovementType.Outbound, + StockSign = -1, + RequiresExternalReference = true, + RequiresValuation = true, + UpdatesAverageCost = false, + IsSystem = true, + IsActive = true, + SortOrder = 40, + CreatedAt = DateTime.UtcNow + }, + new MovementReason + { + Code = "RFOR", + Description = "Reso a fornitore", + MovementType = MovementType.SupplierReturn, + StockSign = -1, + RequiresExternalReference = true, + RequiresValuation = true, + UpdatesAverageCost = false, + IsSystem = true, + IsActive = true, + SortOrder = 50, + CreatedAt = DateTime.UtcNow + }, + new MovementReason + { + Code = "CONS", + Description = "Consumo per produzione", + MovementType = MovementType.Consumption, + StockSign = -1, + RequiresExternalReference = true, + RequiresValuation = true, + UpdatesAverageCost = false, + IsSystem = true, + IsActive = true, + SortOrder = 60, + CreatedAt = DateTime.UtcNow + }, + // Causali trasferimento + new MovementReason + { + Code = "TRAS", + Description = "Trasferimento tra magazzini", + MovementType = MovementType.Transfer, + StockSign = 0, + RequiresExternalReference = false, + RequiresValuation = false, + UpdatesAverageCost = false, + IsSystem = true, + IsActive = true, + SortOrder = 70, + CreatedAt = DateTime.UtcNow + }, + // Causali rettifica + new MovementReason + { + Code = "RET+", + Description = "Rettifica positiva inventario", + MovementType = MovementType.Adjustment, + StockSign = 1, + RequiresExternalReference = false, + RequiresValuation = true, + UpdatesAverageCost = false, + IsSystem = true, + IsActive = true, + SortOrder = 80, + CreatedAt = DateTime.UtcNow + }, + new MovementReason + { + Code = "RET-", + Description = "Rettifica negativa inventario", + MovementType = MovementType.Adjustment, + StockSign = -1, + RequiresExternalReference = false, + RequiresValuation = true, + UpdatesAverageCost = false, + IsSystem = true, + IsActive = true, + SortOrder = 90, + CreatedAt = DateTime.UtcNow + } + }; + _context.MovementReasons.AddRange(reasons); + _logger.LogInformation("Seed {Count} causali movimento di default", reasons.Count); + } + + await _context.SaveChangesAsync(); + } + + #endregion +} diff --git a/src/Apollinare.API/Program.cs b/src/Apollinare.API/Program.cs index 502f013..44c699e 100644 --- a/src/Apollinare.API/Program.cs +++ b/src/Apollinare.API/Program.cs @@ -1,6 +1,7 @@ using Apollinare.API.Hubs; using Apollinare.API.Services; using Apollinare.API.Services.Reports; +using Apollinare.API.Modules.Warehouse.Services; using Apollinare.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using System.Text.Json.Serialization; @@ -20,6 +21,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); +// Warehouse Module Services +builder.Services.AddScoped(); + // Memory cache for module state builder.Services.AddMemoryCache(); @@ -58,18 +62,48 @@ builder.Services.AddOpenApi(); var app = builder.Build(); -// Initialize database -if (app.Environment.IsDevelopment()) +// Apply pending migrations automatically on startup +using (var scope = app.Services.CreateScope()) { - using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); - db.Database.EnsureCreated(); + var logger = scope.ServiceProvider.GetRequiredService>(); + + try + { + var pendingMigrations = await db.Database.GetPendingMigrationsAsync(); + if (pendingMigrations.Any()) + { + logger.LogInformation("Applying {Count} pending migrations: {Migrations}", + pendingMigrations.Count(), + string.Join(", ", pendingMigrations)); + await db.Database.MigrateAsync(); + logger.LogInformation("Migrations applied successfully"); + } + else + { + logger.LogInformation("Database is up to date, no migrations to apply"); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error applying migrations"); + throw; + } + + // Seed data (only in development or if database is empty) DbSeeder.Seed(db); // Seed default modules var moduleService = scope.ServiceProvider.GetRequiredService(); await moduleService.SeedDefaultModulesAsync(); + // Seed warehouse default data + var warehouseService = scope.ServiceProvider.GetRequiredService(); + await warehouseService.SeedDefaultDataAsync(); +} + +if (app.Environment.IsDevelopment()) +{ app.MapOpenApi(); } diff --git a/src/Apollinare.API/apollinare.db-shm b/src/Apollinare.API/apollinare.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..05ea21cb6930084499b15271b770f6def938ec8a GIT binary patch literal 32768 zcmeI5XLD6$6oofns2WH}NI)Q=_nLrGL+?FwEHvp@=)HtyB~rvL*cBCfL&c7wf{F?n zd+deZpldS;bB7POj&t_BlbO9|p2;P7?s`vF4j=D6nZ<2Hry?jPQsWf9?g2BW6y@a} z%{q24`S^kSqSWHO`wkWC&nY~d8PP}7>CxPO5~k$a@?psfoGs-`(AO2fc_8t168^2% zSqW4IRX|lx4V(`y0Lh>_r~y(yDyRw4Ksu-eGC(G%4eEdkL0wP})CXB08#Dk7K_k!@ zGyzRPGteBg04+f)&>CoyZ9zNG9&`X5K@P|Toj_-B5x5w10bM~i&>i#uJwYDm1$u)% zpfBhL`hx*rAQ%J&gCSrj7zT!e5nv=31xAB0ARmkc@Ag9TtAxDs3it_IhDMPM;l0+xbhU^!R;R)SSvHCO}I zf^}d$*Z?+y0Zunk-bt^?PD8^Dd=Ca@h8f}6n(uoLV8yTL7B4=4{Rf=Zw= zr~;~j6p#vPf;5m067LPYAEN#}v^(vk^jeefTF)-M2lRC{kOB05Ozb%E_es2deUILw zXaA1+{cO+)MD4vavjg6H_TNx5_3xvO{(sSXLBINV1Kxl);0<^K-hemY4R{0IfH&X` zcmv*mH{cC;1Kxl);0<^K-hemY4R{0IfH&X`cmv*mH{cC;1Kxl);0<^K-hemY4R{0I zfH&X`cmv*mH{cC;1Kxl);0<^K-hemY4R{0IK&S@v6Wt-FJ$?9{XSNE^Z;4gZ&(nt- z{meQyTRF|l8eo>p1((B)3Ecwl-=n(>LXLhezifS_Q|Rt2E8+lo)b`$x|VZ$EMJG$n%%W7o=&U z>5j;JEJvl7x`0ACNO@M$NO{&On#!#%_fnp2 z<^Ga1ZM3fPxlM5Fsk*N_)`LmaK?p zejTG-3vrx9XoSpk+T9@E(XNHq%OcbybH2P|lFNp+Vnsad?qk}u5XW1DM#!us_mTES z`Ck4YT??(ZMW{*U0(sXYmkn*rigK%bLc12?1dGrJnHh3FX&1;3q-mq|v57TEU1*RQ zC9(}I-zJ~Zu7xuVJpd#*kiHsrLW55HZG(5{6z$s#mDW^H+p wv^SHUmDbNHIQCq9u64+1M<0HNd_cPv;$(}^2$^-{A=2JLdRAJ0t6(zsUnk+-p#T5? literal 0 HcmV?d00001 diff --git a/src/Apollinare.API/apollinare.db-wal b/src/Apollinare.API/apollinare.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..42a4026c97ef94ba9a1d41863c342fd9c7f27130 GIT binary patch literal 976472 zcmeF)349x8oj-obvaQ%T)Fe$snx;`4LZasK5$EVF%SxigmK{k>lhC40q_I7%tWjo0 zZX8OB+LT^grL_Fm{}$*jyRf?~?7@#?ITmQ)2Mg>1+j4Z5<@|9jKU$83a%}(KXJ(!m zjWlB?Wz#y!M=wz1d5-x$@8_A(=&9z>&fD5=_ig$1i+sNGeC+WLOTP5(%#+K$ea8ov zetz}eKl(}9pgL>#OxpBFDa9YU+(-Yz0|F3$00bZa0SG_<0uX=z1R!wM2~=bLV7RBp zfBP0qI9e2Qxx}7fX+jY+NiL~Fa{i{qEiLhMB9=`=vax}oM5L=RLsw+u;?4*?x{`&i zNHUd8>`kO2sgZ0XH8wO9iI1c**>sGyN4iG$=iJJVrjx_5^x??<#No|mRat3DR5f9$ z+!e_t4pCKRb&ETrgsy+i=ko;@O)mXN>u>)25%!tgoJu(B`UL?)00Izz00bZa0SG_< z0uX=z1R!wA0*-lsM|?Zg?T>!{Uyc8k|D-SUM3dUr>9`YzqR^U}Vat-ZT;_H^&+-_+fGX?Hh;>$s2L_g~oJ zU-;ISTr$Uf1hw}Kuz7{rzfb)>g3xwfV98Rtk6_8tB|n@ua0mnf5P$##AOHafKmY;| zfWWyeP#tRthIj7t-=1{&a{N9?)nsMH`So>kV_>$g$eVfSTk^)2IeFRJe4+ua20uX=z1Rwwb2tWV= z5P(3_3$S^PRz5HA?yr36{VjKinNHShW2wTX2S5WM009U<00Izz00bZa0SG_<0_U)R z<357e{qH#XwiEBaaE|*39;)%Wi(Wsc`v}^o;KI;nSXHkI{WkPpp{GOt8Tx+c+o7+A zz8d=1&_5eVEV-E(*YGy{&#Hm>_#PXUn?HyJ|RLc^=P2ZxPiy$uh2|P} zDS66c+$Bo|Ns&&RkQD!9~1-@|W&8^@2%9^d17dT6M6JbLD0uX=z1Rwwb2tWV=5P$##PEUaS z^nf)lpnmztM?W!>7BMeydWgsk0SG_<0uX=z1Rwwb2tWV=5I9Q(7P7ec_XWQ3iG3e< z=$(b*m=`!pdlO+p00Izz00bZa0SG_<0uX=z1Wr$25zEf!1tQ;)->~_-cZ4x7aC(Ty z4FL#100Izz00bZa0SG_<0uVS$1@tKZJ}-68~3thfs>5?BV z{HKM-J2nO<+ZP7@+kYb^;qgBvaOcv_V0hCe|2>B_;b>7DmZ!xjv81I%L6u8t-Jc8N z=|n7>h-6~}Ly1UNU20ckV`qdOUCBaMB$>)4_9oJi)JQgx8XFpl#79z@Y&u5UBVD8W za}8xj)5+mj`fy}_;&3E3mK{l^XlKKTRCcpbXk8Wn@JgQ3|5sSw%{wU^piA z9`6H1q$mq93eM<=hNOX+#w4R{uBFT*N2$)}$=Nz;VJ&7+DuEtT?;OHx)WisN$0We15V zK`J)ZWe!QrirDU;K}pqw(g|Td4V?PUW8K}jHKx%hqJ4V|+4%D_7;ZnZAQ+BD{U>F8 zFvR83xHM5wgcEeQOU}PKoCCv|cv{1wzUW+oBt9t#?0|DSJ<-80D^^-fhv=s*)(n@# z=Lz?YD}wsj|5ZIEZb8aRaXQ8)WJMC_lw14ff_h#@>S_Acm7Noqg&J1x` z93PLl_l}-6FiTp-J#Bs*94Cj{gW*-H{C6KS`mZR_d$$xf{t4Fgm$CKq`iYmBtAEnM z1RKa1na*QHkA9km=gUzs&%4(?F!fr2NpHH@)4b{%m)QuDZmHC1Fb3OeLcy2U~;TwQK!% zXY>&oE0>4mLZwKTI@YT;M@?OJ!f6fF+G2B!R_|-c6vfR)g!3fga+PV1iDcfs_O!PS z2&y=$c%HY0<&rj8oEfeZHK|Mo8eNXLy?@MowU$)l`a6ur8}*=A7E1-{Ldor#E-hCy zaa7PI`QGjLlXN{wml^DH!3%vAP#tRthIj7t-=5TmQ7)I*Gb~Lg`gNhYk1lOwWyW=5 zz&W^F^H}4|s*-(zar8y~#}|)!Y6hNp$xK&wg-TvKC@N~>=abH8GSD8rey86jQ3oA2 z49*d_&oq9(Mfz@BKlFKAMz^!pvOfUdI()%~`7E-0M7LTNko*2Q&XP z%Rfr6Pv+OqbccJk`EjLp`&dYSA3a&oyE!YBr4hAE@6YzDWzNpE6HcpNYm3d*rN-rs zJDc%->F6_O@5~cdVM#E&VT1opzvVhipMU7nPf@3}{#e?mYo(nwa81c`!1(0a#ri4g zt{L4pEtY8lDl1OWK~K|6b$_1cbgoN1E$N1`b6L-FikkJqI{(1zekAagdY@vlla;BX zC4EL`U}$7OpD;B~QdZJ1y;~X+nDw(aqcmffkxqDAw~i?6God;A=(wga_b@+>!#lJ^ z!SMR^{<}8SjFIFNHO(9##_N#V=rEF=!_eRbpZmC&<1I$t0edg*iWnX1JO$SGsWbIj zslcwvJ+A8aiPFTR_bl-qsb~%gkMi@R+tUk!;dSf$uezzG+xO51BeU1_my6swT~B!q zeQuU}?p@9**dt6yMQf_t*=;O6qs@5Dg0ST0IwH=NPTjC97+$&3|Ej3osbd`QO_QFVzpflg2wUyY@>&oH&^d9!HB=+jX!15bh(;=K}Bq z0SG_<0uX=z1Rwwb2tWV=5NLLR^H>AN%yz8N2`D-6sjd=l#4hTR10uX=z1Rwwb2tWV=5P-lrA+U_K zlFtilTldtvA3vGB0rLXqMBk&45P$##AOHafKmY;|fB*y_0DK10uX=z1Rwwb2tWV=5P$##&Iy6@Su6Rxz_;G_>Sfsz`%hqA;GF1tG!g<3 zfB*y_009U<00Izz00bbgf)Vg}ft`PKk5){->1oUhU|>K10uX=z1Rwwb2tWV=5P$## z&Iy4FSS$Iwz&D<#OkeW-kG5i7;GF1tG!g<3fB*y_009U<00Izz00bZqW(0g*Ab!vO zT|atoB#(Ik3=9ZB00Izz00bZa0SG_<0uX?}IU(>O)=EAv@W_>K`|R}oPf3dxF88(h zUf~OVply50@kO`zUa?4PDL+phcN~9FFuZoH|E|k4;b>9JN@XcAEtWLdesxhiorq-< zk!);WC=uzh6S^WBJ0tYyN*1~z$y7G6H<6B{MzWFA*w9cUK9b60(=pl}=^EXit1UK~ zP7cS?ha>wFha<7E>_{?2yBJQSvYUxe+Qf87%sal~uwL&_U z-Ujx{Zq1&NbRxMorMqop9bqL!Bk9DRL^_d*Co)DC>BXssSm#6|BdN$>Vkkkqm`ND@ z(;2;U_XWXlU!VWpm3r3?%Y{l&%v6pt-?B!)s>`MeorA$8$7ziU*Ya}>52KKyd!1t> zsU}KnG`kE-vzKd|nVl)qP^urlJfkKk+9369W1lBV1^0wlMU&HFNxa#!)|9M?YGaK< zG;H;PLeahEQ9&wq=3?ctv02U8z2zLf)^_LEwM)7T-gqn%j}0bh z?D|&(!#zFzd$#Cf_gX;_C*_JN4#{~zAG(cORys#+V}{cjxNgPg8n<*>vxbp#z@~&L zj}+r{ryq`u<0V!oD5B~zkm8c&a*`TVU+{UI$HP8DP9i>CFq?s5qj+3@M#)iacvFO>& zdn(1^sGvhAkHXw6NsZm-Jc~D^JJ06cHM^YM(+Rp@OvbaFQJ>E*g*R>Tzv?#q^nOrM zw2JQ*8O>b^B}5AJ*^|Bq3m3TOG@@UaZ75sD9p@tW_Mi3+5@n5U_0L`_-RGp z1H3L-E{#hQ6-6K6*Qoa4;mp>)vR(gxahE?F46k18f6bTdH)Ok4C_SSUVqCsHVZbXP)0i+; zk~CphQmLmJ6O&U@!i2;=;0z3n4Co)H%*)YXX-YKSck9n??0nN$)4ct#!$(CCsx^r8&tBGFjp4A*U$x23kQ`Ob5)eDt z`Bt)nk#0mnfB*y_009U<00Izz00bZa0SKHE0vEGZ z@_B))4!%tJ{EEUwi#w~nK+6f=f>g_ig_kVsYEjM!3pDZt6KG#$tVVm^feVA-ZQJ}0 z(L$@Ve1HAo!3_&(t81L=(>4@1t%Yh`N}s}twbr7m&INW|)@^rY$V}22(=NH}MP&yB zC10~*yqz#Clq%ywUaKggLMxwI5!gA*l~TQy+h&W;j*JhB0xdslm97gWp z*WMFLj?1NKQBmm^Blb{KZcA@xG&z40g)K~oQ?wem=Sts6bwJSalSa03GNMh;6QX{s`%x1w>4-C^PQEU9Tx zW^34w$`WgL{i?lY>Vb+-(p=Z<9}jO~<`GuH&xmxVYj^WPW)`_+ljijtqC z#kJjYTq{Zwlk7)PsGF$^-7^hP@r+n35~RmZR2bDX25?X=R_tZ8ZT4%Wg4?3+`}AlU zs~Hr_ZrPKxmNE_U`jwRR&ECr?pZ)8*tFtZZUVDOYU1XV^I-Cm{*PkBwYRv41OlJ9& znUmcsgW*-H{CDrv&*^DNl@(Pq|5@!kf16uR>#S{+nd@Bby;OT#o+#C<<8BYYtXLGs zEB9np1;f#({|$mZwvBf=d0KyuQ=NZBoFmbn#*7=4yYzAHXt3%Elv;Npinyw_|lE;Y8$n@S~U(0#qqAMXy zD(SykV4Y|@vL=qxD*q*+m=?!H`fU-n-?QOac-!luw~z2lrCcsbqT;cuctzFd%;1%r z(FCnhd&%H5qwAt^NtqHoekRIFcKK-t?M&8=y~_kBF-U6zxZSv0>-dV zbN29Xo-3^F&eKF)@mWq4u8aBC1!IjS>!+b|USrJ}okuQbtc|t1gv)=|}gNV+cS10uX=z1Rwwb2tWV=5P-n* zDG*^h+O5Atb1mSo3!jd*Sp-MWv8D0 zZCbTu=ekeJ%Ikd-)+pWeEPLM;9aA(ZUlbkpGCA)^vTj##&b zs@f!vxU70yZq~{qvXK<^WM}j)-^Kdfov*n}KXaIk(HAHXRp(zXcAht!iKlhis4qI# zS;NflIG&wHgdXs0H-C;Qa-ovfX!!>B!ECIN;GV|spVW84?=7{D1D@;%&rYsCW7sK4 zT2?|5inE^$;uVGN|I}vBlonM{nHJ|bvCMHNC+(t0%QH0OvIAy|D44n8lVbiRcdocd zr|5AhuixiNheiGU$Xs$)!|$B$dggW3&KEq-)Q9{=Q=_gY$g9Gj&Gq9lIzPUb)i$V4FVp z64P|9k&NfH&Y@;(JgpJNi_A5!Xnh~a?GAT3zX*CA*5VY-ejQN+I)|Q+tYrICMM-<4 zl6C!LC&cMa_>$(leza5ho2JLPoGtud1mux{ojfTTYk3`Kyv87>c-;b{cP~+qN7ORC zDb&?x~`}7 z69j&4u!|a{VtHDNiN#R=c99W^1(yzHVQLf-&uIYg4bhSVBYl;0)hYp zAOHafKmY;|fB*y_009V`34sl)Qa&%R?1{|1Kf3t!=d*c%Hvgz^!3W#-wC-4ZuYc6P z?@a8x+3RdyZ>+3%Xa2>(a8Hl_-eWcE;|z)u6;%`1LN4Z7Ikj6hxviU1o8dXEq2nw* z*LphkvKC$o(a_>4HC0+mwm6rop*3_|RzA^}sWBFFp>oy^?DbrXb!tY12|-`a#U+Qn zSd82L?MenkO%bG{Qn7X)&hUoCQl&Azp^7}=vK$Lr%%w4NT%IZyMOw3m8fwMq+{+8S zNJ|ZqlrtAlvHRD#YKpbpxnzk^%4N9{bA3Wuf^2b@z6#)7V>P3kt#30yi?|uZ`Cn1D z(ajT|!^pOa{=uU{1HVOpujSc0s6g&|t1N29UCR`D4a8X3@6Ml{y1 z>GBvhFW7Qn&b~NvfjEfh`ZkPC*{4GbN|WvSKgW5Pt& z6|FWhyV^vl;GVz^1xc9_=Sp^22(c?xxRI`+*a12$PmAndk4BBkDy@k(Ee=Yw{2VQY znim~Q8rxx}^!KY%$w*fNE-RD1aPRfj$S{U`E~Cl$n>Y(CK!+GSQxer&?I?9dS(D<7&AYb$u{inKaW$)KHZJ{U#X(6P5nA=k z5*b}nsq2E_EnEEetgL+xEozFyE`P17D*pE+&QltHqvy$|bxvz6Jl9E0FCjaQKj+E8 zm|9>2vu;_Su}z3oO`C{N2&NzvBUXU!d7%EYLm(KmY;|fB*y_009U<00Izz zfG!Ya&wO5B^C!Oc+NW;o|7Xk#V01tL0uX=z1Rwwb2tWV=5P$##&Iy4_SS$Iwz=-dnv3^8)8Y-=mQbfB*y_009U< z00Izz00bZafh~-H&kG!oE;;X_ulHTHxbyA4R{z7k;G0|D;eU8xZsE252SN1$3$$-E ze%#CcF7w}8aXS)5 z6|pFZ({f#4eDl^>j|HCV4>=g@I{x6JL0_zy2I4G-!vRf5OZiD!$eH~vM*T3R)VNU0 z%SHWHGh9*=v`V)|>vg-7o#$hnGv`bsiNDaboP({#(K*_9S?}R?Dudn%#TawO;pY;D zp3Js8gD^^YhtL^))q?fG@Xnq7`?k4__z|^CORm?AcVpw`Sx38(=MNd{yoNt`q-UkF ztXC$^GS-bBtMU9L2iH}t?YDtUo_;#S<(xjpTGsRYFfe-H1w7-M0Z<1@MhE7ced#}#b`RuXV)^S z$c0K?qhIWD{~-%n_FtjjtC~q1FH6ddd!BejpFoFQiYIgGkMtWjZZryuQ5G$~KNWK-yhIHR!> zobfbN;S!R0#p-$q;T4hfdlfmi5$uf3C5K0lR;?~ka=1>m=HOQLX_wCny#I&F8*-z+ zp2qhDmcfB*y_009U<00Izz00bZafuferWG6*g5oyU2p?IJ|OX*134A0~YLnjvt^_lGbc(0IS z7LBHLLb3=#Tj!!o9A;e9Jgqk+ti%% z@5W_u8q%E$&p4B5O`?rmR7W&2l8Ou_h7#FCWVW?&oTUve>v%8^oins#Pdbswrjzlk z>zX~Y#>EdfHD`s|$X1)=6thySdR)+Wbe++AF25ug?(g@%VU>Q0%xH4{rh`JUBCu7n zhJ+bWQQfxnIZu*qSx)OL>0ZHHr^)Bx?8gLZ7Y1{l)iQEL$&17Cv^YiYERHj*eI^=F zqzS1s*9kH$P6<*;Doxa%t(<$TKTSGQlWOAlq(DoCiM0MyGL=p2O{60!XP&qs3Y6}8 zIyI8qPK9ITf>$Xs$)!|$Bt=I-i~@;t?b)Am>*N>k^yM0kzL9sRnrrM}=gL_P>CS`E zS)0p&$k)trJx#a5D@6KPQumFdO{F6X&mq(qCBipuO&S>5e&{a1fFvd?-$ zf~xW3sP+Wv%wfF~dSsFF`Wun^#perrCNqY@3w;7JqnIzJh5B%w*RuhA((PlPcKN)( zKcwEg^@g>NY{&Nn&b>JRv={;qfB*y_009U<00Izz00hoSf$gl-d|qJ1f9`z!p26i0 zU|!&y91CbP1Rwwb2tWV=5P$##AOHafoO=TOtjT;{;FU|inf}g|gYU+?z_~XN&|(Nc z00Izz00bZa0SG_<0uVSS1$MAj^Lc^2u}jO-s~<_Sd4co&cl$bj-+6!M>W(*dbS}6y z_%Fe1`&-&0ZLe%=Ykir5j(qEBE1@U;!vckE>9P`HMYyh$?iAP3wA}t7~SQ$(qHYUtC!!N zkt`TR_Ip)sXRO~tO-fX}+I=^4caNa&z-+@cUc0d~*6*e!C3@(p_VAXK{+YVQ&|N}O zUNjn#^xA`wslIk|YxL-qv|s=^wbfT$l&Bv>&a}_7cAl?c2UbF~>boKsz9Qo`%+%33b`cy&8qs?wqxCMWxGv;hnVG#r3-# z6%<-JKuAstX3YuDUD{b2c2|=Wz2(wiIKJ9nUC(!=-!3T^rCe4j%foaptHN5CEb#4v z4SVD%^(Bn`@>JH&x+=S(le>fA9@=}TVeiI3YKJS|sQ08k*1MwRUBU2W zw5Jt~d!jZ@$ka-^jRW3ojN7>z+gF9 z%|bPMwvt$5jVimM)zv$K;W0XLk{&cV*?&aw=LCr7J&+k@fjY4=keI%e5qA!qbesxoy{RGc+l zX-3mS# z69g8m1LM^sj+Z56hTWQKHo#1{e6Ic51-)XZ$q}t~_XNW)r8p#yI4nM61UdT64C%5t z`i(cqwJNV5t<0?3^csv^(~%hL4u)f!{MBs@AuD-WDMqD8^%oYLvH8+D!*mv}i_Azg zM!o);B5B`7ok)>1cA{C8)rz`Uc6;|?eM!$Sth&3R%2qlPudO{3m&?O)p;9D_jI3z- zoz^jH+pfmFTWNNFbH`SSK74s)UC|r2(EAMS(|(_^a%&pi=@eq?-D z6x51h-F@%8g@2Yi@Tp;JDHp3?O-TB>CH%@=@83u^aVQ%}*?9&+m7RN<;2hO|fbKWhc)HodZyBa@PTD|IA^n-79uJvJ^U9|k9=(bYP+-rUC z(W)OjJQ(Y`O258h!^jzpHN0HIF-x=a1{JMcIO%l5ad4?Q9&2MjH7U-JoHe+FH0Pot zylMl;y38}_&+t-Col*KEzmt8M=ko#yY40C@bmF#6oowTI>9o}pBFg(;NpFM^cUAWh&zV?RP(RYy;*6Y#_XX00bZa0SG_<0uX=z1Rwwb2sE+4rL1jyUf@f?zleOa zy3LPyfhO*8Gz|g}fB*y_009U<00Izz00bal3tYxt@_B*lR=@3Y$FBB0ig^JW2(KUj z0SG_<0uX=z1Rwwb2tWV=O)PLZYa5>zh^_nauh%{N_19otpox1NO@jagAOHafKmY;| zfB*y_009Wt0#~q?d|u$dEB~eN_rL$@vzQmKf$$0f5P$##AOHafKmY;|fB*y_(8K~) zvbOPgfm=iB7q)+7csJ$+nz+Z&GzdTd0uX=z1Rwwb2tWV=5P*O!a20#W=LJ4+^8VjV zZr=QFm=~~t@CpJDfB*y_009U<00Izz00bb=!~$2dw()s^)rQ1yef)$fhO*8Gz|g}fB*y_009U< z00Izz00bal3+!bt`MkjUzyG`IemDBv`!FwH1K||}AOHafKmY;|fB*y_009Uz{}fB*y_009U<00Izz00eA-Bzwu{1-}0F?Y~H^ zeg9pU7qEfw3IY&-00bZa0SG_<0uX=z1R&7F0@tv%@p*yIZhz@FfA_#|zJ+;#Chl=G z4FV8=00bZa0SG_<0uX=z1R!7w>}N0eynytTu~*-u-mnhy0yYp{K>z{}fB*y_009U< z00Izz00f#?V2HJi&kMZv$t54yD;}J}yg(E8IGP3l2tWV=5P$##AOHafKmY;|umy(M zOFl2~iVNQL;{E^qnH=T?Y#_XX00bZa0SG_<0uX=z1Rwwb2sE)kinWc;3!FUo=rer_ z|LHZ0Y1@VVD|{`#Xn9S`j?e?43zj^%WZ~kOMgO*_XW^Zl-{`!e<9{u9c0nh$K^ki9?Yt&mvuskyNCsreIfOW0zfHSF{?sE*Rdw$zSc~`yQ63#VN6* zrA0xNOKOh)ot-I*{I!`j(GW#l21j8dmSMSi;bGckpT8QncUhnpdQWulSec?n4z-83toG0F(1zr^zyivpPtmg(yW*Qz{j6|TbZnqKylJJs!s|{<6SI`dXo4miuZJ2cuFYfAYiwb)>IJ)^ zVk{W$U+J%Q)YZ!aNEVDD`@Jf+GuH2+CM7Cf?Y^5qK@;>Hm~FVmYd3br`rXu|M5~Lg z4Th66AbWUok|pLOD>HGq!VZXQ1x1{cD=H0+x}>8GyROSnTh<7oE(2q&slO}wii31$ zQZ&|~Syw>sy^315RhTmn)^`=vGX^s`T3t343=h-(cYA4AdnQED`oo~QVa_nD0<5%W zG|pVnTeHFNWfapoubAR;p+1zH8A`)3Yt?P`E-UL9h@Kd&c4UI#y%fL}&j6@36EtEP zM##(N6p33!y`!+RM5`Op!SG8dmNBnbVuc$ks>aT!{N&gW)%9~m0 zy;0O2+qnTj%TLxj>pOnwsSQM(dW4gf~|`MuhIU*jTrANFBOn zhz>*w(mD{`i{@d zMAw2@sVwU+>4c`YRI-Mqks~`-q%IY?Mr!0^qP6>^(Yb10Fg!w$Ug4o??tu4$Ym}K& zjFML}M}$^3`na-}PKG@-Cqusf33`(iPSBOQWR0c9q_k&TF2$We*%|6aVQ^0{oY>;8 zMx9=-(e|2qrAVqP4KEqgb@i2_d zqn<(36?2AQBsv3l`5;{@Z>YIeHusdz(?{5b{To|Hymnt#$gy`L&AIcN;=%B4`V_R( z*?Rqma(2M5$1WXNYxl=?#10Z(O;?mY!Rw#m`MkihxmQ+`?`^vl-xp|@+rS10KmY;| zfB*y_009U<00IzzKr;%AvPSWFflV8}fB4BC{fUHmfoAM#v zfd(gRfB*y_009U<00Izz00bZa0SGjsK$bO%&kM{P{`S}2`$)8kd4Xo^YP1Og5P$## zAOHafKmY;|fB*y_&>%3zw(xm@&pvzA$3LCD_TMls(BOm(5P$##AOHafKmY;|fB*y_ z0D)!{ILI2s=LLRnCjyg)N{HQEFL2tWV=5P$##AOHaf zKmY;|Xb^ZQ+rsAs{{C0*i0#;$coy>l4Nlkq0SG_<0uX=z1Rwwb2tWV=5NJk$>sX`s zyg@D3N z+K+jG1}AKQ00bZa0SG_<0uX=z1Rwwb2sER>%UGlMyg+h$<(cBo-}@8H3p8U_qfHQi z00bZa0SG_<0uX=z1Rwx`27w&g!si9P8G31T*@l1JiFttrCv1QK1Rwwb2tWV=5P$## zAOHafG^4-`tWkVk;6Dqi4i87FpTWF9Gj=uF1OW&@00Izz00bZa0SG_<0uX2r5ZD$z zFL2p4VgCpI@|H(2FVNtG4G@3;1Rwwb2tWV=5P$##AOL}86gbKn#peZH^3->qnfTz{}fB*y_009U<00Izz00bHY@@xyA7dU$C_{YEVv(is7FVNtG4G@3; z1Rwwb2tWV=5P$##AOL}86ezGp@p*xt760gK*ZlO4zJ+;#X6$OT2?7v+00bZa0SG_< z0uX=z1R&5LAhIodULe%l{qb<%2Oq+`K!X!DKmY;|fB*y_009U<00Izz00f#*V4O9I z&kIEV_VH&fj6AU#^8(G-)o2q0AOHafKmY;|fB*y_009U(E;!x1H()YA)$z4nQi{(YXSn#U_rQk1v)%JgE zA8z|xTdMVgt(OEQTApZ$`d{mNdeQj8|GRKs=jS^Qb*LS`=}>25s-CX~!~6UF)qbf| z5RWGhDK@Y$B3O4JHmny5^{%D>9Ob zbh*{j71`Kj?xrhRy|5SzXQKXU!V%W6JS|R%B~8sGj%%V)5{hYYTvWtTUev3!(vQ0b zWu-TkuL;b`#X@S>5e4_6n}XqC3U04QaD#Hb!oo~drjCk=GqRh_$i`C117i(p)~<8T zI=rH*%N4CgZw!X7r_c_2g(j++RMNZlT0s#f<%%kr{c7!ddM>;{*GfENbk7~FhNNIP z+2gPFxW+mpl|(g{5fw=&)4nASv#eEHllQ`a<-7|0$rOUU3M7Mghdm<@Lp=dmRXumdo=T^^hnfGG35uTeIz^T za306Y#_9Az;W$q(jsm%uqDlE;{V>!6QcfixtNd(acBbepVlW(A=dZ4H5DkgbVo}Z2 z9~H*tWA1S@mZ=HBNMsx}b#_H>D+I%F+WUI9y${KG{n(7l1>=lnR8pRG_qByw_wUFM zE$4&b-4wu5w*YE~hOuvX)_pe)2D{ljEn2=ufK!iVzH6vC3av$s3TCR9R8QTvjT}#%m#2FgH#$h`HuA zqA`=$Fmcq?6}|a}VED34{^|;ApJG{7w5&K)E(#i*8&mR>n09U}HtyP)K`&(P*_py3 zU^UV?q&*i5Cuq-6uRX_U{}VEund^3c)114vN_xg%=ZKzoSulLndVh7bGmhkxFhR|? zG8#b`TP2SO>dVyb-biEZx2hzBqe?LR@_zsA&a*`Qg^@LS9OF&DY8oUYw+o$H8$xu; zsWGLAkhV=pjA}o##`pvtx?Ep$7U*BR?4$qR%Yaik{4^v0?sgAK4%#9 zp z%{Qnsb49^(vZ7v*IPyeiuBU^a-lpmf{yj3g{AbU5gej>wV;ubY*2#t-Y7%RT82hhD zV`I~Y@$gN0rf4=K4&z%pl2jjhUl$FjCZ7U{Cr#=1iNgh+a{U;G`wrq<;$_M zo-yc&QTibJOn85vzZ$m#C{&7KrgBuxD^i(#U}b&7wj~S3zmBwmcQD@d@SqxU7~N#- ztSfrtP$(^2LvDh0SG_<0uX=z1Rwwb2tc6e1vJ(|J}>a|fj^7g z_>w1zm=|dJen$f#009U<00Izz00bZa0SG_<0!*O7p7^}L0~d!5Jap?NEtnTzDtJHu z0uX=z1Rwwb2tWV=5P$##nqFXuay4L?j%nLMqzoUT=fB*y_009U<00Izz00bZa0VXiRp7^}LZQURF+aLec zW!GR{fT`dC0SG_<0uX=z1Rwwb2tWV=5NLXV6Rd@NULdmV?+$$R@%Q~2^8!uZ?`R+d zAOHafKmY;|fB*y_009UuJ@I*g$5P*m-GAW2w_;v^so((t2tWV=5P$##AOHaf zKmY;|XnKKLSPS{Qz}LS1vFgCu2QSCGK-2d-8VCUhKmY;|fB*y_009U<00IzT0EyZ}?d0|F3$00bZa0SG_<0uX=z1R&7#0=Kdj@_B);KA3&_o?qN^ z1Lg&qzTeS62tWV=5P$##AOHafKmY;|fB+M?jXm*sflvP3m221DzqlRq0!#%D2tWV= z5P$##AOHafKmY;|fI!m=yppw$&kGbE|NkEO&G3r~m=|dJen$f#009U<00Izz00bZa z0SG_<0!*OFp7^}Lq95FO?6P0H?+E4vmcfB*y_009U<00Izz00bZafu700Izz00bZa0SG_< z0uX>e(+j+cwUEyXJo))`58S=v>4S?G-R2AX?)5GF!NS4L`#XNpkzerd3l;^Z+rQU- zQ+rF>@z!6p?hCxJ<u-X<3Uq$h3@{T7i7hGa_gBiKap`l^E`%eKUZk3-c9y?RC+EEPz!wHHi>K)a9pyXYRUVjQ9nT2PGhv$e^_f=bh zVJX&9J?0gR*+7Yg(I^e9l4iHKUd+zwd)=II+L;^bb&uRGz(!nsEHv`EtNvj4Ftz2t zDYRuzrC1ylw8>f8@Y>U91F!E?Vz=@}tDCC6VE6#VJaP&#yB#*RitjaMymKBucA1>_ zfn;Z5+{O`g+ik({Wt&>6>v*sSB}J&5e)aNY^jDg#h6CNhLjsq zMWrN6iN>G4d%YT8m)+Q7kWHInw?mj}a1((d8=&S-M}rh`JUB50CaQth{zWT7s}cXvY+ zbvbGa8==%?VDVUoXR<)?EI$zp4^uq5z2dP#qY)|ARp+~F&WIevJ!5j_idKhbg5jGs zwcKtEg}NQ*Mny%E3pw-cI9aG|d0k%Lon8Z`F5|3~dWLR9(zQ9#Wd+0ja4NSR4~FBU zyxvPWsm6=4DmHZa$vJ^}NzVYS40Nd9Tz9C~3s_C4OYq$>XB<{h&p_-P(dydiU^vs? zQcbvn3<)!$GRv`Gr2B54BdW$~+=4T5H69w>m0p29AoT~W z8EEy?0;Hbl1G?)WS1sm_0O}DeCk2|4;bk{r=n6&f4477_L3x|&mPd#)dvV!Z1s>g!i-BEvasiTd2?5er!OxZZaTbo|t9=w&V7paNOO3>f= zs5*MLEeFF_&`wu+>@=eZT1C|dfmQmJxpr$5YTR*kd9{OvSC88Vlp#59zQ-ojL2+ED z6gBHCZKe4hYS?vSCi?VeR$xTXn1eOn&J?YlS6x8w`xKV-zTa4;@iV0z)q|%NmGPN$ z_Mpr>(dxo#Fc^+eOxt+RxWweRK(^y~qZyC)g|U^_?Sj}!WKPC~FnyB0t9D-CUj}bp zc=gc>UX1SxoQXLO)CmCyKmY;|fB*y_009U<00I!0cLH~_i1_yfj_-X;{o=cm_hMdP z-t`j#f&c^{009U<00Izz00bZa0SKH4fqPh`d|u#!y)WB;KyLW~<^|3KHR^-_1Rwwb z2tWV=5P$##AOHaf%sYX5SwwtZpt51l@{fFa%f*-%n0NhzfFJ+?2tWV=5P$##AOHaf zKmY<~Lf}4DDW4bk`Cpyf|ICWFzZ&xbXM!4aLI45~fB*y_009U<00Izz00icpz^hqA zd|u#9e_vMrBei%L<^|?mKOrCpKmY;|fB*y_009U<00Izzz?l$u4Xc#T3w-4Z?*Mmm=`z`)Tk2z5P$##AOHaf zKmY;|fB*y_Fz*Cj&m!XU0=JzXQj4o!b|K~k=3PG_AP7JJ0uX=z1Rwwb2tWV=5P-m$ z5V)UJ%I5{fcVGP3zkBmjAHcl8nV?3U5P$##AOHafKmY;|fB*y_0D*ZY@CFtUpBK2M zV{rKnF}W4<0`sn)5D)|)009U<00Izz00bZa0SG|gOb9%{D&_M6w;z4$XIFgbu7Ah8 zz?q;%oe+Qk1Rwwb2tWV=5P$##AOL}RC-6oV5uX>h<+?{y@t@xQCzuzQcm0HbAOHaf zKmY;|fB*y_009U<00L)1;6YX?pBK3JMOWQ7`L4^qiFtuDL5(^g009U<00Izz00bZa z0SG_<0`pGbA&TfBJ}>Z#oogP@!p}@&USQt!69R$&1Rwwb2tWV=5P$##AOHafoC$#z zs??en_~Ya+UjL!%9)5B$+tYbIU+Bp*u}jnm0SG_<0uX=z1Rwwb2tWV=5P-m$6?m~f z;2Y`f?(N^w)3c>_cciE9((OAh?d#dvyK84p_pbg;-QAaVcT)`QEDrvCftOD7NKb!v z;Ro5gz~ayjU+Cwd{|Nn4=)<>(5GMN&j13uyA2ul|L=1vZ9K?MV*1w{+OakdATSp z>g=FmW13zpv>>ofFJ@#F+7|}{YxE7Hf~v`_OWMt9N7S;UiGihUfem`{u%L;GBoxK= zMXgl%kenAZNiMZ53k0J6xLg{SCMt?>f;LHk3tw5=fJr8pIvkf~tnLO%rqI^;pnmTDJ)is8%kO6M0&Pn!^@ZLOx^~IWmpn`_@qhpXAOHafKmY;|fB*y_ z009Wp2ncPSlR^mmU6%9Jpg?(XmH*}i>yUtizE)^UBW($(UWAQiXf z<*B2A<-RrBd>fXo-DU@VP*fuK76nk%Pe}Dgu z9Xq7bxP0|AE2owPR&jSvZ{PO*9XogJ?mpN;I8ooqrE4zJ$@ItvB%zd_lp;d{eQ;;{ z85T~QkV;adw|`hJh$Ufoq`QB&(flIIv-PM^!j65!zudQOEupX3Y1f;URaK(O6iH^a zrsXLLd_XSB6D47Qsw8O^*A&aJRbiR!3aOR_tR_A$aAfye|M%l-7OiLV01Rwwb2tWV=5P$##AOHafK;VB&AkwkZU!f2A5h2gM5g|vW=xYH} zazv5n`vM}}Ca|}q!xvk6fYq@#Ig(B#bD4N-Fp)YE%Z;WJ2dU2F!I7T+y^^fZ_X+5m z4AVkUDoEO+Z;KSfVx+8y`b`Ye@|FG$-?mVORWuwslrxIv^g2iSQk5xDp`8o#6#?2i zRbv#3lxc#1z67C^WGdIm3iEk^%AR*!_3W`^-nft8ZeQqshvd+OOP*MAH_L?w1Rwwb z2tWV=5P$##AOHafK;X;>>~CA@>)S?OkoWo4E;s%YHr;ry$%;_Y#K<7~&b|3H`Mq@e zy&{+7Df-HMzx8c-JvkDe6vz9wcE?+n`l6fY{sXFPg;Ci)S)ar(Tj(C2mbb^DB_ z--IvHy#nUl2N}A_A);s8O_gKx(d`Cwvw>MjY(kN=3f=HPAF`9h$f(TjFkp4X zg{iVY-`b};8R~937^+AUl998YDjjUmBU@P$*{D#Y+X>{zU?oqHjY{+f-F`vsE>LLv z{(@MEDyPK*G--SH*6yT#sc(~k#X~zN6^jDR8A#N|f+De?^grZck#6DW?yiN(%Mm?$ z4+TWMNwL`T0v8l}?zrU7K7JdU7ibUd_l15K`W(#-JQOO@A9z3j0uX=z1Rwwb2tWV= z5P$##AmAmiroF?rZk_(E{J}(gBpo}F97!by#XL=J=*s|ZZ0qo?@6$8Hve|Uv$dOof zEFFt!nj+F<0L?hiXa7oGDhaDwJA7-`>N&F`2V;AOlCkVaR-P6nXc+}fUP0ejSh-S9 z9v#WVvWZbyrDX+I(U%a`tkE|QB;)aY$wWFiAm!Ot6clL%eX)R-q1v)Znt`B&9jGzO zScCZY1s1RFpIrXt#X&YN5D2}W%?o^i{=owR5P$##AOHafKmY;|fB*y_0D<`<5T==d zRjc$VfK*~6MY9GeQ7+MxL73(S%!Gm1@IW#(G9XMHl}d8Wyg+PlY$!XTFEqgJBUsLw z#peb3(l6ij((HSF$mRuFLw~~N1-=~mAicl?0uX=z1Rwwb2tWV=5P$##AOL|EKwt%Z zcfkA){R8Q-RD9onQlXzUSVc1f=9dFvw9G-0El3cfbps?#nwFQ+?7)f@`m_P9e-NYF zCxn`Lfzfm z0rPo*8zx`-r9FST;XTIp13ah7GlCuT=jGXA?fQRz&31S9nk&D_ML9xC<&TdGk)x7O%FC^-yXYt7&EK7; zAB7)1a`i&0;&5D4^}J2yfYJ+iBVif(Ew+8dXPv?@PEZWFqYN6aWSL0{aDej!j)RZ-DedFZ^{RV`HRvc__~X1Q^>RFETO!l!N% zNu6Ocmy=OFJgx>pBMP_k-sVTz2>5a*}TAlP>V10jnKzK z4~Omu9StQzFQyH6KmY;|fB*y_009U<00Izz00f#!AhMv-w<=2CwWr5uI(d-R@Jmdy zZ}~??6=|CO%X-VYV5e`*mJU4!{m4KfH5eO8n0aC+B(YSW9~ca`chY~}H>w$mjinQ@ zA)%s(LSRi>C;hg7kq{pl9vw==QmK)+JXJ1=La8LTyr`7|SY=cnPbV_T;aFS|RcT7- z3eb)>nJFW~V=4N9g9Jr2#eTDJs#22Vl6X-I1vb$y@U2?E*5^0A1(KGJ(tpR7Yi|0N`uh5I?%cVH z9_{Pf>8t#!tjv38rM|puE&)KFJ@2z0{d&S%>m@flr?Q z6?k%OXAkRq9K%=c;Er|HHGc0)ox{lDsKv3Bdmvi~Mw3ZtbWi8#cKv1()=FMW_YSL;X}=!9 z+K!G6|L&F+e~WLGzG?tP;Lz^jWqWs9WqWoTd!~B>O#k!kZv;frbo+#~MQ^(AA>Zxi z`yT3{n+Yfi{mvDte}FZU&kLOQncv<1$Sk^a1pJqJ4V*B$!v z0`}Yz)VH_m&u#klR{c4kZ)s@__&ZuD`Z#L=pBH$fZF1vBm*07@F)#3-k7fiOr0q(lUqv^F?nsTR{Jh{|#Rm2~F{ zeYwH}YbBo-_*|j)_xs=VjSJYkKwIcb#=O9{L*J*Dct8LG5P$##AOHafKmY;|fB*y_ zFz*FIfpz}w-Me<~=-=Mg+tc0F8u0mB^^HBfecStY?A*1xo8|?4Y+gWLyI>D%5}y~i z?~dMM7rgtDdyRR4zhu8t_~FoBhMo?6ao#r^0YU%*5P$##AOHafKmY;|fB*!VSYT;j zgFhuIQsgC(_|`~9DYl&-*y1-=Z;b4fDymp6i>-?TYy8&wiUC&0T>3D=em7BQSrS;+ zp)W-kp`S63WCGjE(D=N-`7iy{S1U!&xmY|tlFE(*E)K-}dy^ySR5F)|#|9IrBeC3Q zI&qNlB@d3YoFCZMF&sOTGdAS(oFmkveXLo0ULgDZx4!M|zg+cuHZRZ``VqU2;J-po z(F;5v009U<00Izz00bZa0SG_<0uVTh1?(Rr29+~= zy9xMTeSzAv-j+pyH61*S{T{(J42RDPyk+Q(zxVgQ^>2)Mfk%9yFNS_dzfSl_^8=!V z5P$##AOHafKmY;|fB*y_0D*ZZz-}6#Py0KzboXrO-4*HXzEuBTYwh-)K5Kix+}^*X zr*})wc58df1%bW}sz=}6yW84ME#A*s&F2L!ykYz=4^B@1u`w@T-be7G&{OkJ1|dNJ z0uX=z1Rwwb2tWV=5P$##AaKe8>^_1(tA00u^FD$hRt29Ic=nf{zTt~2ulcbxFYr|8 zm!aQceSuSEMn(ug00Izz00bZa0SG_<0uX?}ycA$F0_V531{V1BX@Sm`)_}j`dk(^SfUP@-?+eV!9z{?PfB*y_009U<00Izz00bZafm0W-zb_DU zeqSKZ>frMNpL+Y$KP-B1+lSe_KwIcRU+80@zYaYadNA~pQ{NMELI45~fB*y_009U< z00Izz00bcL2MDk=0@9MI$%;_Y#K@o!85R^tZd(?J`uEA2Sd7HQlBNhnv2|r&r$08K zNLoda)G0ZVEJjA10p<^>jnp77CK z1W$+lGxTq?#=!SNPlUe7^5Ovj2tWV=5P$##AOHafKmY;|fWSN!U^4(M3)p9WePSS> zPYkrQu?@7qKnE>9$fgC@`T{M~ihJq*5Ak_{t)IO6|9$t~wxeubpf&WkkG?JN?a)u? z1s)K900bZa0SG_<0uX=z1Rwwb2%HrHivw%?a#>Uax)DKa?F_8+=PP-sBm_DFVgHm| zl7yB8fpCXVn377=pf=VhJ};2F>gXpQ8@}P!i&?^=*ZV@*B@ZwD<>DTCi3bEA009U< z00Izz00bZaf#*e_dU4w_->TKs73)_n_b>3R?ugTFk5z=Cq|F@A|9SLnahYzemWxFx zVkA8Jw#|_VQPm{5Bt}YdDUz2<1|l4w?U;l(A9%tiEzZdTvJvG5}{E`C=P1E;9xc)l_C{2FKNQ& z$he?UC`8IyC^Fj;^^___rj2?=Woqxpc!sL(;lW;P2dnSWJI?;<_LgP7HLI%|^ue%Z ztzlhJ@{iUvIABpX7^_%f)S~c)}{k#_ieTf1vGl zFYlHs_JzSGAeF9tD4JH+Z#h$4d-{9i|6LLFKX#B8Se4d~938Q~s`3Icv0M|!#3Avh zI4JguePXZJEylz)krV4guRO?x00IagfB*srAblvAt%DV-prOA|+ZK?Q| ziKwy6zpQ$y6=`jhf96+lhAlbcU?>);g&p!rGE-y3!^R5#@Ss(ZIW3uKiL``d@J(*s zQVA(JDWRNbP^QdwIHTj`Y7)#yG#DMSY`c?pvhuP~nNP4n&AO~$wwmcteFgf|BMkBa zkK})(FMacp4mFM-EbiCDV{*p9`{n!s$JAMD2q1s}0tg_000IagfB*srAn<+?P)Ued zCzr5FUQG5E38_&7evb@84J@d4bZBTXmYDUf1hOst^0z*}fIH{D2Ohlk@y}KB0?%u5 zzQO+z|1Mq@&%d8qO2q1s}0tg_000IagfWZ4t zV1C?4>Ge_fL2rAn;fy&(X0qJx;$N_XPWHAJN(kBYe$bt;)hPd*ZFz3dGiUWUf@byb zf^h_0FWz?NrVkvjR9?Vr-Jyx^iV1Oc?4{UOV;9BD*6+w7HUtnr009ILKmY**5I_Kd z-&$bUjB9P@>Uz|Snx?L)xfFBjH?PXsa=tz_C1QKg9+jiSCoDOsUoZ!q@#e>;oiA>n zE7?2HI~4UJ%6L;Fc1%Vrxt{aZh{H1n>=|s70ke8kZb20{=Q1Ok%a*r8j%WAhKWwj* zt@>nmPQ!+o!*y?r$Z!L?Zcf%OJ9NQ_9RKc(kGbt~w*2apjQ$jj;Wz(q=1}QuScY14 zLa5w^p~~b2b)7%eE&gv)!7~rqCHPN9xTq$VeTsGLt&F?52%=lVp@yWg6$r z9HM)@E*sP*pOxCGmC0m3R-22_uS;E~X~r9a;CvEHB_A84cVP` z{<3xMuIbYOeyE=Ha(RWs@5@jL`CYtLuaWhGBleh6uqIr&u%IK~t{Zm>+ow+n_+h%w zlgk|>R;V!5=4Iq_8O&_1PDp6C534MWe8168F}da4>FE;#DwvvcaG`pvL0;eog>QZB zp~yXFsk}ggSgVO!<&F&j1Q0*~0R#|0009ILKmY**rV+TLA)zfutyV`C%FZCwKikW0 z78i`vS0ikfJuZr}>xMgMm-Dt-&%>Ab^1PJcuu@1j=#M+a25gxAbRjVP{fGq!y61WvOrF5=#L|ilM~9;d|tIE$P4WI)GrUr zedMlJ$qSrf&zb5XfB*srAb6fTrdb(A4|_ni@y&Mb&~JFVJxD zb=S4*xa%t_FVG-lUf?ykV?zJ|1Q0*~0R#|0009ILKmdVLU%)gLsD%qI7-@(a3rv5# zL)oamQVk380uSBxx8J^gXnuP&FYub*U*OblI4wW`0R#|0009ILKmY**5I_KdDFRd1 zBe+?X-yGxx(rvpspZ{{l)%Co?q18mr9@mAo$=Q}CFEV2>W2v4gl?L7I<1%)* zsnuAhub+@-IH6dJJhs^>IsSV$+T3g`(sPdImOZ<%rO8;V`!{h5PD_(1FWXY_FB4H? znSWXJR4dZjXe{vyhApq+hGLOg*dedvRBR(2HdgqD2W4H$j#aQDEg=~^Xjyo!VXV}% zZgGb*I$pN+$i_NGqCqw$>uz`QPS8Xv)U3-2W($t0Mp!27)YjVex%7wGxDA3c&; z`klpU9Dy!}5HN~B>98Sy00IagfB*srAbk z@&dE(dD9{U5I_I{1Q0*~0R#|0009K13K)i|)+xM6mDTeC-~9CLdl$|5K{YRMTocEq zUcqYw5I_I{1Q0*~0R#|0009ILIQ0eOJO^fVw1I5PU&`P5dIY{xCViOx0;m2{ zrUeKffB*srAbXQa%LikrVISR7!JQ?$K!!dQ7?vB!D@YNdsDz8*ol zdThZsg8$t0FP|B>d;ce@>k-_ki96rThe|925I_I{1Q0*~0R#|0009ILc&7q6y-n-r zlFJaNSphVEIRdr(KsJ+GtyUaZom5+D%Hjjb-2V{(+XCSPvgOx0)AtRuj*vqihHufB*srAb`C>=SF*UQ4h4CE>-zusnbm7PFvk1>0T}-xfB*srAbSCe)!IFYwy& z$}Pngul*%?foZ@fLI42-5I_I{1Q0*~0R#|0U=|6~<`=j^H7UpoT;IL&7Ynv*d!w2c zIHHLov#7x|1px#QKmY**5I_I{1Q0*~fzw%FuF+w>WmQATu}hHzd4ZPZWBpGaeR4)Ovw99eYyLx<10g}lIN|M36-0R#|0009ILKmY** z5I|s-3)J!g5!K8fFL14S)!4OL|K_?@S=JW&tR{|$L*h|!Q0y1`#9pymjEQX`C)SBx zv0TJsM`KUN9*o@`+b2(DLjVB;5I_I{1Q0*~0R#|0;GGM^+Kgr9NX5-xw#lBb3trni zBUODr=uX&UcCoU_DcW8t?rPoV0xp}MN zxJ9qwj5$VTvb=V7)~eW}ZrQQDP(m0TdT`5}ZF%w{GZr(J>X}k$(A_>RV~3ksjfML9 z30Wy86l;;kHajK9fA2<{n~gk|!BN!+%LleZJ+vS%u=caK6x z&b)&s>9KHI1Q0*~0R#|0009ILKmY**-Xnq9I)x>5yC5%cv?u=$uW!AfKwjWIdI;PG z0R#|0009ILKmY**5I_KdlO#~f3tTR5Utg#2)8|ExXk~pZd4ZGkP`E7u2q1s}0tg_0 z00IagfB*vTk$_>ykp!C7u5K6f7Z`hHZuwLB`_8V;J6O_$-lTR(BIvkO8IjVPJ29^?&%-soAyw*x->28aHagM_ZRrq&70TW_~++*oBjgo@vtF) z00IagfB*srAbKKUz*_f=m-N`#a6Rl9Q zE-RQVII0?9`M5r)9$AnV5EtHRY +/// Codici a barre aggiuntivi per un articolo (multi-barcode support) +/// +public class ArticleBarcode : BaseEntity +{ + /// + /// Articolo di riferimento + /// + public int ArticleId { get; set; } + + /// + /// Codice a barre + /// + public string Barcode { get; set; } = string.Empty; + + /// + /// Tipo codice a barre + /// + public BarcodeType Type { get; set; } = BarcodeType.EAN13; + + /// + /// Descrizione (es. "Confezione da 6", "Pallet") + /// + public string? Description { get; set; } + + /// + /// Quantità associata al barcode (es. 6 per confezione da 6) + /// + public decimal Quantity { get; set; } = 1; + + /// + /// Se è il barcode principale + /// + public bool IsPrimary { get; set; } + + /// + /// Se attivo + /// + public bool IsActive { get; set; } = true; + + // Navigation properties + public WarehouseArticle? Article { get; set; } +} + +/// +/// Tipo di codice a barre +/// +public enum BarcodeType +{ + /// + /// EAN-13 (European Article Number) + /// + EAN13 = 0, + + /// + /// EAN-8 + /// + EAN8 = 1, + + /// + /// UPC-A (Universal Product Code) + /// + UPCA = 2, + + /// + /// UPC-E + /// + UPCE = 3, + + /// + /// Code 128 + /// + Code128 = 4, + + /// + /// Code 39 + /// + Code39 = 5, + + /// + /// QR Code + /// + QRCode = 6, + + /// + /// DataMatrix + /// + DataMatrix = 7, + + /// + /// Codice interno + /// + Internal = 8 +} diff --git a/src/Apollinare.Domain/Entities/Warehouse/ArticleBatch.cs b/src/Apollinare.Domain/Entities/Warehouse/ArticleBatch.cs new file mode 100644 index 0000000..16ed97a --- /dev/null +++ b/src/Apollinare.Domain/Entities/Warehouse/ArticleBatch.cs @@ -0,0 +1,145 @@ +namespace Apollinare.Domain.Entities.Warehouse; + +/// +/// Partita/Lotto di un articolo +/// +public class ArticleBatch : BaseEntity +{ + /// + /// Articolo di riferimento + /// + public int ArticleId { get; set; } + + /// + /// Codice partita/lotto + /// + public string BatchNumber { get; set; } = string.Empty; + + /// + /// Data di produzione + /// + public DateTime? ProductionDate { get; set; } + + /// + /// Data di scadenza + /// + public DateTime? ExpiryDate { get; set; } + + /// + /// Lotto fornitore + /// + public string? SupplierBatch { get; set; } + + /// + /// ID fornitore di origine + /// + public int? SupplierId { get; set; } + + /// + /// Costo specifico della partita + /// + public decimal? UnitCost { get; set; } + + /// + /// Quantità iniziale del lotto + /// + public decimal InitialQuantity { get; set; } + + /// + /// Quantità corrente disponibile + /// + public decimal CurrentQuantity { get; set; } + + /// + /// Quantità riservata + /// + public decimal ReservedQuantity { get; set; } + + /// + /// Stato della partita + /// + public BatchStatus Status { get; set; } = BatchStatus.Available; + + /// + /// Risultato controllo qualità + /// + public QualityStatus? QualityStatus { get; set; } + + /// + /// Data ultimo controllo qualità + /// + public DateTime? LastQualityCheckDate { get; set; } + + /// + /// Certificazioni associate (JSON array) + /// + public string? Certifications { get; set; } + + /// + /// Note sulla partita + /// + public string? Notes { get; set; } + + // Navigation properties + public WarehouseArticle? Article { get; set; } + public ICollection StockLevels { get; set; } = new List(); + public ICollection MovementLines { get; set; } = new List(); + public ICollection Serials { get; set; } = new List(); +} + +/// +/// Stato della partita/lotto +/// +public enum BatchStatus +{ + /// + /// Disponibile per utilizzo + /// + Available = 0, + + /// + /// In quarantena (in attesa controllo qualità) + /// + Quarantine = 1, + + /// + /// Bloccato (non utilizzabile) + /// + Blocked = 2, + + /// + /// Scaduto + /// + Expired = 3, + + /// + /// Esaurito + /// + Depleted = 4 +} + +/// +/// Stato qualità della partita +/// +public enum QualityStatus +{ + /// + /// Non controllato + /// + NotChecked = 0, + + /// + /// Approvato + /// + Approved = 1, + + /// + /// Respinto + /// + Rejected = 2, + + /// + /// Approvato con riserva + /// + ConditionallyApproved = 3 +} diff --git a/src/Apollinare.Domain/Entities/Warehouse/ArticleSerial.cs b/src/Apollinare.Domain/Entities/Warehouse/ArticleSerial.cs new file mode 100644 index 0000000..7f4343d --- /dev/null +++ b/src/Apollinare.Domain/Entities/Warehouse/ArticleSerial.cs @@ -0,0 +1,129 @@ +namespace Apollinare.Domain.Entities.Warehouse; + +/// +/// Seriale/Matricola di un articolo +/// +public class ArticleSerial : BaseEntity +{ + /// + /// Articolo di riferimento + /// + public int ArticleId { get; set; } + + /// + /// Partita di appartenenza (opzionale) + /// + public int? BatchId { get; set; } + + /// + /// Numero seriale/matricola + /// + public string SerialNumber { get; set; } = string.Empty; + + /// + /// Seriale del produttore (se diverso) + /// + public string? ManufacturerSerial { get; set; } + + /// + /// Data di produzione + /// + public DateTime? ProductionDate { get; set; } + + /// + /// Data di scadenza garanzia + /// + public DateTime? WarrantyExpiryDate { get; set; } + + /// + /// Magazzino corrente + /// + public int? CurrentWarehouseId { get; set; } + + /// + /// Stato del seriale + /// + public SerialStatus Status { get; set; } = SerialStatus.Available; + + /// + /// Costo specifico del seriale + /// + public decimal? UnitCost { get; set; } + + /// + /// ID fornitore di origine + /// + public int? SupplierId { get; set; } + + /// + /// ID cliente (se venduto) + /// + public int? CustomerId { get; set; } + + /// + /// Data di vendita + /// + public DateTime? SoldDate { get; set; } + + /// + /// Riferimento documento di vendita + /// + public string? SalesReference { get; set; } + + /// + /// Attributi aggiuntivi (JSON) + /// + public string? Attributes { get; set; } + + /// + /// Note + /// + public string? Notes { get; set; } + + // Navigation properties + public WarehouseArticle? Article { get; set; } + public ArticleBatch? Batch { get; set; } + public WarehouseLocation? CurrentWarehouse { get; set; } + public ICollection MovementLines { get; set; } = new List(); +} + +/// +/// Stato del seriale +/// +public enum SerialStatus +{ + /// + /// Disponibile in magazzino + /// + Available = 0, + + /// + /// Riservato (impegnato per ordine) + /// + Reserved = 1, + + /// + /// Venduto + /// + Sold = 2, + + /// + /// In riparazione + /// + InRepair = 3, + + /// + /// Difettoso/Danneggiato + /// + Defective = 4, + + /// + /// Restituito + /// + Returned = 5, + + /// + /// Dismesso + /// + Disposed = 6 +} diff --git a/src/Apollinare.Domain/Entities/Warehouse/InventoryCount.cs b/src/Apollinare.Domain/Entities/Warehouse/InventoryCount.cs new file mode 100644 index 0000000..384ef7a --- /dev/null +++ b/src/Apollinare.Domain/Entities/Warehouse/InventoryCount.cs @@ -0,0 +1,236 @@ +namespace Apollinare.Domain.Entities.Warehouse; + +/// +/// Testata inventario fisico +/// +public class InventoryCount : BaseEntity +{ + /// + /// Codice inventario + /// + public string Code { get; set; } = string.Empty; + + /// + /// Descrizione inventario + /// + public string Description { get; set; } = string.Empty; + + /// + /// Data inventario + /// + public DateTime InventoryDate { get; set; } + + /// + /// Magazzino (null = tutti i magazzini) + /// + public int? WarehouseId { get; set; } + + /// + /// Categoria articoli (null = tutte) + /// + public int? CategoryId { get; set; } + + /// + /// Tipo di inventario + /// + public InventoryType Type { get; set; } = InventoryType.Full; + + /// + /// Stato inventario + /// + public InventoryStatus Status { get; set; } = InventoryStatus.Draft; + + /// + /// Data inizio conteggio + /// + public DateTime? StartDate { get; set; } + + /// + /// Data fine conteggio + /// + public DateTime? EndDate { get; set; } + + /// + /// Data conferma + /// + public DateTime? ConfirmedDate { get; set; } + + /// + /// Utente che ha confermato + /// + public string? ConfirmedBy { get; set; } + + /// + /// ID movimento di rettifica generato + /// + public int? AdjustmentMovementId { get; set; } + + /// + /// Valore differenze positive + /// + public decimal? PositiveDifferenceValue { get; set; } + + /// + /// Valore differenze negative + /// + public decimal? NegativeDifferenceValue { get; set; } + + /// + /// Note + /// + public string? Notes { get; set; } + + // Navigation properties + public WarehouseLocation? Warehouse { get; set; } + public WarehouseArticleCategory? Category { get; set; } + public StockMovement? AdjustmentMovement { get; set; } + public ICollection Lines { get; set; } = new List(); +} + +/// +/// Riga dettaglio inventario +/// +public class InventoryCountLine : BaseEntity +{ + /// + /// Inventario padre + /// + public int InventoryCountId { get; set; } + + /// + /// Articolo + /// + public int ArticleId { get; set; } + + /// + /// Magazzino + /// + public int WarehouseId { get; set; } + + /// + /// Partita (se gestito a lotti) + /// + public int? BatchId { get; set; } + + /// + /// Ubicazione + /// + public string? LocationCode { get; set; } + + /// + /// Quantità teorica (da sistema) + /// + public decimal TheoreticalQuantity { get; set; } + + /// + /// Quantità contata + /// + public decimal? CountedQuantity { get; set; } + + /// + /// Differenza = CountedQuantity - TheoreticalQuantity + /// + public decimal? Difference => CountedQuantity.HasValue + ? CountedQuantity.Value - TheoreticalQuantity + : null; + + /// + /// Costo unitario per valorizzazione differenza + /// + public decimal? UnitCost { get; set; } + + /// + /// Valore differenza + /// + public decimal? DifferenceValue => Difference.HasValue && UnitCost.HasValue + ? Difference.Value * UnitCost.Value + : null; + + /// + /// Data/ora del conteggio + /// + public DateTime? CountedAt { get; set; } + + /// + /// Utente che ha effettuato il conteggio + /// + public string? CountedBy { get; set; } + + /// + /// Secondo conteggio (per verifica discrepanze) + /// + public decimal? SecondCountQuantity { get; set; } + + /// + /// Utente secondo conteggio + /// + public string? SecondCountBy { get; set; } + + /// + /// Note riga + /// + public string? Notes { get; set; } + + // Navigation properties + public InventoryCount? InventoryCount { get; set; } + public WarehouseArticle? Article { get; set; } + public WarehouseLocation? Warehouse { get; set; } + public ArticleBatch? Batch { get; set; } +} + +/// +/// Tipo di inventario +/// +public enum InventoryType +{ + /// + /// Inventario completo + /// + Full = 0, + + /// + /// Inventario parziale (per categoria/ubicazione) + /// + Partial = 1, + + /// + /// Inventario rotativo (ciclico) + /// + Cyclic = 2, + + /// + /// Inventario a campione + /// + Sample = 3 +} + +/// +/// Stato dell'inventario +/// +public enum InventoryStatus +{ + /// + /// Bozza + /// + Draft = 0, + + /// + /// In corso + /// + InProgress = 1, + + /// + /// Completato (in attesa conferma) + /// + Completed = 2, + + /// + /// Confermato (rettifiche applicate) + /// + Confirmed = 3, + + /// + /// Annullato + /// + Cancelled = 4 +} diff --git a/src/Apollinare.Domain/Entities/Warehouse/MovementReason.cs b/src/Apollinare.Domain/Entities/Warehouse/MovementReason.cs new file mode 100644 index 0000000..680d635 --- /dev/null +++ b/src/Apollinare.Domain/Entities/Warehouse/MovementReason.cs @@ -0,0 +1,65 @@ +namespace Apollinare.Domain.Entities.Warehouse; + +/// +/// Causale movimento di magazzino +/// +public class MovementReason : BaseEntity +{ + /// + /// Codice causale + /// + public string Code { get; set; } = string.Empty; + + /// + /// Descrizione causale + /// + public string Description { get; set; } = string.Empty; + + /// + /// Tipo movimento associato + /// + public MovementType MovementType { get; set; } + + /// + /// Segno del movimento sulla giacenza (+1 carico, -1 scarico) + /// + public int StockSign { get; set; } + + /// + /// Se true, richiede riferimento documento esterno + /// + public bool RequiresExternalReference { get; set; } + + /// + /// Se true, richiede valorizzazione + /// + public bool RequiresValuation { get; set; } = true; + + /// + /// Se true, aggiorna il costo medio + /// + public bool UpdatesAverageCost { get; set; } = true; + + /// + /// Se true, è una causale di sistema (non modificabile) + /// + public bool IsSystem { get; set; } + + /// + /// Se attiva + /// + public bool IsActive { get; set; } = true; + + /// + /// Ordine visualizzazione + /// + public int SortOrder { get; set; } + + /// + /// Note + /// + public string? Notes { get; set; } + + // Navigation properties + public ICollection Movements { get; set; } = new List(); +} diff --git a/src/Apollinare.Domain/Entities/Warehouse/StockLevel.cs b/src/Apollinare.Domain/Entities/Warehouse/StockLevel.cs new file mode 100644 index 0000000..c4fe755 --- /dev/null +++ b/src/Apollinare.Domain/Entities/Warehouse/StockLevel.cs @@ -0,0 +1,72 @@ +namespace Apollinare.Domain.Entities.Warehouse; + +/// +/// Giacenza articolo per magazzino, partita +/// +public class StockLevel : BaseEntity +{ + /// + /// Articolo + /// + public int ArticleId { get; set; } + + /// + /// Magazzino + /// + public int WarehouseId { get; set; } + + /// + /// Partita (opzionale, se gestito a lotti) + /// + public int? BatchId { get; set; } + + /// + /// Quantità fisica in giacenza + /// + public decimal Quantity { get; set; } + + /// + /// Quantità riservata (impegnata per ordini) + /// + public decimal ReservedQuantity { get; set; } + + /// + /// Quantità disponibile = Quantity - ReservedQuantity + /// + public decimal AvailableQuantity => Quantity - ReservedQuantity; + + /// + /// Quantità in ordine (in arrivo) + /// + public decimal OnOrderQuantity { get; set; } + + /// + /// Valore totale della giacenza (calcolato) + /// + public decimal? StockValue { get; set; } + + /// + /// Costo unitario medio per questa giacenza + /// + public decimal? UnitCost { get; set; } + + /// + /// Data ultimo movimento + /// + public DateTime? LastMovementDate { get; set; } + + /// + /// Data ultimo inventario + /// + public DateTime? LastInventoryDate { get; set; } + + /// + /// Ubicazione specifica nel magazzino (scaffale, corridoio, etc.) + /// + public string? LocationCode { get; set; } + + // Navigation properties + public WarehouseArticle? Article { get; set; } + public WarehouseLocation? Warehouse { get; set; } + public ArticleBatch? Batch { get; set; } +} diff --git a/src/Apollinare.Domain/Entities/Warehouse/StockMovement.cs b/src/Apollinare.Domain/Entities/Warehouse/StockMovement.cs new file mode 100644 index 0000000..11e2918 --- /dev/null +++ b/src/Apollinare.Domain/Entities/Warehouse/StockMovement.cs @@ -0,0 +1,201 @@ +namespace Apollinare.Domain.Entities.Warehouse; + +/// +/// Testata movimento di magazzino (carico, scarico, trasferimento, rettifica) +/// +public class StockMovement : BaseEntity +{ + /// + /// Numero documento movimento + /// + public string DocumentNumber { get; set; } = string.Empty; + + /// + /// Data movimento + /// + public DateTime MovementDate { get; set; } + + /// + /// Tipo movimento + /// + public MovementType Type { get; set; } + + /// + /// Causale movimento + /// + public int? ReasonId { get; set; } + + /// + /// Magazzino di origine (per scarichi e trasferimenti) + /// + public int? SourceWarehouseId { get; set; } + + /// + /// Magazzino di destinazione (per carichi e trasferimenti) + /// + public int? DestinationWarehouseId { get; set; } + + /// + /// Riferimento documento esterno (DDT, fattura, ordine) + /// + public string? ExternalReference { get; set; } + + /// + /// Tipo documento esterno + /// + public ExternalDocumentType? ExternalDocumentType { get; set; } + + /// + /// ID fornitore (per carichi da acquisto) + /// + public int? SupplierId { get; set; } + + /// + /// ID cliente (per scarichi per vendita) + /// + public int? CustomerId { get; set; } + + /// + /// Stato del movimento + /// + public MovementStatus Status { get; set; } = MovementStatus.Draft; + + /// + /// Data conferma movimento + /// + public DateTime? ConfirmedDate { get; set; } + + /// + /// Utente che ha confermato + /// + public string? ConfirmedBy { get; set; } + + /// + /// Valore totale movimento + /// + public decimal? TotalValue { get; set; } + + /// + /// Note movimento + /// + public string? Notes { get; set; } + + // Navigation properties + public WarehouseLocation? SourceWarehouse { get; set; } + public WarehouseLocation? DestinationWarehouse { get; set; } + public MovementReason? Reason { get; set; } + public ICollection Lines { get; set; } = new List(); +} + +/// +/// Tipo di movimento magazzino +/// +public enum MovementType +{ + /// + /// Carico (aumento giacenza) + /// + Inbound = 0, + + /// + /// Scarico (diminuzione giacenza) + /// + Outbound = 1, + + /// + /// Trasferimento tra magazzini + /// + Transfer = 2, + + /// + /// Rettifica inventariale (positiva o negativa) + /// + Adjustment = 3, + + /// + /// Produzione (carico da ciclo produttivo) + /// + Production = 4, + + /// + /// Consumo (scarico per produzione) + /// + Consumption = 5, + + /// + /// Reso a fornitore + /// + SupplierReturn = 6, + + /// + /// Reso da cliente + /// + CustomerReturn = 7 +} + +/// +/// Stato del movimento +/// +public enum MovementStatus +{ + /// + /// Bozza (non ancora confermato) + /// + Draft = 0, + + /// + /// Confermato (giacenze aggiornate) + /// + Confirmed = 1, + + /// + /// Annullato + /// + Cancelled = 2 +} + +/// +/// Tipo documento esterno collegato +/// +public enum ExternalDocumentType +{ + /// + /// Ordine fornitore + /// + PurchaseOrder = 0, + + /// + /// DDT entrata + /// + InboundDeliveryNote = 1, + + /// + /// Fattura acquisto + /// + PurchaseInvoice = 2, + + /// + /// Ordine cliente + /// + SalesOrder = 3, + + /// + /// DDT uscita + /// + OutboundDeliveryNote = 4, + + /// + /// Fattura vendita + /// + SalesInvoice = 5, + + /// + /// Ordine di produzione + /// + ProductionOrder = 6, + + /// + /// Documento inventario + /// + InventoryDocument = 7 +} diff --git a/src/Apollinare.Domain/Entities/Warehouse/StockMovementLine.cs b/src/Apollinare.Domain/Entities/Warehouse/StockMovementLine.cs new file mode 100644 index 0000000..3965240 --- /dev/null +++ b/src/Apollinare.Domain/Entities/Warehouse/StockMovementLine.cs @@ -0,0 +1,78 @@ +namespace Apollinare.Domain.Entities.Warehouse; + +/// +/// Riga dettaglio movimento di magazzino +/// +public class StockMovementLine : BaseEntity +{ + /// + /// Movimento padre + /// + public int MovementId { get; set; } + + /// + /// Numero riga + /// + public int LineNumber { get; set; } + + /// + /// Articolo + /// + public int ArticleId { get; set; } + + /// + /// Partita (se gestito a lotti) + /// + public int? BatchId { get; set; } + + /// + /// Seriale (se gestito a seriali) - per movimenti di singoli pezzi + /// + public int? SerialId { get; set; } + + /// + /// Quantità movimentata + /// + public decimal Quantity { get; set; } + + /// + /// Unità di misura + /// + public string UnitOfMeasure { get; set; } = "PZ"; + + /// + /// Costo unitario (per valorizzazione) + /// + public decimal? UnitCost { get; set; } + + /// + /// Valore totale riga + /// + public decimal? LineValue { get; set; } + + /// + /// Ubicazione origine (per prelievi specifici) + /// + public string? SourceLocationCode { get; set; } + + /// + /// Ubicazione destinazione (per posizionamenti specifici) + /// + public string? DestinationLocationCode { get; set; } + + /// + /// Riferimento riga documento esterno + /// + public string? ExternalLineReference { get; set; } + + /// + /// Note riga + /// + public string? Notes { get; set; } + + // Navigation properties + public StockMovement? Movement { get; set; } + public WarehouseArticle? Article { get; set; } + public ArticleBatch? Batch { get; set; } + public ArticleSerial? Serial { get; set; } +} diff --git a/src/Apollinare.Domain/Entities/Warehouse/StockValuation.cs b/src/Apollinare.Domain/Entities/Warehouse/StockValuation.cs new file mode 100644 index 0000000..979be02 --- /dev/null +++ b/src/Apollinare.Domain/Entities/Warehouse/StockValuation.cs @@ -0,0 +1,153 @@ +namespace Apollinare.Domain.Entities.Warehouse; + +/// +/// Storico valorizzazione magazzino per periodo +/// +public class StockValuation : BaseEntity +{ + /// + /// Data della valorizzazione + /// + public DateTime ValuationDate { get; set; } + + /// + /// Periodo di riferimento (YYYYMM) + /// + public int Period { get; set; } + + /// + /// Articolo + /// + public int ArticleId { get; set; } + + /// + /// Magazzino (null = totale tutti i magazzini) + /// + public int? WarehouseId { get; set; } + + /// + /// Quantità a fine periodo + /// + public decimal Quantity { get; set; } + + /// + /// Metodo di valorizzazione usato + /// + public ValuationMethod Method { get; set; } + + /// + /// Costo unitario calcolato + /// + public decimal UnitCost { get; set; } + + /// + /// Valore totale = Quantity * UnitCost + /// + public decimal TotalValue { get; set; } + + /// + /// Quantità in carico nel periodo + /// + public decimal InboundQuantity { get; set; } + + /// + /// Valore carichi nel periodo + /// + public decimal InboundValue { get; set; } + + /// + /// Quantità in scarico nel periodo + /// + public decimal OutboundQuantity { get; set; } + + /// + /// Valore scarichi nel periodo + /// + public decimal OutboundValue { get; set; } + + /// + /// Se è una chiusura definitiva (non più modificabile) + /// + public bool IsClosed { get; set; } + + /// + /// Data chiusura + /// + public DateTime? ClosedDate { get; set; } + + /// + /// Utente che ha chiuso + /// + public string? ClosedBy { get; set; } + + /// + /// Note + /// + public string? Notes { get; set; } + + // Navigation properties + public WarehouseArticle? Article { get; set; } + public WarehouseLocation? Warehouse { get; set; } +} + +/// +/// Dettaglio valorizzazione FIFO/LIFO per layer +/// +public class StockValuationLayer : BaseEntity +{ + /// + /// Articolo + /// + public int ArticleId { get; set; } + + /// + /// Magazzino + /// + public int WarehouseId { get; set; } + + /// + /// Partita (opzionale) + /// + public int? BatchId { get; set; } + + /// + /// Data del layer (data carico originale) + /// + public DateTime LayerDate { get; set; } + + /// + /// Movimento di carico che ha creato il layer + /// + public int? SourceMovementId { get; set; } + + /// + /// Quantità originale del layer + /// + public decimal OriginalQuantity { get; set; } + + /// + /// Quantità residua nel layer + /// + public decimal RemainingQuantity { get; set; } + + /// + /// Costo unitario del layer + /// + public decimal UnitCost { get; set; } + + /// + /// Valore residuo = RemainingQuantity * UnitCost + /// + public decimal RemainingValue => RemainingQuantity * UnitCost; + + /// + /// Se il layer è esaurito + /// + public bool IsExhausted { get; set; } + + // Navigation properties + public WarehouseArticle? Article { get; set; } + public WarehouseLocation? Warehouse { get; set; } + public ArticleBatch? Batch { get; set; } + public StockMovement? SourceMovement { get; set; } +} diff --git a/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticle.cs b/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticle.cs new file mode 100644 index 0000000..6244e06 --- /dev/null +++ b/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticle.cs @@ -0,0 +1,237 @@ +namespace Apollinare.Domain.Entities.Warehouse; + +/// +/// Articolo del modulo magazzino con gestione completa di partite e seriali +/// +public class WarehouseArticle : BaseEntity +{ + /// + /// Codice univoco articolo (SKU) + /// + public string Code { get; set; } = string.Empty; + + /// + /// Descrizione articolo + /// + public string Description { get; set; } = string.Empty; + + /// + /// Descrizione breve per etichette + /// + public string? ShortDescription { get; set; } + + /// + /// Codice a barre principale (EAN/UPC) + /// + public string? Barcode { get; set; } + + /// + /// Codice fornitore/produttore + /// + public string? ManufacturerCode { get; set; } + + /// + /// Categoria articolo + /// + public int? CategoryId { get; set; } + + /// + /// Unità di misura principale (es. PZ, KG, LT, MT) + /// + public string UnitOfMeasure { get; set; } = "PZ"; + + /// + /// Unità di misura secondaria per conversione + /// + public string? SecondaryUnitOfMeasure { get; set; } + + /// + /// Fattore di conversione tra UoM primaria e secondaria + /// + public decimal? UnitConversionFactor { get; set; } + + /// + /// Tipo di gestione magazzino + /// + public StockManagementType StockManagement { get; set; } = StockManagementType.Standard; + + /// + /// Se true, l'articolo è gestito a partite (lotti) + /// + public bool IsBatchManaged { get; set; } + + /// + /// Se true, l'articolo è gestito a seriali + /// + public bool IsSerialManaged { get; set; } + + /// + /// Se true, l'articolo ha scadenza + /// + public bool HasExpiry { get; set; } + + /// + /// Giorni di preavviso scadenza + /// + public int? ExpiryWarningDays { get; set; } + + /// + /// Scorta minima (sotto questo livello scatta alert) + /// + public decimal? MinimumStock { get; set; } + + /// + /// Scorta massima + /// + public decimal? MaximumStock { get; set; } + + /// + /// Punto di riordino + /// + public decimal? ReorderPoint { get; set; } + + /// + /// Quantità di riordino standard + /// + public decimal? ReorderQuantity { get; set; } + + /// + /// Lead time in giorni per approvvigionamento + /// + public int? LeadTimeDays { get; set; } + + /// + /// Metodo di valorizzazione per questo articolo (override del default) + /// + public ValuationMethod? ValuationMethod { get; set; } + + /// + /// Costo standard (per valorizzazione a costo standard) + /// + public decimal? StandardCost { get; set; } + + /// + /// Ultimo costo di acquisto + /// + public decimal? LastPurchaseCost { get; set; } + + /// + /// Costo medio ponderato corrente + /// + public decimal? WeightedAverageCost { get; set; } + + /// + /// Prezzo di vendita base + /// + public decimal? BaseSellingPrice { get; set; } + + /// + /// Peso in Kg + /// + public decimal? Weight { get; set; } + + /// + /// Volume in metri cubi + /// + public decimal? Volume { get; set; } + + /// + /// Larghezza in cm + /// + public decimal? Width { get; set; } + + /// + /// Altezza in cm + /// + public decimal? Height { get; set; } + + /// + /// Profondità in cm + /// + public decimal? Depth { get; set; } + + /// + /// Immagine principale + /// + public byte[]? Image { get; set; } + + /// + /// Mime type immagine + /// + public string? ImageMimeType { get; set; } + + /// + /// Se attivo, l'articolo può essere movimentato + /// + public bool IsActive { get; set; } = true; + + /// + /// Note aggiuntive + /// + public string? Notes { get; set; } + + // Navigation properties + public WarehouseArticleCategory? Category { get; set; } + public ICollection StockLevels { get; set; } = new List(); + public ICollection MovementLines { get; set; } = new List(); + public ICollection Batches { get; set; } = new List(); + public ICollection Serials { get; set; } = new List(); + public ICollection Barcodes { get; set; } = new List(); +} + +/// +/// Tipo di gestione magazzino per l'articolo +/// +public enum StockManagementType +{ + /// + /// Gestione standard (quantità) + /// + Standard = 0, + + /// + /// Non gestito a magazzino (servizi, ecc.) + /// + NotManaged = 1, + + /// + /// Gestione a peso variabile + /// + VariableWeight = 2, + + /// + /// Kit/Distinta base + /// + Kit = 3 +} + +/// +/// Metodo di valorizzazione magazzino +/// +public enum ValuationMethod +{ + /// + /// Costo medio ponderato + /// + WeightedAverage = 0, + + /// + /// First In First Out + /// + FIFO = 1, + + /// + /// Last In First Out + /// + LIFO = 2, + + /// + /// Costo standard + /// + StandardCost = 3, + + /// + /// Costo specifico (per partita/seriale) + /// + SpecificCost = 4 +} diff --git a/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticleCategory.cs b/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticleCategory.cs new file mode 100644 index 0000000..39d83a1 --- /dev/null +++ b/src/Apollinare.Domain/Entities/Warehouse/WarehouseArticleCategory.cs @@ -0,0 +1,72 @@ +namespace Apollinare.Domain.Entities.Warehouse; + +/// +/// Categoria articoli magazzino con struttura gerarchica +/// +public class WarehouseArticleCategory : BaseEntity +{ + /// + /// Codice categoria + /// + public string Code { get; set; } = string.Empty; + + /// + /// Nome categoria + /// + public string Name { get; set; } = string.Empty; + + /// + /// Descrizione + /// + public string? Description { get; set; } + + /// + /// ID categoria padre (per gerarchia) + /// + public int? ParentCategoryId { get; set; } + + /// + /// Livello nella gerarchia (0 = root) + /// + public int Level { get; set; } + + /// + /// Path completo codici (es. "001.002.003") + /// + public string? FullPath { get; set; } + + /// + /// Icona categoria (nome icona MUI) + /// + public string? Icon { get; set; } + + /// + /// Colore categoria (hex) + /// + public string? Color { get; set; } + + /// + /// Metodo di valorizzazione default per articoli di questa categoria + /// + public ValuationMethod? DefaultValuationMethod { get; set; } + + /// + /// Ordine di visualizzazione + /// + public int SortOrder { get; set; } + + /// + /// Se attiva + /// + public bool IsActive { get; set; } = true; + + /// + /// Note + /// + public string? Notes { get; set; } + + // Navigation properties + public WarehouseArticleCategory? ParentCategory { get; set; } + public ICollection ChildCategories { get; set; } = new List(); + public ICollection Articles { get; set; } = new List(); +} diff --git a/src/Apollinare.Domain/Entities/Warehouse/WarehouseLocation.cs b/src/Apollinare.Domain/Entities/Warehouse/WarehouseLocation.cs new file mode 100644 index 0000000..093a56a --- /dev/null +++ b/src/Apollinare.Domain/Entities/Warehouse/WarehouseLocation.cs @@ -0,0 +1,113 @@ +namespace Apollinare.Domain.Entities.Warehouse; + +/// +/// Rappresenta un magazzino fisico o logico +/// +public class WarehouseLocation : BaseEntity +{ + /// + /// Codice univoco del magazzino (es. "MAG01", "CENTRALE") + /// + public string Code { get; set; } = string.Empty; + + /// + /// Nome descrittivo del magazzino + /// + public string Name { get; set; } = string.Empty; + + /// + /// Descrizione estesa + /// + public string? Description { get; set; } + + /// + /// Indirizzo del magazzino + /// + public string? Address { get; set; } + + /// + /// Città + /// + public string? City { get; set; } + + /// + /// Provincia + /// + public string? Province { get; set; } + + /// + /// CAP + /// + public string? PostalCode { get; set; } + + /// + /// Nazione + /// + public string? Country { get; set; } = "Italia"; + + /// + /// Tipo di magazzino (fisico, logico, transito, reso, etc.) + /// + public WarehouseType Type { get; set; } = WarehouseType.Physical; + + /// + /// Se true, è il magazzino predefinito per carichi/scarichi + /// + public bool IsDefault { get; set; } + + /// + /// Se attivo può ricevere movimenti + /// + public bool IsActive { get; set; } = true; + + /// + /// Ordine di visualizzazione + /// + public int SortOrder { get; set; } + + /// + /// Note aggiuntive + /// + public string? Notes { get; set; } + + // Navigation properties + public ICollection StockLevels { get; set; } = new List(); + public ICollection SourceMovements { get; set; } = new List(); + public ICollection DestinationMovements { get; set; } = new List(); +} + +/// +/// Tipo di magazzino +/// +public enum WarehouseType +{ + /// + /// Magazzino fisico standard + /// + Physical = 0, + + /// + /// Magazzino logico (virtuale) + /// + Logical = 1, + + /// + /// Magazzino di transito + /// + Transit = 2, + + /// + /// Magazzino resi + /// + Returns = 3, + + /// + /// Magazzino scarti/difettosi + /// + Defective = 4, + + /// + /// Magazzino conto lavoro + /// + Subcontract = 5 +} diff --git a/src/Apollinare.Infrastructure/Data/AppollinareDbContext.cs b/src/Apollinare.Infrastructure/Data/AppollinareDbContext.cs index 0cbee2f..dcfd3a4 100644 --- a/src/Apollinare.Infrastructure/Data/AppollinareDbContext.cs +++ b/src/Apollinare.Infrastructure/Data/AppollinareDbContext.cs @@ -1,4 +1,5 @@ using Apollinare.Domain.Entities; +using Apollinare.Domain.Entities.Warehouse; using Microsoft.EntityFrameworkCore; namespace Apollinare.Infrastructure.Data; @@ -40,6 +41,22 @@ public class AppollinareDbContext : DbContext public DbSet AppModules => Set(); public DbSet ModuleSubscriptions => Set(); + // Warehouse module entities + public DbSet WarehouseLocations => Set(); + public DbSet WarehouseArticles => Set(); + public DbSet WarehouseArticleCategories => Set(); + public DbSet ArticleBatches => Set(); + public DbSet ArticleSerials => Set(); + public DbSet ArticleBarcodes => Set(); + public DbSet StockLevels => Set(); + public DbSet StockMovements => Set(); + public DbSet StockMovementLines => Set(); + public DbSet MovementReasons => Set(); + public DbSet StockValuations => Set(); + public DbSet StockValuationLayers => Set(); + public DbSet InventoryCounts => Set(); + public DbSet InventoryCountLines => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -256,5 +273,339 @@ public class AppollinareDbContext : DbContext .HasForeignKey(e => e.ModuleId) .OnDelete(DeleteBehavior.Cascade); }); + + // =============================================== + // WAREHOUSE MODULE ENTITIES + // =============================================== + + // WarehouseLocation + modelBuilder.Entity(entity => + { + entity.ToTable("WarehouseLocations"); + entity.HasIndex(e => e.Code).IsUnique(); + entity.HasIndex(e => e.IsDefault); + entity.HasIndex(e => e.IsActive); + }); + + // WarehouseArticleCategory + modelBuilder.Entity(entity => + { + entity.ToTable("WarehouseArticleCategories"); + entity.HasIndex(e => e.Code).IsUnique(); + entity.HasIndex(e => e.ParentCategoryId); + entity.HasIndex(e => e.FullPath); + + entity.HasOne(e => e.ParentCategory) + .WithMany(c => c.ChildCategories) + .HasForeignKey(e => e.ParentCategoryId) + .OnDelete(DeleteBehavior.Restrict); + }); + + // WarehouseArticle + modelBuilder.Entity(entity => + { + entity.ToTable("WarehouseArticles"); + entity.HasIndex(e => e.Code).IsUnique(); + entity.HasIndex(e => e.Barcode); + entity.HasIndex(e => e.CategoryId); + entity.HasIndex(e => e.IsActive); + + entity.Property(e => e.StandardCost).HasPrecision(18, 4); + entity.Property(e => e.LastPurchaseCost).HasPrecision(18, 4); + entity.Property(e => e.WeightedAverageCost).HasPrecision(18, 4); + entity.Property(e => e.BaseSellingPrice).HasPrecision(18, 4); + entity.Property(e => e.MinimumStock).HasPrecision(18, 4); + entity.Property(e => e.MaximumStock).HasPrecision(18, 4); + entity.Property(e => e.ReorderPoint).HasPrecision(18, 4); + entity.Property(e => e.ReorderQuantity).HasPrecision(18, 4); + entity.Property(e => e.UnitConversionFactor).HasPrecision(18, 6); + entity.Property(e => e.Weight).HasPrecision(18, 4); + entity.Property(e => e.Volume).HasPrecision(18, 6); + + entity.HasOne(e => e.Category) + .WithMany(c => c.Articles) + .HasForeignKey(e => e.CategoryId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // ArticleBatch + modelBuilder.Entity(entity => + { + entity.ToTable("ArticleBatches"); + entity.HasIndex(e => new { e.ArticleId, e.BatchNumber }).IsUnique(); + entity.HasIndex(e => e.ExpiryDate); + entity.HasIndex(e => e.Status); + + entity.Property(e => e.UnitCost).HasPrecision(18, 4); + entity.Property(e => e.InitialQuantity).HasPrecision(18, 4); + entity.Property(e => e.CurrentQuantity).HasPrecision(18, 4); + entity.Property(e => e.ReservedQuantity).HasPrecision(18, 4); + + entity.HasOne(e => e.Article) + .WithMany(a => a.Batches) + .HasForeignKey(e => e.ArticleId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // ArticleSerial + modelBuilder.Entity(entity => + { + entity.ToTable("ArticleSerials"); + entity.HasIndex(e => new { e.ArticleId, e.SerialNumber }).IsUnique(); + entity.HasIndex(e => e.Status); + entity.HasIndex(e => e.CurrentWarehouseId); + + entity.Property(e => e.UnitCost).HasPrecision(18, 4); + + entity.HasOne(e => e.Article) + .WithMany(a => a.Serials) + .HasForeignKey(e => e.ArticleId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Batch) + .WithMany(b => b.Serials) + .HasForeignKey(e => e.BatchId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(e => e.CurrentWarehouse) + .WithMany() + .HasForeignKey(e => e.CurrentWarehouseId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // ArticleBarcode + modelBuilder.Entity(entity => + { + entity.ToTable("ArticleBarcodes"); + entity.HasIndex(e => e.Barcode).IsUnique(); + entity.HasIndex(e => e.ArticleId); + + entity.Property(e => e.Quantity).HasPrecision(18, 4); + + entity.HasOne(e => e.Article) + .WithMany(a => a.Barcodes) + .HasForeignKey(e => e.ArticleId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // StockLevel + modelBuilder.Entity(entity => + { + entity.ToTable("StockLevels"); + entity.HasIndex(e => new { e.ArticleId, e.WarehouseId, e.BatchId }).IsUnique(); + entity.HasIndex(e => e.WarehouseId); + entity.HasIndex(e => e.LocationCode); + + entity.Property(e => e.Quantity).HasPrecision(18, 4); + entity.Property(e => e.ReservedQuantity).HasPrecision(18, 4); + entity.Property(e => e.OnOrderQuantity).HasPrecision(18, 4); + entity.Property(e => e.StockValue).HasPrecision(18, 4); + entity.Property(e => e.UnitCost).HasPrecision(18, 4); + + entity.HasOne(e => e.Article) + .WithMany(a => a.StockLevels) + .HasForeignKey(e => e.ArticleId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Warehouse) + .WithMany(w => w.StockLevels) + .HasForeignKey(e => e.WarehouseId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Batch) + .WithMany(b => b.StockLevels) + .HasForeignKey(e => e.BatchId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // MovementReason + modelBuilder.Entity(entity => + { + entity.ToTable("MovementReasons"); + entity.HasIndex(e => e.Code).IsUnique(); + entity.HasIndex(e => e.MovementType); + entity.HasIndex(e => e.IsActive); + }); + + // StockMovement + modelBuilder.Entity(entity => + { + entity.ToTable("StockMovements"); + entity.HasIndex(e => e.DocumentNumber).IsUnique(); + entity.HasIndex(e => e.MovementDate); + entity.HasIndex(e => e.Type); + entity.HasIndex(e => e.Status); + entity.HasIndex(e => e.ExternalReference); + + entity.Property(e => e.TotalValue).HasPrecision(18, 4); + + entity.HasOne(e => e.SourceWarehouse) + .WithMany(w => w.SourceMovements) + .HasForeignKey(e => e.SourceWarehouseId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(e => e.DestinationWarehouse) + .WithMany(w => w.DestinationMovements) + .HasForeignKey(e => e.DestinationWarehouseId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(e => e.Reason) + .WithMany(r => r.Movements) + .HasForeignKey(e => e.ReasonId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // StockMovementLine + modelBuilder.Entity(entity => + { + entity.ToTable("StockMovementLines"); + entity.HasIndex(e => new { e.MovementId, e.LineNumber }).IsUnique(); + entity.HasIndex(e => e.ArticleId); + entity.HasIndex(e => e.BatchId); + entity.HasIndex(e => e.SerialId); + + entity.Property(e => e.Quantity).HasPrecision(18, 4); + entity.Property(e => e.UnitCost).HasPrecision(18, 4); + entity.Property(e => e.LineValue).HasPrecision(18, 4); + + entity.HasOne(e => e.Movement) + .WithMany(m => m.Lines) + .HasForeignKey(e => e.MovementId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Article) + .WithMany(a => a.MovementLines) + .HasForeignKey(e => e.ArticleId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(e => e.Batch) + .WithMany(b => b.MovementLines) + .HasForeignKey(e => e.BatchId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(e => e.Serial) + .WithMany(s => s.MovementLines) + .HasForeignKey(e => e.SerialId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // StockValuation + modelBuilder.Entity(entity => + { + entity.ToTable("StockValuations"); + entity.HasIndex(e => new { e.Period, e.ArticleId, e.WarehouseId }).IsUnique(); + entity.HasIndex(e => e.ValuationDate); + entity.HasIndex(e => e.IsClosed); + + entity.Property(e => e.Quantity).HasPrecision(18, 4); + entity.Property(e => e.UnitCost).HasPrecision(18, 4); + entity.Property(e => e.TotalValue).HasPrecision(18, 4); + entity.Property(e => e.InboundQuantity).HasPrecision(18, 4); + entity.Property(e => e.InboundValue).HasPrecision(18, 4); + entity.Property(e => e.OutboundQuantity).HasPrecision(18, 4); + entity.Property(e => e.OutboundValue).HasPrecision(18, 4); + + entity.HasOne(e => e.Article) + .WithMany() + .HasForeignKey(e => e.ArticleId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Warehouse) + .WithMany() + .HasForeignKey(e => e.WarehouseId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // StockValuationLayer + modelBuilder.Entity(entity => + { + entity.ToTable("StockValuationLayers"); + entity.HasIndex(e => new { e.ArticleId, e.WarehouseId, e.LayerDate }); + entity.HasIndex(e => e.IsExhausted); + + entity.Property(e => e.OriginalQuantity).HasPrecision(18, 4); + entity.Property(e => e.RemainingQuantity).HasPrecision(18, 4); + entity.Property(e => e.UnitCost).HasPrecision(18, 4); + + entity.HasOne(e => e.Article) + .WithMany() + .HasForeignKey(e => e.ArticleId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Warehouse) + .WithMany() + .HasForeignKey(e => e.WarehouseId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Batch) + .WithMany() + .HasForeignKey(e => e.BatchId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(e => e.SourceMovement) + .WithMany() + .HasForeignKey(e => e.SourceMovementId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // InventoryCount + modelBuilder.Entity(entity => + { + entity.ToTable("InventoryCounts"); + entity.HasIndex(e => e.Code).IsUnique(); + entity.HasIndex(e => e.InventoryDate); + entity.HasIndex(e => e.Status); + + entity.Property(e => e.PositiveDifferenceValue).HasPrecision(18, 4); + entity.Property(e => e.NegativeDifferenceValue).HasPrecision(18, 4); + + entity.HasOne(e => e.Warehouse) + .WithMany() + .HasForeignKey(e => e.WarehouseId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(e => e.Category) + .WithMany() + .HasForeignKey(e => e.CategoryId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(e => e.AdjustmentMovement) + .WithMany() + .HasForeignKey(e => e.AdjustmentMovementId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // InventoryCountLine + modelBuilder.Entity(entity => + { + entity.ToTable("InventoryCountLines"); + entity.HasIndex(e => new { e.InventoryCountId, e.ArticleId, e.WarehouseId, e.BatchId }).IsUnique(); + entity.HasIndex(e => e.ArticleId); + + entity.Property(e => e.TheoreticalQuantity).HasPrecision(18, 4); + entity.Property(e => e.CountedQuantity).HasPrecision(18, 4); + entity.Property(e => e.SecondCountQuantity).HasPrecision(18, 4); + entity.Property(e => e.UnitCost).HasPrecision(18, 4); + + entity.HasOne(e => e.InventoryCount) + .WithMany(i => i.Lines) + .HasForeignKey(e => e.InventoryCountId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Article) + .WithMany() + .HasForeignKey(e => e.ArticleId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(e => e.Warehouse) + .WithMany() + .HasForeignKey(e => e.WarehouseId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(e => e.Batch) + .WithMany() + .HasForeignKey(e => e.BatchId) + .OnDelete(DeleteBehavior.SetNull); + }); } } diff --git a/src/Apollinare.Infrastructure/Data/warehouse_tables.sql b/src/Apollinare.Infrastructure/Data/warehouse_tables.sql new file mode 100644 index 0000000..eeae9d7 --- /dev/null +++ b/src/Apollinare.Infrastructure/Data/warehouse_tables.sql @@ -0,0 +1,421 @@ +-- ===================================================== +-- APOLLINARE WAREHOUSE MODULE - DATABASE TABLES +-- SQLite +-- ===================================================== + +-- Magazzini +CREATE TABLE IF NOT EXISTS WarehouseLocations ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Code TEXT NOT NULL UNIQUE, + Name TEXT NOT NULL, + Description TEXT, + Address TEXT, + City TEXT, + Province TEXT, + PostalCode TEXT, + Country TEXT DEFAULT 'Italia', + Type INTEGER NOT NULL DEFAULT 0, + IsDefault INTEGER NOT NULL DEFAULT 0, + IsActive INTEGER NOT NULL DEFAULT 1, + SortOrder INTEGER NOT NULL DEFAULT 0, + Notes TEXT, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT +); + +CREATE INDEX IF NOT EXISTS IX_WarehouseLocations_Code ON WarehouseLocations(Code); +CREATE INDEX IF NOT EXISTS IX_WarehouseLocations_IsDefault ON WarehouseLocations(IsDefault); +CREATE INDEX IF NOT EXISTS IX_WarehouseLocations_IsActive ON WarehouseLocations(IsActive); + +-- Categorie Articoli +CREATE TABLE IF NOT EXISTS WarehouseArticleCategories ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Code TEXT NOT NULL UNIQUE, + Name TEXT NOT NULL, + Description TEXT, + ParentCategoryId INTEGER, + Level INTEGER NOT NULL DEFAULT 0, + FullPath TEXT, + Icon TEXT, + Color TEXT, + DefaultValuationMethod INTEGER, + SortOrder INTEGER NOT NULL DEFAULT 0, + IsActive INTEGER NOT NULL DEFAULT 1, + Notes TEXT, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT, + FOREIGN KEY (ParentCategoryId) REFERENCES WarehouseArticleCategories(Id) ON DELETE RESTRICT +); + +CREATE INDEX IF NOT EXISTS IX_WarehouseArticleCategories_Code ON WarehouseArticleCategories(Code); +CREATE INDEX IF NOT EXISTS IX_WarehouseArticleCategories_ParentCategoryId ON WarehouseArticleCategories(ParentCategoryId); +CREATE INDEX IF NOT EXISTS IX_WarehouseArticleCategories_FullPath ON WarehouseArticleCategories(FullPath); + +-- Articoli Magazzino +CREATE TABLE IF NOT EXISTS WarehouseArticles ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Code TEXT NOT NULL UNIQUE, + Description TEXT NOT NULL, + ShortDescription TEXT, + Barcode TEXT, + ManufacturerCode TEXT, + CategoryId INTEGER, + UnitOfMeasure TEXT NOT NULL DEFAULT 'PZ', + SecondaryUnitOfMeasure TEXT, + UnitConversionFactor REAL, + StockManagement INTEGER NOT NULL DEFAULT 0, + IsBatchManaged INTEGER NOT NULL DEFAULT 0, + IsSerialManaged INTEGER NOT NULL DEFAULT 0, + HasExpiry INTEGER NOT NULL DEFAULT 0, + ExpiryWarningDays INTEGER, + MinimumStock REAL, + MaximumStock REAL, + ReorderPoint REAL, + ReorderQuantity REAL, + LeadTimeDays INTEGER, + ValuationMethod INTEGER, + StandardCost REAL, + LastPurchaseCost REAL, + WeightedAverageCost REAL, + BaseSellingPrice REAL, + Weight REAL, + Volume REAL, + Width REAL, + Height REAL, + Depth REAL, + Image BLOB, + ImageMimeType TEXT, + IsActive INTEGER NOT NULL DEFAULT 1, + Notes TEXT, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT, + FOREIGN KEY (CategoryId) REFERENCES WarehouseArticleCategories(Id) ON DELETE SET NULL +); + +CREATE INDEX IF NOT EXISTS IX_WarehouseArticles_Code ON WarehouseArticles(Code); +CREATE INDEX IF NOT EXISTS IX_WarehouseArticles_Barcode ON WarehouseArticles(Barcode); +CREATE INDEX IF NOT EXISTS IX_WarehouseArticles_CategoryId ON WarehouseArticles(CategoryId); +CREATE INDEX IF NOT EXISTS IX_WarehouseArticles_IsActive ON WarehouseArticles(IsActive); + +-- Partite/Lotti +CREATE TABLE IF NOT EXISTS ArticleBatches ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + ArticleId INTEGER NOT NULL, + BatchNumber TEXT NOT NULL, + ProductionDate TEXT, + ExpiryDate TEXT, + SupplierBatch TEXT, + SupplierId INTEGER, + UnitCost REAL, + InitialQuantity REAL NOT NULL DEFAULT 0, + CurrentQuantity REAL NOT NULL DEFAULT 0, + ReservedQuantity REAL NOT NULL DEFAULT 0, + Status INTEGER NOT NULL DEFAULT 0, + QualityStatus INTEGER, + LastQualityCheckDate TEXT, + Certifications TEXT, + Notes TEXT, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT, + FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE CASCADE, + UNIQUE(ArticleId, BatchNumber) +); + +CREATE INDEX IF NOT EXISTS IX_ArticleBatches_ArticleId_BatchNumber ON ArticleBatches(ArticleId, BatchNumber); +CREATE INDEX IF NOT EXISTS IX_ArticleBatches_ExpiryDate ON ArticleBatches(ExpiryDate); +CREATE INDEX IF NOT EXISTS IX_ArticleBatches_Status ON ArticleBatches(Status); + +-- Seriali/Matricole +CREATE TABLE IF NOT EXISTS ArticleSerials ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + ArticleId INTEGER NOT NULL, + BatchId INTEGER, + SerialNumber TEXT NOT NULL, + ManufacturerSerial TEXT, + ProductionDate TEXT, + WarrantyExpiryDate TEXT, + CurrentWarehouseId INTEGER, + Status INTEGER NOT NULL DEFAULT 0, + UnitCost REAL, + SupplierId INTEGER, + CustomerId INTEGER, + SoldDate TEXT, + SalesReference TEXT, + Attributes TEXT, + Notes TEXT, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT, + FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE CASCADE, + FOREIGN KEY (BatchId) REFERENCES ArticleBatches(Id) ON DELETE SET NULL, + FOREIGN KEY (CurrentWarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE SET NULL, + UNIQUE(ArticleId, SerialNumber) +); + +CREATE INDEX IF NOT EXISTS IX_ArticleSerials_ArticleId_SerialNumber ON ArticleSerials(ArticleId, SerialNumber); +CREATE INDEX IF NOT EXISTS IX_ArticleSerials_Status ON ArticleSerials(Status); +CREATE INDEX IF NOT EXISTS IX_ArticleSerials_CurrentWarehouseId ON ArticleSerials(CurrentWarehouseId); + +-- Barcode aggiuntivi +CREATE TABLE IF NOT EXISTS ArticleBarcodes ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + ArticleId INTEGER NOT NULL, + Barcode TEXT NOT NULL UNIQUE, + Type INTEGER NOT NULL DEFAULT 0, + Description TEXT, + Quantity REAL NOT NULL DEFAULT 1, + IsPrimary INTEGER NOT NULL DEFAULT 0, + IsActive INTEGER NOT NULL DEFAULT 1, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT, + FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS IX_ArticleBarcodes_Barcode ON ArticleBarcodes(Barcode); +CREATE INDEX IF NOT EXISTS IX_ArticleBarcodes_ArticleId ON ArticleBarcodes(ArticleId); + +-- Giacenze +CREATE TABLE IF NOT EXISTS StockLevels ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + ArticleId INTEGER NOT NULL, + WarehouseId INTEGER NOT NULL, + BatchId INTEGER, + Quantity REAL NOT NULL DEFAULT 0, + ReservedQuantity REAL NOT NULL DEFAULT 0, + OnOrderQuantity REAL NOT NULL DEFAULT 0, + StockValue REAL, + UnitCost REAL, + LastMovementDate TEXT, + LastInventoryDate TEXT, + LocationCode TEXT, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT, + FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE CASCADE, + FOREIGN KEY (WarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE CASCADE, + FOREIGN KEY (BatchId) REFERENCES ArticleBatches(Id) ON DELETE SET NULL, + UNIQUE(ArticleId, WarehouseId, BatchId) +); + +CREATE INDEX IF NOT EXISTS IX_StockLevels_ArticleId_WarehouseId_BatchId ON StockLevels(ArticleId, WarehouseId, BatchId); +CREATE INDEX IF NOT EXISTS IX_StockLevels_WarehouseId ON StockLevels(WarehouseId); +CREATE INDEX IF NOT EXISTS IX_StockLevels_LocationCode ON StockLevels(LocationCode); + +-- Causali Movimento +CREATE TABLE IF NOT EXISTS MovementReasons ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Code TEXT NOT NULL UNIQUE, + Description TEXT NOT NULL, + MovementType INTEGER NOT NULL, + StockSign INTEGER NOT NULL, + RequiresExternalReference INTEGER NOT NULL DEFAULT 0, + RequiresValuation INTEGER NOT NULL DEFAULT 1, + UpdatesAverageCost INTEGER NOT NULL DEFAULT 1, + IsSystem INTEGER NOT NULL DEFAULT 0, + IsActive INTEGER NOT NULL DEFAULT 1, + SortOrder INTEGER NOT NULL DEFAULT 0, + Notes TEXT, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT +); + +CREATE INDEX IF NOT EXISTS IX_MovementReasons_Code ON MovementReasons(Code); +CREATE INDEX IF NOT EXISTS IX_MovementReasons_MovementType ON MovementReasons(MovementType); +CREATE INDEX IF NOT EXISTS IX_MovementReasons_IsActive ON MovementReasons(IsActive); + +-- Movimenti di Magazzino (Testata) +CREATE TABLE IF NOT EXISTS StockMovements ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + DocumentNumber TEXT NOT NULL UNIQUE, + MovementDate TEXT NOT NULL, + Type INTEGER NOT NULL, + ReasonId INTEGER, + SourceWarehouseId INTEGER, + DestinationWarehouseId INTEGER, + ExternalReference TEXT, + ExternalDocumentType INTEGER, + SupplierId INTEGER, + CustomerId INTEGER, + Status INTEGER NOT NULL DEFAULT 0, + ConfirmedDate TEXT, + ConfirmedBy TEXT, + TotalValue REAL, + Notes TEXT, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT, + FOREIGN KEY (ReasonId) REFERENCES MovementReasons(Id) ON DELETE SET NULL, + FOREIGN KEY (SourceWarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE RESTRICT, + FOREIGN KEY (DestinationWarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE RESTRICT +); + +CREATE INDEX IF NOT EXISTS IX_StockMovements_DocumentNumber ON StockMovements(DocumentNumber); +CREATE INDEX IF NOT EXISTS IX_StockMovements_MovementDate ON StockMovements(MovementDate); +CREATE INDEX IF NOT EXISTS IX_StockMovements_Type ON StockMovements(Type); +CREATE INDEX IF NOT EXISTS IX_StockMovements_Status ON StockMovements(Status); +CREATE INDEX IF NOT EXISTS IX_StockMovements_ExternalReference ON StockMovements(ExternalReference); + +-- Movimenti di Magazzino (Righe) +CREATE TABLE IF NOT EXISTS StockMovementLines ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + MovementId INTEGER NOT NULL, + LineNumber INTEGER NOT NULL, + ArticleId INTEGER NOT NULL, + BatchId INTEGER, + SerialId INTEGER, + Quantity REAL NOT NULL, + UnitOfMeasure TEXT NOT NULL DEFAULT 'PZ', + UnitCost REAL, + LineValue REAL, + SourceLocationCode TEXT, + DestinationLocationCode TEXT, + ExternalLineReference TEXT, + Notes TEXT, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT, + FOREIGN KEY (MovementId) REFERENCES StockMovements(Id) ON DELETE CASCADE, + FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE RESTRICT, + FOREIGN KEY (BatchId) REFERENCES ArticleBatches(Id) ON DELETE SET NULL, + FOREIGN KEY (SerialId) REFERENCES ArticleSerials(Id) ON DELETE SET NULL, + UNIQUE(MovementId, LineNumber) +); + +CREATE INDEX IF NOT EXISTS IX_StockMovementLines_MovementId_LineNumber ON StockMovementLines(MovementId, LineNumber); +CREATE INDEX IF NOT EXISTS IX_StockMovementLines_ArticleId ON StockMovementLines(ArticleId); +CREATE INDEX IF NOT EXISTS IX_StockMovementLines_BatchId ON StockMovementLines(BatchId); +CREATE INDEX IF NOT EXISTS IX_StockMovementLines_SerialId ON StockMovementLines(SerialId); + +-- Valorizzazione Magazzino per Periodo +CREATE TABLE IF NOT EXISTS StockValuations ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + ValuationDate TEXT NOT NULL, + Period INTEGER NOT NULL, + ArticleId INTEGER NOT NULL, + WarehouseId INTEGER, + Quantity REAL NOT NULL DEFAULT 0, + Method INTEGER NOT NULL DEFAULT 0, + UnitCost REAL NOT NULL DEFAULT 0, + TotalValue REAL NOT NULL DEFAULT 0, + InboundQuantity REAL NOT NULL DEFAULT 0, + InboundValue REAL NOT NULL DEFAULT 0, + OutboundQuantity REAL NOT NULL DEFAULT 0, + OutboundValue REAL NOT NULL DEFAULT 0, + IsClosed INTEGER NOT NULL DEFAULT 0, + ClosedDate TEXT, + ClosedBy TEXT, + Notes TEXT, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT, + FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE CASCADE, + FOREIGN KEY (WarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE SET NULL, + UNIQUE(Period, ArticleId, WarehouseId) +); + +CREATE INDEX IF NOT EXISTS IX_StockValuations_Period_ArticleId_WarehouseId ON StockValuations(Period, ArticleId, WarehouseId); +CREATE INDEX IF NOT EXISTS IX_StockValuations_ValuationDate ON StockValuations(ValuationDate); +CREATE INDEX IF NOT EXISTS IX_StockValuations_IsClosed ON StockValuations(IsClosed); + +-- Layer Valorizzazione FIFO/LIFO +CREATE TABLE IF NOT EXISTS StockValuationLayers ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + ArticleId INTEGER NOT NULL, + WarehouseId INTEGER NOT NULL, + BatchId INTEGER, + LayerDate TEXT NOT NULL, + SourceMovementId INTEGER, + OriginalQuantity REAL NOT NULL DEFAULT 0, + RemainingQuantity REAL NOT NULL DEFAULT 0, + UnitCost REAL NOT NULL DEFAULT 0, + IsExhausted INTEGER NOT NULL DEFAULT 0, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT, + FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE CASCADE, + FOREIGN KEY (WarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE CASCADE, + FOREIGN KEY (BatchId) REFERENCES ArticleBatches(Id) ON DELETE SET NULL, + FOREIGN KEY (SourceMovementId) REFERENCES StockMovements(Id) ON DELETE SET NULL +); + +CREATE INDEX IF NOT EXISTS IX_StockValuationLayers_ArticleId_WarehouseId_LayerDate ON StockValuationLayers(ArticleId, WarehouseId, LayerDate); +CREATE INDEX IF NOT EXISTS IX_StockValuationLayers_IsExhausted ON StockValuationLayers(IsExhausted); + +-- Inventari Fisici (Testata) +CREATE TABLE IF NOT EXISTS InventoryCounts ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Code TEXT NOT NULL UNIQUE, + Description TEXT NOT NULL, + InventoryDate TEXT NOT NULL, + WarehouseId INTEGER, + CategoryId INTEGER, + Type INTEGER NOT NULL DEFAULT 0, + Status INTEGER NOT NULL DEFAULT 0, + StartDate TEXT, + EndDate TEXT, + ConfirmedDate TEXT, + ConfirmedBy TEXT, + AdjustmentMovementId INTEGER, + PositiveDifferenceValue REAL, + NegativeDifferenceValue REAL, + Notes TEXT, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT, + FOREIGN KEY (WarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE SET NULL, + FOREIGN KEY (CategoryId) REFERENCES WarehouseArticleCategories(Id) ON DELETE SET NULL, + FOREIGN KEY (AdjustmentMovementId) REFERENCES StockMovements(Id) ON DELETE SET NULL +); + +CREATE INDEX IF NOT EXISTS IX_InventoryCounts_Code ON InventoryCounts(Code); +CREATE INDEX IF NOT EXISTS IX_InventoryCounts_InventoryDate ON InventoryCounts(InventoryDate); +CREATE INDEX IF NOT EXISTS IX_InventoryCounts_Status ON InventoryCounts(Status); + +-- Inventari Fisici (Righe) +CREATE TABLE IF NOT EXISTS InventoryCountLines ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + InventoryCountId INTEGER NOT NULL, + ArticleId INTEGER NOT NULL, + WarehouseId INTEGER NOT NULL, + BatchId INTEGER, + LocationCode TEXT, + TheoreticalQuantity REAL NOT NULL DEFAULT 0, + CountedQuantity REAL, + UnitCost REAL, + CountedAt TEXT, + CountedBy TEXT, + SecondCountQuantity REAL, + SecondCountBy TEXT, + Notes TEXT, + CreatedAt TEXT, + CreatedBy TEXT, + UpdatedAt TEXT, + UpdatedBy TEXT, + FOREIGN KEY (InventoryCountId) REFERENCES InventoryCounts(Id) ON DELETE CASCADE, + FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE RESTRICT, + FOREIGN KEY (WarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE RESTRICT, + FOREIGN KEY (BatchId) REFERENCES ArticleBatches(Id) ON DELETE SET NULL, + UNIQUE(InventoryCountId, ArticleId, WarehouseId, BatchId) +); + +CREATE INDEX IF NOT EXISTS IX_InventoryCountLines_InventoryCountId_ArticleId ON InventoryCountLines(InventoryCountId, ArticleId, WarehouseId, BatchId); +CREATE INDEX IF NOT EXISTS IX_InventoryCountLines_ArticleId ON InventoryCountLines(ArticleId); diff --git a/src/Apollinare.Infrastructure/Migrations/20251129134709_InitialCreate.Designer.cs b/src/Apollinare.Infrastructure/Migrations/20251129134709_InitialCreate.Designer.cs new file mode 100644 index 0000000..971a9a3 --- /dev/null +++ b/src/Apollinare.Infrastructure/Migrations/20251129134709_InitialCreate.Designer.cs @@ -0,0 +1,3018 @@ +// +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("20251129134709_InitialCreate")] + partial class InitialCreate + { + /// + 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.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("Note") + .HasColumnType("TEXT"); + + b.Property("Numero") + .HasColumnType("INTEGER"); + + b.Property("Ordine") + .HasColumnType("INTEGER"); + + b.Property("Sconto") + .HasColumnType("TEXT"); + + b.Property("TipoOspiteId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventoId"); + + b.HasIndex("TipoOspiteId"); + + b.ToTable("EventiDettaglioOspiti"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoDettaglioPrelievo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticoloId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("EventoId") + .HasColumnType("INTEGER"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("QtaCalcolata") + .HasColumnType("TEXT"); + + b.Property("QtaEffettiva") + .HasColumnType("TEXT"); + + b.Property("QtaRichiesta") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArticoloId"); + + b.HasIndex("EventoId"); + + b.ToTable("EventiDettaglioPrelievo"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoDettaglioRisorsa", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Costo") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("EventoId") + .HasColumnType("INTEGER"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OraFine") + .HasColumnType("TEXT"); + + b.Property("OraInizio") + .HasColumnType("TEXT"); + + b.Property("OreLavoro") + .HasColumnType("TEXT"); + + b.Property("RisorsaId") + .HasColumnType("INTEGER"); + + b.Property("Ruolo") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventoId"); + + b.HasIndex("RisorsaId"); + + b.ToTable("EventiDettaglioRisorse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("Cap") + .HasColumnType("TEXT"); + + b.Property("Citta") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DistanzaKm") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Indirizzo") + .HasColumnType("TEXT"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("Provincia") + .HasColumnType("TEXT"); + + b.Property("Referente") + .HasColumnType("TEXT"); + + b.Property("Telefono") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Nome"); + + b.ToTable("Location"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.ModuleSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoRenew") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LastRenewalDate") + .HasColumnType("TEXT"); + + b.Property("ModuleId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("PaidPrice") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("SubscriptionType") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ModuleId") + .IsUnique(); + + b.ToTable("ModuleSubscriptions"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.ReportFont", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FontData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FontFamily") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FontStyle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FontFamily"); + + b.ToTable("ReportFonts"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.ReportImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("Categoria") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Categoria"); + + b.ToTable("ReportImages"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.ReportTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("Categoria") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Descrizione") + .HasColumnType("TEXT"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Orientation") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PageSize") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TemplateJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Thumbnail") + .HasColumnType("BLOB"); + + b.Property("ThumbnailMimeType") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Categoria"); + + b.HasIndex("Nome"); + + b.ToTable("ReportTemplates"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Risorsa", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("Cognome") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("Telefono") + .HasColumnType("TEXT"); + + b.Property("TipoRisorsaId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TipoRisorsaId"); + + b.ToTable("Risorse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoEvento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .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("TipoPastoId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TipoPastoId"); + + b.ToTable("TipiEvento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoMateriale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .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("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TipiMateriale"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoOspite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .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("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TipiOspite"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoPasto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .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("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TipiPasto"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoRisorsa", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .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("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TipiRisorsa"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Utente", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("Cognome") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Nome") + .HasColumnType("TEXT"); + + b.Property("Ruolo") + .HasColumnType("TEXT"); + + b.Property("SolaLettura") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Utenti"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.VirtualDataset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("Categoria") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConfigurationJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Descrizione") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Icon") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Categoria"); + + b.HasIndex("Nome") + .IsUnique(); + + b.ToTable("VirtualDatasets"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleBarcode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("Barcode") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArticleId"); + + b.HasIndex("Barcode") + .IsUnique(); + + b.ToTable("ArticleBarcodes", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("BatchNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Certifications") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("CurrentQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("ExpiryDate") + .HasColumnType("TEXT"); + + b.Property("InitialQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("LastQualityCheckDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ProductionDate") + .HasColumnType("TEXT"); + + b.Property("QualityStatus") + .HasColumnType("INTEGER"); + + b.Property("ReservedQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("SupplierBatch") + .HasColumnType("TEXT"); + + b.Property("SupplierId") + .HasColumnType("INTEGER"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExpiryDate"); + + b.HasIndex("Status"); + + b.HasIndex("ArticleId", "BatchNumber") + .IsUnique(); + + b.ToTable("ArticleBatches", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleSerial", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("Attributes") + .HasColumnType("TEXT"); + + b.Property("BatchId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("CurrentWarehouseId") + .HasColumnType("INTEGER"); + + b.Property("CustomerId") + .HasColumnType("INTEGER"); + + b.Property("ManufacturerSerial") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ProductionDate") + .HasColumnType("TEXT"); + + b.Property("SalesReference") + .HasColumnType("TEXT"); + + b.Property("SerialNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SoldDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("SupplierId") + .HasColumnType("INTEGER"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("WarrantyExpiryDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BatchId"); + + b.HasIndex("CurrentWarehouseId"); + + b.HasIndex("Status"); + + b.HasIndex("ArticleId", "SerialNumber") + .IsUnique(); + + b.ToTable("ArticleSerials", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.InventoryCount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdjustmentMovementId") + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConfirmedBy") + .HasColumnType("TEXT"); + + b.Property("ConfirmedDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("InventoryDate") + .HasColumnType("TEXT"); + + b.Property("NegativeDifferenceValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("PositiveDifferenceValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("WarehouseId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AdjustmentMovementId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("InventoryDate"); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.ToTable("InventoryCounts", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.InventoryCountLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("BatchId") + .HasColumnType("INTEGER"); + + b.Property("CountedAt") + .HasColumnType("TEXT"); + + b.Property("CountedBy") + .HasColumnType("TEXT"); + + b.Property("CountedQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("InventoryCountId") + .HasColumnType("INTEGER"); + + b.Property("LocationCode") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("SecondCountBy") + .HasColumnType("TEXT"); + + b.Property("SecondCountQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("TheoreticalQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("WarehouseId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArticleId"); + + b.HasIndex("BatchId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("InventoryCountId", "ArticleId", "WarehouseId", "BatchId") + .IsUnique(); + + b.ToTable("InventoryCountLines", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.MovementReason", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsSystem") + .HasColumnType("INTEGER"); + + b.Property("MovementType") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("RequiresExternalReference") + .HasColumnType("INTEGER"); + + b.Property("RequiresValuation") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("StockSign") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("UpdatesAverageCost") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive"); + + b.HasIndex("MovementType"); + + b.ToTable("MovementReasons", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockLevel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("BatchId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("LastInventoryDate") + .HasColumnType("TEXT"); + + b.Property("LastMovementDate") + .HasColumnType("TEXT"); + + b.Property("LocationCode") + .HasColumnType("TEXT"); + + b.Property("OnOrderQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("ReservedQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("StockValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("WarehouseId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BatchId"); + + b.HasIndex("LocationCode"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ArticleId", "WarehouseId", "BatchId") + .IsUnique(); + + b.ToTable("StockLevels", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockMovement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfirmedBy") + .HasColumnType("TEXT"); + + b.Property("ConfirmedDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("CustomerId") + .HasColumnType("INTEGER"); + + b.Property("DestinationWarehouseId") + .HasColumnType("INTEGER"); + + b.Property("DocumentNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExternalDocumentType") + .HasColumnType("INTEGER"); + + b.Property("ExternalReference") + .HasColumnType("TEXT"); + + b.Property("MovementDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ReasonId") + .HasColumnType("INTEGER"); + + b.Property("SourceWarehouseId") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("SupplierId") + .HasColumnType("INTEGER"); + + b.Property("TotalValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DestinationWarehouseId"); + + b.HasIndex("DocumentNumber") + .IsUnique(); + + b.HasIndex("ExternalReference"); + + b.HasIndex("MovementDate"); + + b.HasIndex("ReasonId"); + + b.HasIndex("SourceWarehouseId"); + + b.HasIndex("Status"); + + b.HasIndex("Type"); + + b.ToTable("StockMovements", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockMovementLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("BatchId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DestinationLocationCode") + .HasColumnType("TEXT"); + + b.Property("ExternalLineReference") + .HasColumnType("TEXT"); + + b.Property("LineNumber") + .HasColumnType("INTEGER"); + + b.Property("LineValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("MovementId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("SerialId") + .HasColumnType("INTEGER"); + + b.Property("SourceLocationCode") + .HasColumnType("TEXT"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UnitOfMeasure") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArticleId"); + + b.HasIndex("BatchId"); + + b.HasIndex("SerialId"); + + b.HasIndex("MovementId", "LineNumber") + .IsUnique(); + + b.ToTable("StockMovementLines", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockValuation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("ClosedBy") + .HasColumnType("TEXT"); + + b.Property("ClosedDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("InboundQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("InboundValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("IsClosed") + .HasColumnType("INTEGER"); + + b.Property("Method") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OutboundQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("OutboundValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Period") + .HasColumnType("INTEGER"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("TotalValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("ValuationDate") + .HasColumnType("TEXT"); + + b.Property("WarehouseId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArticleId"); + + b.HasIndex("IsClosed"); + + b.HasIndex("ValuationDate"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("Period", "ArticleId", "WarehouseId") + .IsUnique(); + + b.ToTable("StockValuations", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockValuationLayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("BatchId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("IsExhausted") + .HasColumnType("INTEGER"); + + b.Property("LayerDate") + .HasColumnType("TEXT"); + + b.Property("OriginalQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("RemainingQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("SourceMovementId") + .HasColumnType("INTEGER"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("WarehouseId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BatchId"); + + b.HasIndex("IsExhausted"); + + b.HasIndex("SourceMovementId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ArticleId", "WarehouseId", "LayerDate"); + + b.ToTable("StockValuationLayers", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Barcode") + .HasColumnType("TEXT"); + + b.Property("BaseSellingPrice") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Depth") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiryWarningDays") + .HasColumnType("INTEGER"); + + b.Property("HasExpiry") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("TEXT"); + + b.Property("Image") + .HasColumnType("BLOB"); + + b.Property("ImageMimeType") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsBatchManaged") + .HasColumnType("INTEGER"); + + b.Property("IsSerialManaged") + .HasColumnType("INTEGER"); + + b.Property("LastPurchaseCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("LeadTimeDays") + .HasColumnType("INTEGER"); + + b.Property("ManufacturerCode") + .HasColumnType("TEXT"); + + b.Property("MaximumStock") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("MinimumStock") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ReorderPoint") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("ReorderQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("SecondaryUnitOfMeasure") + .HasColumnType("TEXT"); + + b.Property("ShortDescription") + .HasColumnType("TEXT"); + + b.Property("StandardCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("StockManagement") + .HasColumnType("INTEGER"); + + b.Property("UnitConversionFactor") + .HasPrecision(18, 6) + .HasColumnType("TEXT"); + + b.Property("UnitOfMeasure") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("ValuationMethod") + .HasColumnType("INTEGER"); + + b.Property("Volume") + .HasPrecision(18, 6) + .HasColumnType("TEXT"); + + b.Property("Weight") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("WeightedAverageCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Barcode"); + + b.HasIndex("CategoryId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive"); + + b.ToTable("WarehouseArticles", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseArticleCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Color") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DefaultValuationMethod") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FullPath") + .HasColumnType("TEXT"); + + b.Property("Icon") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ParentCategoryId") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("FullPath"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("WarehouseArticleCategories", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("City") + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Country") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .HasColumnType("TEXT"); + + b.Property("Province") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive"); + + b.HasIndex("IsDefault"); + + b.ToTable("WarehouseLocations", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Articolo", b => + { + b.HasOne("Apollinare.Domain.Entities.CodiceCategoria", "Categoria") + .WithMany("Articoli") + .HasForeignKey("CategoriaId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.TipoMateriale", "TipoMateriale") + .WithMany("Articoli") + .HasForeignKey("TipoMaterialeId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Categoria"); + + b.Navigation("TipoMateriale"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Evento", b => + { + b.HasOne("Apollinare.Domain.Entities.Cliente", "Cliente") + .WithMany("Eventi") + .HasForeignKey("ClienteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Location", "Location") + .WithMany("Eventi") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.TipoEvento", "TipoEvento") + .WithMany("Eventi") + .HasForeignKey("TipoEventoId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cliente"); + + b.Navigation("Location"); + + b.Navigation("TipoEvento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoAcconto", b => + { + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("Acconti") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Evento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoAllegato", b => + { + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("Allegati") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Evento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoAltroCosto", b => + { + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("AltriCosti") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Evento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoDegustazione", b => + { + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("Degustazioni") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Evento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoDettaglioOspiti", b => + { + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("DettagliOspiti") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.TipoOspite", "TipoOspite") + .WithMany("DettagliOspiti") + .HasForeignKey("TipoOspiteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Evento"); + + b.Navigation("TipoOspite"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoDettaglioPrelievo", b => + { + b.HasOne("Apollinare.Domain.Entities.Articolo", "Articolo") + .WithMany("DettagliPrelievo") + .HasForeignKey("ArticoloId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("DettagliPrelievo") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Articolo"); + + b.Navigation("Evento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoDettaglioRisorsa", b => + { + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("DettagliRisorse") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Risorsa", "Risorsa") + .WithMany("DettagliRisorse") + .HasForeignKey("RisorsaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Evento"); + + b.Navigation("Risorsa"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.ModuleSubscription", b => + { + b.HasOne("Apollinare.Domain.Entities.AppModule", "Module") + .WithOne("Subscription") + .HasForeignKey("Apollinare.Domain.Entities.ModuleSubscription", "ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Risorsa", b => + { + b.HasOne("Apollinare.Domain.Entities.TipoRisorsa", "TipoRisorsa") + .WithMany("Risorse") + .HasForeignKey("TipoRisorsaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("TipoRisorsa"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoEvento", b => + { + b.HasOne("Apollinare.Domain.Entities.TipoPasto", "TipoPasto") + .WithMany("TipiEvento") + .HasForeignKey("TipoPastoId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("TipoPasto"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleBarcode", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany("Barcodes") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Article"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleBatch", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany("Batches") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Article"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleSerial", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany("Serials") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.ArticleBatch", "Batch") + .WithMany("Serials") + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "CurrentWarehouse") + .WithMany() + .HasForeignKey("CurrentWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Article"); + + b.Navigation("Batch"); + + b.Navigation("CurrentWarehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.InventoryCount", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.StockMovement", "AdjustmentMovement") + .WithMany() + .HasForeignKey("AdjustmentMovementId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticleCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AdjustmentMovement"); + + b.Navigation("Category"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.InventoryCountLine", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany() + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.ArticleBatch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.InventoryCount", "InventoryCount") + .WithMany("Lines") + .HasForeignKey("InventoryCountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Article"); + + b.Navigation("Batch"); + + b.Navigation("InventoryCount"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockLevel", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany("StockLevels") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.ArticleBatch", "Batch") + .WithMany("StockLevels") + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "Warehouse") + .WithMany("StockLevels") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Article"); + + b.Navigation("Batch"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockMovement", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "DestinationWarehouse") + .WithMany("DestinationMovements") + .HasForeignKey("DestinationWarehouseId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.MovementReason", "Reason") + .WithMany("Movements") + .HasForeignKey("ReasonId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "SourceWarehouse") + .WithMany("SourceMovements") + .HasForeignKey("SourceWarehouseId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DestinationWarehouse"); + + b.Navigation("Reason"); + + b.Navigation("SourceWarehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockMovementLine", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany("MovementLines") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.ArticleBatch", "Batch") + .WithMany("MovementLines") + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.StockMovement", "Movement") + .WithMany("Lines") + .HasForeignKey("MovementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.ArticleSerial", "Serial") + .WithMany("MovementLines") + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Article"); + + b.Navigation("Batch"); + + b.Navigation("Movement"); + + b.Navigation("Serial"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockValuation", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany() + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Article"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockValuationLayer", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany() + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.ArticleBatch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.StockMovement", "SourceMovement") + .WithMany() + .HasForeignKey("SourceMovementId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Article"); + + b.Navigation("Batch"); + + b.Navigation("SourceMovement"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticleCategory", "Category") + .WithMany("Articles") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseArticleCategory", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticleCategory", "ParentCategory") + .WithMany("ChildCategories") + .HasForeignKey("ParentCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.AppModule", b => + { + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Articolo", b => + { + b.Navigation("DettagliPrelievo"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Cliente", b => + { + b.Navigation("Eventi"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.CodiceCategoria", b => + { + b.Navigation("Articoli"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Evento", b => + { + b.Navigation("Acconti"); + + b.Navigation("Allegati"); + + b.Navigation("AltriCosti"); + + b.Navigation("Degustazioni"); + + b.Navigation("DettagliOspiti"); + + b.Navigation("DettagliPrelievo"); + + b.Navigation("DettagliRisorse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Location", b => + { + b.Navigation("Eventi"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Risorsa", b => + { + b.Navigation("DettagliRisorse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoEvento", b => + { + b.Navigation("Eventi"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoMateriale", b => + { + b.Navigation("Articoli"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoOspite", b => + { + b.Navigation("DettagliOspiti"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoPasto", b => + { + b.Navigation("TipiEvento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoRisorsa", b => + { + b.Navigation("Risorse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleBatch", b => + { + b.Navigation("MovementLines"); + + b.Navigation("Serials"); + + b.Navigation("StockLevels"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleSerial", b => + { + b.Navigation("MovementLines"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.InventoryCount", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.MovementReason", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockMovement", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", b => + { + b.Navigation("Barcodes"); + + b.Navigation("Batches"); + + b.Navigation("MovementLines"); + + b.Navigation("Serials"); + + b.Navigation("StockLevels"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseArticleCategory", b => + { + b.Navigation("Articles"); + + b.Navigation("ChildCategories"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", b => + { + b.Navigation("DestinationMovements"); + + b.Navigation("SourceMovements"); + + b.Navigation("StockLevels"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Apollinare.Infrastructure/Migrations/20251129134709_InitialCreate.cs b/src/Apollinare.Infrastructure/Migrations/20251129134709_InitialCreate.cs new file mode 100644 index 0000000..f85999b --- /dev/null +++ b/src/Apollinare.Infrastructure/Migrations/20251129134709_InitialCreate.cs @@ -0,0 +1,1952 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Apollinare.Infrastructure.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AppModules", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Code = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: true), + Icon = table.Column(type: "TEXT", nullable: true), + BasePrice = table.Column(type: "TEXT", precision: 18, scale: 2, nullable: false), + MonthlyMultiplier = table.Column(type: "TEXT", precision: 5, scale: 2, nullable: false), + SortOrder = table.Column(type: "INTEGER", nullable: false), + IsCore = table.Column(type: "INTEGER", nullable: false), + Dependencies = table.Column(type: "TEXT", nullable: true), + RoutePath = table.Column(type: "TEXT", nullable: true), + IsAvailable = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AppModules", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Clienti", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RagioneSociale = table.Column(type: "TEXT", nullable: false), + Indirizzo = table.Column(type: "TEXT", nullable: true), + Cap = table.Column(type: "TEXT", nullable: true), + Citta = table.Column(type: "TEXT", nullable: true), + Provincia = table.Column(type: "TEXT", nullable: true), + Telefono = table.Column(type: "TEXT", nullable: true), + Email = table.Column(type: "TEXT", nullable: true), + Pec = table.Column(type: "TEXT", nullable: true), + CodiceFiscale = table.Column(type: "TEXT", nullable: true), + PartitaIva = table.Column(type: "TEXT", nullable: true), + CodiceDestinatario = table.Column(type: "TEXT", nullable: true), + Note = table.Column(type: "TEXT", nullable: true), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Clienti", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "CodiciCategoria", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Codice = table.Column(type: "TEXT", nullable: false), + Descrizione = table.Column(type: "TEXT", nullable: false), + CoeffA = table.Column(type: "TEXT", nullable: false), + CoeffB = table.Column(type: "TEXT", nullable: false), + CoeffS = table.Column(type: "TEXT", nullable: false), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CodiciCategoria", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Configurazioni", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Chiave = table.Column(type: "TEXT", nullable: false), + Valore = table.Column(type: "TEXT", nullable: true), + Descrizione = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Configurazioni", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Location", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Nome = table.Column(type: "TEXT", nullable: false), + Indirizzo = table.Column(type: "TEXT", nullable: true), + Cap = table.Column(type: "TEXT", nullable: true), + Citta = table.Column(type: "TEXT", nullable: true), + Provincia = table.Column(type: "TEXT", nullable: true), + Telefono = table.Column(type: "TEXT", nullable: true), + Email = table.Column(type: "TEXT", nullable: true), + Referente = table.Column(type: "TEXT", nullable: true), + DistanzaKm = table.Column(type: "TEXT", nullable: true), + Note = table.Column(type: "TEXT", nullable: true), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Location", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "MovementReasons", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Code = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: false), + MovementType = table.Column(type: "INTEGER", nullable: false), + StockSign = table.Column(type: "INTEGER", nullable: false), + RequiresExternalReference = table.Column(type: "INTEGER", nullable: false), + RequiresValuation = table.Column(type: "INTEGER", nullable: false), + UpdatesAverageCost = table.Column(type: "INTEGER", nullable: false), + IsSystem = table.Column(type: "INTEGER", nullable: false), + IsActive = table.Column(type: "INTEGER", nullable: false), + SortOrder = table.Column(type: "INTEGER", nullable: false), + Notes = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MovementReasons", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ReportFonts", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Nome = table.Column(type: "TEXT", nullable: false), + FontFamily = table.Column(type: "TEXT", nullable: false), + FontStyle = table.Column(type: "TEXT", nullable: false), + FontData = table.Column(type: "BLOB", nullable: false), + MimeType = table.Column(type: "TEXT", nullable: false), + FileSize = table.Column(type: "INTEGER", nullable: false), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ReportFonts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ReportImages", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Nome = table.Column(type: "TEXT", nullable: false), + Categoria = table.Column(type: "TEXT", nullable: false), + ImageData = table.Column(type: "BLOB", nullable: false), + MimeType = table.Column(type: "TEXT", nullable: false), + Width = table.Column(type: "INTEGER", nullable: false), + Height = table.Column(type: "INTEGER", nullable: false), + FileSize = table.Column(type: "INTEGER", nullable: false), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ReportImages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ReportTemplates", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Nome = table.Column(type: "TEXT", nullable: false), + Descrizione = table.Column(type: "TEXT", nullable: true), + Categoria = table.Column(type: "TEXT", nullable: false), + TemplateJson = table.Column(type: "TEXT", nullable: false), + Thumbnail = table.Column(type: "BLOB", nullable: true), + ThumbnailMimeType = table.Column(type: "TEXT", nullable: true), + PageSize = table.Column(type: "TEXT", nullable: false), + Orientation = table.Column(type: "TEXT", nullable: false), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ReportTemplates", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TipiMateriale", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Codice = table.Column(type: "TEXT", nullable: false), + Descrizione = table.Column(type: "TEXT", nullable: false), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TipiMateriale", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TipiOspite", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Codice = table.Column(type: "TEXT", nullable: false), + Descrizione = table.Column(type: "TEXT", nullable: false), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TipiOspite", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TipiPasto", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Codice = table.Column(type: "TEXT", nullable: false), + Descrizione = table.Column(type: "TEXT", nullable: false), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TipiPasto", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TipiRisorsa", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Codice = table.Column(type: "TEXT", nullable: false), + Descrizione = table.Column(type: "TEXT", nullable: false), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TipiRisorsa", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Utenti", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Username = table.Column(type: "TEXT", nullable: false), + Nome = table.Column(type: "TEXT", nullable: true), + Cognome = table.Column(type: "TEXT", nullable: true), + Email = table.Column(type: "TEXT", nullable: true), + SolaLettura = table.Column(type: "INTEGER", nullable: false), + Attivo = table.Column(type: "INTEGER", nullable: false), + Ruolo = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Utenti", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "VirtualDatasets", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Nome = table.Column(type: "TEXT", nullable: false), + DisplayName = table.Column(type: "TEXT", nullable: false), + Descrizione = table.Column(type: "TEXT", nullable: true), + Icon = table.Column(type: "TEXT", nullable: false), + Categoria = table.Column(type: "TEXT", nullable: false), + ConfigurationJson = table.Column(type: "TEXT", nullable: false), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_VirtualDatasets", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "WarehouseArticleCategories", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Code = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: true), + ParentCategoryId = table.Column(type: "INTEGER", nullable: true), + Level = table.Column(type: "INTEGER", nullable: false), + FullPath = table.Column(type: "TEXT", nullable: true), + Icon = table.Column(type: "TEXT", nullable: true), + Color = table.Column(type: "TEXT", nullable: true), + DefaultValuationMethod = table.Column(type: "INTEGER", nullable: true), + SortOrder = table.Column(type: "INTEGER", nullable: false), + IsActive = table.Column(type: "INTEGER", nullable: false), + Notes = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WarehouseArticleCategories", x => x.Id); + table.ForeignKey( + name: "FK_WarehouseArticleCategories_WarehouseArticleCategories_ParentCategoryId", + column: x => x.ParentCategoryId, + principalTable: "WarehouseArticleCategories", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "WarehouseLocations", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Code = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: true), + Address = table.Column(type: "TEXT", nullable: true), + City = table.Column(type: "TEXT", nullable: true), + Province = table.Column(type: "TEXT", nullable: true), + PostalCode = table.Column(type: "TEXT", nullable: true), + Country = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "INTEGER", nullable: false), + IsDefault = table.Column(type: "INTEGER", nullable: false), + IsActive = table.Column(type: "INTEGER", nullable: false), + SortOrder = table.Column(type: "INTEGER", nullable: false), + Notes = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WarehouseLocations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ModuleSubscriptions", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ModuleId = table.Column(type: "INTEGER", nullable: false), + IsEnabled = table.Column(type: "INTEGER", nullable: false), + SubscriptionType = table.Column(type: "INTEGER", nullable: false), + StartDate = table.Column(type: "TEXT", nullable: true), + EndDate = table.Column(type: "TEXT", nullable: true), + AutoRenew = table.Column(type: "INTEGER", nullable: false), + Notes = table.Column(type: "TEXT", nullable: true), + LastRenewalDate = table.Column(type: "TEXT", nullable: true), + PaidPrice = table.Column(type: "TEXT", precision: 18, scale: 2, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ModuleSubscriptions", x => x.Id); + table.ForeignKey( + name: "FK_ModuleSubscriptions_AppModules_ModuleId", + column: x => x.ModuleId, + principalTable: "AppModules", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Articoli", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Codice = table.Column(type: "TEXT", nullable: false), + Descrizione = table.Column(type: "TEXT", nullable: false), + TipoMaterialeId = table.Column(type: "INTEGER", nullable: true), + CategoriaId = table.Column(type: "INTEGER", nullable: true), + QtaDisponibile = table.Column(type: "TEXT", nullable: true), + QtaStdA = table.Column(type: "TEXT", nullable: true), + QtaStdB = table.Column(type: "TEXT", nullable: true), + QtaStdS = table.Column(type: "TEXT", nullable: true), + UnitaMisura = table.Column(type: "TEXT", nullable: true), + Immagine = table.Column(type: "BLOB", nullable: true), + MimeType = table.Column(type: "TEXT", nullable: true), + Note = table.Column(type: "TEXT", nullable: true), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Articoli", x => x.Id); + table.ForeignKey( + name: "FK_Articoli_CodiciCategoria_CategoriaId", + column: x => x.CategoriaId, + principalTable: "CodiciCategoria", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Articoli_TipiMateriale_TipoMaterialeId", + column: x => x.TipoMaterialeId, + principalTable: "TipiMateriale", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "TipiEvento", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Codice = table.Column(type: "TEXT", nullable: false), + Descrizione = table.Column(type: "TEXT", nullable: false), + TipoPastoId = table.Column(type: "INTEGER", nullable: true), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TipiEvento", x => x.Id); + table.ForeignKey( + name: "FK_TipiEvento_TipiPasto_TipoPastoId", + column: x => x.TipoPastoId, + principalTable: "TipiPasto", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "Risorse", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Nome = table.Column(type: "TEXT", nullable: false), + Cognome = table.Column(type: "TEXT", nullable: true), + Telefono = table.Column(type: "TEXT", nullable: true), + Email = table.Column(type: "TEXT", nullable: true), + TipoRisorsaId = table.Column(type: "INTEGER", nullable: true), + Note = table.Column(type: "TEXT", nullable: true), + Attivo = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Risorse", x => x.Id); + table.ForeignKey( + name: "FK_Risorse_TipiRisorsa_TipoRisorsaId", + column: x => x.TipoRisorsaId, + principalTable: "TipiRisorsa", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "WarehouseArticles", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Code = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: false), + ShortDescription = table.Column(type: "TEXT", nullable: true), + Barcode = table.Column(type: "TEXT", nullable: true), + ManufacturerCode = table.Column(type: "TEXT", nullable: true), + CategoryId = table.Column(type: "INTEGER", nullable: true), + UnitOfMeasure = table.Column(type: "TEXT", nullable: false), + SecondaryUnitOfMeasure = table.Column(type: "TEXT", nullable: true), + UnitConversionFactor = table.Column(type: "TEXT", precision: 18, scale: 6, nullable: true), + StockManagement = table.Column(type: "INTEGER", nullable: false), + IsBatchManaged = table.Column(type: "INTEGER", nullable: false), + IsSerialManaged = table.Column(type: "INTEGER", nullable: false), + HasExpiry = table.Column(type: "INTEGER", nullable: false), + ExpiryWarningDays = table.Column(type: "INTEGER", nullable: true), + MinimumStock = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + MaximumStock = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + ReorderPoint = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + ReorderQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + LeadTimeDays = table.Column(type: "INTEGER", nullable: true), + ValuationMethod = table.Column(type: "INTEGER", nullable: true), + StandardCost = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + LastPurchaseCost = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + WeightedAverageCost = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + BaseSellingPrice = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + Weight = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + Volume = table.Column(type: "TEXT", precision: 18, scale: 6, nullable: true), + Width = table.Column(type: "TEXT", nullable: true), + Height = table.Column(type: "TEXT", nullable: true), + Depth = table.Column(type: "TEXT", nullable: true), + Image = table.Column(type: "BLOB", nullable: true), + ImageMimeType = table.Column(type: "TEXT", nullable: true), + IsActive = table.Column(type: "INTEGER", nullable: false), + Notes = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WarehouseArticles", x => x.Id); + table.ForeignKey( + name: "FK_WarehouseArticles_WarehouseArticleCategories_CategoryId", + column: x => x.CategoryId, + principalTable: "WarehouseArticleCategories", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "StockMovements", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DocumentNumber = table.Column(type: "TEXT", nullable: false), + MovementDate = table.Column(type: "TEXT", nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + ReasonId = table.Column(type: "INTEGER", nullable: true), + SourceWarehouseId = table.Column(type: "INTEGER", nullable: true), + DestinationWarehouseId = table.Column(type: "INTEGER", nullable: true), + ExternalReference = table.Column(type: "TEXT", nullable: true), + ExternalDocumentType = table.Column(type: "INTEGER", nullable: true), + SupplierId = table.Column(type: "INTEGER", nullable: true), + CustomerId = table.Column(type: "INTEGER", nullable: true), + Status = table.Column(type: "INTEGER", nullable: false), + ConfirmedDate = table.Column(type: "TEXT", nullable: true), + ConfirmedBy = table.Column(type: "TEXT", nullable: true), + TotalValue = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + Notes = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_StockMovements", x => x.Id); + table.ForeignKey( + name: "FK_StockMovements_MovementReasons_ReasonId", + column: x => x.ReasonId, + principalTable: "MovementReasons", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_StockMovements_WarehouseLocations_DestinationWarehouseId", + column: x => x.DestinationWarehouseId, + principalTable: "WarehouseLocations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_StockMovements_WarehouseLocations_SourceWarehouseId", + column: x => x.SourceWarehouseId, + principalTable: "WarehouseLocations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Eventi", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Codice = table.Column(type: "TEXT", nullable: true), + DataEvento = table.Column(type: "TEXT", nullable: false), + OraInizio = table.Column(type: "TEXT", nullable: true), + OraFine = table.Column(type: "TEXT", nullable: true), + ClienteId = table.Column(type: "INTEGER", nullable: true), + LocationId = table.Column(type: "INTEGER", nullable: true), + TipoEventoId = table.Column(type: "INTEGER", nullable: true), + Stato = table.Column(type: "INTEGER", nullable: false), + Descrizione = table.Column(type: "TEXT", nullable: true), + NumeroOspiti = table.Column(type: "INTEGER", nullable: true), + NumeroOspitiAdulti = table.Column(type: "INTEGER", nullable: true), + NumeroOspitiBambini = table.Column(type: "INTEGER", nullable: true), + NumeroOspitiSeduti = table.Column(type: "INTEGER", nullable: true), + NumeroOspitiBuffet = table.Column(type: "INTEGER", nullable: true), + CostoTotale = table.Column(type: "TEXT", nullable: true), + CostoPersona = table.Column(type: "TEXT", nullable: true), + TotaleAcconti = table.Column(type: "TEXT", nullable: true), + Saldo = table.Column(type: "TEXT", nullable: true), + DataScadenzaPreventivo = table.Column(type: "TEXT", nullable: true), + NoteInterne = table.Column(type: "TEXT", nullable: true), + NoteCliente = table.Column(type: "TEXT", nullable: true), + NoteCucina = table.Column(type: "TEXT", nullable: true), + NoteAllestimento = table.Column(type: "TEXT", nullable: true), + Confermato = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Eventi", x => x.Id); + table.ForeignKey( + name: "FK_Eventi_Clienti_ClienteId", + column: x => x.ClienteId, + principalTable: "Clienti", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Eventi_Location_LocationId", + column: x => x.LocationId, + principalTable: "Location", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Eventi_TipiEvento_TipoEventoId", + column: x => x.TipoEventoId, + principalTable: "TipiEvento", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "ArticleBarcodes", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ArticleId = table.Column(type: "INTEGER", nullable: false), + Barcode = table.Column(type: "TEXT", nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + Description = table.Column(type: "TEXT", nullable: true), + Quantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + IsPrimary = table.Column(type: "INTEGER", nullable: false), + IsActive = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ArticleBarcodes", x => x.Id); + table.ForeignKey( + name: "FK_ArticleBarcodes_WarehouseArticles_ArticleId", + column: x => x.ArticleId, + principalTable: "WarehouseArticles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ArticleBatches", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ArticleId = table.Column(type: "INTEGER", nullable: false), + BatchNumber = table.Column(type: "TEXT", nullable: false), + ProductionDate = table.Column(type: "TEXT", nullable: true), + ExpiryDate = table.Column(type: "TEXT", nullable: true), + SupplierBatch = table.Column(type: "TEXT", nullable: true), + SupplierId = table.Column(type: "INTEGER", nullable: true), + UnitCost = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + InitialQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + CurrentQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + ReservedQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + QualityStatus = table.Column(type: "INTEGER", nullable: true), + LastQualityCheckDate = table.Column(type: "TEXT", nullable: true), + Certifications = table.Column(type: "TEXT", nullable: true), + Notes = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ArticleBatches", x => x.Id); + table.ForeignKey( + name: "FK_ArticleBatches_WarehouseArticles_ArticleId", + column: x => x.ArticleId, + principalTable: "WarehouseArticles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "StockValuations", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ValuationDate = table.Column(type: "TEXT", nullable: false), + Period = table.Column(type: "INTEGER", nullable: false), + ArticleId = table.Column(type: "INTEGER", nullable: false), + WarehouseId = table.Column(type: "INTEGER", nullable: true), + Quantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + Method = table.Column(type: "INTEGER", nullable: false), + UnitCost = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + TotalValue = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + InboundQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + InboundValue = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + OutboundQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + OutboundValue = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + IsClosed = table.Column(type: "INTEGER", nullable: false), + ClosedDate = table.Column(type: "TEXT", nullable: true), + ClosedBy = table.Column(type: "TEXT", nullable: true), + Notes = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_StockValuations", x => x.Id); + table.ForeignKey( + name: "FK_StockValuations_WarehouseArticles_ArticleId", + column: x => x.ArticleId, + principalTable: "WarehouseArticles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_StockValuations_WarehouseLocations_WarehouseId", + column: x => x.WarehouseId, + principalTable: "WarehouseLocations", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "InventoryCounts", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Code = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: false), + InventoryDate = table.Column(type: "TEXT", nullable: false), + WarehouseId = table.Column(type: "INTEGER", nullable: true), + CategoryId = table.Column(type: "INTEGER", nullable: true), + Type = table.Column(type: "INTEGER", nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + StartDate = table.Column(type: "TEXT", nullable: true), + EndDate = table.Column(type: "TEXT", nullable: true), + ConfirmedDate = table.Column(type: "TEXT", nullable: true), + ConfirmedBy = table.Column(type: "TEXT", nullable: true), + AdjustmentMovementId = table.Column(type: "INTEGER", nullable: true), + PositiveDifferenceValue = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + NegativeDifferenceValue = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + Notes = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_InventoryCounts", x => x.Id); + table.ForeignKey( + name: "FK_InventoryCounts_StockMovements_AdjustmentMovementId", + column: x => x.AdjustmentMovementId, + principalTable: "StockMovements", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_InventoryCounts_WarehouseArticleCategories_CategoryId", + column: x => x.CategoryId, + principalTable: "WarehouseArticleCategories", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_InventoryCounts_WarehouseLocations_WarehouseId", + column: x => x.WarehouseId, + principalTable: "WarehouseLocations", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "EventiAcconti", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EventoId = table.Column(type: "INTEGER", nullable: false), + DataPagamento = table.Column(type: "TEXT", nullable: true), + Importo = table.Column(type: "TEXT", nullable: false), + Ordine = table.Column(type: "INTEGER", nullable: false), + AConferma = table.Column(type: "INTEGER", nullable: false), + Descrizione = table.Column(type: "TEXT", nullable: true), + MetodoPagamento = table.Column(type: "TEXT", nullable: true), + Note = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EventiAcconti", x => x.Id); + table.ForeignKey( + name: "FK_EventiAcconti_Eventi_EventoId", + column: x => x.EventoId, + principalTable: "Eventi", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EventiAllegati", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EventoId = table.Column(type: "INTEGER", nullable: false), + NomeFile = table.Column(type: "TEXT", nullable: false), + MimeType = table.Column(type: "TEXT", nullable: true), + Contenuto = table.Column(type: "BLOB", nullable: true), + Note = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EventiAllegati", x => x.Id); + table.ForeignKey( + name: "FK_EventiAllegati_Eventi_EventoId", + column: x => x.EventoId, + principalTable: "Eventi", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EventiAltriCosti", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EventoId = table.Column(type: "INTEGER", nullable: false), + Descrizione = table.Column(type: "TEXT", nullable: false), + CostoUnitario = table.Column(type: "TEXT", nullable: false), + Quantita = table.Column(type: "TEXT", nullable: false), + Ordine = table.Column(type: "INTEGER", nullable: false), + ApplicaIva = table.Column(type: "INTEGER", nullable: false), + AliquotaIva = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EventiAltriCosti", x => x.Id); + table.ForeignKey( + name: "FK_EventiAltriCosti_Eventi_EventoId", + column: x => x.EventoId, + principalTable: "Eventi", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EventiDegustazioni", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EventoId = table.Column(type: "INTEGER", nullable: false), + DataDegustazione = table.Column(type: "TEXT", nullable: false), + Ora = table.Column(type: "TEXT", nullable: true), + NumeroPersone = table.Column(type: "INTEGER", nullable: true), + NumeroPaganti = table.Column(type: "INTEGER", nullable: true), + CostoDegustazione = table.Column(type: "TEXT", nullable: true), + Detraibile = table.Column(type: "INTEGER", nullable: false), + Menu = table.Column(type: "TEXT", nullable: true), + Luogo = table.Column(type: "TEXT", nullable: true), + Note = table.Column(type: "TEXT", nullable: true), + Completata = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EventiDegustazioni", x => x.Id); + table.ForeignKey( + name: "FK_EventiDegustazioni_Eventi_EventoId", + column: x => x.EventoId, + principalTable: "Eventi", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EventiDettaglioOspiti", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EventoId = table.Column(type: "INTEGER", nullable: false), + TipoOspiteId = table.Column(type: "INTEGER", nullable: false), + Numero = table.Column(type: "INTEGER", nullable: false), + CostoUnitario = table.Column(type: "TEXT", nullable: true), + Sconto = table.Column(type: "TEXT", nullable: true), + Ordine = table.Column(type: "INTEGER", nullable: false), + Note = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EventiDettaglioOspiti", x => x.Id); + table.ForeignKey( + name: "FK_EventiDettaglioOspiti_Eventi_EventoId", + column: x => x.EventoId, + principalTable: "Eventi", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EventiDettaglioOspiti_TipiOspite_TipoOspiteId", + column: x => x.TipoOspiteId, + principalTable: "TipiOspite", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EventiDettaglioPrelievo", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EventoId = table.Column(type: "INTEGER", nullable: false), + ArticoloId = table.Column(type: "INTEGER", nullable: false), + QtaRichiesta = table.Column(type: "TEXT", nullable: true), + QtaCalcolata = table.Column(type: "TEXT", nullable: true), + QtaEffettiva = table.Column(type: "TEXT", nullable: true), + Note = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EventiDettaglioPrelievo", x => x.Id); + table.ForeignKey( + name: "FK_EventiDettaglioPrelievo_Articoli_ArticoloId", + column: x => x.ArticoloId, + principalTable: "Articoli", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EventiDettaglioPrelievo_Eventi_EventoId", + column: x => x.EventoId, + principalTable: "Eventi", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EventiDettaglioRisorse", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EventoId = table.Column(type: "INTEGER", nullable: false), + RisorsaId = table.Column(type: "INTEGER", nullable: false), + OreLavoro = table.Column(type: "TEXT", nullable: true), + Costo = table.Column(type: "TEXT", nullable: true), + OraInizio = table.Column(type: "TEXT", nullable: true), + OraFine = table.Column(type: "TEXT", nullable: true), + Ruolo = table.Column(type: "TEXT", nullable: true), + Note = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EventiDettaglioRisorse", x => x.Id); + table.ForeignKey( + name: "FK_EventiDettaglioRisorse_Eventi_EventoId", + column: x => x.EventoId, + principalTable: "Eventi", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EventiDettaglioRisorse_Risorse_RisorsaId", + column: x => x.RisorsaId, + principalTable: "Risorse", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ArticleSerials", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ArticleId = table.Column(type: "INTEGER", nullable: false), + BatchId = table.Column(type: "INTEGER", nullable: true), + SerialNumber = table.Column(type: "TEXT", nullable: false), + ManufacturerSerial = table.Column(type: "TEXT", nullable: true), + ProductionDate = table.Column(type: "TEXT", nullable: true), + WarrantyExpiryDate = table.Column(type: "TEXT", nullable: true), + CurrentWarehouseId = table.Column(type: "INTEGER", nullable: true), + Status = table.Column(type: "INTEGER", nullable: false), + UnitCost = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + SupplierId = table.Column(type: "INTEGER", nullable: true), + CustomerId = table.Column(type: "INTEGER", nullable: true), + SoldDate = table.Column(type: "TEXT", nullable: true), + SalesReference = table.Column(type: "TEXT", nullable: true), + Attributes = table.Column(type: "TEXT", nullable: true), + Notes = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ArticleSerials", x => x.Id); + table.ForeignKey( + name: "FK_ArticleSerials_ArticleBatches_BatchId", + column: x => x.BatchId, + principalTable: "ArticleBatches", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_ArticleSerials_WarehouseArticles_ArticleId", + column: x => x.ArticleId, + principalTable: "WarehouseArticles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ArticleSerials_WarehouseLocations_CurrentWarehouseId", + column: x => x.CurrentWarehouseId, + principalTable: "WarehouseLocations", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "StockLevels", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ArticleId = table.Column(type: "INTEGER", nullable: false), + WarehouseId = table.Column(type: "INTEGER", nullable: false), + BatchId = table.Column(type: "INTEGER", nullable: true), + Quantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + ReservedQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + OnOrderQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + StockValue = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + UnitCost = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + LastMovementDate = table.Column(type: "TEXT", nullable: true), + LastInventoryDate = table.Column(type: "TEXT", nullable: true), + LocationCode = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_StockLevels", x => x.Id); + table.ForeignKey( + name: "FK_StockLevels_ArticleBatches_BatchId", + column: x => x.BatchId, + principalTable: "ArticleBatches", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_StockLevels_WarehouseArticles_ArticleId", + column: x => x.ArticleId, + principalTable: "WarehouseArticles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_StockLevels_WarehouseLocations_WarehouseId", + column: x => x.WarehouseId, + principalTable: "WarehouseLocations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "StockValuationLayers", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ArticleId = table.Column(type: "INTEGER", nullable: false), + WarehouseId = table.Column(type: "INTEGER", nullable: false), + BatchId = table.Column(type: "INTEGER", nullable: true), + LayerDate = table.Column(type: "TEXT", nullable: false), + SourceMovementId = table.Column(type: "INTEGER", nullable: true), + OriginalQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + RemainingQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + UnitCost = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + IsExhausted = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_StockValuationLayers", x => x.Id); + table.ForeignKey( + name: "FK_StockValuationLayers_ArticleBatches_BatchId", + column: x => x.BatchId, + principalTable: "ArticleBatches", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_StockValuationLayers_StockMovements_SourceMovementId", + column: x => x.SourceMovementId, + principalTable: "StockMovements", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_StockValuationLayers_WarehouseArticles_ArticleId", + column: x => x.ArticleId, + principalTable: "WarehouseArticles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_StockValuationLayers_WarehouseLocations_WarehouseId", + column: x => x.WarehouseId, + principalTable: "WarehouseLocations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "InventoryCountLines", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + InventoryCountId = table.Column(type: "INTEGER", nullable: false), + ArticleId = table.Column(type: "INTEGER", nullable: false), + WarehouseId = table.Column(type: "INTEGER", nullable: false), + BatchId = table.Column(type: "INTEGER", nullable: true), + LocationCode = table.Column(type: "TEXT", nullable: true), + TheoreticalQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + CountedQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + UnitCost = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + CountedAt = table.Column(type: "TEXT", nullable: true), + CountedBy = table.Column(type: "TEXT", nullable: true), + SecondCountQuantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + SecondCountBy = table.Column(type: "TEXT", nullable: true), + Notes = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_InventoryCountLines", x => x.Id); + table.ForeignKey( + name: "FK_InventoryCountLines_ArticleBatches_BatchId", + column: x => x.BatchId, + principalTable: "ArticleBatches", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_InventoryCountLines_InventoryCounts_InventoryCountId", + column: x => x.InventoryCountId, + principalTable: "InventoryCounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_InventoryCountLines_WarehouseArticles_ArticleId", + column: x => x.ArticleId, + principalTable: "WarehouseArticles", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_InventoryCountLines_WarehouseLocations_WarehouseId", + column: x => x.WarehouseId, + principalTable: "WarehouseLocations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "StockMovementLines", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + MovementId = table.Column(type: "INTEGER", nullable: false), + LineNumber = table.Column(type: "INTEGER", nullable: false), + ArticleId = table.Column(type: "INTEGER", nullable: false), + BatchId = table.Column(type: "INTEGER", nullable: true), + SerialId = table.Column(type: "INTEGER", nullable: true), + Quantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + UnitOfMeasure = table.Column(type: "TEXT", nullable: false), + UnitCost = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + LineValue = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + SourceLocationCode = table.Column(type: "TEXT", nullable: true), + DestinationLocationCode = table.Column(type: "TEXT", nullable: true), + ExternalLineReference = table.Column(type: "TEXT", nullable: true), + Notes = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_StockMovementLines", x => x.Id); + table.ForeignKey( + name: "FK_StockMovementLines_ArticleBatches_BatchId", + column: x => x.BatchId, + principalTable: "ArticleBatches", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_StockMovementLines_ArticleSerials_SerialId", + column: x => x.SerialId, + principalTable: "ArticleSerials", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_StockMovementLines_StockMovements_MovementId", + column: x => x.MovementId, + principalTable: "StockMovements", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_StockMovementLines_WarehouseArticles_ArticleId", + column: x => x.ArticleId, + principalTable: "WarehouseArticles", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_AppModules_Code", + table: "AppModules", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AppModules_SortOrder", + table: "AppModules", + column: "SortOrder"); + + migrationBuilder.CreateIndex( + name: "IX_ArticleBarcodes_ArticleId", + table: "ArticleBarcodes", + column: "ArticleId"); + + migrationBuilder.CreateIndex( + name: "IX_ArticleBarcodes_Barcode", + table: "ArticleBarcodes", + column: "Barcode", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ArticleBatches_ArticleId_BatchNumber", + table: "ArticleBatches", + columns: new[] { "ArticleId", "BatchNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ArticleBatches_ExpiryDate", + table: "ArticleBatches", + column: "ExpiryDate"); + + migrationBuilder.CreateIndex( + name: "IX_ArticleBatches_Status", + table: "ArticleBatches", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_ArticleSerials_ArticleId_SerialNumber", + table: "ArticleSerials", + columns: new[] { "ArticleId", "SerialNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ArticleSerials_BatchId", + table: "ArticleSerials", + column: "BatchId"); + + migrationBuilder.CreateIndex( + name: "IX_ArticleSerials_CurrentWarehouseId", + table: "ArticleSerials", + column: "CurrentWarehouseId"); + + migrationBuilder.CreateIndex( + name: "IX_ArticleSerials_Status", + table: "ArticleSerials", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_Articoli_CategoriaId", + table: "Articoli", + column: "CategoriaId"); + + migrationBuilder.CreateIndex( + name: "IX_Articoli_Codice", + table: "Articoli", + column: "Codice", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Articoli_TipoMaterialeId", + table: "Articoli", + column: "TipoMaterialeId"); + + migrationBuilder.CreateIndex( + name: "IX_Clienti_PartitaIva", + table: "Clienti", + column: "PartitaIva"); + + migrationBuilder.CreateIndex( + name: "IX_Clienti_RagioneSociale", + table: "Clienti", + column: "RagioneSociale"); + + migrationBuilder.CreateIndex( + name: "IX_Configurazioni_Chiave", + table: "Configurazioni", + column: "Chiave", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Eventi_ClienteId", + table: "Eventi", + column: "ClienteId"); + + migrationBuilder.CreateIndex( + name: "IX_Eventi_Codice", + table: "Eventi", + column: "Codice"); + + migrationBuilder.CreateIndex( + name: "IX_Eventi_DataEvento", + table: "Eventi", + column: "DataEvento"); + + migrationBuilder.CreateIndex( + name: "IX_Eventi_LocationId", + table: "Eventi", + column: "LocationId"); + + migrationBuilder.CreateIndex( + name: "IX_Eventi_Stato", + table: "Eventi", + column: "Stato"); + + migrationBuilder.CreateIndex( + name: "IX_Eventi_TipoEventoId", + table: "Eventi", + column: "TipoEventoId"); + + migrationBuilder.CreateIndex( + name: "IX_EventiAcconti_EventoId", + table: "EventiAcconti", + column: "EventoId"); + + migrationBuilder.CreateIndex( + name: "IX_EventiAllegati_EventoId", + table: "EventiAllegati", + column: "EventoId"); + + migrationBuilder.CreateIndex( + name: "IX_EventiAltriCosti_EventoId", + table: "EventiAltriCosti", + column: "EventoId"); + + migrationBuilder.CreateIndex( + name: "IX_EventiDegustazioni_EventoId", + table: "EventiDegustazioni", + column: "EventoId"); + + migrationBuilder.CreateIndex( + name: "IX_EventiDettaglioOspiti_EventoId", + table: "EventiDettaglioOspiti", + column: "EventoId"); + + migrationBuilder.CreateIndex( + name: "IX_EventiDettaglioOspiti_TipoOspiteId", + table: "EventiDettaglioOspiti", + column: "TipoOspiteId"); + + migrationBuilder.CreateIndex( + name: "IX_EventiDettaglioPrelievo_ArticoloId", + table: "EventiDettaglioPrelievo", + column: "ArticoloId"); + + migrationBuilder.CreateIndex( + name: "IX_EventiDettaglioPrelievo_EventoId", + table: "EventiDettaglioPrelievo", + column: "EventoId"); + + migrationBuilder.CreateIndex( + name: "IX_EventiDettaglioRisorse_EventoId", + table: "EventiDettaglioRisorse", + column: "EventoId"); + + migrationBuilder.CreateIndex( + name: "IX_EventiDettaglioRisorse_RisorsaId", + table: "EventiDettaglioRisorse", + column: "RisorsaId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryCountLines_ArticleId", + table: "InventoryCountLines", + column: "ArticleId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryCountLines_BatchId", + table: "InventoryCountLines", + column: "BatchId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryCountLines_InventoryCountId_ArticleId_WarehouseId_BatchId", + table: "InventoryCountLines", + columns: new[] { "InventoryCountId", "ArticleId", "WarehouseId", "BatchId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_InventoryCountLines_WarehouseId", + table: "InventoryCountLines", + column: "WarehouseId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryCounts_AdjustmentMovementId", + table: "InventoryCounts", + column: "AdjustmentMovementId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryCounts_CategoryId", + table: "InventoryCounts", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryCounts_Code", + table: "InventoryCounts", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_InventoryCounts_InventoryDate", + table: "InventoryCounts", + column: "InventoryDate"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryCounts_Status", + table: "InventoryCounts", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryCounts_WarehouseId", + table: "InventoryCounts", + column: "WarehouseId"); + + migrationBuilder.CreateIndex( + name: "IX_Location_Nome", + table: "Location", + column: "Nome"); + + migrationBuilder.CreateIndex( + name: "IX_ModuleSubscriptions_ModuleId", + table: "ModuleSubscriptions", + column: "ModuleId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_MovementReasons_Code", + table: "MovementReasons", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_MovementReasons_IsActive", + table: "MovementReasons", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_MovementReasons_MovementType", + table: "MovementReasons", + column: "MovementType"); + + migrationBuilder.CreateIndex( + name: "IX_ReportFonts_FontFamily", + table: "ReportFonts", + column: "FontFamily"); + + migrationBuilder.CreateIndex( + name: "IX_ReportImages_Categoria", + table: "ReportImages", + column: "Categoria"); + + migrationBuilder.CreateIndex( + name: "IX_ReportTemplates_Categoria", + table: "ReportTemplates", + column: "Categoria"); + + migrationBuilder.CreateIndex( + name: "IX_ReportTemplates_Nome", + table: "ReportTemplates", + column: "Nome"); + + migrationBuilder.CreateIndex( + name: "IX_Risorse_TipoRisorsaId", + table: "Risorse", + column: "TipoRisorsaId"); + + migrationBuilder.CreateIndex( + name: "IX_StockLevels_ArticleId_WarehouseId_BatchId", + table: "StockLevels", + columns: new[] { "ArticleId", "WarehouseId", "BatchId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_StockLevels_BatchId", + table: "StockLevels", + column: "BatchId"); + + migrationBuilder.CreateIndex( + name: "IX_StockLevels_LocationCode", + table: "StockLevels", + column: "LocationCode"); + + migrationBuilder.CreateIndex( + name: "IX_StockLevels_WarehouseId", + table: "StockLevels", + column: "WarehouseId"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovementLines_ArticleId", + table: "StockMovementLines", + column: "ArticleId"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovementLines_BatchId", + table: "StockMovementLines", + column: "BatchId"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovementLines_MovementId_LineNumber", + table: "StockMovementLines", + columns: new[] { "MovementId", "LineNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_StockMovementLines_SerialId", + table: "StockMovementLines", + column: "SerialId"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_DestinationWarehouseId", + table: "StockMovements", + column: "DestinationWarehouseId"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_DocumentNumber", + table: "StockMovements", + column: "DocumentNumber", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_ExternalReference", + table: "StockMovements", + column: "ExternalReference"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_MovementDate", + table: "StockMovements", + column: "MovementDate"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_ReasonId", + table: "StockMovements", + column: "ReasonId"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_SourceWarehouseId", + table: "StockMovements", + column: "SourceWarehouseId"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_Status", + table: "StockMovements", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_Type", + table: "StockMovements", + column: "Type"); + + migrationBuilder.CreateIndex( + name: "IX_StockValuationLayers_ArticleId_WarehouseId_LayerDate", + table: "StockValuationLayers", + columns: new[] { "ArticleId", "WarehouseId", "LayerDate" }); + + migrationBuilder.CreateIndex( + name: "IX_StockValuationLayers_BatchId", + table: "StockValuationLayers", + column: "BatchId"); + + migrationBuilder.CreateIndex( + name: "IX_StockValuationLayers_IsExhausted", + table: "StockValuationLayers", + column: "IsExhausted"); + + migrationBuilder.CreateIndex( + name: "IX_StockValuationLayers_SourceMovementId", + table: "StockValuationLayers", + column: "SourceMovementId"); + + migrationBuilder.CreateIndex( + name: "IX_StockValuationLayers_WarehouseId", + table: "StockValuationLayers", + column: "WarehouseId"); + + migrationBuilder.CreateIndex( + name: "IX_StockValuations_ArticleId", + table: "StockValuations", + column: "ArticleId"); + + migrationBuilder.CreateIndex( + name: "IX_StockValuations_IsClosed", + table: "StockValuations", + column: "IsClosed"); + + migrationBuilder.CreateIndex( + name: "IX_StockValuations_Period_ArticleId_WarehouseId", + table: "StockValuations", + columns: new[] { "Period", "ArticleId", "WarehouseId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_StockValuations_ValuationDate", + table: "StockValuations", + column: "ValuationDate"); + + migrationBuilder.CreateIndex( + name: "IX_StockValuations_WarehouseId", + table: "StockValuations", + column: "WarehouseId"); + + migrationBuilder.CreateIndex( + name: "IX_TipiEvento_TipoPastoId", + table: "TipiEvento", + column: "TipoPastoId"); + + migrationBuilder.CreateIndex( + name: "IX_Utenti_Username", + table: "Utenti", + column: "Username", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_VirtualDatasets_Categoria", + table: "VirtualDatasets", + column: "Categoria"); + + migrationBuilder.CreateIndex( + name: "IX_VirtualDatasets_Nome", + table: "VirtualDatasets", + column: "Nome", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseArticleCategories_Code", + table: "WarehouseArticleCategories", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseArticleCategories_FullPath", + table: "WarehouseArticleCategories", + column: "FullPath"); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseArticleCategories_ParentCategoryId", + table: "WarehouseArticleCategories", + column: "ParentCategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseArticles_Barcode", + table: "WarehouseArticles", + column: "Barcode"); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseArticles_CategoryId", + table: "WarehouseArticles", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseArticles_Code", + table: "WarehouseArticles", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseArticles_IsActive", + table: "WarehouseArticles", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseLocations_Code", + table: "WarehouseLocations", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseLocations_IsActive", + table: "WarehouseLocations", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseLocations_IsDefault", + table: "WarehouseLocations", + column: "IsDefault"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ArticleBarcodes"); + + migrationBuilder.DropTable( + name: "Configurazioni"); + + migrationBuilder.DropTable( + name: "EventiAcconti"); + + migrationBuilder.DropTable( + name: "EventiAllegati"); + + migrationBuilder.DropTable( + name: "EventiAltriCosti"); + + migrationBuilder.DropTable( + name: "EventiDegustazioni"); + + migrationBuilder.DropTable( + name: "EventiDettaglioOspiti"); + + migrationBuilder.DropTable( + name: "EventiDettaglioPrelievo"); + + migrationBuilder.DropTable( + name: "EventiDettaglioRisorse"); + + migrationBuilder.DropTable( + name: "InventoryCountLines"); + + migrationBuilder.DropTable( + name: "ModuleSubscriptions"); + + migrationBuilder.DropTable( + name: "ReportFonts"); + + migrationBuilder.DropTable( + name: "ReportImages"); + + migrationBuilder.DropTable( + name: "ReportTemplates"); + + migrationBuilder.DropTable( + name: "StockLevels"); + + migrationBuilder.DropTable( + name: "StockMovementLines"); + + migrationBuilder.DropTable( + name: "StockValuationLayers"); + + migrationBuilder.DropTable( + name: "StockValuations"); + + migrationBuilder.DropTable( + name: "Utenti"); + + migrationBuilder.DropTable( + name: "VirtualDatasets"); + + migrationBuilder.DropTable( + name: "TipiOspite"); + + migrationBuilder.DropTable( + name: "Articoli"); + + migrationBuilder.DropTable( + name: "Eventi"); + + migrationBuilder.DropTable( + name: "Risorse"); + + migrationBuilder.DropTable( + name: "InventoryCounts"); + + migrationBuilder.DropTable( + name: "AppModules"); + + migrationBuilder.DropTable( + name: "ArticleSerials"); + + migrationBuilder.DropTable( + name: "CodiciCategoria"); + + migrationBuilder.DropTable( + name: "TipiMateriale"); + + migrationBuilder.DropTable( + name: "Clienti"); + + migrationBuilder.DropTable( + name: "Location"); + + migrationBuilder.DropTable( + name: "TipiEvento"); + + migrationBuilder.DropTable( + name: "TipiRisorsa"); + + migrationBuilder.DropTable( + name: "StockMovements"); + + migrationBuilder.DropTable( + name: "ArticleBatches"); + + migrationBuilder.DropTable( + name: "TipiPasto"); + + migrationBuilder.DropTable( + name: "MovementReasons"); + + migrationBuilder.DropTable( + name: "WarehouseLocations"); + + migrationBuilder.DropTable( + name: "WarehouseArticles"); + + migrationBuilder.DropTable( + name: "WarehouseArticleCategories"); + } + } +} diff --git a/src/Apollinare.Infrastructure/Migrations/AppollinareDbContextModelSnapshot.cs b/src/Apollinare.Infrastructure/Migrations/AppollinareDbContextModelSnapshot.cs new file mode 100644 index 0000000..3a8eddd --- /dev/null +++ b/src/Apollinare.Infrastructure/Migrations/AppollinareDbContextModelSnapshot.cs @@ -0,0 +1,3015 @@ +// +using System; +using Apollinare.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Apollinare.Infrastructure.Migrations +{ + [DbContext(typeof(AppollinareDbContext))] + partial class AppollinareDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(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.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("Note") + .HasColumnType("TEXT"); + + b.Property("Numero") + .HasColumnType("INTEGER"); + + b.Property("Ordine") + .HasColumnType("INTEGER"); + + b.Property("Sconto") + .HasColumnType("TEXT"); + + b.Property("TipoOspiteId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventoId"); + + b.HasIndex("TipoOspiteId"); + + b.ToTable("EventiDettaglioOspiti"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoDettaglioPrelievo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticoloId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("EventoId") + .HasColumnType("INTEGER"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("QtaCalcolata") + .HasColumnType("TEXT"); + + b.Property("QtaEffettiva") + .HasColumnType("TEXT"); + + b.Property("QtaRichiesta") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArticoloId"); + + b.HasIndex("EventoId"); + + b.ToTable("EventiDettaglioPrelievo"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoDettaglioRisorsa", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Costo") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("EventoId") + .HasColumnType("INTEGER"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OraFine") + .HasColumnType("TEXT"); + + b.Property("OraInizio") + .HasColumnType("TEXT"); + + b.Property("OreLavoro") + .HasColumnType("TEXT"); + + b.Property("RisorsaId") + .HasColumnType("INTEGER"); + + b.Property("Ruolo") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventoId"); + + b.HasIndex("RisorsaId"); + + b.ToTable("EventiDettaglioRisorse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("Cap") + .HasColumnType("TEXT"); + + b.Property("Citta") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DistanzaKm") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Indirizzo") + .HasColumnType("TEXT"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("Provincia") + .HasColumnType("TEXT"); + + b.Property("Referente") + .HasColumnType("TEXT"); + + b.Property("Telefono") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Nome"); + + b.ToTable("Location"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.ModuleSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoRenew") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LastRenewalDate") + .HasColumnType("TEXT"); + + b.Property("ModuleId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("PaidPrice") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("SubscriptionType") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ModuleId") + .IsUnique(); + + b.ToTable("ModuleSubscriptions"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.ReportFont", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FontData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FontFamily") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FontStyle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FontFamily"); + + b.ToTable("ReportFonts"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.ReportImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("Categoria") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Categoria"); + + b.ToTable("ReportImages"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.ReportTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("Categoria") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Descrizione") + .HasColumnType("TEXT"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Orientation") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PageSize") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TemplateJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Thumbnail") + .HasColumnType("BLOB"); + + b.Property("ThumbnailMimeType") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Categoria"); + + b.HasIndex("Nome"); + + b.ToTable("ReportTemplates"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Risorsa", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("Cognome") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("Telefono") + .HasColumnType("TEXT"); + + b.Property("TipoRisorsaId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TipoRisorsaId"); + + b.ToTable("Risorse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoEvento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .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("TipoPastoId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TipoPastoId"); + + b.ToTable("TipiEvento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoMateriale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .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("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TipiMateriale"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoOspite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .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("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TipiOspite"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoPasto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .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("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TipiPasto"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoRisorsa", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .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("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TipiRisorsa"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Utente", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("Cognome") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Nome") + .HasColumnType("TEXT"); + + b.Property("Ruolo") + .HasColumnType("TEXT"); + + b.Property("SolaLettura") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Utenti"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.VirtualDataset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attivo") + .HasColumnType("INTEGER"); + + b.Property("Categoria") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConfigurationJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Descrizione") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Icon") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Categoria"); + + b.HasIndex("Nome") + .IsUnique(); + + b.ToTable("VirtualDatasets"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleBarcode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("Barcode") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArticleId"); + + b.HasIndex("Barcode") + .IsUnique(); + + b.ToTable("ArticleBarcodes", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("BatchNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Certifications") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("CurrentQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("ExpiryDate") + .HasColumnType("TEXT"); + + b.Property("InitialQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("LastQualityCheckDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ProductionDate") + .HasColumnType("TEXT"); + + b.Property("QualityStatus") + .HasColumnType("INTEGER"); + + b.Property("ReservedQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("SupplierBatch") + .HasColumnType("TEXT"); + + b.Property("SupplierId") + .HasColumnType("INTEGER"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExpiryDate"); + + b.HasIndex("Status"); + + b.HasIndex("ArticleId", "BatchNumber") + .IsUnique(); + + b.ToTable("ArticleBatches", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleSerial", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("Attributes") + .HasColumnType("TEXT"); + + b.Property("BatchId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("CurrentWarehouseId") + .HasColumnType("INTEGER"); + + b.Property("CustomerId") + .HasColumnType("INTEGER"); + + b.Property("ManufacturerSerial") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ProductionDate") + .HasColumnType("TEXT"); + + b.Property("SalesReference") + .HasColumnType("TEXT"); + + b.Property("SerialNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SoldDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("SupplierId") + .HasColumnType("INTEGER"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("WarrantyExpiryDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BatchId"); + + b.HasIndex("CurrentWarehouseId"); + + b.HasIndex("Status"); + + b.HasIndex("ArticleId", "SerialNumber") + .IsUnique(); + + b.ToTable("ArticleSerials", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.InventoryCount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdjustmentMovementId") + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConfirmedBy") + .HasColumnType("TEXT"); + + b.Property("ConfirmedDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("InventoryDate") + .HasColumnType("TEXT"); + + b.Property("NegativeDifferenceValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("PositiveDifferenceValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("WarehouseId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AdjustmentMovementId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("InventoryDate"); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.ToTable("InventoryCounts", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.InventoryCountLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("BatchId") + .HasColumnType("INTEGER"); + + b.Property("CountedAt") + .HasColumnType("TEXT"); + + b.Property("CountedBy") + .HasColumnType("TEXT"); + + b.Property("CountedQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("InventoryCountId") + .HasColumnType("INTEGER"); + + b.Property("LocationCode") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("SecondCountBy") + .HasColumnType("TEXT"); + + b.Property("SecondCountQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("TheoreticalQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("WarehouseId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArticleId"); + + b.HasIndex("BatchId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("InventoryCountId", "ArticleId", "WarehouseId", "BatchId") + .IsUnique(); + + b.ToTable("InventoryCountLines", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.MovementReason", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsSystem") + .HasColumnType("INTEGER"); + + b.Property("MovementType") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("RequiresExternalReference") + .HasColumnType("INTEGER"); + + b.Property("RequiresValuation") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("StockSign") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("UpdatesAverageCost") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive"); + + b.HasIndex("MovementType"); + + b.ToTable("MovementReasons", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockLevel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("BatchId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("LastInventoryDate") + .HasColumnType("TEXT"); + + b.Property("LastMovementDate") + .HasColumnType("TEXT"); + + b.Property("LocationCode") + .HasColumnType("TEXT"); + + b.Property("OnOrderQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("ReservedQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("StockValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("WarehouseId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BatchId"); + + b.HasIndex("LocationCode"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ArticleId", "WarehouseId", "BatchId") + .IsUnique(); + + b.ToTable("StockLevels", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockMovement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfirmedBy") + .HasColumnType("TEXT"); + + b.Property("ConfirmedDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("CustomerId") + .HasColumnType("INTEGER"); + + b.Property("DestinationWarehouseId") + .HasColumnType("INTEGER"); + + b.Property("DocumentNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExternalDocumentType") + .HasColumnType("INTEGER"); + + b.Property("ExternalReference") + .HasColumnType("TEXT"); + + b.Property("MovementDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ReasonId") + .HasColumnType("INTEGER"); + + b.Property("SourceWarehouseId") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("SupplierId") + .HasColumnType("INTEGER"); + + b.Property("TotalValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DestinationWarehouseId"); + + b.HasIndex("DocumentNumber") + .IsUnique(); + + b.HasIndex("ExternalReference"); + + b.HasIndex("MovementDate"); + + b.HasIndex("ReasonId"); + + b.HasIndex("SourceWarehouseId"); + + b.HasIndex("Status"); + + b.HasIndex("Type"); + + b.ToTable("StockMovements", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockMovementLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("BatchId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DestinationLocationCode") + .HasColumnType("TEXT"); + + b.Property("ExternalLineReference") + .HasColumnType("TEXT"); + + b.Property("LineNumber") + .HasColumnType("INTEGER"); + + b.Property("LineValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("MovementId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("SerialId") + .HasColumnType("INTEGER"); + + b.Property("SourceLocationCode") + .HasColumnType("TEXT"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UnitOfMeasure") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArticleId"); + + b.HasIndex("BatchId"); + + b.HasIndex("SerialId"); + + b.HasIndex("MovementId", "LineNumber") + .IsUnique(); + + b.ToTable("StockMovementLines", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockValuation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("ClosedBy") + .HasColumnType("TEXT"); + + b.Property("ClosedDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("InboundQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("InboundValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("IsClosed") + .HasColumnType("INTEGER"); + + b.Property("Method") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OutboundQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("OutboundValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Period") + .HasColumnType("INTEGER"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("TotalValue") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("ValuationDate") + .HasColumnType("TEXT"); + + b.Property("WarehouseId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArticleId"); + + b.HasIndex("IsClosed"); + + b.HasIndex("ValuationDate"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("Period", "ArticleId", "WarehouseId") + .IsUnique(); + + b.ToTable("StockValuations", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockValuationLayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArticleId") + .HasColumnType("INTEGER"); + + b.Property("BatchId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("IsExhausted") + .HasColumnType("INTEGER"); + + b.Property("LayerDate") + .HasColumnType("TEXT"); + + b.Property("OriginalQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("RemainingQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("SourceMovementId") + .HasColumnType("INTEGER"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("WarehouseId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BatchId"); + + b.HasIndex("IsExhausted"); + + b.HasIndex("SourceMovementId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ArticleId", "WarehouseId", "LayerDate"); + + b.ToTable("StockValuationLayers", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Barcode") + .HasColumnType("TEXT"); + + b.Property("BaseSellingPrice") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Depth") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiryWarningDays") + .HasColumnType("INTEGER"); + + b.Property("HasExpiry") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("TEXT"); + + b.Property("Image") + .HasColumnType("BLOB"); + + b.Property("ImageMimeType") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsBatchManaged") + .HasColumnType("INTEGER"); + + b.Property("IsSerialManaged") + .HasColumnType("INTEGER"); + + b.Property("LastPurchaseCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("LeadTimeDays") + .HasColumnType("INTEGER"); + + b.Property("ManufacturerCode") + .HasColumnType("TEXT"); + + b.Property("MaximumStock") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("MinimumStock") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ReorderPoint") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("ReorderQuantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("SecondaryUnitOfMeasure") + .HasColumnType("TEXT"); + + b.Property("ShortDescription") + .HasColumnType("TEXT"); + + b.Property("StandardCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("StockManagement") + .HasColumnType("INTEGER"); + + b.Property("UnitConversionFactor") + .HasPrecision(18, 6) + .HasColumnType("TEXT"); + + b.Property("UnitOfMeasure") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.Property("ValuationMethod") + .HasColumnType("INTEGER"); + + b.Property("Volume") + .HasPrecision(18, 6) + .HasColumnType("TEXT"); + + b.Property("Weight") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("WeightedAverageCost") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Barcode"); + + b.HasIndex("CategoryId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive"); + + b.ToTable("WarehouseArticles", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseArticleCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Color") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DefaultValuationMethod") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FullPath") + .HasColumnType("TEXT"); + + b.Property("Icon") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ParentCategoryId") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("FullPath"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("WarehouseArticleCategories", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("City") + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Country") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .HasColumnType("TEXT"); + + b.Property("Province") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive"); + + b.HasIndex("IsDefault"); + + b.ToTable("WarehouseLocations", (string)null); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Articolo", b => + { + b.HasOne("Apollinare.Domain.Entities.CodiceCategoria", "Categoria") + .WithMany("Articoli") + .HasForeignKey("CategoriaId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.TipoMateriale", "TipoMateriale") + .WithMany("Articoli") + .HasForeignKey("TipoMaterialeId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Categoria"); + + b.Navigation("TipoMateriale"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Evento", b => + { + b.HasOne("Apollinare.Domain.Entities.Cliente", "Cliente") + .WithMany("Eventi") + .HasForeignKey("ClienteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Location", "Location") + .WithMany("Eventi") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.TipoEvento", "TipoEvento") + .WithMany("Eventi") + .HasForeignKey("TipoEventoId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cliente"); + + b.Navigation("Location"); + + b.Navigation("TipoEvento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoAcconto", b => + { + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("Acconti") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Evento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoAllegato", b => + { + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("Allegati") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Evento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoAltroCosto", b => + { + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("AltriCosti") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Evento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoDegustazione", b => + { + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("Degustazioni") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Evento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoDettaglioOspiti", b => + { + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("DettagliOspiti") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.TipoOspite", "TipoOspite") + .WithMany("DettagliOspiti") + .HasForeignKey("TipoOspiteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Evento"); + + b.Navigation("TipoOspite"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoDettaglioPrelievo", b => + { + b.HasOne("Apollinare.Domain.Entities.Articolo", "Articolo") + .WithMany("DettagliPrelievo") + .HasForeignKey("ArticoloId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("DettagliPrelievo") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Articolo"); + + b.Navigation("Evento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.EventoDettaglioRisorsa", b => + { + b.HasOne("Apollinare.Domain.Entities.Evento", "Evento") + .WithMany("DettagliRisorse") + .HasForeignKey("EventoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Risorsa", "Risorsa") + .WithMany("DettagliRisorse") + .HasForeignKey("RisorsaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Evento"); + + b.Navigation("Risorsa"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.ModuleSubscription", b => + { + b.HasOne("Apollinare.Domain.Entities.AppModule", "Module") + .WithOne("Subscription") + .HasForeignKey("Apollinare.Domain.Entities.ModuleSubscription", "ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Risorsa", b => + { + b.HasOne("Apollinare.Domain.Entities.TipoRisorsa", "TipoRisorsa") + .WithMany("Risorse") + .HasForeignKey("TipoRisorsaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("TipoRisorsa"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoEvento", b => + { + b.HasOne("Apollinare.Domain.Entities.TipoPasto", "TipoPasto") + .WithMany("TipiEvento") + .HasForeignKey("TipoPastoId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("TipoPasto"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleBarcode", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany("Barcodes") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Article"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleBatch", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany("Batches") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Article"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleSerial", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany("Serials") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.ArticleBatch", "Batch") + .WithMany("Serials") + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "CurrentWarehouse") + .WithMany() + .HasForeignKey("CurrentWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Article"); + + b.Navigation("Batch"); + + b.Navigation("CurrentWarehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.InventoryCount", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.StockMovement", "AdjustmentMovement") + .WithMany() + .HasForeignKey("AdjustmentMovementId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticleCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AdjustmentMovement"); + + b.Navigation("Category"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.InventoryCountLine", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany() + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.ArticleBatch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.InventoryCount", "InventoryCount") + .WithMany("Lines") + .HasForeignKey("InventoryCountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Article"); + + b.Navigation("Batch"); + + b.Navigation("InventoryCount"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockLevel", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany("StockLevels") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.ArticleBatch", "Batch") + .WithMany("StockLevels") + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "Warehouse") + .WithMany("StockLevels") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Article"); + + b.Navigation("Batch"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockMovement", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "DestinationWarehouse") + .WithMany("DestinationMovements") + .HasForeignKey("DestinationWarehouseId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.MovementReason", "Reason") + .WithMany("Movements") + .HasForeignKey("ReasonId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "SourceWarehouse") + .WithMany("SourceMovements") + .HasForeignKey("SourceWarehouseId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DestinationWarehouse"); + + b.Navigation("Reason"); + + b.Navigation("SourceWarehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockMovementLine", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany("MovementLines") + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.ArticleBatch", "Batch") + .WithMany("MovementLines") + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.StockMovement", "Movement") + .WithMany("Lines") + .HasForeignKey("MovementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.ArticleSerial", "Serial") + .WithMany("MovementLines") + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Article"); + + b.Navigation("Batch"); + + b.Navigation("Movement"); + + b.Navigation("Serial"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockValuation", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany() + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Article"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockValuationLayer", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", "Article") + .WithMany() + .HasForeignKey("ArticleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.ArticleBatch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.StockMovement", "SourceMovement") + .WithMany() + .HasForeignKey("SourceMovementId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Article"); + + b.Navigation("Batch"); + + b.Navigation("SourceMovement"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticleCategory", "Category") + .WithMany("Articles") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseArticleCategory", b => + { + b.HasOne("Apollinare.Domain.Entities.Warehouse.WarehouseArticleCategory", "ParentCategory") + .WithMany("ChildCategories") + .HasForeignKey("ParentCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.AppModule", b => + { + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Articolo", b => + { + b.Navigation("DettagliPrelievo"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Cliente", b => + { + b.Navigation("Eventi"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.CodiceCategoria", b => + { + b.Navigation("Articoli"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Evento", b => + { + b.Navigation("Acconti"); + + b.Navigation("Allegati"); + + b.Navigation("AltriCosti"); + + b.Navigation("Degustazioni"); + + b.Navigation("DettagliOspiti"); + + b.Navigation("DettagliPrelievo"); + + b.Navigation("DettagliRisorse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Location", b => + { + b.Navigation("Eventi"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Risorsa", b => + { + b.Navigation("DettagliRisorse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoEvento", b => + { + b.Navigation("Eventi"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoMateriale", b => + { + b.Navigation("Articoli"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoOspite", b => + { + b.Navigation("DettagliOspiti"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoPasto", b => + { + b.Navigation("TipiEvento"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.TipoRisorsa", b => + { + b.Navigation("Risorse"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleBatch", b => + { + b.Navigation("MovementLines"); + + b.Navigation("Serials"); + + b.Navigation("StockLevels"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.ArticleSerial", b => + { + b.Navigation("MovementLines"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.InventoryCount", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.MovementReason", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.StockMovement", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseArticle", b => + { + b.Navigation("Barcodes"); + + b.Navigation("Batches"); + + b.Navigation("MovementLines"); + + b.Navigation("Serials"); + + b.Navigation("StockLevels"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseArticleCategory", b => + { + b.Navigation("Articles"); + + b.Navigation("ChildCategories"); + }); + + modelBuilder.Entity("Apollinare.Domain.Entities.Warehouse.WarehouseLocation", b => + { + b.Navigation("DestinationMovements"); + + b.Navigation("SourceMovements"); + + b.Navigation("StockLevels"); + }); +#pragma warning restore 612, 618 + } + } +}