Перейти к содержанию

Использование noop-метода корректировки для кастомных триггеров

Введение

noop_correction (no-operation correction) — это специальный метод корректировки, который не изменяет данные напрямую, а только регистрирует метаданные о корректировке и запускает триггеры. Этот паттерн используется когда обработка данных должна происходить в кастомном триггере, а не в стандартном методе корректировки.

Основные концепции

Работа с ревизиями vs. работа с корректировками

В Planiqum существует два подхода к обработке изменений данных:

1. Традиционный подход: Работа с ревизиями

Настройка параметра:

  • Parameter.use_revision = True
  • Parameter.track_corrections = False (опционально)

Как работает:

  1. Пользователь вводит данные на любом уровне иерархии (например, "Всего по стране = 1000")
  2. Метод корректировки (например, split_proportionally_by_base) сразу каскадирует данные на базовый уровень иерархии (например, по регионам пропорционально весам)
  3. Данные записываются в таблицу фактов (fact__parameter_name)
  4. Данные записываются в таблицу ревизий (revision__measure_key)
  5. Создаётся запись RevisionMeasure (связь ревизия-мера)
  6. Триггеры запускаются с уже обработанными данными на базовом уровне

Результат: Данные каскадированы стандартным методом на базовый уровень.

2. Паттерн "отложенной обработки": Работа с корректировками

Настройка параметра:

  • Parameter.track_corrections = True ⚠️ Обязательно!
  • Parameter.use_revision = False (таблицы ревизий не создаются)

Как работает:

  1. Пользователь вводит данные на любом уровне иерархии
  2. Метод корректировки noop_correction НЕ обрабатывает данные, а только:
  3. ✅ Сохраняет метаданные в RevisionCorrection (кто, когда, описание, фильтры)
  4. ✅ Сохраняет детали в RevisionCorrectionDetail (какая мера, какие изменения, UOM, валюта)
  5. ✅ Регистрирует "пустое" изменение меры в FactManager
  6. НЕ записывает данные в таблицу фактов
  7. Триггер запускается и получает:
  8. Пустую FactTable (len=0)
  9. Информацию о мере
  10. Доступ к RevisionCorrection через revision
  11. Триггер самостоятельно:
  12. Читает оригинальные данные из RevisionCorrectionDetail.changes
  13. Применяет кастомную логику каскадирования/трансформации
  14. Записывает результат в таблицу фактов (или отправляет во внешнюю систему)

Результат: Данные обрабатываются кастомной логикой в триггере.

Сценарии использования

Когда использовать noop_correction + кастомный триггер:

  • Сложная бизнес-логика каскадирования, которую нельзя выразить стандартными методами
  • Интеграция с внешними системами (триггер отправляет данные во внешнюю систему для обработки)
  • Условное каскадирование (разная логика в зависимости от контекста)
  • Многошаговая обработка (чтение данных из нескольких параметров, сложные вычисления)
  • Асинхронная обработка (триггер ставит задачу в очередь, обработка происходит позже)

Когда использовать традиционные методы:

  • Стандартное пропорциональное распределение
  • Простое копирование значений
  • Типовые сценарии каскадирования

Настройка меры для работы с noop

Шаг 1: Активировать хранение истории корректировок

В админке параметра установите:

Parameter.track_corrections = True  # ⚠️ Обязательно!

Это активирует сохранение метаданных корректировок в моделях RevisionCorrection и RevisionCorrectionDetail.

Шаг 2: Подключить noop-метод корректировки

В админке меры: 1. Перейдите в раздел "Методы корректировки" 2. Добавьте метод noop_correction 3. Установите его как метод по умолчанию (опционально)

Альтернативно через код:

from planiqum.core.scripts.models import CorrectionMethod, MeasureCorrectionMethod

noop_method = CorrectionMethod.objects.get(shortname='noop_correction')
MeasureCorrectionMethod.objects.create(
    measure=your_measure,
    correction_method=noop_method,
    is_default=True
)

Шаг 3: Создать и подключить кастомный триггер

Создайте Python-скрипт триггера (например, в your_app/scripts/triggers.py):

from planiqum.core.scripts.decorators import pq_script, SCRIPT_TYPE

