1. Почему розничный реплениш — хороший стресс-тест для ERP

ERP-демо красивы. Реальные операции — нет.

Розничное пополнение запасов — один из первых процессов, где ERP-системы перестают быть «формами и документами» и становятся движками принятия решений. Он заставляет платформу обрабатывать:

  • данные продаж во времени
  • корректировки прогноза (сезонность, промо)
  • ограничения поставщиков (lead time, MOQ, кратность упаковки)
  • логику нескольких складов
  • автоматическую генерацию документов
  • процессы согласования людьми
  • объяснимость решений системы

Это делает его идеальным кейсом для сравнения ERPNext и MyCompany.

Цель этой статьи — не маркетинг. Цель — ответить на практический вопрос уровня CTO:

Сколько инженерных усилий уходит на реализацию реального розничного процесса?

И да — мы будем считать строки кода.

Схема процесса розничного пополнения запасов

2. Бизнес-спецификация (практическая, не выдуманная)

Сценарий

  • 15 розничных магазинов
  • 1 центральный склад
  • 4 основных поставщика
  • ежедневные продажи через POS
  • еженедельные промоакции
  • смешанная стратегия: трансфер / закупка

Требования

Каждую ночь (02:00):

Для каждого SKU в каждом магазине:

  1. Рассчитать средние ежедневные продажи (последние 28 дней)
  2. Скорректировать на промо-коэффициент (если активен)
  3. Умножить на lead time поставщика
  4. Вычесть:
    • текущий остаток
    • входящие заказы поставщику
    • входящие трансферы
  5. Применить:
    • MOQ (минимальное количество заказа)
    • округление до кратности упаковки
  6. При наличии дефицита:
    • предпочесть трансфер с центрального склада (если есть)
    • иначе предложить заказ поставщику
  7. Создать черновики документов
  8. Записать объяснение (почему именно такое количество)
  9. Дать UI-экран:
    • просмотр предложений
    • утверждение
    • отклонение
    • пересчёт

Пример объяснимости:

«Расчётная потребность: 94 ед. (ADS 4.7 × 14 дней − 32 остаток − 40 входящих). Округлено до 96 из-за упаковки по 12».

3. Реализация на ERPNext

ERPNext построен на фреймворке Frappe (Python-бэкенд, JavaScript-фронтенд).

Платформа уже поддерживает:

  • остатки
  • уровни перезаказа
  • материальные заявки (Material Requests)
  • заказы поставщикам (Purchase Orders)
  • трансферы

Но как только добавляются промо, правила для нескольких магазинов, MOQ и кратность упаковки, логика «покупать или переместить» и объяснимость — встроенный реордер быстро заканчивается.

3.1 Архитектурный подход

Типичная реализация:

  • кастомный DocType: Replenishment Rule
  • задание по расписанию на Python
  • кастомный отчёт на серверной стороне
  • клиентские действия в UI
  • хуки для автоматизации и связки модулей

3.2 Расширение модели данных

{
              "doctype": "Replenishment Rule",
              "fields": [
                { "fieldname": "item", "fieldtype": "Link", "options": "Item" },
                { "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse" },
                { "fieldname": "lead_time_days", "fieldtype": "Int" },
                { "fieldname": "promotion_factor", "fieldtype": "Float" },
                { "fieldname": "moq", "fieldtype": "Int" },
                { "fieldname": "pack_size", "fieldtype": "Int" }
              ]
            }

Типичный объём: ~120–180 строк кода вместе с метаданными.

3.3 Задание по расписанию (основная логика)

# app/replenishment/scheduler.py

            import frappe


            def nightly_replenishment():
                rules = frappe.get_all("Replenishment Rule", fields="*")

                for rule in rules:
                    avg_sales = calculate_average_sales(rule.item, rule.warehouse)
                    adjusted_sales = avg_sales * (rule.promotion_factor or 1)

                    required_qty = adjusted_sales * rule.lead_time_days

                    current_stock = get_stock(rule.item, rule.warehouse)
                    incoming = get_incoming(rule.item, rule.warehouse)

                    deficit = required_qty - current_stock - incoming

                    if deficit > 0:
                        rounded_qty = round_to_pack(deficit, rule.pack_size, rule.moq)
                        create_suggestion(rule, rounded_qty)
            

