
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
249 lines
7.6 KiB
TypeScript
249 lines
7.6 KiB
TypeScript
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,
|
||
}
|
||
} |