import { prisma } from '@/lib/prisma' /** * СИСТЕМА УПРАВЛЕНИЯ СКЛАДСКИМИ ОСТАТКАМИ V2 * * Автоматически обновляет инвентарь при: * - Приемке поставок (увеличивает остатки) * - Отгрузке селлерам (уменьшает остатки) * - Списании брака (уменьшает остатки) */ export interface InventoryMovement { fulfillmentCenterId: string productId: string quantity: number type: 'INCOMING' | 'OUTGOING' | 'DEFECT' sourceId: string // ID поставки или отгрузки sourceType: 'SUPPLY_ORDER' | 'SELLER_SHIPMENT' | 'DEFECT_WRITEOFF' unitCost?: number // Себестоимость для расчета средней цены notes?: string } /** * Основная функция обновления инвентаря */ export async function updateInventory(movement: InventoryMovement): Promise { const { fulfillmentCenterId, productId, quantity, type, unitCost } = movement // Находим или создаем запись в инвентаре const inventory = await prisma.fulfillmentConsumableInventory.upsert({ where: { fulfillmentCenterId_productId: { fulfillmentCenterId, productId, }, }, create: { fulfillmentCenterId, productId, currentStock: type === 'INCOMING' ? quantity : -quantity, totalReceived: type === 'INCOMING' ? quantity : 0, totalShipped: type === 'OUTGOING' ? quantity : 0, averageCost: unitCost || 0, lastSupplyDate: type === 'INCOMING' ? new Date() : undefined, lastUsageDate: type === 'OUTGOING' ? new Date() : undefined, }, update: { // Обновляем остатки в зависимости от типа движения currentStock: { increment: type === 'INCOMING' ? quantity : -quantity, }, totalReceived: { increment: type === 'INCOMING' ? quantity : 0, }, totalShipped: { increment: type === 'OUTGOING' ? quantity : 0, }, lastSupplyDate: type === 'INCOMING' ? new Date() : undefined, lastUsageDate: type === 'OUTGOING' ? new Date() : undefined, }, include: { product: true, }, }) // Пересчитываем среднюю себестоимость при поступлении if (type === 'INCOMING' && unitCost) { await recalculateAverageCost(inventory.id, quantity, unitCost) } console.log('✅ Inventory updated:', { productName: inventory.product.name, movement: `${type === 'INCOMING' ? '+' : '-'}${quantity}`, newStock: inventory.currentStock, fulfillmentCenter: fulfillmentCenterId, }) } /** * Пересчет средней себестоимости по методу взвешенной средней */ async function recalculateAverageCost(inventoryId: string, newQuantity: number, newUnitCost: number): Promise { const inventory = await prisma.fulfillmentConsumableInventory.findUnique({ where: { id: inventoryId }, }) if (!inventory) return // Рассчитываем новую среднюю стоимость const oldTotalCost = parseFloat(inventory.averageCost.toString()) * (inventory.currentStock - newQuantity) const newTotalCost = newUnitCost * newQuantity const totalQuantity = inventory.currentStock const newAverageCost = totalQuantity > 0 ? (oldTotalCost + newTotalCost) / totalQuantity : newUnitCost await prisma.fulfillmentConsumableInventory.update({ where: { id: inventoryId }, data: { averageCost: newAverageCost, }, }) } /** * Обработка приемки поставки V2 */ export async function processSupplyOrderReceipt( supplyOrderId: string, items: Array<{ productId: string receivedQuantity: number unitPrice: number }>, ): Promise { console.log(`🔄 Processing supply order receipt: ${supplyOrderId}`) // Получаем информацию о поставке const supplyOrder = await prisma.fulfillmentConsumableSupplyOrder.findUnique({ where: { id: supplyOrderId }, include: { fulfillmentCenter: true }, }) if (!supplyOrder) { throw new Error(`Supply order not found: ${supplyOrderId}`) } // Обрабатываем каждую позицию for (const item of items) { await updateInventory({ fulfillmentCenterId: supplyOrder.fulfillmentCenterId, productId: item.productId, quantity: item.receivedQuantity, type: 'INCOMING', sourceId: supplyOrderId, sourceType: 'SUPPLY_ORDER', unitCost: item.unitPrice, notes: `Приемка заказа ${supplyOrderId}`, }) } console.log(`✅ Supply order ${supplyOrderId} processed successfully`) } /** * Обработка отгрузки селлеру */ export async function processSellerShipment( fulfillmentCenterId: string, sellerId: string, items: Array<{ productId: string shippedQuantity: number }>, ): Promise { console.log(`🔄 Processing seller shipment to ${sellerId}`) // Обрабатываем каждую позицию for (const item of items) { // Проверяем достаточность остатков const inventory = await prisma.fulfillmentConsumableInventory.findUnique({ where: { fulfillmentCenterId_productId: { fulfillmentCenterId, productId: item.productId, }, }, include: { product: true }, }) if (!inventory || inventory.currentStock < item.shippedQuantity) { throw new Error( `Insufficient stock for product ${inventory?.product.name}. ` + `Available: ${inventory?.currentStock || 0}, Required: ${item.shippedQuantity}`, ) } await updateInventory({ fulfillmentCenterId, productId: item.productId, quantity: item.shippedQuantity, type: 'OUTGOING', sourceId: sellerId, sourceType: 'SELLER_SHIPMENT', notes: `Отгрузка селлеру ${sellerId}`, }) } console.log(`✅ Seller shipment to ${sellerId} processed successfully`) } /** * Проверка критически низких остатков */ export async function checkLowStockAlerts(fulfillmentCenterId: string): Promise> { const lowStockItems = await prisma.fulfillmentConsumableInventory.findMany({ where: { fulfillmentCenterId, OR: [ { currentStock: { lte: prisma.fulfillmentConsumableInventory.fields.minStock } }, { currentStock: 0 }, ], }, include: { product: true, }, }) return lowStockItems.map(item => ({ productId: item.productId, productName: item.product.name, currentStock: item.currentStock, minStock: item.minStock, })) } /** * Получение статистики склада */ export async function getInventoryStats(fulfillmentCenterId: string) { const stats = await prisma.fulfillmentConsumableInventory.aggregate({ where: { fulfillmentCenterId }, _count: { id: true }, _sum: { currentStock: true, totalReceived: true, totalShipped: true, }, }) const lowStockCount = await prisma.fulfillmentConsumableInventory.count({ where: { fulfillmentCenterId, currentStock: { lte: prisma.fulfillmentConsumableInventory.fields.minStock }, }, }) return { totalProducts: stats._count.id, totalStock: stats._sum.currentStock || 0, totalReceived: stats._sum.totalReceived || 0, totalShipped: stats._sum.totalShipped || 0, lowStockCount, } }