Контроль перерасхода материалов по спецификации в ERPNext
АйТи Фреш
IT-аутсорсинг и бизнес

Как не купить лишнего: контроль перерасхода материалов по спецификации договора в ERPNext

Автор: , директор ООО «АйТи-Фреш» · · ~16 мин чтения
Весы: арматура перевешивает стопку договора, красный шлагбаум закрывает проезд грузовику с новой партией
Перебор по спецификации лучше остановить на заявке, до того как лишняя партия доедет до объекта.

Короткий ответ: штатного контроля закупок по спецификации договора в ERPNext нет, потому что Budget считает рубли, а не тонны. Нужны свой справочник спецификации, ссылка на её строку в заявке и проверка суммы по всем заявкам при сохранении. Ниже схема, код, отчёт остатка и честные оговорки по версиям.

Почему штатный ERPNext не остановит перебор по количеству

Типичная картина у небольшого подрядчика: договор подписан, в приложении лежит спецификация на сто с лишним позиций, а прорабы на объектах подают заявки со смартфона. Каждая заявка по отдельности выглядит разумно. Перебор виден только в сумме, и обычно его замечают, когда вторая фура с арматурой уже разгружена. Если вы строите снабжение на ERPNext именно ради контроля, сначала надо честно понять, что система из коробки умеет, а что нет. Под такие задачи мы делаем настройку ERPNext под ключ, и первым делом всегда разбираем эту таблицу.

МеханизмЧто считаетГде срабатываетНужна доработка
Budgetденьги по счёту расходов для Project или Cost Centerзаявка, заказ, фактические расходынет, но количество не видит
Over Order Allowanceдопуск заказа над заявкой, в процентахзаказ поставщикунет, проверьте на своей версии
Over Receipt Allowanceдопуск приёмки над заказом, в процентахприёмканет
Over Billing Allowanceдопуск счёта над заказом по суммесчёт поставщиканет
Своя проверка по спецификацииколичество по строке договора, суммарно по всем заявкамзаявка, при сохранениида

Главная мысль таблицы: допуски Over Order, Over Receipt и Over Billing сравнивают документы между собой по цепочке «заявка, заказ, приёмка, счёт». Опорной точки «сколько всего разрешено договором» у них нет, потому что договор в ERPNext не хранит перечень материалов. Budget устроен иначе: он лимитирует сумму по счёту расходов в разрезе проекта или центра затрат, а при превышении может остановить документ (Stop), предупредить (Warn) или промолчать (Ignore), причём отдельно для заявки, заказа и фактических расходов.

Поэтому ограничение «не больше 12 тонн арматуры на объект» Budget не выразит. Допустим, по статье «материалы» общий денежный потолок не пробит, потому что на другой позиции вы сэкономили, а арматуры заказано уже 13,5 тонны. Система промолчит. Затраты проекта в поле total_purchase_cost к тому же считаются по счетам поставщиков, то есть появляются только после проведения Purchase Invoice, когда деньги почти потрачены. Количественный контроль придётся добавить самим, об этом остальная статья.

Спецификацию договора не стоит хранить как BOM. BOM в ERPNext описывает состав производимого изделия, а перечень материалов по договору подряда меняется допсоглашениями и требует отдельного DocType.

Как устроен контроль: спецификация, ссылка в заявке, проверка при сохранении

Конструкция минимальна. Первый DocType, «Спецификация договора» (в коде Contract BOQ): ссылка на Project, номер договора, статус. Второй, дочерняя таблица «Позиции спецификации» (Contract BOQ Item, флаг Is Child Table): ссылка на Item, плановое количество planned_qty, единица измерения и allowed_overage_pct, допустимый процент превышения для этой позиции. Допуск я задаю построчно, а не одним числом на всё: на штучных позициях он нулевой, а на сыпучих материалах и метраже без запаса на подрезку и бой не обойтись.

