diff --git a/routes.py b/routes.py index 0671907..72a4dd0 100644 --- a/routes.py +++ b/routes.py @@ -328,32 +328,103 @@ def render_olap(): if req_module.login(): result = req_module.take_olap(json_body) + # --- НАЧАЛО НОВОЙ УЛУЧШЕННОЙ ЛОГИКИ ОБРАБОТКИ ДАННЫХ --- + if 'data' not in result or not isinstance(result['data'], list): flash(_('Error: Unexpected response format from RMS for report "%(name)s".', name=preset.get('name', report_id)), 'error') + current_app.logger.error(f"Unexpected API response for report {report_id} ('{preset.get('name')}'). Response: {result}") return redirect(url_for('.index')) - data_to_insert = [] - if result['data']: - headers = list(result['data'][0].keys()) - data_to_insert.append(headers) - for item in result['data']: - data_to_insert.append([item.get(h) for h in headers]) + report_data = result['data'] + + # Если отчет пуст, очищаем лист и уведомляем пользователя. + if not report_data: + gs_client.clear_and_write_data(sheet_title, []) + flash(_('Report "%(name)s" returned no data for the selected period. Sheet "%(sheet)s" has been cleared.', name=preset.get('name', report_id), sheet=sheet_title), 'warning') + return redirect(url_for('.index')) + # Здесь будет храниться наш итоговый "плоский" список словарей + processed_data = [] + + # Проверяем структуру отчета: сводный (pivoted) или простой (flat) + first_item = report_data[0] + is_pivoted = isinstance(first_item, dict) and 'row' in first_item and 'cells' in first_item + + if is_pivoted: + current_app.logger.info(f"Processing a pivoted report: {preset.get('name', report_id)}") + # "Разворачиваем" (unpivot) данные в плоский список словарей + for row_item in report_data: + row_values = row_item.get('row', {}) + cells = row_item.get('cells', []) + if not cells: + # Обрабатываем строки, у которых может не быть данных в ячейках + processed_data.append(row_values.copy()) + else: + for cell in cells: + new_flat_row = row_values.copy() + new_flat_row.update(cell.get('col', {})) + new_flat_row.update(cell.get('values', {})) + processed_data.append(new_flat_row) + else: + current_app.logger.info(f"Processing a simple flat report: {preset.get('name', report_id)}") + # Данные уже в виде плоского списка, просто присваиваем + processed_data = [item for item in report_data if isinstance(item, dict)] + + # --- Универсальное формирование заголовков и данных --- + + # 1. Собираем все уникальные ключи из всех строк для гарантии целостности. + all_keys = set() + for row in processed_data: + all_keys.update(row.keys()) + + # 2. Создаем упорядоченный список заголовков для лучшей читаемости. + # Используем поля из пресета для определения логического порядка. + row_group_fields = preset.get('groupByRowFields', []) + col_group_fields = preset.get('groupByColFields', []) + agg_fields = preset.get('aggregateFields', []) + + ordered_headers = [] + # Сначала добавляем известные поля из пресета в логической последовательности. + for field in row_group_fields + col_group_fields + agg_fields: + if field in all_keys: + ordered_headers.append(field) + all_keys.remove(field) + # Добавляем любые другие (неожиданные) поля, отсортировав их по алфавиту. + ordered_headers.extend(sorted(list(all_keys))) + + # 3. Собираем итоговый список списков для Google Sheets, приводя все значения к строкам. + data_to_insert = [ordered_headers] + for row in processed_data: + row_data = [] + for header in ordered_headers: + value_str = str(row.get(header, '')) + if value_str.startswith(('=', '+', '-', '@')): + row_data.append("'" + value_str) + else: + row_data.append(value_str) + # Преобразуем None в пустую строку, а все остальное в строковое представление. + # Это предотвращает потенциальные ошибки типов со стороны Google Sheets API. + data_to_insert.append(row_data) + + gs_client.clear_and_write_data(sheet_title, data_to_insert) - if len(data_to_insert) > 1: - flash(_('Report "%(name)s" data successfully written to sheet "%(sheet)s".', name=preset.get('name', report_id), sheet=sheet_title), 'success') - else: - flash(_('Report "%(name)s" returned no data for the selected period. Sheet "%(sheet)s" has been cleared.', name=preset.get('name', report_id), sheet=sheet_title), 'warning') + rows_count = len(data_to_insert) - 1 + flash(_('Report "%(name)s" data (%(rows)s rows) successfully written to sheet "%(sheet)s".', + name=preset.get('name', report_id), + rows=rows_count, + sheet=sheet_title), 'success') else: flash(_('Error authorizing on RMS server when trying to get a report.'), 'error') except ValueError as ve: flash(_('Data Error: %(error)s', error=str(ve)), 'error') except gspread.exceptions.APIError as api_err: - flash(_('Google API Error accessing sheet "%(sheet)s". Check service account permissions.', sheet=sheet_title), 'error') + flash(_('Google API Error accessing sheet "%(sheet)s". Check service account permissions.', sheet=sheet_title or _('Unknown')), 'error') + current_app.logger.error(f"Google API Error for sheet '{sheet_title}': {api_err}", exc_info=True) except Exception as e: flash(_('An unexpected error occurred: %(error)s', error=str(e)), 'error') + current_app.logger.error(f"Unexpected error in render_olap: {e}", exc_info=True) finally: if req_module and req_module.token: req_module.logout()