Membuat Radio Multichoice Menjadi Select di Odoo Survey, Kustomisasi Odoo

Odoo survey modul merupakan modul yang sangat powerful untuk mengumpulkan feedback dan juga form dari pelanggan maupun dari publik. Namun dalam beberapa case perlu dibuatkan kustomisasi salah satunya menjadikan multichoice menjadi select.

Odoo Survey Module Multichoice to Dropdown Select

Dropdown select memiliki kelebihan lebih praktis jika dibandingkan dengan multichoice dengan tetap memiliki fitur untuk show hide question lain.

Dalam case ini memang merupakan permintaan khusus dari customer kami. Hasil akhirnya seperti ini :

Pertama kali inherit model question untuk menambahkan opsi memunculkan radio atau dropdown

inherit model question survey
class CustomSurveyQuestion(models.Model):
    _inherit = 'survey.question'

    radio_as_selection = fields.Boolean('Display Radio as Selection')

Selanjutnya inherit tampilan yang ada di view

<template id="question_simple_choice_inherit" inherit_id="survey.question_simple_choice">
	<xpath expr="//div[@class='row o_survey_form_choice']" position="replace">
		<div class="row o_survey_form_choice"
			t-att-data-name="question.id"
			data-question-type="simple_choice">
			
			<t t-if="question.radio_as_selection == False">
				<!-- Radio Mode -->
				<t t-set="item_idx" t-value="0"/>
				<div t-attf-class="col-lg-12 d-flex flex-wrap">
					<t t-set="has_correct_answer" t-value="scoring_display_correction and any(label.is_correct for label in question.suggested_answer_ids)"/>
					<t t-foreach='question.suggested_answer_ids' t-as='label'>
						<t t-set="item_idx" t-value="label_index"/>
						<t t-set="answer_selected" t-value="answer_line and answer_line.suggested_answer_id.id == label.id"/>
						<t t-set="is_correct" t-value="label.is_correct"/>

						<!--Used for print mode with corrections -->
						<t t-set="answer_class" t-if="not has_correct_answer" t-value="''" />
						<t t-set="answer_class" t-elif="is_correct" t-value="'bg-success'" />
						<t t-set="answer_class" t-elif="not is_correct" t-value="'bg-danger'" />

						<label t-att-for="str(question.id) + '_' + str(label.id)"
							t-att-class="'o_survey_choice_btn me-2 mb-2 py-1 px-3 rounded %s %s' % (answer_class, 'o_survey_selected' if answer_selected else '')">
							<t t-call="survey.survey_selection_key">
								<t t-set="selection_key_class" t-value="'position-relative o_survey_radio_btn float-start d-flex'"/>
							</t>
							<span class="ms-2 text-break" t-field='label.value'/>
							<input t-att-id="str(question.id) + '_' + str(label.id)" type="radio" t-att-value='label.id'
								t-attf-class="o_survey_form_choice_item invisible position-absolute #{'o_survey_form_choice_item_selected' if answer_selected else ''}"
								t-att-name='question.id'
								t-att-checked="'checked' if answer_selected else None"
								t-att-data-selection-key="letters[item_idx] if useKeySelection else ''"/>
							<t t-if="has_correct_answer and answer_selected">
								<!-- While displaying results: change icons to have a check mark for a right answer and a cross for a wrong one -->
								<i t-if="is_correct" class="float-end mt-1 position-relative d-inline fa fa-check-circle"/>
								<i t-else="" class="float-end mt-1 position-relative d-inline fa fa-times-circle"/>
							</t>
							<t t-else="">
								<i class="fa fa-check-circle float-end mt-1 position-relative"></i>
								<i class="fa fa-circle-thin float-end mt-1 position-relative"></i>
							</t>
							<t t-call="survey.question_suggested_value_image"/>
						</label>
					</t>
				</div>
				<div t-if='question.comments_allowed and question.comment_count_as_answer' class="js_comments col-lg-12" >
					<div class="d-flex flex-wrap">
						<label t-att-class="'o_survey_choice_btn form-label me-2 py-1 px-3 rounded %s' % ('o_survey_selected' if comment_line else '')">
							<t t-set="item_idx" t-value="item_idx + 1"/>
							<t t-call="survey.survey_selection_key">
								<t t-set="selection_key_class" t-value="'position-relative o_survey_radio_btn float-start d-flex'"/>
							</t>
							<input type="radio" class="o_survey_form_choice_item o_survey_js_form_other_comment invisible position-absolute" value="-1"
								t-att-name='question.id'
								t-att-checked="comment_line and 'checked' or None"
								t-att-data-selection-key="letters[item_idx] if useKeySelection else ''"/>
							<span class="ms-2" t-out="question.comments_message or default_comments_message" />
							<i class="fa fa-check-circle float-end mt-1 position-relative"></i>
							<i class="fa fa-circle-thin float-end mt-1 position-relative"></i>
						</label>
					</div>
					<div t-attf-class="o_survey_comment_container mt-3 py-0 px-1  #{'d-none' if not comment_line else ''}">
						<textarea type="text" class="form-control o_survey_question_text_box bg-transparent text-dark rounded-0 p-0"
								t-att-disabled="None if comment_line else 'disabled'"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
					</div>
				</div>
				<div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12 o_survey_comment_container mx-1 mt-3 ps-3 pe-4">
					<textarea type="text" class="form-control o_survey_comment o_survey_question_text_box bg-transparent text-dark rounded-0 p-0"
							t-att-placeholder="question.comments_message or default_comments_message if not survey_form_readonly else ''"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
				</div>
			</t>

			<t t-if="question.radio_as_selection == True">
				<!-- Select Mode -->
				<div class="col-lg-12 d-flex flex-wrap">
					<t t-set="has_correct_answer" t-value="scoring_display_correction and any(label.is_correct for label in question.suggested_answer_ids)"/>
					<select t-att-name='question.id' class="form-select w-100 o_survey_form_choice_item" data-question-type="selection" type="radio" >
						<option t-att-value="''" t-if="not answer_line">Select an answer</option>
						<t t-foreach='question.suggested_answer_ids' t-as='label'>
							<t t-set="answer_selected" t-value="answer_line and answer_line.suggested_answer_id.id == label.id"/>
							<t t-set="is_correct" t-value="label.is_correct"/>
							<option t-att-value='label.id'
									t-att-selected="'selected' if answer_selected else None"
									t-att-class="'%s' % ('bg-success' if is_correct else 'bg-danger' if has_correct_answer else '')">
								<span t-field='label.value'/>
							</option>
						</t>
					</select>
				</div>
				<div t-if='question.comments_allowed and question.comment_count_as_answer' class="js_comments col-lg-12 mt-3">
					<textarea type="text" class="form-control o_survey_question_text_box bg-transparent text-dark rounded-0 p-0"
							t-att-placeholder="question.comments_message or default_comments_message if not survey_form_readonly else ''"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
				</div>
				<div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12 o_survey_comment_container mx-1 mt-3 ps-3 pe-4">
					<textarea type="text" class="form-control o_survey_comment o_survey_question_text_box bg-transparent text-dark rounded-0 p-0"
							t-att-placeholder="question.comments_message or default_comments_message if not survey_form_readonly else ''"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
				</div>
			</t>

		</div>
	</xpath>
</template>

Selanjutnya override javascript supaya dropdown select tetap bisa menyimpan data

_prepareSubmitValues: function (formData, params) {
        var self = this;
        formData.forEach(function (value, key) {
            switch (key) {
                case 'csrf_token':
                case 'token':
                case 'page_id':
                case 'question_id':
                    params[key] = value;
                    break;
            }
        });
        // Get all question answers by question type
        var address = {}
        var names = {}
        var matrix = {}
        var self = this;
        this.$('[data-question-type]').each(function () {
            switch ($(this).data('questionType')) {
                case 'text_box':
                case 'char_box':
                case 'numerical_box':
                    params[this.name] = this.value;
                    break;
                case 'date':
                    params = self._prepareSubmitDates(params, this.name, this.value, false);
                    break;
                case 'datetime':
                    params = self._prepareSubmitDates(params, this.name, this.value, true);
                    break;
                case 'simple_choice_radio':
                case 'multiple_choice':
                    params = self._prepareSubmitChoices(params, $(this), $(this).data('name'));
                    break;
                case 'url':
                    params[this.name] = this.value;
                    break;
                case 'email':
                    params[this.name] = this.value;
                    break;
                case 'many2one':
                    params[this.name] = [this.value, $(this).find("option:selected").attr('data-value')];
                    break;
                case 'week':
                    params[this.name] = this.value;
                    break;
                case 'color':
                    params[this.name] = this.value;
                    break;
                case 'time':
                    params[this.name] = this.value;
                    break;
                case 'range':
                    params[this.name] = this.value;
                    break;
                case 'password':
                    params[this.name] = this.value;
                    break;
                case 'month':
                    params[this.name] = this.value
                    break;
                case 'address':
                    address[this.name] = this.value
                    if (this.name.endsWith('pin')){
                        address[this.name.split("-")[0]+'-country'] = self.$el.find(`#${this.name.split("-")[0]+'-country'}`).val(),
                        address[this.name.split("-")[0]+'-state'] = self.$el.find(`#${this.name.split("-")[0]+'-state'}`).val()
                        params[this.name.split("-")[0]] = address
                        address = {}
                        break;
                    }
                    break;
                case 'custom':
                    if (this.name == 'matrix-end'){
                        params[this.id] = matrix}
                    if ($(this).attr('id') === 'select' && this.name){
                       matrix[this.name] = $(this).find("option:selected").attr('data-value')}
                    if ($(this).attr('id') !== 'select' && this.name){
                       matrix[this.name] = this.value
                    }
                case 'matrix':
                    params = self._prepareSubmitAnswersMatrix(params, $(this));
                    break;
                case 'name':
                    names[this.name] = this.value
                    if (this.name.endsWith('last')){
                        params[this.name.split("-")[0]] = names
                        break;
                    }
                    break;
                case 'selection':
                    params[this.name] = this.value
                    break;
                case 'file':
                    if ($(this)[0].files[0]){
                        params[this.name] = [$(this).data('file-name'), $(this)[0].files[0]['name']]
                        break;
                    }
                    break;
                case 'many2many':
                    params[this.name] = self.$el.find(`.${this.name}`).val()
                    break;
                    }
                });
             },

Untuk tetap bisa menggunakan conditional display maka override javascript berikut ini

_onChangeChoiceItem: function (event) {
                //console.log('q', this.options.triggeredQuestionsByAnswer);
                var self = this;
                var $target = $(event.currentTarget);
                var $choiceItemGroup = $target.closest('.o_survey_form_choice');
                var $otherItem = $choiceItemGroup.find('.o_survey_js_form_other_comment');
                var $commentInput = $choiceItemGroup.find('textarea[type="text"]');
 
                if ($otherItem.prop('checked') || $commentInput.hasClass('o_survey_comment')) {
                    $commentInput.enable();
                    $commentInput.closest('.o_survey_comment_container').removeClass('d-none');
                    if ($otherItem.prop('checked')) {
                        $commentInput.focus();
                    }
                } else {
                    $commentInput.val('');
                    $commentInput.closest('.o_survey_comment_container').addClass('d-none');
                    $commentInput.enable(false);
                }
        
                var $matrixBtn = $target.closest('.o_survey_matrix_btn');
                if ($target.attr('type') === 'radio') {
                    var isQuestionComplete = false;
                    if ($matrixBtn.length > 0) {
                        $matrixBtn.closest('tr').find('td').removeClass('o_survey_selected');
                        if ($target.is(':checked')) {
                            $matrixBtn.addClass('o_survey_selected');
                        }
                        if (this.options.questionsLayout === 'page_per_question') {
                            var subQuestionsIds = $matrixBtn.closest('table').data('subQuestions');
                            var completedQuestions = [];
                            subQuestionsIds.forEach(function (id) {
                                if (self.$('tr#' + id).find('input:checked').length !== 0) {
                                    completedQuestions.push(id);
                                }
                            });
                            isQuestionComplete = completedQuestions.length === subQuestionsIds.length;
                        }
                    } else {
                        var previouslySelectedAnswer = $choiceItemGroup.find('label.o_survey_selected');
                        previouslySelectedAnswer.removeClass('o_survey_selected');
        
                        var newlySelectedAnswer = $target.closest('label');
                        if (newlySelectedAnswer.find('input').val() !== previouslySelectedAnswer.find('input').val()) {
                            newlySelectedAnswer.addClass('o_survey_selected');
                            isQuestionComplete = this.options.questionsLayout === 'page_per_question';
                        }
        
                        // Conditional display
                        if (this.options.questionsLayout !== 'page_per_question') {
                            var treatedQuestionIds = [];  // Needed to avoid show (1st 'if') then immediately hide (2nd 'if') question during conditional propagation cascade
                            if (Object.keys(this.options.triggeredQuestionsByAnswer).includes(previouslySelectedAnswer.find('input').val())) {
                                // Hide and clear depending question
                                this.options.triggeredQuestionsByAnswer[previouslySelectedAnswer.find('input').val()].forEach(function (questionId) {
                                    var dependingQuestion = $('.js_question-wrapper#' + questionId);
        
                                    dependingQuestion.addClass('d-none');
                                    self._clearQuestionInputs(dependingQuestion);
        
                                    treatedQuestionIds.push(questionId);
                                });
                                // Remove answer from selected answer
                                self.selectedAnswers.splice(self.selectedAnswers.indexOf(parseInt($target.val())), 1);
                            }
                            if (Object.keys(this.options.triggeredQuestionsByAnswer).includes($target.val())) {
                                console.log('show');
                                // Display depending question
                                this.options.triggeredQuestionsByAnswer[$target.val()].forEach(function (questionId) {
                                    if (!treatedQuestionIds.includes(questionId)) {
                                        var dependingQuestion = $('.js_question-wrapper#' + questionId);
                                        dependingQuestion.removeClass('d-none');
                                    }
                                });
                                // Add answer to selected answer
                                this.selectedAnswers.push(parseInt($target.val()));
                            } else {
                                console.log('tambahan');
                                var obj = this.options.triggeredQuestionsByAnswer;
                                // Mengambil semua elemen option dari $target
                                var options = $target.find('option').map(function() {
                                    return $(this).val(); // Mengambil nilai dari elemen option
                                }).get(); // Mengembalikan hasil sebagai array

                                // Mengecek apakah salah satu nilai option ada di properti objek obj
                                var matchedEntries = options.filter(function(optionValue) {
                                    return obj.hasOwnProperty(optionValue); // Menyaring nilai yang ada sebagai kunci di objek obj
                                });

                                var k;
                                // Menampilkan nilai dari objek obj yang cocok
                                if (matchedEntries.length > 0) {
                                    matchedEntries.forEach(function(key) {
                                        //console.log('Key:', key, 'Value:', obj[key]); // Menampilkan key dan nilai dari objek obj
                                        k = key;
                                    });
                                }

                                if (typeof k !== 'undefined') {
                                    this.options.triggeredQuestionsByAnswer[k].forEach(function (questionId) {
                                        var dependingQuestion = $('.js_question-wrapper#' + questionId);
            
                                        dependingQuestion.addClass('d-none');
                                        self._clearQuestionInputs(dependingQuestion);
            
                                        treatedQuestionIds.push(questionId);
                                    });
                                }
                            }
                        }
                    }
                    // Auto Submit Form
                    var isLastQuestion = this.$('button[value="finish"]').length !== 0;
                    var questionHasComment = $target.closest('.o_survey_form_choice').find('.o_survey_comment').length !== 0
                                                || $target.hasClass('o_survey_js_form_other_comment');
                    if (!isLastQuestion && this.options.usersCanGoBack && isQuestionComplete && !questionHasComment) {
                        this._submitForm({});
                    }
                } else {  // $target.attr('type') === 'checkbox'
                    if ($matrixBtn.length > 0) {
                        $matrixBtn.toggleClass('o_survey_selected', !$matrixBtn.hasClass('o_survey_selected'));
                    } else {
                        var $label = $target.closest('label');
                        $label.toggleClass('o_survey_selected', !$label.hasClass('o_survey_selected'));
        
                        // Conditional display
                        if (this.options.questionsLayout !== 'page_per_question' && Object.keys(this.options.triggeredQuestionsByAnswer).includes($target.val())) {
                            var isInputSelected = $label.hasClass('o_survey_selected');
                            // Hide and clear or display depending question
                            this.options.triggeredQuestionsByAnswer[$target.val()].forEach(function (questionId) {
                                var dependingQuestion = $('.js_question-wrapper#' + questionId);
                                dependingQuestion.toggleClass('d-none', !isInputSelected);
                                if (!isInputSelected) {
                                    self._clearQuestionInputs(dependingQuestion);
                                }
                            });
                            // Add/remove answer to/from selected answer
                            if (!isInputSelected) {
                                self.selectedAnswers.splice(self.selectedAnswers.indexOf(parseInt($target.val())), 1);
                            } else {
                                self.selectedAnswers.push(parseInt($target.val()));
                            }
                        }
                    }
                }
            }

Semoga bermanfaat.


Leave a Comment

Your email address will not be published. Required fields are marked *

1