@pq_script(
    shortname='process_custom_correction',
    label='Обработка кастомной корректировки',
    script_type=SCRIPT_TYPE.TRIGGER
)
def process_custom_correction(revision, fact_manager, measure, facts, user=None, **kwargs):
    """
    Кастомный триггер для обработки корректировок через noop-метод
    """
    from planiqum.core.parameters.models import RevisionCorrection

    # Получаем метаданные корректировки
    correction = RevisionCorrection.objects.filter(revision=revision).first()
    if not correction:
        return []

    # Обрабатываем каждую изменённую меру
    for detail in correction.details.all():
        # Ваша кастомная логика обработки
        process_measure_changes(detail, fact_manager)

    return []

Подключите триггер к мере в админке: 1. Перейдите в раздел "Триггеры" 2. Добавьте ваш триггер 3. Установите auto_run = True 4. Установите приоритет (порядок выполнения)

Разработка триггеров на Python

Сигнатура триггера

def your_trigger(
    revision=None,        # Revision instance
    fact_manager=None,    # FactManager instance  
    measure=None,         # Measure instance (может быть None при TRIGGER_BY_FACTS=False)
    facts=None,           # FactTable instance (может быть None)
    user=None,            # User ID
    **kwargs
):
    """Ваш триггер"""
    pass

⚠️ Важно: При настройке TRIGGER_BY_FACTS=False параметры measure и facts передаются как None. В этом случае информацию нужно получать из fact_manager.changed_data или из моделей.

Получение списка изменённых мер

from planiqum.core.parameters.models import RevisionCorrection

def your_trigger(revision, fact_manager, **kwargs):
    # Получаем корректировку по ревизии
    correction = RevisionCorrection.objects.filter(revision=revision).first()

    if not correction:
        return []

    # Получаем все изменённые меры
    for detail in correction.details.all():
        measure = detail.measure
        # Обрабатываем меру...

Чтение метаданных корректировки

# Общая информация о корректировке
correction = RevisionCorrection.objects.filter(revision=revision).first()

# Кто внёс изменения
user = correction.created_by
username = user.username

# Когда были внесены изменения
timestamp = correction.created_at

# Описание корректировки
description = correction.description

# Причина корректировки (если указана)
reason_code = correction.reason.code if correction.reason else None

# На каких уровнях иерархии вносились изменения
dimensions = correction.dimensions
# Формат: [{'name': 'Клиент', 'level': 35}, {'name': 'Товар', 'level': 15}]

# Применённые фильтры
import json
filter_chunks = json.loads(correction.filter_data) if correction.filter_data else []
# Можно восстановить объект Filter:
from planiqum.core.filters.libs.filter import Filter
filter_obj = Filter(filter_chunks)

Чтение данных изменений

# Для конкретной меры
detail = correction.details.filter(measure=your_measure).first()

# Данные изменений
changes = detail.changes
# Формат: [[[item_id1, item_id2, ...], new_value], ...]

# Пример обработки
for change in changes:
    dimension_values = change[0]  # Список ID элементов иерархий
    new_value = change[1]          # Введённое пользователем значение

    # dimension_values соответствуют dimensions из RevisionCorrection
    # Например, если dimensions = [{'name': 'Клиент', 'level': 35}, {'name': 'Товар', 'level': 15}]
    # То dimension_values = [client_item_id, product_item_id]

    # Ваша обработка...

# Единицы измерения и валюта
uom = detail.uom  # Uom instance или None
currency = detail.currency  # Currency instance или None

# Метод корректировки (будет 'noop_correction')
method = detail.correction_method

Полный пример триггера

from planiqum.core.scripts.decorators import pq_script, SCRIPT_TYPE
from planiqum.core.parameters.models import RevisionCorrection
from planiqum.core.hierarchy.models import Item

@pq_script(
    shortname='custom_cascade_trigger',
    label='Кастомное каскадирование данных',
    script_type=SCRIPT_TYPE.TRIGGER
)
def custom_cascade_trigger(revision, fact_manager, measure=None, facts=None, **kwargs):
    """
    Триггер для кастомной обработки корректировок через noop-метод.

    Читает данные из RevisionCorrectionDetail и применяет 
    кастомную логику каскадирования на базовый уровень.
    """
    # Получаем корректировку
    correction = RevisionCorrection.objects.filter(revision=revision).first()
    if not correction:
        return []

    # Обрабатываем каждую изменённую меру
    for detail in correction.details.all():
        current_measure = detail.measure
        changes = detail.changes

        # Получаем информацию об измерениях
        dimensions_info = correction.dimensions
        # Формат: [{'name': 'Horizon', 'level': 10}, {'name': 'Material', 'level': 4}]

        # Обрабатываем каждое изменение
        for change in changes:
            dimension_values = change[0]  # [item_id1, item_id2, ...]
            user_value = change[1]         # Введённое значение

            # Получаем элементы иерархий
            items = Item.objects.filter(id__in=dimension_values)
            items_dict = {item.id: item for item in items}

            # Ваша кастомная логика каскадирования
            # Например, распределяем значение по дочерним элементам с учётом бизнес-правил

            # 1. Получаем дочерние элементы для каскадирования
            base_items = get_base_level_items(items_dict, dimensions_info)

            # 2. Применяем кастомные веса/правила
            weights = calculate_custom_weights(base_items, current_measure)

            # 3. Распределяем значение
            for base_item, weight in weights.items():
                cascaded_value = user_value * weight
                # Записываем в таблицу фактов через fact_manager
                # ... ваш код записи ...

    return []


def get_base_level_items(items_dict, dimensions_info):
    """Получение элементов базового уровня для каскадирования"""
    # Ваша логика
    pass


def calculate_custom_weights(items, measure):
    """Расчёт кастомных весов для распределения"""
    # Ваша логика
    pass

Работа с SQL-запросами (для внешних систем)

Получение списка изменённых мер по ревизии

SELECT DISTINCT
    m.id AS measure_id,
    m.key AS measure_key,
    m.shortname AS measure_name,
    p.id AS parameter_id,
    p.key AS parameter_key,
    p.shortname AS parameter_name
FROM 
    core_revision_correction_details rcd
    JOIN core_revision_corrections rc ON rcd.correction_id = rc.id
    JOIN core_parameter_measure m ON rcd.measure_id = m.id
    JOIN core_parameter p ON m.parameter_id = p.id
WHERE 
    rc.revision_id = :revision_id;

Получение метаданных корректировки

SELECT 
    rc.id AS correction_id,
    rc.revision_id,
    rc.created_at,
    rc.description,
    rc.cells_changed,
    rc.dimensions,      -- JSON: [{"name": "Horizon", "level": 10}, ...]
    rc.filter_data,     -- JSON: chunks фильтра
    u.username AS created_by_username,
    cr.code AS reason_code,
    cr.description AS reason_description
FROM 
    core_revision_corrections rc
    JOIN auth_user u ON rc.created_by_id = u.id
    LEFT JOIN core_parameter_correctionreason cr ON rc.reason_id = cr.id
WHERE 
    rc.revision_id = :revision_id;

Получение деталей изменений

SELECT 
    rcd.id AS detail_id,
    rcd.layer_id,
    rcd.changes,        -- JSON: [[[item_id1, item_id2], value], ...]
    rcd.cells_changed,
    m.id AS measure_id,
    m.key AS measure_key,
    m.shortname AS measure_name,
    cm.shortname AS correction_method,
    uom.shortname AS uom,
    c.shortname AS currency
FROM 
    core_revision_correction_details rcd
    JOIN core_revision_corrections rc ON rcd.correction_id = rc.id
    JOIN core_parameter_measure m ON rcd.measure_id = m.id
    LEFT JOIN core_script_correction_method scm ON rcd.correction_method_id = scm.script_ptr_id
    LEFT JOIN core_script cm ON scm.script_ptr_id = cm.id
    LEFT JOIN core_conversion_uom uom ON rcd.uom_id = uom.id
    LEFT JOIN core_conversion_currency c ON rcd.currency_id = c.id
WHERE 
    rc.revision_id = :revision_id;

Декодирование JSON-данных в SQL

Извлечение измерений

-- PostgreSQL: Распаковка dimensions
SELECT 
    rc.revision_id,
    jsonb_array_elements(rc.dimensions::jsonb) AS dimension
FROM core_revision_corrections rc
WHERE rc.revision_id = :revision_id;

-- Результат:
-- {"name": "Horizon", "level": 10}
-- {"name": "Material", "level": 4}

-- Получение конкретных полей
SELECT 
    rc.revision_id,
    dim->>'name' AS dimension_name,
    (dim->>'level')::integer AS level_id
FROM 
    core_revision_corrections rc,
    jsonb_array_elements(rc.dimensions::jsonb) AS dim
WHERE 
    rc.revision_id = :revision_id;

Извлечение изменений (changes)

-- PostgreSQL: Распаковка changes
SELECT 
    rcd.id AS detail_id,
    m.key AS measure_key,
    change AS change_data
FROM 
    core_revision_correction_details rcd
    JOIN core_parameter_measure m ON rcd.measure_id = m.id,
    jsonb_array_elements(rcd.changes::jsonb) AS change
WHERE 
    rcd.correction_id = :correction_id;

-- Результат (каждое изменение в отдельной строке):
-- detail_id | measure_key | change_data
-- 1         | qty         | [[737, 713], 120]
-- 1         | qty         | [[737, 644], 60]

-- Извлечение значений из change
SELECT 
    rcd.id AS detail_id,
    m.key AS measure_key,
    change->0 AS dimension_values,  -- [737, 713]
    (change->1)::numeric AS new_value       -- 120
FROM 
    core_revision_correction_details rcd
    JOIN core_parameter_measure m ON rcd.measure_id = m.id,
    jsonb_array_elements(rcd.changes::jsonb) AS change
WHERE 
    rcd.correction_id = :correction_id;

Извлечение ID элементов иерархий

-- Получение отдельных ID элементов из dimension_values
SELECT 
    rcd.id AS detail_id,
    m.key AS measure_key,
    jsonb_array_elements(change->0)::integer AS item_id,
    (change->1)::numeric AS new_value
FROM 
    core_revision_correction_details rcd
    JOIN core_parameter_measure m ON rcd.measure_id = m.id,
    jsonb_array_elements(rcd.changes::jsonb) AS change
WHERE 
    rcd.correction_id = :correction_id;

-- Результат:
-- detail_id | measure_key | item_id | new_value
-- 1         | qty         | 737     | 120
-- 1         | qty         | 713     | 120
-- 1         | qty         | 737     | 60
-- 1         | qty         | 644     | 60

Полный пример SQL-запроса для внешней системы

-- Получить все изменения для обработки во внешней системе
WITH correction_data AS (
    SELECT 
        rc.id AS correction_id,
        rc.revision_id,
        rc.created_at,
        rc.description,
        rc.dimensions,
        u.username AS created_by
    FROM 
        core_revision_corrections rc
        JOIN auth_user u ON rc.created_by_id = u.id
    WHERE 
        rc.revision_id = :revision_id
),
changes_expanded AS (
    SELECT 
        cd.correction_id,
        cd.revision_id,
        cd.created_at,
        cd.description,
        cd.created_by,
        rcd.id AS detail_id,
        m.id AS measure_id,
        m.key AS measure_key,
        m.shortname AS measure_name,
        change->0 AS dimension_values,
        (change->1)::numeric AS new_value,
        uom.shortname AS uom,
        c.shortname AS currency
    FROM 
        correction_data cd
        JOIN core_revision_correction_details rcd ON rcd.correction_id = cd.correction_id
        JOIN core_parameter_measure m ON rcd.measure_id = m.id
        LEFT JOIN core_conversion_uom uom ON rcd.uom_id = uom.id
        LEFT JOIN core_conversion_currency c ON rcd.currency_id = c.id,
        jsonb_array_elements(rcd.changes::jsonb) AS change
)
SELECT * FROM changes_expanded;

Формат данных в моделях

RevisionCorrection

Поле Тип Описание Пример
revision_id FK Ссылка на Revision 90
created_by_id FK Пользователь 1
created_at DateTime Время создания 2025-10-03 14:13:28
description Text Комментарий пользователя "Корректировка плана"
cells_changed Integer Количество изменённых ячеек 3
dimensions JSON Измерения [{"name": "Клиент", "level": 35}]
filter_data JSON Фильтр [{"dimension": "region", "elements": [...]}]
reason_id FK Причина корректировки 1

RevisionCorrectionDetail

Поле Тип Описание Пример
correction_id FK Ссылка на RevisionCorrection 1
layer_id Integer Идентификатор слоя -2
measure_id FK Мера которая изменялась 45
changes JSON Список изменений [[[737, 713], 120], [[737, 644], 60]]
cells_changed Integer Количество ячеек 2
uom_id FK Единица измерения 3 или NULL
currency_id FK Валюта 2 или NULL
correction_method_id FK Метод корректировки 7

Формат поля changes

changes = [
    [[item_id_dim1, item_id_dim2, ...], new_value],
    [[item_id_dim1, item_id_dim2, ...], new_value],
    ...
]

Пример:

# Для параметра с измерениями [Клиент (level=35), Товар (level=15)]
changes = [
    [[737, 713], 120],  # Клиент 737, Товар 713 → значение 120
    [[737, 644], 60],   # Клиент 737, Товар 644 → значение 60
]

# Порядок item_id соответствует порядку в dimensions
dimensions = [
    {'name': 'Клиент', 'level': 35},   # 0-й элемент → dimension_values[0]
    {'name': 'Товар', 'level': 15}     # 1-й элемент → dimension_values[1]
]

Примеры использования

Пример 1: Простое каскадирование с логированием

@pq_script(shortname='simple_cascade_with_log', script_type=SCRIPT_TYPE.TRIGGER)
def simple_cascade_with_log(revision, fact_manager, **kwargs):
    """Каскадирует данные на базовый уровень с подробным логированием"""
    import logging
    from planiqum.core.parameters.models import RevisionCorrection
    from planiqum.core.hierarchy.models import Item

    logger = logging.getLogger(__name__)

    correction = RevisionCorrection.objects.filter(revision=revision).first()
    if not correction:
        return []

    for detail in correction.details.all():
        measure = detail.measure
        parameter = measure.parameter

        logger.info(f"Обработка меры {measure.key}, изменений: {len(detail.changes)}")

        for change in detail.changes:
            dimension_values = change[0]
            value = change[1]

            # Получаем элементы иерархий
            items = Item.objects.filter(id__in=dimension_values)

            # Находим все дочерние элементы базового уровня
            base_items = []
            for item in items:
                base_items.extend(item.get_descendants_at_base_level())

            # Распределяем пропорционально (равномерно)
            distributed_value = value / len(base_items) if base_items else 0

            # Записываем факты через fact_manager
            facts_table = fact_manager.get_facts(parameter, measure)
            for base_item in base_items:
                # Находим или создаём факт и устанавливаем значение
                # ... код записи через fact_manager ...
                pass

    return []

Пример 2: Отправка данных во внешнюю систему

@pq_script(shortname='send_to_external_system', script_type=SCRIPT_TYPE.TRIGGER)
def send_to_external_system(revision, **kwargs):
    """Отправляет данные корректировки во внешнюю систему для обработки"""
    import requests
    from planiqum.core.parameters.models import RevisionCorrection

    correction = RevisionCorrection.objects.filter(revision=revision).first()
    if not correction:
        return []

    # Формируем данные для отправки
    payload = {
        'revision_id': revision.id,
        'timestamp': correction.created_at.isoformat(),
        'user': correction.created_by.username,
        'description': correction.description,
        'measures': []
    }

    for detail in correction.details.all():
        measure_data = {
            'measure_key': detail.measure.key,
            'parameter_key': detail.measure.parameter.key,
            'changes': detail.changes,
            'uom': detail.uom.shortname if detail.uom else None,
            'currency': detail.currency.shortname if detail.currency else None
        }
        payload['measures'].append(measure_data)

    # Отправляем во внешнюю систему
    response = requests.post(
        'https://external-system.com/api/process-correction',
        json=payload,
        timeout=30
    )

    if response.status_code != 200:
        return [(1, f"Ошибка отправки в внешнюю систему: {response.text}")]

    return []

Пример 3: SQL-скрипт для внешней системы

-- Скрипт для получения всех данных корректировки для обработки
-- Использовать в хранимой процедуре или внешнем приложении

-- Шаг 1: Получить основную информацию о корректировке
SELECT 
    rc.id AS correction_id,
    rc.revision_id,
    rc.created_at,
    rc.description,
    u.username AS created_by,
    rc.dimensions::text AS dimensions_json,
    rc.filter_data AS filter_json
FROM 
    core_revision_corrections rc
    JOIN auth_user u ON rc.created_by_id = u.id
WHERE 
    rc.revision_id = :revision_id;

-- Шаг 2: Получить изменения с расшифровкой элементов иерархий
WITH changes_raw AS (
    SELECT 
        rcd.id AS detail_id,
        m.key AS measure_key,
        p.key AS parameter_key,
        change->0 AS dimension_values_json,
        (change->1)::numeric AS new_value,
        uom.shortname AS uom,
        c.shortname AS currency,
        rc.dimensions AS dimensions_info
    FROM 
        core_revision_correction_details rcd
        JOIN core_revision_corrections rc ON rcd.correction_id = rc.id
        JOIN core_parameter_measure m ON rcd.measure_id = m.id
        JOIN core_parameter p ON m.parameter_id = p.id
        LEFT JOIN core_conversion_uom uom ON rcd.uom_id = uom.id
        LEFT JOIN core_conversion_currency c ON rcd.currency_id = c.id,
        jsonb_array_elements(rcd.changes::jsonb) AS change
    WHERE 
        rc.revision_id = :revision_id
),
items_expanded AS (
    SELECT 
        cr.*,
        item_index,
        item_id::integer AS item_id
    FROM 
        changes_raw cr,
        jsonb_array_elements(cr.dimension_values_json) WITH ORDINALITY AS t(item_id, item_index)
)
SELECT 
    ie.detail_id,
    ie.measure_key,
    ie.parameter_key,
    ie.item_index - 1 AS dimension_index,
    (ie.dimensions_info->>(ie.item_index - 1))::jsonb->>'name' AS dimension_name,
    (ie.dimensions_info->>(ie.item_index - 1))::jsonb->'level' AS level_id,
    ie.item_id,
    hi.shortname AS item_code,
    hi.description AS item_name,
    ie.new_value,
    ie.uom,
    ie.currency
FROM 
    items_expanded ie
    JOIN core_hierarchy_item hi ON hi.id = ie.item_id
ORDER BY 
    ie.detail_id, ie.dimension_index;

Пример интеграции с внешней системой

Триггер в Planiqum

@pq_script(shortname='notify_external_system', script_type=SCRIPT_TYPE.TRIGGER)
def notify_external_system(revision, **kwargs):
    """
    Уведомляет внешнюю систему о корректировке.
    Отправляет только revision_id, внешняя система сама читает детали через SQL.
    """
    import requests
    from django.conf import settings

    # Отправляем только ID ревизии
    response = requests.post(
        f"{settings.EXTERNAL_SYSTEM_URL}/api/process-correction",
        json={'revision_id': revision.id},
        headers={'Authorization': f'Bearer {settings.EXTERNAL_SYSTEM_TOKEN}'},
        timeout=30
    )

    if response.status_code != 200:
        return [(1, f"Ошибка уведомления внешней системы: {response.text}")]

    return []

Обработчик во внешней системе (псевдокод)

# external_system/handlers.py
def process_correction(revision_id):
    """Обработчик корректировки во внешней системе"""

    # Подключаемся к БД Planiqum
    conn = psycopg2.connect(PLANIQUM_DB_CONN_STRING)
    cursor = conn.cursor()

    # Получаем метаданные
    cursor.execute("""
        SELECT rc.dimensions, rc.filter_data, rc.description
        FROM core_revision_corrections rc
        WHERE rc.revision_id = %s
    """, [revision_id])

    correction_meta = cursor.fetchone()
    dimensions = json.loads(correction_meta[0])

    # Получаем изменения
    cursor.execute("""
        SELECT 
            m.key AS measure_key,
            rcd.changes,
            uom.shortname AS uom,
            c.shortname AS currency
        FROM 
            core_revision_correction_details rcd
            JOIN core_revision_corrections rc ON rcd.correction_id = rc.id
            JOIN core_parameter_measure m ON rcd.measure_id = m.id
            LEFT JOIN core_conversion_uom uom ON rcd.uom_id = uom.id
            LEFT JOIN core_conversion_currency c ON rcd.currency_id = c.id
        WHERE 
            rc.revision_id = %s
    """, [revision_id])

    for row in cursor.fetchall():
        measure_key = row[0]
        changes = json.loads(row[1])

        # Обрабатываем каждое изменение
        for change in changes:
            dimension_values = change[0]
            new_value = change[1]

            # Ваша логика обработки в внешней системе
            process_in_external_system(measure_key, dimension_values, new_value, dimensions)

    conn.close()

Рекомендации и best practices

1. Обработка ошибок

def your_trigger(revision, fact_manager, **kwargs):
    from planiqum.core.parameters.models import RevisionCorrection

    try:
        correction = RevisionCorrection.objects.filter(revision=revision).first()
        if not correction:
            return [(1, "Корректировка не найдена для ревизии")]

        # Ваша логика...

    except Exception as e:
        # Возвращаем ошибку для логирования
        return [(2, f"Ошибка обработки: {str(e)}")]

    return []  # Успех

2. Проверка наличия изменений

correction = RevisionCorrection.objects.filter(revision=revision).first()
if not correction or not correction.details.exists():
    # Нет изменений для обработки
    return []

3. Получение элементов иерархий

from planiqum.core.hierarchy.models import Item

# Собираем все ID элементов
all_item_ids = set()
for detail in correction.details.all():
    for change in detail.changes:
        dimension_values = change[0]
        all_item_ids.update(dimension_values)

# Получаем все элементы одним запросом (эффективно)
items_dict = {item.id: item for item in Item.objects.filter(id__in=all_item_ids)}

# Используем в обработке
for change in detail.changes:
    dimension_values = change[0]
    items = [items_dict[item_id] for item_id in dimension_values]

4. Работа с фильтрами

import json
from planiqum.core.filters.libs.filter import Filter

# Восстановление объекта фильтра
filter_chunks = json.loads(correction.filter_data) if correction.filter_data else []
filter_obj = Filter(filter_chunks)

# Использование фильтра для получения фактов
facts = fact_manager.get_facts(
    parameter=measure.parameter,
    measure=measure,
    filter_=filter_obj
)

Решение проблем

Триггер не запускается

Проблема: После корректировки с noop_correction триггер не выполняется.

Решение:

  1. Проверьте что триггер подключён к мере: MeasureTrigger.objects.filter(measure=your_measure, auto_run=True)
  2. Проверьте что noop_correction подключён как метод корректировки для меры
  3. Проверьте логи: триггер может выполняться, но падать с ошибкой

RevisionCorrection не создаётся

Проблема: После корректировки нет данных в RevisionCorrection.

Решение:

  1. Убедитесь что Parameter.track_corrections = True
  2. Проверьте что хотя бы одна мера в корректировке имеет track_corrections=True на уровне параметра

Данные записываются в факты при noop

Проблема: Несмотря на noop_correction, данные попадают в таблицу фактов.

Решение:

  1. Это невозможно при корректной реализации noop_correction
  2. Проверьте что используется именно noop_correction, а не другой метод
  3. Возможно триггер записывает данные — это нормально, триггер должен обработать корректировку

Сравнение подходов

Аспект Традиционный метод noop + Триггер
Каскадирование Автоматическое стандартными методами Кастомное в триггере
Запись данных Метод корректировки → факты Триггер → факты
Гибкость Ограничена методом Полная свобода в триггере
Сложность Простая настройка Требует написания кода триггера
История RevisionMeasure (опционально) RevisionCorrection (обязательно)
Уровень данных Базовый уровень иерархии Уровень ввода пользователя
Интеграция Только внутри Planiqum Легко интегрировать с внешними системами

Ссылки