import base64 import io import logging from odoo import models, fields from PIL import Image _logger = logging.getLogger(__name__) class SurveyXlsReport(models.AbstractModel): _name = 'report.survey_excel_export.survey_xls' _inherit = 'report.report_xlsx.abstract' def generate_xlsx_report(self, workbook, data, wizard): # 1. Determine which questions to export all_questions = wizard.survey_id.question_ids if wizard.type_export == 'session': questions = all_questions.filtered(lambda q: q.page_id.is_page) elif wizard.type_export == 'no_session': questions = all_questions.filtered(lambda q: not q.page_id.is_page) else: questions = all_questions.filtered(lambda q: not q.is_page) # 2. Define Headers headers = ['Survey Title', 'Respondent', 'Completion Date', 'Email'] + [q.title for q in questions] sheet = workbook.add_worksheet('Survey Answers') bold = workbook.add_format({'bold': True}) for col, header in enumerate(headers): sheet.write(0, col, header, bold) # 3. Fetch Survey Inputs domain = [('survey_id', '=', wizard.survey_id.id)] if wizard.export_type == 'completed': domain.append(('state', '=', 'done')) if wizard.date_from: domain.append(('create_date', '>=', wizard.date_from)) if wizard.date_to: domain.append(('create_date', '<=', wizard.date_to)) surveys = self.env['survey.user_input'].search(domain) # 4. Write Data row = 1 if wizard.export_type == 'group_by_partner': partners = surveys.mapped('partner_id') or [False] for partner in partners: partner_name = partner.name if partner else "Undefined Partner" sheet.merge_range(row, 0, row, len(headers) - 1, partner_name, bold) row += 1 partner_surveys = surveys.filtered(lambda s: s.partner_id == partner) row = self._write_survey_rows(sheet, partner_surveys, row, headers, questions) else: row = self._write_survey_rows(sheet, surveys, row, headers, questions) def _get_answer_value(self, answer_lines, question): """Retrieve answer value based on Odoo 19 question_type fields.""" if not answer_lines: return "" try: q_type = question.question_type if q_type == 'text_box': return answer_lines.value_text_box or "" elif q_type == 'char_box': return answer_lines.value_char_box or "" elif q_type == 'numerical_box': return str(answer_lines.value_numerical_box) if answer_lines.value_numerical_box is not None else "" elif q_type == 'scale': return str(answer_lines.value_scale) if answer_lines.value_scale else "" elif q_type == 'date': return answer_lines.value_date or "" elif q_type == 'datetime': return answer_lines.value_datetime or "" # ✅ FIXED: Handle simple_choice & multiple_choice properly elif question.question_type in ['simple_choice', 'multiple_choice']: # Join values from suggested answers return ", ".join(answer_lines.suggested_answer_id.mapped('value')) if answer_lines.suggested_answer_id else "" # answer_lines may contain 1 record (simple) or N records (multiple) # .mapped() safely extracts the value from all records selected_values = answer_lines.mapped('value_suggested.value') return ", ".join(filter(None, selected_values)) # ✅ FIXED: Handle matrix answers properly elif q_type == 'matrix': parts = [] for line in answer_lines: row_val = line.matrix_row_id.value if line.matrix_row_id else "" col_val = line.value_suggested.value if line.value_suggested else "" if row_val and col_val: parts.append(f"{row_val}: {col_val}") return ", ".join(parts) else: return "" except Exception as e: _logger.error(f"Error getting answer for '{question.title}': {e}") return "" def _write_survey_rows(self, sheet, surveys, row, headers, questions): DEFAULT_ROW_HEIGHT = 100 DEFAULT_COLUMN_WIDTH = 20 POINTS_TO_PIXELS = 1.33 CHARACTERS_TO_PIXELS = 7 for survey_input in surveys: # Write Metadata sheet.write(row, 0, survey_input.survey_id.title) sheet.write(row, 1, survey_input.partner_id.name or survey_input.display_name) sheet.write(row, 2, survey_input.create_date.strftime('%Y-%m-%d %H:%M:%S') if survey_input.create_date else "") sheet.write(row, 3, survey_input.email or "") # Write Answers for col_offset, question in enumerate(questions, start=4): answer_line = survey_input.user_input_line_ids.filtered( lambda l: l.question_id == question ) value = self._get_answer_value(answer_line, question) # Handle custom file/image question type safely if question.question_type == 'que_sh_file' and value: try: # Decode base64 image data image_bytes = base64.b64decode(value) image_stream = io.BytesIO(image_bytes) img = Image.open(image_stream) # Resize to fit cell MAX_W, MAX_H = 150, 100 img.thumbnail((MAX_W, MAX_H), Image.Resampling.LANCZOS) resized_stream = io.BytesIO() img.save(resized_stream, format='PNG') resized_stream.seek(0) w_px, h_px = img.size cell_w_px = DEFAULT_COLUMN_WIDTH * CHARACTERS_TO_PIXELS cell_h_px = DEFAULT_ROW_HEIGHT * POINTS_TO_PIXELS x_offset = max(0, (cell_w_px - w_px) / 2) y_offset = max(0, (cell_h_px - h_px) / 2) sheet.set_row(row, DEFAULT_ROW_HEIGHT) sheet.set_column(col_offset, col_offset, DEFAULT_COLUMN_WIDTH) sheet.insert_image( row, col_offset, "temp_image.png", {'image_data': resized_stream, 'x_offset': x_offset, 'y_offset': y_offset} ) except Exception as e: sheet.write(row, col_offset, f"Image Error: {str(e)}") else: sheet.write(row, col_offset, value or '') row += 1 return row