Как не купить лишнего: контроль перерасхода материалов по спецификации договора в ERPNext
Короткий ответ: штатного контроля закупок по спецификации договора в 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, когда деньги почти потрачены. Количественный контроль придётся добавить самим, об этом остальная статья.
Как устроен контроль: спецификация, ссылка в заявке, проверка при сохранении
Конструкция минимальна. Первый 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 попадают только итоги по позициям: количество, единица, цена. Дублировать смету внутри системы бессмысленно, она нужна лишь как лимит.
Проверка срабатывает при сохранении заявки. Логика простая: для каждой строки со ссылкой на спецификацию берём сумму по всем остальным активным заявкам на закупку, добавляем количество из текущей и сравниваем с лимитом плюс допуск. Если больше, сохранение прерывается, а прораб видит сообщение с цифрами. Заявка до закупки просто не доходит.
- Contract BOQ: Project, номер договора, статус
- Contract BOQ Item: Item, planned_qty, UOM, allowed_overage_pct
- Material Request Item: custom_contract_boq и custom_boq_item_ref
- Purchase Order Item и Purchase Receipt Item: поле с тем же именем для сквозной ссылки
Что делает 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 считает деньги и срабатывает на заявке, заказе и расходах, допуски считают проценты и срабатывают на заказе, приёмке и счёте, а контроль по спецификации считает количество и срабатывает на заявке. Только последний требует доработки. Когда исключение всё же нужно, для него предусматривают роль с правом обхода либо правят спецификацию допсоглашением. Маршрут такого решения описан в материале про согласование договоров без почтовой цепочки, и решение принимает руководитель, а не прораб.
Где такой контроль не подходит и что он не закроет
Честные пределы. Контроль работает, только если у каждой строки заявки есть ссылка на строку спецификации. Прораб, который пишет заявку «на всякий случай» без ссылки, обходит систему легально, поэтому поле ссылки делают обязательным, а свободные заявки выносят в отдельный тип с согласованием. Второй предел: заказ поставщику можно создать напрямую, минуя заявку. Тогда ту же проверку нужно повторить на 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 | планово по договору |
| Заявка 1 | 8 | уже проведена |
| Заявка 2 | 3 | уже проведена |
| Итого запрошено | 11 | остаток до лимита 1 |
| Новая заявка прораба | 2,5 | не сохраняется |
| Итого с новой заявкой | 13,5 | перебор на 1,5 |
Что сделали дальше. Прораб увидел остаток в сообщении и подал заявку на 1 тонну, чтобы закрыть лимит. Недостающие 1,5 тонны директор согласовал отдельно, после чего сметчик оформил допсоглашение и поднял плановое количество в спецификации. Весь разбор занял минуты, а не недели сверки. Честная оговорка: на таком масштабе системе нужно две-три недели притирки. Прорабы поначалу воспринимают блокировку как помеху, и помогает не запрет, а объяснение, что она экономит им время на разборках. Поэтому в тексте ошибки есть все числа: лимит, уже заявлено, в этой заявке, итого.
Частые вопросы
Как ограничить количество материалов в заявке по договору?
Штатного ограничения нет. Заводят свой документ «Спецификация договора» с плановым количеством по позициям, добавляют в строку заявки ссылку на позицию спецификации и при сохранении суммируют все активные заявки по этой позиции. Если сумма больше лимита с допуском, система выдаёт ошибку с цифрами, и заявка не сохраняется. Допуск удобно задавать построчно, а не общим процентом.
Чем контроль по количеству отличается от 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 такие роли уже есть, для своей проверки их придётся описать самим. Решение об исключении принимает руководитель, а не прораб, и причина фиксируется в заявке, чтобы потом было видно, кто и зачем согласовал перебор.
Источники
- Frappe Framework Docs — Server Script — События документа (Before Save, Before Submit), отключение скриптов по умолчанию с версии 15, песочница RestrictedPython; проверено 01.10.2026: https://docs.frappe.io/framework/user/en/desk/scripting/server-script
- ERPNext Docs — Buying Settings — Описание Over Order Allowance в документации актуальных веток: https://docs.frappe.io/erpnext/buying-settings
- ERPNext Docs — Budget — Бюджет по Project и Cost Center, действия Stop, Warn, Ignore для заявки, заказа и фактических расходов: https://docs.frappe.io/erpnext/budget
- ERPNext Docs — Material Request — Типы и статусы заявки, связь с заказом поставщику: https://docs.frappe.io/erpnext/material-request
- frappe/erpnext, ветка version-15 — status_updater.py — Проверка перерасхода количества и допуск на получение, роль для обхода: https://github.com/frappe/erpnext/blob/version-15/erpnext/controllers/status_updater.py
- frappe/erpnext, ветка version-15 — budget.py — Расчёт бюджета по счёту расходов, действия при превышении: https://github.com/frappe/erpnext/blob/version-15/erpnext/accounts/doctype/budget/budget.py



