From a33bda5b6229ac0a9dd7a1ac88d01cd9b7d77222 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Fri, 15 May 2026 17:34:29 +0700 Subject: [PATCH] first commit --- __init__.py | 2 + __manifest__.py | 20 +++++++ __pycache__/__init__.cpython-312.pyc | Bin 0 -> 211 bytes models/__init__.py | 4 ++ models/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 305 bytes .../survey_question.cpython-312.pyc | Bin 0 -> 1659 bytes .../__pycache__/survey_survey.cpython-312.pyc | Bin 0 -> 2009 bytes .../survey_user_input.cpython-312.pyc | Bin 0 -> 4065 bytes models/survey_question.py | 22 ++++++++ models/survey_survey.py | 28 ++++++++++ models/survey_user_input.py | 51 ++++++++++++++++++ views/survey_question_views.xml | 17 ++++++ views/survey_survey_views.xml | 13 +++++ views/survey_user_input_views.xml | 34 ++++++++++++ 14 files changed, 191 insertions(+) create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/survey_question.cpython-312.pyc create mode 100644 models/__pycache__/survey_survey.cpython-312.pyc create mode 100644 models/__pycache__/survey_user_input.cpython-312.pyc create mode 100644 models/survey_question.py create mode 100644 models/survey_survey.py create mode 100644 models/survey_user_input.py create mode 100644 views/survey_question_views.xml create mode 100644 views/survey_survey_views.xml create mode 100644 views/survey_user_input_views.xml diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..a0fdc10 --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..cdec054 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Survey Scoring Extended', + 'version': '1.0', + 'category': 'Marketing/Surveys', + 'summary': 'Extended scoring for scale questions and average scoring formula', + 'description': """ +Extended scoring for scale questions and average scoring formula in surveys. + """, + 'author': 'Suherdy Yacob', + 'depends': ['survey'], + 'data': [ + 'views/survey_survey_views.xml', + 'views/survey_question_views.xml', + 'views/survey_user_input_views.xml', + ], + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..799f1d974436cf4bbd2c7dc9d72b961e18199bda GIT binary patch literal 211 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 literal 0 HcmV?d00001 diff --git a/models/survey_question.py b/models/survey_question.py new file mode 100644 index 0000000..2fa88df --- /dev/null +++ b/models/survey_question.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models + +class SurveyQuestion(models.Model): + _inherit = 'survey.question' + + scale_multiplier = fields.Float("Scale Multiplier", default=1.0, help="Multiply the scale value by this number for scoring.") + + @api.depends('question_type', 'scoring_type', 'answer_date', 'answer_datetime', 'answer_numerical_box', 'suggested_answer_ids.is_correct') + def _compute_is_scored_question(self): + super()._compute_is_scored_question() + for question in self: + if question.scoring_type == 'no_scoring': + question.is_scored_question = False + elif question.question_type == 'scale': + # Allow is_scored_question to be True for scale if set by user + # We don't want super() to force it to False. + # Since super() set it to False, we might want to default it to True if it's an assessment? + if question.survey_id.survey_type == 'assessment': + question.is_scored_question = True + elif question.question_type not in ['simple_choice', 'multiple_choice', 'date', 'datetime', 'numerical_box']: + question.is_scored_question = False diff --git a/models/survey_survey.py b/models/survey_survey.py new file mode 100644 index 0000000..5220f1c --- /dev/null +++ b/models/survey_survey.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models + +class SurveySurvey(models.Model): + _inherit = 'survey.survey' + + scoring_formula = fields.Selection([ + ('total', 'Total Score'), + ('average', 'Average Score'), + ], string='Scoring Formula', default='total', required=True) + + @api.depends( + 'question_and_page_ids', + 'question_and_page_ids.suggested_answer_ids', + 'question_and_page_ids.suggested_answer_ids.answer_score', + 'question_and_page_ids.question_type', + 'question_and_page_ids.scale_max', + 'question_and_page_ids.scale_multiplier', + ) + def _compute_scoring_max_obtainable(self): + super()._compute_scoring_max_obtainable() + for survey in self: + scale_max_obtainable = sum( + (question.scale_max * question.scale_multiplier) + for question in survey.question_ids + if question.question_type == 'scale' and question.is_scored_question + ) + survey.scoring_max_obtainable += scale_max_obtainable diff --git a/models/survey_user_input.py b/models/survey_user_input.py new file mode 100644 index 0000000..cdb44eb --- /dev/null +++ b/models/survey_user_input.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models + +class SurveyUserInput(models.Model): + _inherit = 'survey.user_input' + + scoring_formula = fields.Selection(related='survey_id.scoring_formula') + scoring_average = fields.Float("Average Score", compute='_compute_scoring_values', store=True) + + @api.depends('user_input_line_ids.answer_score', 'user_input_line_ids.question_id', 'predefined_question_ids.answer_score', 'survey_id.scoring_formula') + def _compute_scoring_values(self): + super()._compute_scoring_values() + for user_input in self: + # Re-calculate total_possible_score to include scale questions + scored_questions = user_input.predefined_question_ids.filtered(lambda q: q.is_scored_question) + + # Recalculate max possible score (sum of max points for each scored question) + new_max = 0 + for question in scored_questions: + if question.question_type == 'scale': + new_max += question.scale_max * question.scale_multiplier + elif question.question_type == 'simple_choice': + new_max += max([score for score in question.mapped('suggested_answer_ids.answer_score') if score > 0], default=0) + elif question.question_type == 'multiple_choice': + new_max += sum(score for score in question.mapped('suggested_answer_ids.answer_score') if score > 0) + else: + new_max += question.answer_score + + # Calculate total score obtained + total_score = sum(user_input.user_input_line_ids.mapped('answer_score')) + + # Update fields + user_input.scoring_total = total_score + user_input.scoring_average = total_score / (len(scored_questions) or 1) + + if new_max > 0: + user_input.scoring_percentage = round((total_score / new_max) * 100, 2) + else: + user_input.scoring_percentage = 0 + +class SurveyUserInputLine(models.Model): + _inherit = 'survey.user_input.line' + + @api.depends('answer_type', 'value_text_box', 'value_numerical_box', 'value_date', 'value_datetime', 'value_scale', + 'suggested_answer_id', 'user_input_id') + def _compute_answer_score(self): + super()._compute_answer_score() + for line in self: + if line.answer_type == 'scale' and line.question_id.is_scored_question: + line.answer_score = line.value_scale * line.question_id.scale_multiplier + line.answer_is_correct = True # Scale answers are considered "correct" if they provide points diff --git a/views/survey_question_views.xml b/views/survey_question_views.xml new file mode 100644 index 0000000..dde4fee --- /dev/null +++ b/views/survey_question_views.xml @@ -0,0 +1,17 @@ + + + + survey.question.view.form.inherit.scale.multiplier + survey.question + + + + + + + + scoring_type == 'no_scoring' or question_type not in ['numerical_box', 'date', 'datetime', 'scale'] + + + + diff --git a/views/survey_survey_views.xml b/views/survey_survey_views.xml new file mode 100644 index 0000000..e63d560 --- /dev/null +++ b/views/survey_survey_views.xml @@ -0,0 +1,13 @@ + + + + survey.survey.view.form.inherit.scoring.formula + survey.survey + + + + + + + + diff --git a/views/survey_user_input_views.xml b/views/survey_user_input_views.xml new file mode 100644 index 0000000..86b42bc --- /dev/null +++ b/views/survey_user_input_views.xml @@ -0,0 +1,34 @@ + + + + survey.user_input.view.form.inherit.scoring + survey.user_input + + + + + + + + + + scoring_type == 'no_scoring' or scoring_formula == 'average' + + + scoring_type == 'no_scoring' or scoring_formula == 'average' + + + + + + + + + +