This commit is contained in:
2025-07-26 04:41:47 +03:00
parent 019e4f90c7
commit f5cf4c32da
17 changed files with 2386 additions and 931 deletions

127
utils.py
View File

@@ -5,51 +5,44 @@ from datetime import datetime
# Настройка логирования
logger = logging.getLogger(__name__)
# Уровень логирования уже должен быть настроен в app.py или основном модуле
# logger.setLevel(logging.DEBUG) # Можно убрать, если настраивается глобально
# Функция load_temps удалена, так как пресеты загружаются из API RMS
def generate_template_from_preset(preset):
"""
Генерирует один шаблон запроса OLAP на основе пресета,
подставляя плейсхолдеры для дат в соответствующий фильтр.
Генерирует один шаблон запроса OLAP на основе пресета из RMS API.
Функция заменяет существующие фильтры по дате на универсальные плейсхолдеры
Jinja2 (`{{ from_date }}` и `{{ to_date }}`).
Args:
preset (dict): Словарь с пресетом OLAP-отчета из API RMS.
Returns:
dict: Словарь, представляющий шаблон запроса OLAP, готовый для рендеринга.
Возвращает None, если входной preset некорректен.
Raises:
ValueError: Если preset не является словарем или не содержит необходимых ключей.
ValueError: Если пресет некорректен (не словарь или отсутствуют ключи).
Exception: Другие непредвиденные ошибки при обработке.
"""
if not isinstance(preset, dict):
logger.error("Ошибка генерации шаблона: входной 'preset' не является словарем.")
raise ValueError("Preset должен быть словарем.")
if not all(k in preset for k in ["reportType", "groupByRowFields", "aggregateFields", "filters"]):
logger.error(f"Ошибка генерации шаблона: пресет {preset.get('id', 'N/A')} не содержит всех необходимых ключей.")
raise ValueError("Пресет не содержит необходимых ключей (reportType, groupByRowFields, aggregateFields, filters).")
raise ValueError("Пресет должен быть словарем.")
required_keys = ["reportType", "groupByRowFields", "aggregateFields", "filters"]
if not all(k in preset for k in required_keys):
logger.error(f"Ошибка генерации шаблона: пресет {preset.get('id', 'N/A')} не содержит необходимых ключей.")
raise ValueError("Пресет не содержит ключей: reportType, groupByRowFields, aggregateFields, filters.")
try:
# Копируем основные поля из пресета
template = {
"reportType": preset["reportType"],
"groupByRowFields": preset.get("groupByRowFields", []), # Используем get для необязательных полей
"aggregateFields": preset.get("aggregateFields", []),
"filters": preset.get("filters", {}) # Работаем с копией фильтров
}
# Создаем глубокую копию, чтобы не изменять оригинальный объект пресета
template = json.loads(json.dumps(preset))
# --- Обработка фильтров дат ---
# Создаем копию словаря фильтров, чтобы безопасно удалять элементы
current_filters = dict(template.get("filters", {})) # Используем get с default
# Удаляем ненужные для запроса поля, которые приходят из API
template.pop('id', None)
template.pop('name', None)
current_filters = template.get("filters", {})
filters_to_remove = []
date_filter_found_and_modified = False
# Сначала найдем и удалим все существующие фильтры типа DateRange
# Находим и запоминаем все существующие фильтры типа DateRange для удаления
for key, value in current_filters.items():
if isinstance(value, dict) and value.get("filterType") == "DateRange":
filters_to_remove.append(key)
@@ -58,28 +51,23 @@ def generate_template_from_preset(preset):
del current_filters[key]
logger.debug(f"Удален существующий DateRange фильтр '{key}' из пресета {preset.get('id', 'N/A')}.")
# Теперь добавляем правильный фильтр дат в зависимости от типа отчета
# Определяем правильный ключ для фильтра по дате на основе типа отчета
report_type = template["reportType"]
date_filter_key = None
if report_type in ["SALES", "DELIVERIES"]:
# Для отчетов SALES и DELIVERIES используем "OpenDate.Typed"
# См. https://ru.iiko.help/articles/api-documentations/olap-2/a/h3__951638809
# Для отчетов по продажам и доставкам используется "OpenDate.Typed"
date_filter_key = "OpenDate.Typed"
logger.debug(f"Для отчета {report_type} ({preset.get('id', 'N/A')}) будет использован фильтр '{date_filter_key}'.")
current_filters[date_filter_key] = {
"filterType": "DateRange",
"from": "{{ from_date }}",
"to": "{{ to_date }}",
"includeLow": True,
"includeHigh": True
}
date_filter_found_and_modified = True # Считаем, что мы успешно добавили нужный фильтр
elif report_type == "TRANSACTIONS":
# Для отчетов по проводкам (TRANSACTIONS) используем "DateTime.DateTyped"
# См. комментарий пользователя и общие практики iiko API
# Для отчетов по проводкам используется "DateTime.DateTyped"
date_filter_key = "DateTime.DateTyped"
logger.debug(f"Для отчета {report_type} ({preset.get('id', 'N/A')}) будет использован фильтр '{date_filter_key}'.")
else:
logger.warning(
f"Для типа отчета '{report_type}' (пресет {preset.get('id', 'N/A')}) нет стандартного ключа даты. "
f"Фильтр по дате не будет добавлен автоматически."
)
if date_filter_key:
logger.debug(f"Для отчета {report_type} будет использован фильтр '{date_filter_key}'.")
current_filters[date_filter_key] = {
"filterType": "DateRange",
"from": "{{ from_date }}",
@@ -87,73 +75,46 @@ def generate_template_from_preset(preset):
"includeLow": True,
"includeHigh": True
}
date_filter_found_and_modified = True # Считаем, что мы успешно добавили нужный фильтр
else:
# Для ВСЕХ ОСТАЛЬНЫХ типов отчетов:
# Пытаемся найти *любой* ключ, который может содержать дату (логика по умолчанию).
# Это менее надежно, чем явное указание ключей для SALES/DELIVERIES/TRANSACTIONS.
# Если в пресете для других типов отчетов нет стандартного поля даты,
# или оно называется иначе, этот блок может не сработать корректно.
# Мы уже удалили все DateRange фильтры. Если для этого типа отчета
# нужен был какой-то специфический DateRange фильтр, он был удален.
# Это потенциальная проблема, если неизвестные типы отчетов полагаются
# на предопределенные DateRange фильтры с другими ключами.
# Пока оставляем так: если тип отчета неизвестен, DateRange фильтр не добавляется.
logger.warning(f"Для неизвестного типа отчета '{report_type}' ({preset.get('id', 'N/A')}) не удалось автоматически определить стандартный ключ фильтра даты. "
f"Фильтр по дате не будет добавлен автоматически. Если он нужен, пресет должен содержать его с другим filterType или его нужно добавить вручную.")
# В этом случае date_filter_found_and_modified останется False
# Обновляем фильтры в шаблоне
logger.info(f"Шаблон для пресета {preset.get('id', 'N/A')} ('{preset.get('name', '')}') успешно сгенерирован с фильтром даты.")
template["filters"] = current_filters
# Логируем результат
if date_filter_found_and_modified:
logger.info(f"Шаблон для пресета {preset.get('id', 'N/A')} ('{preset.get('name', '')}') успешно сгенерирован с фильтром даты.")
else:
logger.warning(f"Шаблон для пресета {preset.get('id', 'N/A')} ('{preset.get('name', '')}') сгенерирован, но фильтр даты не был добавлен/модифицирован (тип отчета: {report_type}).")
return template
except Exception as e:
logger.error(f"Непредвиденная ошибка при генерации шаблона из пресета {preset.get('id', 'N/A')}: {str(e)}", exc_info=True)
raise # Перевыбрасываем ошибку
raise
def render_temp(template_dict, context):
"""
Рендерит шаблон (представленный словарем) с использованием Jinja2.
Рендерит шаблон (представленный словарем) с использованием Jinja2,
подставляя значения из контекста (например, даты).
Args:
template_dict (dict): Словарь, представляющий шаблон OLAP-запроса.
template_dict (dict): Словарь-шаблон OLAP-запроса.
context (dict): Словарь с переменными для рендеринга (например, {'from_date': '...', 'to_date': '...'}).
Returns:
dict: Словарь с отрендеренным OLAP-запросом.
dict: Словарь с отрендеренным OLAP-запросом, готовый к отправке.
Raises:
Exception: Ошибки при рендеринге или парсинге JSON.
"""
try:
# Преобразуем словарь шаблона в строку JSON для Jinja
# Преобразуем словарь шаблона в строку JSON
template_str = json.dumps(template_dict)
# Рендерим строку с помощью Jinja
# Рендерим строку с помощью Jinja, подставляя переменные из context
rendered_str = Template(template_str).render(context)
# Преобразуем отрендеренную строку обратно в словарь Python
rendered_dict = json.loads(rendered_str)
logger.info('Шаблон OLAP-запроса успешно отрендерен.')
logger.info('Шаблон OLAP-запроса успешно отрендерен с датами.')
return rendered_dict
except Exception as e:
logger.error(f"Ошибка рендеринга шаблона: {str(e)}", exc_info=True)
raise
def get_dates(start_date, end_date):
"""
Проверяет даты на корректность и формат YYYY-MM-DD.
Проверяет и форматирует даты.
Args:
start_date (str): Дата начала в формате 'YYYY-MM-DD'.
@@ -169,8 +130,8 @@ def get_dates(start_date, end_date):
try:
start = datetime.strptime(start_date, date_format)
end = datetime.strptime(end_date, date_format)
except ValueError:
logger.error(f"Некорректный формат дат: start='{start_date}', end='{end_date}'. Ожидается YYYY-MM-DD.")
except (ValueError, TypeError):
logger.error(f"Некорректный формат или тип дат: start='{start_date}', end='{end_date}'. Ожидается YYYY-MM-DD.")
raise ValueError("Некорректный формат даты. Используйте YYYY-MM-DD.")
if start > end: