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

Работа с отчетами в Planiqum

Базовые понятия

Перед началом работы с отчетами рекомендуется ознакомиться с базовыми понятиями Planiqum, особенно с разделами: - Иерархии - Параметры - Измерения - Меры - Таблицы фактов

Структура отчета

Отчет в Planiqum состоит из: - Набора параметров, из которых берутся данные - Набора измерений, по которым группируются данные - Набора мер, которые отображаются в отчете - Фильтров, которые ограничивают данные

Формат AJAX запроса

Для получения данных отчета используется AJAX запрос к эндпоинту /reports/data/. Запрос должен содержать:

{
    "layers": [
        {
            "parameter": "parameter1",  // ключ параметра
            "measure": "1",            // ID меры (в виде строки!)
            "dimensions": ["dpu", "month"]  // ключи измерений
        }
    ],
    "filters": {
        "dpu": ["DPU1", "DPU2"],
        "month": ["2024-01", "2024-02"]
    }
}

Важные моменты при формировании запроса

  1. ID меры всегда должен передаваться в виде строки, даже если это числовой ID
  2. Ключи измерений должны соответствовать ключам в таблице фактов (без префикса dim_)
  3. Значения в фильтрах должны соответствовать формату элементов иерархии
  4. Порядок измерений в запросе может отличаться от порядка в таблице фактов

Формат ответа

Ответ API содержит: - Массив строк с данными - Массив заголовков - Информацию о правах доступа

{
    "data": [
        ["DPU1", "2024-01", 100.0],
        ["DPU1", "2024-02", 200.0],
        ["DPU2", "2024-01", 300.0],
        ["DPU2", "2024-02", 400.0]
    ],
    "headers": ["DPU", "Month", "Quantity"],
    "canEdit": true
}

Особенности ответа

  1. Порядок колонок в ответе может отличаться от порядка в запросе
  2. Значения в ответе всегда приводятся к строковому типу
  3. Числовые значения могут быть округлены
  4. Пустые значения представляются как null

Тестирование

При написании тестов для отчетов важно учитывать:

  1. Необходимость создания всех зависимых объектов:
  2. Типы и уровни иерархий
  3. Элементы иерархий
  4. Параметры и их измерения
  5. Меры параметров
  6. Данные в таблицах фактов

  7. Правильную последовательность действий:

  8. Создание иерархий
  9. Создание параметров
  10. Синхронизация параметров
  11. Заполнение данными
  12. Отправка запроса

  13. Проверку прав доступа:

  14. Создание пользователя с нужными правами
  15. Проверка доступности параметров
  16. Проверка возможности редактирования

Пример теста

def test_report_data_ajax_basic(login_client, parameter1_data, parameter2_data):
    """
    Тест проверяет базовую функциональность получения данных отчета через AJAX.

    Подробнее о работе с отчетами в Planiqum:
    docs/dev/reports.md
    """
    # Подготовка данных
    parameter1, measure1 = parameter1_data
    parameter2, measure2 = parameter2_data

    # Формирование запроса
    data = {
        "layers": [
            {
                "parameter": parameter1.key,
                "measure": str(measure1.pk),  # ID меры как строка
                "dimensions": ["dpu", "month"]
            }
        ],
        "filters": {
            "dpu": ["DPU1", "DPU2"],
            "month": ["2024-01", "2024-02"]
        }
    }

    # Отправка запроса
    response = login_client.post(
        "/reports/data/",
        data=json.dumps(data),
        content_type="application/json"
    )

    # Проверка ответа
    assert response.status_code == 200
    result = json.loads(response.content)
    assert "data" in result
    assert "headers" in result
    assert "canEdit" in result

Поддержка единиц измерения (UOM) и валют (Currency)

Planiqum поддерживает автоматическую конвертацию значений мер в различные единицы измерения и валюты.

  • UOM (единицы измерения) позволяют отображать значения в разных физических единицах (например, кг, шт, паллеты).
  • Currency (валюты) позволяют отображать значения в разных денежных единицах (например, RUR, kRUR, mlnRUR).