Вспомогательные функции (запросы остатков, SQL-джойны, отчёты, округление, создание документов, идемпотентность, логирование) добавляют ещё ~180–300 строк.

3.4 Отчёт о предложениях

SELECT
                item,
                warehouse,
                suggested_qty,
                explanation
            FROM `tabReplenishment Suggestion`
            WHERE status = 'Draft';
            

Типичный объём: ~70–120 строк.

3.5 Клиентское действие «утвердить»

frappe.ui.form.on("Replenishment Suggestion", {
                approve(frm) {
                    frappe.call({
                        method: "app.replenishment.approve",
                        args: { name: frm.doc.name },
                        callback() {
                            frm.reload_doc();
                        }
                    });
                }
            });
            

Типичный объём: ~40–80 строк.

3.6 Итог по объёму кода ERPNext

КомпонентLOC (примерно)
Основная логика на Python250–400
Отчёты70–120
Клиентский JS40–80
JSON-метаданные120–200
Всего (LOC в репозитории)480–800

Только бизнес-логика (LOC): ~320–600


4. Реализация на MyCompany

MyCompany построена на lsFusion — декларативной платформе бизнес-логики.

Вместо процедурных заданий вы описываете:

  • свойства данных
  • вычисляемые выражения
  • действия
  • формы

4.1 Определение данных

CLASS Item;
            CLASS Warehouse;
            CLASS Rule;

            item            = DATA Item (Rule);
            warehouse       = DATA Warehouse (Rule);
            leadTime        = DATA INTEGER (Rule);
            promotionFactor = DATA NUMERIC[10,2] (Rule);
            moq             = DATA INTEGER (Rule);
            packSize        = DATA INTEGER (Rule);
            

4.2 Вычисляемые свойства

avgSales(Item i, Warehouse w) =
                SUM quantity(Sale s)
                WHERE s.item = i
                  AND s.warehouse = w
                  AND s.date >= currentDate() - 28
                / 28;

            requiredQty(Rule r) =
                avgSales(item(r), warehouse(r))
                * promotionFactor(r)
                * leadTime(r);

            deficit(Rule r) =
                requiredQty(r)
                - currentStock(item(r), warehouse(r))
                - incoming(item(r), warehouse(r));
            

4.3 Логика округления

roundedQty(Rule r) =
                MAX(
                    moq(r),
                    CEIL(deficit(r) / packSize(r)) * packSize(r)
                )
                IF deficit(r) > 0;
            

4.4 Действие

generateSuggestions() {
                FOR r IN Rule DO {
                    IF deficit(r) > 0 THEN
                        NEW Suggestion {
                            rule        = r;
                            quantity    = roundedQty(r);
                            explanation =
                                "ADS × LT – stock – incoming = " + deficit(r);
                        };
                }
            }
            

4.5 UI

FORM suggestions
                OBJECTS s = Suggestion
                PROPERTIES s.rule, s.quantity, s.explanation;
            

4.6 Итог по объёму кода MyCompany

КомпонентLOC (примерно)
Определения данных~25
Бизнес-логика (свойства)~60–90
Округление~10
Действия~30–40
UI~20–30
Итого~145–195

5. Сравнение бок о бок

МетрикаERPNextMyCompany
Парадигма логикиПроцедурная (Python + связки)Декларативная (свойства + действия)
Планирование заданийЯвное задание по расписанию + связкиМодель, ориентированная на действия (обычно меньше связок)
Сборка UIЧасто требует клиентских скриптовДекларативные формы
LOC (только логика)~320–600~100–150
LOC (весь репозиторий)~480–800~145–200

6. Архитектурные последствия

Расширения ERPNext часто размазывают логику между Python, JavaScript и метаданными. MyCompany, как правило, централизует бизнес-правила как декларативную модель.

Больше строк кода обычно означает:

  • более высокую когнитивную нагрузку
  • больше интеграционных «связок»
  • больше поверхности для регрессий

Это автоматически не делает одну платформу «лучше». Это говорит о том, какую цену изменений вы покупаете.


7. Экономика разработки с ИИ

Есть и современное измерение, которое нельзя игнорировать: объём и структура кода влияют на то, насколько эффективно ИИ-инструменты помогают, рефакторят и генерируют расширения.

  • Стоимость контекста: больше файлов и слоёв-связок обычно увеличивают объём контекста для корректной помощи ИИ — и поднимают стоимость инференса.
  • Стоимость верификации: фрагментированная процедурная логика часто требует больше тестовой обвязки и проверок во времени выполнения.
  • Стоимость рефакторинга: больше точек связи затрудняет безопасный автоматический рефакторинг (и для людей, и для ИИ).
  • Стоимость фиксации знаний: разрозненная логика увеличивает объём документации, усилия на промптинг и время супервизии.

Иначе говоря, LOC — это уже не только прокси для стоимости поддержки людьми. Это всё больше прокси для стоимости эволюции с ИИ.


8. Когда какая платформа выигрывает

Выбирайте ERPNext, если:

  • нужен широкий ERP быстро (учёт + закупки + остатки) и хочется крупную экосистему;
  • правила пополнения относительно стандартные и меняются редко;
  • предпочитаете Python и привычное сообществу решение, а не парадигму «сначала модель».

Выбирайте MyCompany, если:

  • конкурентное преимущество — в кастомных бизнес-правилах и быстрых итерациях;
  • правила часто меняются, и хочется, чтобы система оставалась объяснимой;
  • хочется ERP как конструктор: модель, которую вы развиваете, а не продукт, который вы патчите.

9. Вывод

Подсчёт строк кода — не всё. Но в сложных бизнес-процессах LOC коррелирует с когнитивной нагрузкой, а она — со стоимостью долгосрочной поддержки.

В этом сценарии пополнения:

  • ERPNext обычно требует ~в 2–4 раза больше кастомного кода;
  • MyCompany часто выражает те же правила на меньшей и более централизованной поверхности логики.

Приложение A. Симуляция git diff (как честно это измерить)

Если хотите, чтобы сравнение «сколько строк» было защитимым, измеряйте его реальным diff репозитория и LOC-инструментом (например, cloc) с прозрачными правилами включения. Ниже — симулированный, но структурно реалистичный пример.

ERPNext (приложение Frappe) — симулированный diff

$ git diff --stat

              app/replenishment/hooks.py                                              |  34 +++++++
              app/replenishment/scheduler.py                                          | 140 +++++++++++++++++++++
              app/replenishment/api.py                                                |  88 +++++++++++++
              app/replenishment/utils/sales.py                                        |  74 ++++++++++++
              app/replenishment/utils/stock.py                                        |  92 +++++++++++++++
              app/replenishment/utils/rounding.py                                     |  38 ++++++++
              app/replenishment/doctype/replenishment_rule/replenishment_rule.json    | 165 +++++++++++++++++++++++++
              app/replenishment/doctype/replenishment_suggestion/replenishment_suggestion.json | 142 +++++++++++++++++++++++
              app/replenishment/report/replenishment_suggestions/replenishment_suggestions.py |  76 +++++++++++++
              app/replenishment/report/replenishment_suggestions/replenishment_suggestions.js |  58 ++++++++++
              app/replenishment/public/js/replenishment_suggestion_form.js            |  62 +++++++++++
              12 files changed, 971 insertions(+)
              

Типичная интерпретация:

  • LOC только логики = scheduler + utils + API + Python отчётов + минимум JS (~320–600).
  • Общий LOC репозитория включает JSON-метаданные (~480–900+ в зависимости от UI/фикстур отчётов).

MyCompany (модуль lsFusion) — симулированный diff

$ git diff --stat

              modules/replenishment/Replenishment.lsf        | 182 +++++++++++++++++++++++++++
              modules/replenishment/ReplenishmentForms.lsf   |  54 ++++++++
              modules/replenishment/ReplenishmentDocs.lsf    |  31 ++++
              3 files changed, 267 insertions(+)
              

Дисциплина, которая делает сравнения LOC осмысленными: одна и та же спецификация, один и тот же метод измерения, прозрачные правила включения.

Быстрая обратная связь

Было полезно?

Короткий сигнал помогает нам выбирать темы для следующих материалов.

Что ещё почитать:

Если вас волнуют практические последствия архитектуры ERP и снижение зависимости от подрядчиков — больше материалов на DevLab Blog: