Вы внедрили стратегию MTO (Make-to-Order). Ваш алгоритм идеально высчитывает дефицит. Но в конце месяца вы открываете баланс и видите, что склад снова переполнен неликвидным металлом, а денег на счету нет.
Почему это происходит? Потому что в большинстве учетных систем кнопка «Создать заказ поставщику» доступна всегда. Снабженец, движимый желанием «подстраховаться» или получить скидку за объем, игнорирует расчетный дефицит и покупает с запасом.
Hard Block: Архитектура запрета
В ModernERP Pro мы убрали возможность создавать `PurchaseOrder` (Заказ Поставщику) "из воздуха". Вы не можете просто зайти в раздел Закупок, нажать "Плюс" и набить корзину материалами.
Единственный способ инициировать закупку сырья (`Material`) — это пройти через жесткий процесс валидации.
Шаг 1: Автоматическая генерация PurchaseRequisition (Заявка)
Когда статус производственного заказа меняется на `APPROVED`, алгоритм расчета дефицита анализирует складские остатки. Если не хватает 100 кг стали, система сама генерирует внутреннюю сущность `PurchaseRequisition` (Заявку на закупку) на эти 100 кг. В этой заявке жестко прописана связь с конкретным `ProductionOrder`.
Шаг 2: Конвертация, а не создание
В кабинете снабженца нет кнопки «Создать заказ на металл». У него есть интерфейс обработки заявок. Он выбирает заявки, выбирает поставщика и нажимает кнопку «Конвертировать в PurchaseOrder». Даже если снабженец попытается схитрить и увеличить количество, контроллер выдаст ошибку валидации.
Исключения: Сущность Consumble (Расходники)
Конечно, никто не закупает изоленту, сверла, ветошь или перчатки строго под заказ клиента. Ожидание поставки пачки саморезов парализует работу всего цеха.
Для таких позиций в архитектуре ModernERP Pro предусмотрена отдельная сущность — Consumble (Расходник). Это принципиальное архитектурное отличие от «сырья».
🛠 Полиморфизм в контроллере закупок
Если посмотреть в PurchaseOrderController, можно увидеть, как система разделяет потоки. Сырье (Material) жестко идет в себестоимость по спецификации, а расходники (Consumble) закупаются независимо, по стратегии Min/Max или прямым запросом цеха:
$item = $purchaseOrder->getMaterial()
?? $purchaseOrder->getDetail()
?? $purchaseOrder->getConsumble();
// Приходование остатков идет полиморфно
$movement->setItem($item);
Такой подход позволяет нам полностью блокировать ручные закупки `Material`, защищая MTO-стратегию, но при этом оставлять снабженцу свободу для оперативного пополнения `Consumble`.
Бизнес-результат
Внедрив такую жесткую блокировку и разделение сущностей, вы достигаете трех целей:
- Защита оборотного капитала: Ни один рубль не может быть потрачен на основное сырье без привязки к конкретному оплаченному заказу клиента.
- Точность себестоимости: Расходники (`Consumble`) списываются на накладные расходы цеха, а дорогой металл (`Material`) идет строго в Cost Roll-up изделия.
- Ликвидация неликвида: Склад сырья больше не забивается "страховочными" запасами.
- Прозрачность снабжения: Снабженец превращается из «покупателя» в диспетчера поставок. Его KPI больше не зависит от скидок за объем, он зависит от соблюдения сроков поставки по утвержденным заявкам.