Дальше связываем заявку со спецификацией. В строку заявки (Material Request Item) добавляем два Custom Field: custom_contract_boq со ссылкой на договор и custom_boq_item_ref со ссылкой на строку спецификации. Выбирать позицию из списка надёжнее, чем вводить идентификатор руками. Такое же поле с тем же именем полезно создать в Purchase Order Item и Purchase Receipt Item: стандартный маппинг переносит значения полей с совпадающими именами, так что ссылка поедет по цепочке сама. Проверьте это на тестовой заявке, прежде чем рассчитывать на отчёты.

Откуда берётся спецификация. Набирать сто сорок строк вручную никто не будет, поэтому позиции загружают из Excel через Data Import: сметчик отдаёт файл, мы приводим колонки к шаблону импорта. Сметный расчёт остаётся во внешней программе, а в ERPNext попадают только итоги по позициям: количество, единица, цена. Дублировать смету внутри системы бессмысленно, она нужна лишь как лимит.

Проверка срабатывает при сохранении заявки. Логика простая: для каждой строки со ссылкой на спецификацию берём сумму по всем остальным активным заявкам на закупку, добавляем количество из текущей и сравниваем с лимитом плюс допуск. Если больше, сохранение прерывается, а прораб видит сообщение с цифрами. Заявка до закупки просто не доходит.

Схема: спецификация договора, заявка, проверка при сохранении, заказ поставщику, приёмка; при переборе заявка не сохраняется
Проверка стоит между заявкой и закупкой: перебор останавливается до заказа поставщику.

Что делает Server Script и где он легко ломается

Проверку удобно оформить как Server Script на событиях Before Save и Before Submit для Material Request: событие Before Save показывает ошибку ещё в черновике, а Before Submit страхует проведение. Ниже мой вариант. Он исправляет два места, из-за которых пример из интернета падает на реальных данных.

# Server Script: DocType Event, событие Before Save (черновик) и Before Submit, DocType = Material Request
if doc.material_request_type == "Purchase":
    for row in doc.items:
        ref = row.get("custom_boq_item_ref")
        if not ref:
            continue

        boq = frappe.db.get_value(
            "Contract BOQ Item", ref,
            ["planned_qty", "allowed_overage_pct", "item_code"], as_dict=True,
        )
        if not boq:
            frappe.throw("Строка спецификации " + str(ref) + " не найдена")

        # уже заявлено по строке спецификации: активные заявки на закупку, кроме текущей
        res = frappe.db.sql(
            """SELECT IFNULL(SUM(mri.stock_qty), 0)
               FROM `tabMaterial Request Item` mri
               JOIN `tabMaterial Request` mr ON mr.name = mri.parent
               WHERE mri.custom_boq_item_ref = %s
                 AND mr.docstatus < 2
                 AND mr.material_request_type = 'Purchase'
                 AND mr.name != %s""",
            (ref, doc.name),
        )
        already = res[0][0] if res else 0

        # то, что просит текущая заявка по этой же строке (в ней может быть несколько строк на одну позицию)
        in_doc = 0
        for r in doc.items:
            if r.get("custom_boq_item_ref") == ref:
                in_doc = in_doc + r.stock_qty

        total = already + in_doc
        cap = boq.planned_qty * (1 + (boq.allowed_overage_pct or 0) / 100.0)

        if total > cap + 0.0001:
            frappe.throw(
                "Перебор по спецификации договора: " + str(boq.item_code)
                + ". Лимит " + str(boq.planned_qty) + " (с допуском " + str(cap) + ")"
                + ", уже заявлено " + str(already) + ", в этой заявке " + str(in_doc)
                + ", итого " + str(total) + ".",
                title="Контроль спецификации",
            )

Первое: frappe.db.sql возвращает список кортежей, а не число, поэтому значение берётся как res[0][0]. Встречающийся вариант result or 0.0 складывает кортеж с числом и падает уже на первой заявке. Второе: считать надо в stock_qty, то есть в базовых единицах, иначе заявка в упаковках и спецификация в штуках разойдутся. Третье: в сумму не должны попадать отменённые заявки (docstatus 2), заявки другого типа (перемещение между складами не закупка) и сама текущая заявка.

