На большинстве заводов Склад Сырья (Raw Materials) — это священное место. Там висят камеры, сидит строгий кладовщик, а на дверях замок. Но как только материал выдается мастеру в цех, он попадает в «слепую зону» — Незавершенное производство (WIP - Work in Progress).
В старых учетных системах выдача в цех оформляется как Списание. То есть актив буквально исчезает с баланса компании. Вы не знаете, превратился ли этот металл в станок, лежит ли он под верстаком или уже сдан в металлолом предприимчивым сварщиком.
Топология трех складов
Для наведения порядка мы рекомендуем виртуально или физически разделить завод минимум на три зоны (Склада):
- RAW (Сырье): Хранит закупленные материалы.
- WIP (Цеховая кладовая): Хранит материалы, находящиеся в работе, и полуфабрикаты. Зона ответственности начальника производства.
- Finished Goods (СГ): Склад готовой продукции, ожидающей отгрузки клиенту.
Архитектура документа StockTransfer
Перемещение между этими зонами оформляется сущностью `StockTransfer`. Это не просто бумажка, это сложный финансовый транзакционный механизм.
1. Перемещаем не номенклатуру, а Партии (Batches)
Взгляните на структуру `StockTransferItem` в нашем ядре. Строка перемещения ссылается не просто на товар, она ссылается на конкретную Партию (`Batch`).
Почему это критически важно? Когда лист стали переезжает со Склада Сырья в WIP-кладовую, он забирает с собой свою закупочную цену. Это гарантирует, что алгоритм Cost Roll-up посчитает себестоимость изделия из правильных денег, а не из "среднерыночных" фантазий.
2. Защита от баз данных "с привидениями"
При редактировании перемещений часто возникает проблема: пользователь удалил строку в интерфейсе, а в базе данных она осталась "висеть" как сирота. В сущности `StockTransfer` мы применили жесткое правило ORM:
#[ORM\OneToMany(
targetEntity: StockTransferItem::class,
mappedBy: 'stockTransfer',
cascade: ['persist', 'remove'],
orphanRemoval: true // <--- Уничтожает "зависшие" строки
)]
private $items;
Транзакция в Ledger: Принцип двойной записи
Самое интересное происходит в контроллере `StockTransferController` в момент проведения документа. Когда перемещение получает статус `COMPLETED`, алгоритм генерирует не одну, а две записи в `StockMovement` (наш Ledger).
🛠 Идеальный баланс
Метод createMovement вызывается дважды для каждой строки:
- Первый вызов:
qty < 0→ генерируетStockMovementType::OUT(Расход) со Склада А. - Второй вызов:
qty > 0→ генерируетStockMovementType::IN(Приход) на Склад Б.
В результате баланс компании (Total Assets) не меняется ни на копейку, но материальная ответственность переходит от Кладовщика к Начальнику цеха.
Защита от конфликтов ("Я отправил!" — "Я не получал!")
Чтобы исключить споры между сотрудниками, `StockTransfer` имеет встроенный State Machine (конечный автомат) статусов:
- Draft (Черновик): Кладовщик собирает паллету.
- Sent (Отправлено): Груз уехал. Списано с RAW, но еще не появилось в WIP. Товар "в пути".
- Completed (Завершено): Мастер цеха принял паллету, пересчитал и нажал кнопку. Только в этот момент товар падает на баланс WIP и появляется в поле зрения сборщиков. Поле `completedBy` навсегда записывает, кто именно принял ответственность.
Транзакция перемещения
Партия: Стальной лист 3мм (Batch #12)
Draft (Черновик)