💡 Как мы разрулили ситуацию
Мы собрали систему, которая:
- Показывает остатки на всех складах в реальном времени (никаких «я думаю, у нас ещё есть»)
- Сама считает среднюю стоимость при новых поставках (прощай, калькулятор)
- Разруливает все типы операций: поставки, возвраты, перемещения между точками
- Не даёт уйти в минус — система просто не позволит (да, бывает и такое!)
🧱 На чём собрали
- TypeScript — чтобы сложная бизнес-логика не превратилась в кашу
- Prisma ORM — для работы с базой без боли
- Node.js — потому что быстро и надёжно
1. Анатомия основного метода расчёта баланса
🎯 Вот что происходит, когда кто-то добавляет или списывает ингредиенты:
Начальная проверка
Первым делом система проверяет, что цена за единицу не отрицательная. Это базовая защита от нелогичных операций:
if (pricePerUnit < 0) {
throw new Error('Unit price cannot be negative.');
}
Получение текущего состояния
Далее система запрашивает актуальные данные по конкретному ингредиенту на конкретном складе:
const currentBalance = await this.fetchCurrentBalance(
warehouseId,
ingredientId,
prismaTransaction,
);
const currentQuantity = currentBalance?.quantity || 0;
const currentAvgCost = currentBalance?.avgCostPerUnit.toNumber() || 0;
Если этого ингредиента раньше не было, то количество и средняя стоимость будут нулевыми.
Расчёт нового количества
Простое математическое сложение текущего количества и изменения:
const newQuantity = currentQuantity + quantityChange;
Умная логика изменения стоимости
Ключевой момент: не каждая операция должна влиять на среднюю стоимость ингредиента.
// Не всё, что двигается, меняет среднюю стоимость!
const shouldAffectAvgCost = this.shouldMovementAffectAverageCost(reason);
let newAvgCost = currentAvgCost;
Система проверяет тип операции (поставка, списание, перемещение) и решает, надо ли пересчитывать стоимость.
Пересчёт средней стоимости
Если операция влияет на стоимость (например, новая поставка по другой цене), запускается специальный алгоритм расчёта:
if (shouldAffectAvgCost) {
// Только новые поставки влияют на среднюю цену
newAvgCost = calculateNewAverageCost(
currentQuantity,
currentAvgCost,
quantityChange,
pricePerUnit,
newQuantity,
);
}
Сохранение результатов
В завершение метод сохраняет новые значения количества и средней стоимости в базу данных:
return this.saveBalanceUpdate(
warehouseId,
ingredientId,
newQuantity,
newAvgCost,
prismaTransaction,
);
Все эти операции происходят внутри транзакции базы данных, так что если что-то пойдёт не так, вся операция будет отменена, и данные останутся согласованными.
2. Не всё так просто с ценой
🎯 Секрет в том, что не каждое движение товара должно влиять на себестоимость. Иначе всё полетит к чертям.
private shouldMovementAffectAverageCost(reason: MovementReason): boolean {
return (
reason === MovementReason.supply ||
reason === MovementReason.supply_cancel ||
reason === MovementReason.supply_edit
);
}
3. Магия средневзвешенной стоимости
🎯 Вот эта штука превращает хаос цен на рынке в чёткое понимание, сколько на самом деле стоит ваш товар:
Представьте: сегодня помидоры стоят 100 рублей, завтра — 150, а послезавтра — 80. По какой цене считать блюда?
Наш алгоритм берёт все закупки, их объемы и цены, а затем выдаёт одну честную цифру — сколько в среднем стоит каждый ваш помидор прямо сейчас. Никаких фантазий и «ну примерно». Точная математика.
export function calculateNewAverageCost(
currentQuantity: number,
currentAvgCost: number,
quantityChange: number,
newUnitCost: number,
newTotalQuantity: number,
): number {
// Если ничего нет — цена ноль
if (newTotalQuantity === 0) return 0;
if (quantityChange === 0) return currentAvgCost;
// Математика, которая спасает бизнес
const currentValue = currentQuantity * currentAvgCost;
const newValue = quantityChange * newUnitCost;
const totalValue = currentValue + newValue;
return totalValue / newTotalQuantity;
}
Если у вас было 10 кг муки по 50₽ и вы докупили 5 кг по 60₽, то средняя цена не просто «где-то 55₽» — она ровно 53,33₽ за кг. Без калькулятора и Excel-таблиц, всё автоматически.
Особенно это важно, когда цены скачут как сумасшедшие или когда вы закупаетесь у разных поставщиков. Система всегда знает реальную стоимость каждого грамма вашего товара.
4. Защита от «упс, что-то пошло не так»
🎯 Всё происходит в транзакциях, чтобы не было половинчатых операций:
async processInventoryMovement(movementData: InventoryMovementData): Promise<void> {
return this.prisma.$transaction(async (tx) => {
// Фиксируем движение
const movement = await this.movementRepository.create(movementData, tx);
// Обновляем баланс
await this.inventoryBalanceService.updateBalance(
movementData.warehouseId,
movementData.ingredientId,
movementData.quantity,
movementData.unitPrice,
movementData.reason,
tx
);
// Пишем, кто накосячил, если что
await this.auditService.logOperation({
type: 'INVENTORY_MOVEMENT',
entityId: movement.id,
userId: movementData.userId,
metadata: { reason: movementData.reason }
}, tx);
});
}
5. Хитрый трюк с нулевыми остатками
🎯 Когда запас заканчивается, мы сбрасываем историю цен — это избавляет от призраков прошлого:
// Если всё распродали — забываем старую цену
if (newQuantity === 0 && !this.isCancellationMovement(reason)) {
newAvgCost = 0;
}
✅ Что получилось в итоге
- Наконец-то видно реальную себестоимость блюд — никаких сюрпризов
- Точная финансовая отчётность без шаманства с цифрами
- Полная история всего, что происходило на складе (кто, когда, что и почему)
🧠 Что можно сделать ещё круче
- Научить систему предсказывать спрос, чтобы не заказывать лишнего
- Сделать мобильное приложение для быстрого перемещения между складами
- Добавить сканирование штрих-кодов, чтобы не тыкать в экран
- Автоматически сравнивать цены поставщиков, чтобы находить лучшие предложения
- Отслеживать, где пропадают продукты (и кто виноват)
📩 Тоже тонете в бумажках или не знаете, сколько на самом деле зарабатываете?
Дайте нам автоматизировать этот ад. Напишите, и мы покажем, как превратить ваш бизнес-хаос в работающую систему, которая приносит реальные деньги.