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 0000000..05ea21c Binary files /dev/null and b/src/Apollinare.API/apollinare.db-shm differ diff --git a/src/Apollinare.API/apollinare.db-wal b/src/Apollinare.API/apollinare.db-wal new file mode 100644 index 0000000..42a4026 Binary files /dev/null and b/src/Apollinare.API/apollinare.db-wal differ diff --git a/src/Apollinare.Domain/Entities/Warehouse/ArticleBarcode.cs b/src/Apollinare.Domain/Entities/Warehouse/ArticleBarcode.cs new file mode 100644 index 0000000..9a4774a --- /dev/null +++ b/src/Apollinare.Domain/Entities/Warehouse/ArticleBarcode.cs @@ -0,0 +1,96 @@ +namespace Apollinare.Domain.Entities.Warehouse; + +/// +/// 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 + } + } +}