feat: модульная архитектура sidebar и улучшения навигации
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
249
src/lib/inventory-management.ts
Normal file
249
src/lib/inventory-management.ts
Normal file
@ -0,0 +1,249 @@
|
||||
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,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user