import json import logging from jinja2 import Template from datetime import datetime # Настройка логирования logger = logging.getLogger(__name__) def generate_template_from_preset(preset): """ Генерирует один шаблон запроса OLAP на основе пресета из RMS API. Функция заменяет существующие фильтры по дате на универсальные плейсхолдеры Jinja2 (`{{ from_date }}` и `{{ to_date }}`). Args: preset (dict): Словарь с пресетом OLAP-отчета из API RMS. Returns: dict: Словарь, представляющий шаблон запроса OLAP, готовый для рендеринга. Raises: ValueError: Если пресет некорректен (не словарь или отсутствуют ключи). Exception: Другие непредвиденные ошибки при обработке. """ if not isinstance(preset, dict): logger.error("Ошибка генерации шаблона: входной 'preset' не является словарем.") 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 = json.loads(json.dumps(preset)) # Удаляем ненужные для запроса поля, которые приходят из API template.pop('id', None) template.pop('name', None) current_filters = template.get("filters", {}) filters_to_remove = [] # Находим и запоминаем все существующие фильтры типа DateRange для удаления for key, value in current_filters.items(): if isinstance(value, dict) and value.get("filterType") == "DateRange": filters_to_remove.append(key) for key in filters_to_remove: 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"]: # Для отчетов по продажам и доставкам используется "OpenDate.Typed" date_filter_key = "OpenDate.Typed" elif report_type == "TRANSACTIONS": # Для отчетов по проводкам используется "DateTime.DateTyped" date_filter_key = "DateTime.DateTyped" 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 }}", "to": "{{ to_date }}", "includeLow": True, "includeHigh": True } logger.info(f"Шаблон для пресета {preset.get('id', 'N/A')} ('{preset.get('name', '')}') успешно сгенерирован с фильтром даты.") template["filters"] = current_filters return template except Exception as e: logger.error(f"Непредвиденная ошибка при генерации шаблона из пресета {preset.get('id', 'N/A')}: {str(e)}", exc_info=True) raise def render_temp(template_dict, context): """ Рендерит шаблон (представленный словарем) с использованием Jinja2, подставляя значения из контекста (например, даты). Args: template_dict (dict): Словарь-шаблон OLAP-запроса. context (dict): Словарь с переменными для рендеринга (например, {'from_date': '...', 'to_date': '...'}). Returns: dict: Словарь с отрендеренным OLAP-запросом, готовый к отправке. Raises: Exception: Ошибки при рендеринге или парсинге JSON. """ try: # Преобразуем словарь шаблона в строку JSON template_str = json.dumps(template_dict) # Рендерим строку с помощью Jinja, подставляя переменные из context rendered_str = Template(template_str).render(context) # Преобразуем отрендеренную строку обратно в словарь Python rendered_dict = json.loads(rendered_str) 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): """ Проверяет и форматирует даты. Args: start_date (str): Дата начала в формате 'YYYY-MM-DD'. end_date (str): Дата окончания в формате 'YYYY-MM-DD'. Returns: tuple: Кортеж (start_date, end_date), если даты корректны. Raises: ValueError: Если формат дат некорректен или дата начала позже даты окончания. """ date_format = "%Y-%m-%d" try: start = datetime.strptime(start_date, date_format) end = datetime.strptime(end_date, date_format) except (ValueError, TypeError): logger.error(f"Некорректный формат или тип дат: start='{start_date}', end='{end_date}'. Ожидается YYYY-MM-DD.") raise ValueError("Некорректный формат даты. Используйте YYYY-MM-DD.") if start > end: logger.error(f"Дата начала '{start_date}' не может быть позже даты окончания '{end_date}'.") raise ValueError("Дата начала не может быть позже даты окончания.") return start_date, end_date