Модели и их взаимосвязи

  1. Модель Measure:

    class Measure(models.Model):
        parameter = models.ForeignKey(Parameter, on_delete=models.CASCADE)
        name = models.CharField(max_length=255)
        supports_currency = models.BooleanField(default=False)  # Поддержка валют
        # ... другие поля
    

  2. Модель HierarchyLevel:

    class HierarchyLevel(models.Model):
        hierarchy = models.ForeignKey(Hierarchy, on_delete=models.CASCADE)
        name = models.CharField(max_length=255)
        use_conversion_factors = models.BooleanField(default=False)  # Поддержка UOM
        # ... другие поля
    

  3. Модель Currency:

    class Currency(models.Model):
        code = models.CharField(max_length=10)  # Например, "RUR", "USD"
        name = models.CharField(max_length=255)
        factor = models.DecimalField(max_digits=20, decimal_places=10)  # Коэффициент конвертации
        # ... другие поля
    

  4. Модель UOM:

    class UOM(models.Model):
        code = models.CharField(max_length=10)  # Например, "kg", "pcs"
        name = models.CharField(max_length=255)
        # ... другие поля
    

  5. Модель ConversionFactor:

    class ConversionFactor(models.Model):
        element = models.ForeignKey(Element, on_delete=models.CASCADE)
        uom = models.ForeignKey(UOM, on_delete=models.CASCADE)
        factor = models.DecimalField(max_digits=20, decimal_places=10)
        # ... другие поля
    

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

  1. Поддержка валют:
  2. Для меры параметра может быть включена поддержка валюты (флаг supports_currency).
  3. В запросе данных для слоя (layer) можно указать поле currency:

    {
        "layers": [
            {
                "parameter": "parameter1",
                "measure": "1",
                "dimensions": ["dpu", "month"],
                "currency": 2  // ID валюты (опционально)
            }
        ],
        "filters": { ... }
    }
    

  4. Поддержка единиц измерения:

  5. Для уровня иерархии может быть включена поддержка единиц измерения (флаг use_conversion_factors).
  6. В запросе данных для слоя (layer) можно указать поле uom:
    {
        "layers": [
            {
                "parameter": "parameter1",
                "measure": "1",
                "dimensions": ["dpu", "month"],
                "uom": 13  // ID единицы измерения (опционально)
            }
        ],
        "filters": { ... }
    }
    

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

  1. Конвертация валют:

    # Пример запроса с конвертацией валют
    data = {
        "layers": [
            {
                "parameter": "sales",
                "measure": "1",
                "dimensions": ["dpu", "month"],
                "currency": 2  # Конвертация в USD
            }
        ],
        "filters": {
            "dpu": ["DPU1"],
            "month": ["2024-01"]
        }
    }
    

  2. Конвертация единиц измерения:

    # Пример запроса с конвертацией единиц измерения
    data = {
        "layers": [
            {
                "parameter": "production",
                "measure": "1",
                "dimensions": ["dpu", "month"],
                "uom": 13  # Конвертация в тонны
            }
        ],
        "filters": {
            "dpu": ["DPU1"],
            "month": ["2024-01"]
        }
    }
    

  3. Комбинированная конвертация:

    # Пример запроса с конвертацией валют и единиц измерения
    data = {
        "layers": [
            {
                "parameter": "sales",
                "measure": "1",
                "dimensions": ["dpu", "month"],
                "currency": 2,  # Конвертация в USD
                "uom": 13      # Конвертация в тонны
            }
        ],
        "filters": {
            "dpu": ["DPU1"],
            "month": ["2024-01"]
        }
    }
    

Особенности реализации

  1. Порядок конвертации:
  2. Сначала применяется конвертация единиц измерения (UOM)
  3. Затем применяется конвертация валют (Currency)

  4. Коэффициенты конвертации:

  5. Для валют: коэффициент берется из модели Currency
  6. Для UOM: коэффициент берется из модели ConversionFactor для конкретного элемента иерархии

  7. Обработка ошибок:

  8. Если указана валюта для меры без поддержки валют, возвращается ошибка
  9. Если указана единица измерения для уровня без поддержки UOM, возвращается ошибка
  10. Если не найден коэффициент конвертации, возвращается ошибка

Подробнее о моделях и настройках см. Конвертации: Валюты и Единицы измерения.

Для пользователей доступна подробная документация по работе с валютами и единицами измерения в разделе Конвертации.

Класс Filter

Класс /src/planiqum/core/filters/libs/filter.py: Filter предоставляет функциональность для работы с фильтрами данных в системе.

Основные методы

get_readable()

Возвращает читаемое представление фильтра с текстовыми названиями элементов и уровней иерархии.

Возвращаемая структура:

[
    {
        'dimension': 'День',
        'level': 1,
        'level_key': 'horizon__day',
        'elements': [
            {
                'id': 165,
                'level': 1,
                'level_key': 'horizon__day',
                'item_str': 'День - 2024-01-01'  # Формат: "Имя уровня - shortname элемента"
            }
        ],
        'isnot': False,
        'group': 'Build no forecast'  # Опционально, если элемент принадлежит группе
    }
]

get_multistring()

Преобразует фильтр в многострочное текстовое представление для отображения пользователям.

Принцип работы: 1. Использует существующий метод get_readable() для получения читаемого представления фильтра 2. Преобразует полученные данные в форматированную многострочную строку 3. Группирует элементы по: - Группам фильтра (если есть поле group) - Измерениям - Уровням иерархии

Формат вывода: - Элементы без группы: • {Измерение}: {Уровень1} - [элементы], {Уровень2} - [элементы] - Элементы с группой:

Группа "название":
  • {Измерение}: {Уровень1} - [элементы]

Использование:

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

# Инициализация из JSON-строки
filter_json = '[{"dimension":"День","level":1,"elements":[{"id":165,"level":1}],"isnot":false}]'
filter_obj = Filter(filter_json)

# Получение многострочного текстового представления
text = filter_obj.get_multistring()
print(text)
# Вывод:
# • День: День - [2024-01-01]

Пример с несколькими уровнями и группами:

filter_json = '''[
    {"dimension":"День","level":1,"elements":[{"id":165,"level":1},{"id":166,"level":1}],"isnot":false},
    {"dimension":"Товар","level":10,"elements":[{"id":100,"level":10},{"id":101,"level":10}],"isnot":false},
    {"dimension":"Товар","level":11,"elements":[{"id":200,"level":11}],"isnot":false},
    {"name":"Клиент","level":36,"elements":[{"level":39,"id":563}],"dimension":"Клиент","group":"Build no forecast"}
]'''

text = Filter(filter_json).get_multistring()
print(text)
# Вывод:
# • День: День - [2024-01-01, 2024-01-02]
# • Товар: Неделя - [2024-08-12, 2024-08-19], Номер недели - [2024-02-05]
# Группа "Build no forecast":
#   • Клиент: Подход к прогнозированию - [no_fcst_client]

Применение в системе

Метод get_multistring() используется для отображения фильтров в Excel-отчетах:

Пример использования в коде: - /src/planiqum/core/parameters/models.py: RevisionCorrection.format_filter — форматирование фильтра корректировки для отображения пользователю - /src/planiqum/core/parameters/libs/revisions_history/corrections.py: build_correction_details_dataframe — построение Excel-отчета с деталями корректировки

Код из RevisionCorrection.format_filter():

def format_filter(self):
    """
    Форматирует фильтр в читаемый вид с учетом групп.
    """
    try:
        if not self.filter_data:
            return None

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

        # Создаем объект Filter и используем его метод get_multistring()
        filter_obj = Filter(self.filter_data)
        return filter_obj.get_multistring()

    except Exception as e:
        return f"Фильтр применен (детали недоступны)"

Код из build_correction_details_dataframe():

# Форматируем filter_data в читаемый вид на лету
filter_data_row = meta_df[meta_df['Ключ'] == 'filter_data']
if not filter_data_row.empty:
    filter_json = filter_data_row['Значение'].iloc[0]
    if filter_json:
        try:
            from planiqum.core.filters.libs.filter import Filter
            filter_obj = Filter(filter_json)
            formatted_text = filter_obj.get_multistring()
            meta_df.loc[filter_data_row.index, 'Значение'] = formatted_text
        except Exception as e:
            meta_df.loc[filter_data_row.index, 'Значение'] = "Фильтр применен (детали недоступны)"

Рекомендации по использованию

  1. Для отображения фильтров пользователям: Используйте get_multistring() вместо сырого JSON
  2. Для хранения в БД: Храните только filter_data в формате JSON, не дублируйте отформатированное представление
  3. Для Excel-отчетов: Форматируйте фильтр на лету при построении отчета
  4. Для обработки старых данных: Метод автоматически обрабатывает фильтры без поля dimension, получая его из уровня иерархии

Особенности реализации

  • Без дополнительных запросов к БД: Метод использует данные из get_readable(), который уже содержит всю необходимую информацию
  • Извлечение имени уровня: Имя уровня извлекается из item_str (часть до " - ")
  • Извлечение shortname элемента: Shortname элемента извлекается из item_str (часть после " - ")
  • Обработка ошибок: В случае ошибки возвращается строка "Фильтр применен (детали недоступны)"