Рубрики
Код-ноухау

💰 Убили хаос на складе: реальная средняя стоимость без головной боли

Клиент: сеть ресторанов с 5 точками, которая тонула в бумажках и Excel-таблицах. Не понимали, сколько стоят их ингредиенты на самом деле, а значит — сколько они зарабатывают.

Проблема: полный бардак с запасами на нескольких складах, никто не знал точную себестоимость блюд.

Спойлер: это убивало прибыль.

💡 Как мы разрулили ситуацию

Мы собрали систему, которая:

  • Показывает остатки на всех складах в реальном времени (никаких «я думаю, у нас ещё есть»)
  • Сама считает среднюю стоимость при новых поставках (прощай, калькулятор)
  • Разруливает все типы операций: поставки, возвраты, перемещения между точками
  • Не даёт уйти в минус — система просто не позволит (да, бывает и такое!)

🧱 На чём собрали

  • 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;
}

✅ Что получилось в итоге

  • Наконец-то видно реальную себестоимость блюд — никаких сюрпризов
  • Точная финансовая отчётность без шаманства с цифрами
  • Полная история всего, что происходило на складе (кто, когда, что и почему)

🧠 Что можно сделать ещё круче

  • Научить систему предсказывать спрос, чтобы не заказывать лишнего
  • Сделать мобильное приложение для быстрого перемещения между складами
  • Добавить сканирование штрих-кодов, чтобы не тыкать в экран
  • Автоматически сравнивать цены поставщиков, чтобы находить лучшие предложения
  • Отслеживать, где пропадают продукты (и кто виноват)

📩 Тоже тонете в бумажках или не знаете, сколько на самом деле зарабатываете?

Дайте нам автоматизировать этот ад. Напишите, и мы покажем, как превратить ваш бизнес-хаос в работающую систему, которая приносит реальные деньги.