Теперь про то, что ломает такой скрипт на практике. Начиная с версии 15 Server Scripts по умолчанию выключены: об этом прямо сказано в документации Frappe, и на самодельной установке легко потерять полдня, выясняя, почему проверка «не срабатывает». Включение серверных скриптов осознанное решение администратора. Если логика должна жить годами, надёжнее вынести её в собственное приложение с хуком validate. Там проще писать тесты, и там доступна блокировка строки, о которой ниже. Работает ли в песочнице именно ваш набор функций, проверьте на тестовом сайте своей версии: RestrictedPython ограничивает доступные вызовы, поэтому в примере я обошёлся конкатенацией строк, а не str.format.

Вторая типичная поломка, гонка. Два прораба на разных объектах одной минутой проводят заявки на одну позицию, оба скрипта видят остаток без учёта конкурента, и вместе заявки превышают лимит. На команде из пяти человек шанс невелик, но не нулевой. Решение в собственном приложении: читать строку спецификации с блокировкой frappe.db.get_value(..., for_update=True). В Server Script это неудобно, поэтому там риск проще компенсировать утренним отчётом остатка.

Как видеть остаток по спецификации: заявлено, заказано, получено

Блокировка без отчёта решает половину задачи. Руководителю нужен один экран по договору: лимит, сколько заявлено, сколько заказано поставщикам, сколько физически получено и сколько осталось. Это три разных числа: заявлено бывает с запасом, заказано является частью заявленного, получено частью заказанного. Стандартные поля ordered_qty, received_qty и per_ordered есть в документах, но свести их по строкам спецификации сами не умеют. Нужен свой Query Report, который соединяет Contract BOQ Item со строками заявок, заказов и приёмок.

SELECT
  b.item_code AS "Позиция:Link/Item:220",
  b.planned_qty AS "Лимит:Float:90",
  IFNULL(r.requested, 0) AS "Заявлено:Float:100",
  IFNULL(o.ordered, 0) AS "Заказано:Float:100",
  IFNULL(g.received, 0) AS "Получено:Float:100",
  b.planned_qty - IFNULL(r.requested, 0) AS "Остаток к заявке:Float:130"
FROM `tabContract BOQ Item` b
LEFT JOIN (
  SELECT mri.custom_boq_item_ref ref, SUM(mri.stock_qty) requested
  FROM `tabMaterial Request Item` mri
  JOIN `tabMaterial Request` mr ON mr.name = mri.parent
  WHERE mr.docstatus < 2 AND mr.material_request_type = 'Purchase'
  GROUP BY mri.custom_boq_item_ref
) r ON r.ref = b.name
LEFT JOIN (
  SELECT poi.custom_boq_item_ref ref, SUM(poi.stock_qty) ordered
  FROM `tabPurchase Order Item` poi
  JOIN `tabPurchase Order` po ON po.name = poi.parent
  WHERE po.docstatus = 1
  GROUP BY poi.custom_boq_item_ref
) o ON o.ref = b.name
LEFT JOIN (
  SELECT pri.custom_boq_item_ref ref, SUM(pri.stock_qty) received
  FROM `tabPurchase Receipt Item` pri
  JOIN `tabPurchase Receipt` pr ON pr.name = pri.parent
  WHERE pr.docstatus = 1
  GROUP BY pri.custom_boq_item_ref
) g ON g.ref = b.name
WHERE b.parent = %(contract)s

Фильтр contract передаёт номер договора из шапки отчёта. Столбцы отчёта дают ответ на вопрос «сколько ещё можно заявить», а разница между «заявлено» и «заказано» показывает, что снабженец пока не купил. В версии 16 исправлено смешение полей requested_qty и ordered_qty в цепочке заявка-заказ, а в v15 такое смешение возможно, поэтому на своей версии сверьте цифры отчёта с ручным пересчётом по двум-трём позициям.

