Files
sfera-new/src/lib/inventory-management.ts
2025-08-30 15:51:41 +03:00

249 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<void> {
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<void> {
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<void> {
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<void> {
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<Array<{
productId: string
productName: string
currentStock: number
minStock: number
}>> {
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,
}
}