from odoo import models, fields, api import json import textwrap class SurveySurvey(models.Model): _inherit = 'survey.survey' def get_dashboard_data(self): self.ensure_one() # Get completed responses only completed = self.user_input_ids.filtered(lambda u: u.state == 'done') total = len(completed) completion_rate = round((total / max(len(self.user_input_ids), 1)) * 100, 1) if self.user_input_ids else 0 questions_data = [] for q in self.question_and_page_ids: if q.is_page: questions_data.append({'type': 'page', 'id': q.id, 'title': q.title or ''}) continue # Filter lines for this question lines = completed.mapped('user_input_line_ids').filtered(lambda l: l.question_id == q) q_data = { 'type': 'question', 'id': q.id, 'title': q.title or '', 'qtype': q.question_type or '', 'stats': [], 'text_answers': [] } # Choice / Matrix / Scale if q.question_type in ['simple_choice', 'multiple_choice', 'matrix', 'scale']: for ans in q.suggested_answer_ids: count = len(lines.filtered(lambda l: l.suggested_answer_id == ans)) pct = round((count / total * 100) if total else 0, 2) q_data['stats'].append({ 'label': ans.value or '', 'count': count, 'percent': pct }) # Text / Char elif q.question_type in ['text_box', 'char_box']: for line in lines: val = line.value_text_box or line.value_char_box if val: q_data['text_answers'].append(str(val)) # Numerical / Date elif q.question_type in ['numerical_box', 'date', 'datetime']: for line in lines: val = getattr(line, f'value_{q.question_type}', None) if val is not None: q_data['text_answers'].append(str(val)) questions_data.append(q_data) # ✅ Serialize in Python (fixes QWeb KeyError: 'JSON') questions_json = json.dumps(questions_data, default=str, ensure_ascii=False) return { 'survey': self, 'total_responses': total, 'completion_rate': completion_rate, 'questions': questions_data, 'questions_json': questions_json, 'print_url': f'/survey/print/{self.access_token}' if self.access_token else '#' } class SurveyUserInputLine(models.Model): _inherit = 'survey.user_input.line' display_answer = fields.Char( string="Normalized Answer", compute='_compute_display_answer', store=True, index=True ) @api.depends('answer_type', 'value_char_box', 'value_text_box', 'value_numerical_box', 'value_scale', 'value_date', 'value_datetime', 'suggested_answer_id', 'suggested_answer_id.value', 'matrix_row_id', 'matrix_row_id.value') def _compute_display_answer(self): for line in self: answer = '' # 🟢 Choice Questions (simple_choice, multiple_choice, matrix) if line.answer_type == 'suggestion': col_val = line.suggested_answer_id.value if line.suggested_answer_id else '' row_val = line.matrix_row_id.value if line.matrix_row_id else '' if row_val and col_val: # Matrix question: "Row Label: Column Label" answer = f"{row_val}: {col_val}" else: # Simple/Multiple choice: just the selected option answer = col_val # 📝 Text Answers elif line.answer_type == 'char_box': answer = (line.value_char_box or '').strip() elif line.answer_type == 'text_box': # Optional: truncate long text for dashboard readability txt = (line.value_text_box or '').strip() answer = textwrap.shorten(txt, width=100, placeholder=" [...]") if txt else '' # 🔢 Numeric Answers elif line.answer_type == 'numerical_box': answer = str(line.value_numerical_box) if line.value_numerical_box is not None else '' elif line.answer_type == 'scale': answer = str(line.value_scale) if line.value_scale else '' # 📅 Date/Time Answers elif line.answer_type == 'date': answer = fields.Date.to_string(line.value_date) if line.value_date else '' elif line.answer_type == 'datetime': answer = fields.Datetime.to_string( fields.Datetime.context_timestamp(self.env.user, line.value_datetime) ) if line.value_datetime else '' # ⚪ Skipped or Unknown else: answer = 'Skipped' if line.skipped else '' line.display_answer = answer