Практический приём: сохранить отчёт и отправлять его раз в неделю директору через Auto Email Report, а на рабочем столе вывести Number Card с числом позиций, выбранных больше чем на 90 процентов. Снабженец смотрит отчёт утром и успевает договориться с поставщиком, а не узнаёт о перерасходе постфактум.

Что остаётся за штатными допусками Over Order, Over Receipt и Over Billing

Свою проверку не нужно путать с допусками и не нужно их отключать: это три разных страховки на разных этапах. Over Receipt Allowance ограничивает приёмку сверх заказа и настраивается в Stock Settings или в карточке номенклатуры, а перебор разрешает только роль, указанная в настройках. Over Billing Allowance в Accounts Settings не даёт выставить счёт на сумму больше заказа, обход тоже через роль. Эти механизмы ловят излишек поставщика, а не излишек заявок.

С Over Order Allowance аккуратнее. В документации Buying Settings для актуальных веток поле описано как допуск заказа над заявкой. В коде ветки v15 отдельного поля с таким именем я не нашёл: перерасход заказанного над заявкой проверяет общий механизм статусов StatusUpdater с допуском на получение. Поэтому перед настройкой откройте Buying Settings на своей версии и проверьте поведение на тестовой заявке, а не по скриншотам из интернета.

Сравнение трёх страховок в одном месте: Budget считает деньги и срабатывает на заявке, заказе и расходах, допуски считают проценты и срабатывают на заказе, приёмке и счёте, а контроль по спецификации считает количество и срабатывает на заявке. Только последний требует доработки. Когда исключение всё же нужно, для него предусматривают роль с правом обхода либо правят спецификацию допсоглашением. Маршрут такого решения описан в материале про согласование договоров без почтовой цепочки, и решение принимает руководитель, а не прораб.

Таблица трёх страховок ERPNext: Budget считает деньги, допуски проценты, проверка по спецификации количество и требует доработки
Только проверка по спецификации считает количество, и только она требует доработки.

Где такой контроль не подходит и что он не закроет

Честные пределы. Контроль работает, только если у каждой строки заявки есть ссылка на строку спецификации. Прораб, который пишет заявку «на всякий случай» без ссылки, обходит систему легально, поэтому поле ссылки делают обязательным, а свободные заявки выносят в отдельный тип с согласованием. Второй предел: заказ поставщику можно создать напрямую, минуя заявку. Тогда ту же проверку нужно повторить на Purchase Order, иначе контроль работает, пока все дисциплинированны.

Третье, что остаётся за рамками: единицы измерения и пересчёты. Если спецификация в штуках, а закупка в упаковках, без корректного коэффициента пересчёта лимит будет считаться неверно. Известная особенность Budget на стадии заявки: при альтернативной единице с коэффициентом не 1:1 возможна ложная блокировка, потому что количество берётся в базовых единицах, а цена задана за альтернативную. Если у вас включены и Budget, и проверка по спецификации, проверьте оба контура на позиции с упаковками.

И последнее: производительность. В Frappe v15 создание заказа из заявки на сотню-полторы строк может заметно тормозить из-за множественных пересчётов цен по прайс-листу. Для спецификации в сто сорок позиций это не теория, проверяйте на реальных данных, а не на трёх строках демо. Для компаний, где материалов десятки тысяч позиций и сложные тендеры, отдельный разговор: там нужны другие инструменты. Начать сравнение систем помогут материалы про выбор системы учёта на Linux.

Как «Арматурный Дом» поймал заявку на 13,5 т при лимите 12 т (условный пример)

Покажу на условном примере: строительная фирма «Арматурный Дом», пять рабочих мест, один из объектов по договору подряда. Название и цифры условные, но арифметика честная. В спецификации договора по арматуре заложено 12 тонн. До пилота заявки жили в мессенджере, а сметчик сверял остатки по таблице раз в неделю.

