first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
from . import models
from . import survey
+81
View File
@@ -0,0 +1,81 @@
from odoo import models, fields, api
class SurveyDashboardView(models.Model):
_name = 'survey.dashboard.view'
_description = 'Survey Dashboard View'
_auto = False
_order = 'create_date desc'
_rec_name = 'question_id'
survey_id = fields.Many2one('survey.survey', string='Survey', readonly=True)
user_input_id = fields.Many2one('survey.user_input', string='User Input', readonly=True)
partner_id = fields.Many2one('res.partner', string='Respondent', readonly=True)
question_id = fields.Many2one('survey.question', string='Question', readonly=True)
answer_type = fields.Selection([
('simple_choice', 'Multiple choice: only one answer'),
('multiple_choice', 'Multiple choice: multiple answers allowed'),
('text_box', 'Multiple Lines Text Box'),
('char_box', 'Single Line Text Box'),
('numerical_box', 'Numerical Value'),
('scale', 'Scale'),
('date', 'Date'),
('datetime', 'Datetime'),
('matrix', 'Matrix')
], string='Question Type', readonly=True)
answer_value = fields.Char(string='Answer Value', readonly=True)
response_count = fields.Integer(string='Count', readonly=True)
create_date = fields.Datetime(string='Response Date', readonly=True)
is_done = fields.Boolean(string='Completed', readonly=True)
def init(self):
# Drop view first to allow structural changes
self.env.cr.execute("DROP VIEW IF EXISTS survey_dashboard_view")
self.env.cr.execute("""
CREATE VIEW survey_dashboard_view AS (
SELECT
ROW_NUMBER() OVER () as id,
sui.id as user_input_id,
sui.survey_id as survey_id,
sui.partner_id as partner_id,
sq.id as question_id,
sq.question_type as answer_type,
-- ✅ FIX: Explicitly cast ALL branches to TEXT
CASE
WHEN sq.question_type = 'matrix' THEN
CONCAT(sqa_row.value::text, ': ', sqa_col.value::text)
WHEN sq.question_type IN ('simple_choice', 'multiple_choice') THEN
sqa_col.value::text
ELSE COALESCE(
uls.value_text_box::text,
uls.value_char_box::text,
uls.value_date::text,
uls.value_datetime::text,
uls.value_numerical_box::text,
uls.value_scale::text,
''
)
END as answer_value,
1 as response_count,
sui.create_date as create_date,
(sui.state = 'done') as is_done
FROM survey_user_input sui
LEFT JOIN survey_user_input_line uls ON uls.user_input_id = sui.id
LEFT JOIN survey_question sq ON sq.id = uls.question_id
-- Joins for Multiple Choice and Matrix answers
LEFT JOIN survey_question_answer sqa_col ON uls.suggested_answer_id = sqa_col.id
LEFT JOIN survey_question_answer sqa_row ON uls.matrix_row_id = sqa_row.id
WHERE sui.state IN ('done')
AND sq.is_page = False
AND sq.question_type IS NOT NULL
)
""")
+130
View File
@@ -0,0 +1,130 @@
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