Ситуация, которую мы разыграли на стенде: по объекту уже проведены две заявки, 8 тонн и 3 тонны, то есть 11 тонн из 12. Прораб подаёт новую заявку на 2,5 тонны. Итого получается 13,5 тонны, перебор 1,5 тонны над лимитом. Допуск по этой позиции нулевой, поэтому при сохранении заявки скрипт останавливает её и выдаёт сообщение с цифрами: лимит 12, уже заявлено 11, в этой заявке 2,5, итого 13,5. Заявка не сохранена, до закупки не дошла.

Что считаемТоннКомментарий
Лимит по спецификации12планово по договору
Заявка 18уже проведена
Заявка 23уже проведена
Итого запрошено11остаток до лимита 1
Новая заявка прораба2,5не сохраняется
Итого с новой заявкой13,5перебор на 1,5

Что сделали дальше. Прораб увидел остаток в сообщении и подал заявку на 1 тонну, чтобы закрыть лимит. Недостающие 1,5 тонны директор согласовал отдельно, после чего сметчик оформил допсоглашение и поднял плановое количество в спецификации. Весь разбор занял минуты, а не недели сверки. Честная оговорка: на таком масштабе системе нужно две-три недели притирки. Прорабы поначалу воспринимают блокировку как помеху, и помогает не запрет, а объяснение, что она экономит им время на разборках. Поэтому в тексте ошибки есть все числа: лимит, уже заявлено, в этой заявке, итого.

Шкала арматуры: 8 и 3 тонны уже заявлены, новая заявка на 2,5 тонны выходит за лимит 12 тонн на 1,5 тонны
Одиннадцать тонн уже заявлено, новые 2,5 тонны выводят объём на 13,5 против лимита 12.

Частые вопросы

Как ограничить количество материалов в заявке по договору?

Штатного ограничения нет. Заводят свой документ «Спецификация договора» с плановым количеством по позициям, добавляют в строку заявки ссылку на позицию спецификации и при сохранении суммируют все активные заявки по этой позиции. Если сумма больше лимита с допуском, система выдаёт ошибку с цифрами, и заявка не сохраняется. Допуск удобно задавать построчно, а не общим процентом.

Чем контроль по количеству отличается от Budget?

Budget ограничивает деньги: сумму по счёту расходов в разрезе проекта или центра затрат, с режимами Stop, Warn и Ignore. Он не знает, что на объект положено не больше 12 тонн арматуры. Количественный контроль по позициям договора отдельная доработка, и в проект она входит только тогда, когда закреплена в техническом задании. Лучше держать оба контура: деньги и количество.

Server Script в ERPNext включён по умолчанию?

Нет. Начиная с Frappe v15 серверные скрипты по умолчанию выключены для безопасности общих установок, об этом сказано в документации Frappe. Включение осознанное решение администратора. Если проверка нужна надолго, надёжнее положить её в собственное приложение с хуком validate: так её можно покрыть тестами и обновлять вместе с кодом.

Работает ли Over Order Allowance в ERPNext v15?

В документации актуальных версий поле Over Order Allowance описано в Buying Settings, но в коде ветки v15 отдельного поля с таким именем нет: перерасход заказа над заявкой проверяет StatusUpdater с допуском на получение. Поэтому не полагайтесь на скриншоты из интернета: откройте Buying Settings своей версии и проверьте поведение на тестовой заявке.

Что делать, если закупка нужна сверх спецификации?

Для исключений предусматривают роль с правом провести заявку сверх лимита либо правят спецификацию через допсоглашение. Для приёмки и счёта в ERPNext такие роли уже есть, для своей проверки их придётся описать самим. Решение об исключении принимает руководитель, а не прораб, и причина фиксируется в заявке, чтобы потом было видно, кто и зачем согласовал перебор.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи