1 Commits

Author SHA1 Message Date
07d0af5e7f [ADD] survey_xlsx_expand_multiple_choice, survey_xlsx_extra_fields
Some checks failed
pre-commit / pre-commit (pull_request) Has been cancelled
survey_xlsx_expand_multiple_choice: expand multiple_choice questions into
one Oui/Non column per option, and matrix questions into one column per
row (value = selected option). Relies on the extension hooks added to
survey_xlsx.

survey_xlsx_extra_fields: bridge (auto_install) with survey_extra_fields
that excludes 'file' question types from the XLSX export.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:49:48 +02:00
30 changed files with 1723 additions and 465 deletions

View File

@@ -1,42 +0,0 @@
#!/bin/bash
#
# Run the Python tests of the given Odoo modules.
#
# This script is meant to be executed *inside* an Elabore doodba image
# (docker.0k.io/mirror/odoo:rc_<version>-ELABORE-LIGHT), with a
# PostgreSQL service reachable at $PGHOST. The modules under test are
# read from the current working directory (the checked-out repository),
# which is prepended to the addons path so that the PR's code takes
# precedence over the modules already baked in the image.
#
# Usage: odoo-tests.sh <module1,module2,...>
set -euo pipefail
modules="${1:?usage: odoo-tests.sh <module1,module2,...>}"
export PGHOST=postgres
export PGUSER=odoo
export PGPASSWORD=odoopassword
export PGDATABASE="test_$(basename "$PWD" | tr '.-' '__')"
export WITHOUT_DEMO=all
export DB_FILTER='.*'
# Generate /opt/odoo/auto/odoo.conf from the environment and wait for
# the database to accept connections.
/opt/odoo/common/entrypoint
# Odoo test-tags format: "/module1,/module2" (one leading slash).
tags="/$(echo "$modules" | tr ',' ',/')"
exec odoo \
--config=/opt/odoo/auto/odoo.conf \
--max-cron-threads=0 \
--limit-time-real=10000 \
--workers=0 \
--test-enable \
--stop-after-init \
--addons-path="$PWD,/opt/odoo/auto/addons" \
-d "$PGDATABASE" \
-i "$modules" \
--test-tags "$tags"

View File

@@ -2,6 +2,8 @@ name: pre-commit
on:
pull_request:
branches:
- "18.0*"
jobs:
pre-commit:
@@ -13,10 +15,10 @@ jobs:
python-version: "3.11"
- name: Get python version
run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
- uses: https://gitea.com/actions/cache@v3
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
# - uses: actions/cache@v4
# with:
# path: ~/.cache/pre-commit
# key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
- name: Install pre-commit
run: pip install pre-commit
- name: Run pre-commit

View File

@@ -1,79 +0,0 @@
name: tests
on:
pull_request:
branches:
- "18.0*"
workflow_dispatch:
jobs:
test:
runs-on: odoo-ci
container:
image: docker.0k.io/mirror/odoo:rc_18.0-ELABORE-LIGHT
services:
postgres:
image: docker.0k.io/postgres:17.2.0-myc
env:
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoopassword
POSTGRES_DB: postgres
steps:
# The doodba image ships no Node.js, so actions/checkout@v4 cannot
# run here. The repository is private, so the job token is required.
- name: Checkout
env:
SECRET_GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eu
token="${GITEA_TOKEN:-${GITHUB_TOKEN:-${SECRET_GITEA_TOKEN:-${SECRET_GITHUB_TOKEN:-}}}}"
if [ -z "$token" ]; then
echo "::error::No job token available to clone the repository"
exit 1
fi
server="${GITHUB_SERVER_URL:-https://git.elabore.coop}"
repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is not set}"
ref="${GITHUB_REF:-${GITHUB_SHA:?neither GITHUB_REF nor GITHUB_SHA is set}}"
auth_url="$(printf '%s' "${server%/}/${repo}.git" |
sed -E "s#^https://#https://${token}@#")"
git init -q .
git config --global --add safe.directory "$PWD"
git remote add origin "$auth_url"
git fetch -q --depth=1 origin "$ref"
git checkout -q FETCH_HEAD
- name: Detect changed modules
id: changed
run: |
set -eu
if [ -n "${GITHUB_BASE_REF:-}" ]; then
git fetch -q --depth=1 origin "refs/heads/$GITHUB_BASE_REF"
base_rev="FETCH_HEAD"
scope="diff against $GITHUB_BASE_REF"
else
base_rev=""
scope="all installable modules (manual run)"
fi
if [ -n "$base_rev" ]; then
modules="$(
git diff --name-only "$base_rev" HEAD |
awk -F/ '{print $1}' | sort -u |
while read -r dir; do
[ -f "$dir/__manifest__.py" ] && echo "$dir"
done | paste -sd, -
)"
else
modules="$(
for dir in */; do
dir="${dir%/}"
[ -f "$dir/__manifest__.py" ] && echo "$dir"
done | paste -sd, -
)"
fi
echo "modules=$modules" >> "$GITHUB_OUTPUT"
echo "Modules to test ($scope): ${modules:-<none>}"
- name: Run Odoo tests
if: steps.changed.outputs.modules != ''
run: bash .gitea/scripts/odoo-tests.sh "${{ steps.changed.outputs.modules }}"

161
.gitignore vendored
View File

@@ -1,42 +1,40 @@
*.docx
.~*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
/.venv
/.pytest_cache
/.ruff_cache
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
bin/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
*.eggs
MANIFEST
# Windows installers
*.msi
# Debian packages
*.deb
# Redhat packages
*.rpm
# MacOS packages
*.dmg
*.pkg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
@@ -45,44 +43,123 @@ pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
# Pycharm
.idea
# Eclipse
.settings
# Visual Studio cache/options directory
.vs/
.vscode
# OSX Files
.DS_Store
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Flask stuff:
instance/
.webassets-cache
# Rope
.ropeproject
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# Backup files
*~
*.swp
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# OCA rules
!static/lib/

View File

@@ -22,7 +22,7 @@ exclude: |
(LICENSE.*|COPYING.*)
default_language_version:
python: python3
node: "22.9.0"
node: "16.17.0"
repos:
- repo: local
hooks:
@@ -38,12 +38,8 @@ repos:
entry: found a en.po file
language: fail
files: '[a-zA-Z0-9_]*/i18n/en\.po$'
- repo: https://github.com/sbidoul/whool
rev: v1.3
hooks:
- id: whool-init
- repo: https://github.com/oca/maintainer-tools
rev: b89f767503be6ab2b11e4f50a7557cb20066e667
rev: f9b919b9868143135a9c9cb03021089cabba8223
hooks:
# update the NOT INSTALLABLE ADDONS section above
- id: oca-update-pre-commit-excluded-addons
@@ -53,13 +49,16 @@ repos:
$(git rev-parse --show-toplevel))"'
- id: oca-gen-addon-readme
entry:
bash -c 'oca-gen-addon-readme --addons-dir=. --branch=$(git symbolic-ref
bash -c 'oca-gen-addon-readme
--addons-dir=.
--branch=$(git symbolic-ref
refs/remotes/origin/HEAD | sed "s@^refs/remotes/origin/@@")
--repo-name=$(basename $(git rev-parse --show-toplevel)) --org-name="Elabore"
--if-source-changed --keep-source-digest --convert-fragments-to-markdown'
- id: oca-gen-external-dependencies
--repo-name=$(basename $(git rev-parse --show-toplevel))
--org-name="Elabore"
--if-source-changed --keep-source-digest'
- repo: https://github.com/OCA/odoo-pre-commit-hooks
rev: v0.0.33
rev: v0.1.4
hooks:
- id: oca-checks-odoo-module
- id: oca-checks-po
@@ -78,8 +77,8 @@ repos:
files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$
language: node
additional_dependencies:
- "prettier@3.3.3"
- "@prettier/plugin-xml@3.4.1"
- "prettier@2.7.1"
- "@prettier/plugin-xml@2.2.0"
- repo: local
hooks:
- id: eslint
@@ -92,11 +91,10 @@ repos:
types: [javascript]
language: node
additional_dependencies:
- "eslint@9.12.0"
- "eslint-plugin-jsdoc@50.3.1"
- "globals@16.0.0"
- "eslint@8.24.0"
- "eslint-plugin-jsdoc@"
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
# exclude autogenerated files
@@ -117,8 +115,23 @@ repos:
- id: check-xml
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: https://github.com/PyCQA/docformatter
rev: v1.7.7
hooks:
- id: docformatter
args: [
"--in-place", # modify the files
"--recursive", # run on all the files
"--wrap-summaries",
"88", # max length of 1st line
"--wrap-descriptions",
"88", # max length of other lines
"--pre-summary-newline", # new line before a long summary
"--make-summary-multi-line", # force summary on multilines
]
additional_dependencies: ["tomli"] # if Python <3.11
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.8
rev: v0.12.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]

View File

@@ -25,25 +25,19 @@ disable=all
enable=anomalous-backslash-in-string,
api-one-deprecated,
api-one-multi-together,
assignment-from-none,
attribute-deprecated,
class-camelcase,
dangerous-default-value,
dangerous-view-replace-wo-priority,
development-status-allowed,
duplicate-id-csv,
duplicate-key,
duplicate-xml-fields,
duplicate-xml-record-id,
eval-referenced,
incoherent-interpreter-exec-perm,
openerp-exception-warning,
redundant-modulename-xml,
relative-import,
rst-syntax-error,
wrong-tabs-instead-of-spaces,
xml-syntax-error,
assignment-from-none,
attribute-deprecated,
dangerous-default-value,
development-status-allowed,
duplicate-key,
eval-used,
incoherent-interpreter-exec-perm,
license-allowed,
manifest-author-string,
manifest-deprecated-key,
@@ -54,68 +48,73 @@ enable=anomalous-backslash-in-string,
method-inverse,
method-required-super,
method-search,
openerp-exception-warning,
pointless-statement,
pointless-string-statement,
print-used,
redundant-keyword-arg,
redundant-modulename-xml,
reimported,
relative-import,
return-in-init,
rst-syntax-error,
sql-injection,
too-few-format-args,
translation-field,
translation-required,
unreachable,
use-vim-comment,
wrong-tabs-instead-of-spaces,
xml-syntax-error,
attribute-string-redundant,
character-not-valid-in-resource-link,
consider-merging-classes-inherited,
context-overridden,
create-user-wo-reset-password,
dangerous-filter-wo-user,
dangerous-qweb-replace-wo-priority,
deprecated-data-xml-node,
deprecated-openerp-xml-node,
duplicate-po-message-definition,
file-not-used,
missing-newline-extrafiles,
old-api7-method-defined,
po-msgstr-variables,
po-syntax-error,
str-format-used,
unnecessary-utf8-coding-comment,
xml-attribute-translatable,
xml-deprecated-qweb-directive,
xml-deprecated-tree-attribute,
attribute-string-redundant,
consider-merging-classes-inherited,
context-overridden,
except-pass,
file-not-used,
invalid-commit,
manifest-maintainers-list,
missing-newline-extrafiles,
missing-readme,
missing-return,
odoo-addons-relative-import,
old-api7-method-defined,
po-msgstr-variables,
po-syntax-error,
renamed-field-parameter,
resource-not-exist,
str-format-used,
test-folder-imported,
translation-contains-variable,
translation-positional-used,
website-manifest-key-not-valid-uri,
external-request-timeout,
missing-manifest-dependency,
too-complex,,
create-user-wo-reset-password,
dangerous-filter-wo-user,
file-not-used,
missing-newline-extrafiles,
no-utf8-coding-comment,
old-api7-method-defined,
unnecessary-utf8-coding-comment,
website-manifest-key-not-valid-uri,
xml-attribute-translatable,
xml-deprecated-qweb-directive,
xml-deprecated-tree-attribute,
external-request-timeout,
# messages that do not cause the lint step to fail
consider-merging-classes-inherited,
create-user-wo-reset-password,
dangerous-filter-wo-user,
deprecated-module,
file-not-used,
invalid-commit,
missing-manifest-dependency,
missing-newline-extrafiles,
missing-readme,
no-utf8-coding-comment,
odoo-addons-relative-import,
old-api7-method-defined,
redefined-builtin,
manifest-external-assets
too-complex,
unnecessary-utf8-coding-comment
[REPORTS]

View File

@@ -17,25 +17,19 @@ disable=all
enable=anomalous-backslash-in-string,
api-one-deprecated,
api-one-multi-together,
assignment-from-none,
attribute-deprecated,
class-camelcase,
dangerous-default-value,
dangerous-view-replace-wo-priority,
development-status-allowed,
duplicate-id-csv,
duplicate-key,
duplicate-xml-fields,
duplicate-xml-record-id,
eval-referenced,
incoherent-interpreter-exec-perm,
openerp-exception-warning,
redundant-modulename-xml,
relative-import,
rst-syntax-error,
wrong-tabs-instead-of-spaces,
xml-syntax-error,
assignment-from-none,
attribute-deprecated,
dangerous-default-value,
development-status-allowed,
duplicate-key,
eval-used,
incoherent-interpreter-exec-perm,
license-allowed,
manifest-author-string,
manifest-deprecated-key,
@@ -46,50 +40,56 @@ enable=anomalous-backslash-in-string,
method-inverse,
method-required-super,
method-search,
openerp-exception-warning,
pointless-statement,
pointless-string-statement,
print-used,
redundant-keyword-arg,
redundant-modulename-xml,
reimported,
relative-import,
return-in-init,
rst-syntax-error,
sql-injection,
too-few-format-args,
translation-field,
translation-required,
unreachable,
use-vim-comment,
wrong-tabs-instead-of-spaces,
xml-syntax-error,
attribute-string-redundant,
character-not-valid-in-resource-link,
consider-merging-classes-inherited,
context-overridden,
create-user-wo-reset-password,
dangerous-filter-wo-user,
dangerous-qweb-replace-wo-priority,
deprecated-data-xml-node,
deprecated-openerp-xml-node,
duplicate-po-message-definition,
file-not-used,
missing-newline-extrafiles,
old-api7-method-defined,
po-msgstr-variables,
po-syntax-error,
str-format-used,
unnecessary-utf8-coding-comment,
xml-attribute-translatable,
xml-deprecated-qweb-directive,
xml-deprecated-tree-attribute,
attribute-string-redundant,
consider-merging-classes-inherited,
context-overridden,
except-pass,
file-not-used,
invalid-commit,
manifest-maintainers-list,
missing-newline-extrafiles,
missing-readme,
missing-return,
odoo-addons-relative-import,
old-api7-method-defined,
po-msgstr-variables,
po-syntax-error,
renamed-field-parameter,
resource-not-exist,
str-format-used,
test-folder-imported,
translation-contains-variable,
translation-positional-used,
unnecessary-utf8-coding-comment,
website-manifest-key-not-valid-uri,
xml-attribute-translatable,
xml-deprecated-qweb-directive,
xml-deprecated-tree-attribute,
external-request-timeout
[REPORTS]

View File

@@ -1,205 +0,0 @@
var globals = require('globals');
jsdoc = require("eslint-plugin-jsdoc");
const config = [{
plugins: {
jsdoc,
},
languageOptions: {
globals: {
_: "readonly",
$: "readonly",
fuzzy: "readonly",
jQuery: "readonly",
moment: "readonly",
odoo: "readonly",
openerp: "readonly",
owl: "readonly",
luxon: "readonly",
QUnit: "readonly",
...globals.browser,
},
ecmaVersion: 2024,
sourceType: "script",
},
rules: {
"accessor-pairs": "warn",
"array-callback-return": "warn",
"callback-return": "warn",
"capitalized-comments": ["warn", "always", {
ignoreConsecutiveComments: true,
ignoreInlineComments: true,
}],
complexity: ["warn", 15],
"constructor-super": "warn",
"dot-notation": "warn",
eqeqeq: "warn",
"global-require": "warn",
"handle-callback-err": "warn",
"id-blacklist": "warn",
"id-match": "warn",
"init-declarations": "error",
"max-depth": "warn",
"max-nested-callbacks": "warn",
"max-statements-per-line": "warn",
"no-alert": "warn",
"no-array-constructor": "warn",
"no-caller": "warn",
"no-case-declarations": "warn",
"no-class-assign": "warn",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-condition": "warn",
"no-control-regex": "warn",
"no-debugger": "error",
"no-delete-var": "warn",
"no-div-regex": "warn",
"no-dupe-args": "error",
"no-dupe-class-members": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-duplicate-imports": "error",
"no-else-return": "warn",
"no-empty-character-class": "warn",
"no-empty-function": "error",
"no-empty-pattern": "error",
"no-empty": "warn",
"no-eq-null": "error",
"no-eval": "error",
"no-ex-assign": "error",
"no-extend-native": "warn",
"no-extra-bind": "warn",
"no-extra-boolean-cast": "warn",
"no-extra-label": "warn",
"no-fallthrough": "warn",
"no-func-assign": "error",
"no-global-assign": "error",
"no-implicit-coercion": ["warn", {
allow: ["~"],
}],
"no-implicit-globals": "warn",
"no-implied-eval": "warn",
"no-inline-comments": "warn",
"no-inner-declarations": "warn",
"no-invalid-regexp": "warn",
"no-irregular-whitespace": "warn",
"no-iterator": "warn",
"no-label-var": "warn",
"no-labels": "warn",
"no-lone-blocks": "warn",
"no-lonely-if": "error",
"no-mixed-requires": "error",
"no-multi-str": "warn",
"no-native-reassign": "error",
"no-negated-condition": "warn",
"no-negated-in-lhs": "error",
"no-new-func": "warn",
"no-new-object": "warn",
"no-new-require": "warn",
"no-new-symbol": "warn",
"no-new-wrappers": "warn",
"no-new": "warn",
"no-obj-calls": "warn",
"no-octal-escape": "warn",
"no-octal": "warn",
"no-param-reassign": "warn",
"no-path-concat": "warn",
"no-process-env": "warn",
"no-process-exit": "warn",
"no-proto": "warn",
"no-prototype-builtins": "warn",
"no-redeclare": "warn",
"no-regex-spaces": "warn",
"no-restricted-globals": "warn",
"no-restricted-imports": "warn",
"no-restricted-modules": "warn",
"no-restricted-syntax": "warn",
"no-return-assign": "error",
"no-script-url": "warn",
"no-self-assign": "warn",
"no-self-compare": "warn",
"no-sequences": "warn",
"no-shadow-restricted-names": "warn",
"no-shadow": "warn",
"no-sparse-arrays": "warn",
"no-sync": "warn",
"no-this-before-super": "warn",
"no-throw-literal": "warn",
"no-undef-init": "warn",
"no-undef": "error",
"no-unmodified-loop-condition": "warn",
"no-unneeded-ternary": "error",
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unused-expressions": "error",
"no-unused-labels": "error",
"no-unused-vars": "error",
"no-use-before-define": "error",
"no-useless-call": "warn",
"no-useless-computed-key": "warn",
"no-useless-concat": "warn",
"no-useless-constructor": "warn",
"no-useless-escape": "warn",
"no-useless-rename": "warn",
"no-void": "warn",
"no-with": "warn",
"operator-assignment": ["error", "always"],
"prefer-const": "warn",
radix: "warn",
"require-yield": "warn",
"sort-imports": "warn",
"spaced-comment": ["error", "always"],
strict: ["error", "function"],
"use-isnan": "error",
"jsdoc/check-tag-names": "warn",
"jsdoc/check-types": "warn",
"jsdoc/require-param-description": "off",
"jsdoc/require-return": "off",
"jsdoc/require-return-description": "off",
"jsdoc/require-return-type": "off",
"valid-typeof": "warn",
yoda: "warn",
},
settings: {
jsdoc: {
tagNamePreference: {
arg: "param",
argument: "param",
augments: "extends",
constructor: "class",
exception: "throws",
func: "function",
method: "function",
prop: "property",
return: "returns",
virtual: "abstract",
yield: "yields",
},
preferredTypes: {
array: "Array",
bool: "Boolean",
boolean: "Boolean",
number: "Number",
object: "Object",
str: "String",
string: "String",
},
},
},
}, {
files: ["**/*.esm.js", "**/*test.js"],
languageOptions: {
ecmaVersion: 2024,
sourceType: "module",
},
}];
module.exports = config

View File

@@ -1,14 +0,0 @@
/** @type {import('prettier').Config} */
const config = {
// https://github.com/prettier/prettier/issues/15388#issuecomment-1717746872
plugins: [require.resolve("@prettier/plugin-xml")],
bracketSpacing: false,
printWidth: 88,
proseWrap: "always",
semi: true,
trailingComma: "es5",
xmlWhitespaceSensitivity: "preserve",
};
module.exports = config;

View File

@@ -0,0 +1,109 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
====================================
Survey XLSX - Expand Multiple Choice
====================================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:2ec53fabb2863ebd536f204a7cb4fa4833a634a5711a9e325ed64f50a4c3c4b6
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/gitea-Elabore%2Fsurvey--tools-lightgray.png
:target: https://git.elabore.coop/Elabore/survey-tools/tree/18.0/survey_xlsx_expand_multiple_choice
:alt: Elabore/survey-tools
|badge1| |badge2| |badge3|
This module improves the **Survey Results XLSX export** provided by
``survey_xlsx`` for questions that can hold several answers.
By default such questions are exported as a single column containing every
selected value joined together, which is hard to analyse in a spreadsheet.
This module splits them into dedicated columns:
* **Multiple choice** questions (*multiple answers allowed*): one column per
possible answer, with ``Oui`` / ``Non`` as value.
* **Matrix** questions: one column per matrix row, with the selected option
as value.
.. warning::
This module relies on report extension hooks that are **not part of the
standard** ``survey_xlsx`` yet. They are introduced by this pull request:
https://github.com/elabore-coop/survey/pull/1
You must run a ``survey_xlsx`` that includes these hooks (the PR branch,
until it is merged upstream). Installed against a plain ``survey_xlsx``,
this module installs without error but the export **silently falls back**
to the default one-column-per-question behaviour.
**Table of contents**
.. contents::
:local:
Usage
=====
Export the results of a survey as usual:
#. Go to *Surveys* and open a survey.
#. Print the *Survey Results XLSX* report.
In the generated spreadsheet:
* A multiple choice question ``Favorite colors`` with options *Red*, *Green*
and *Blue* produces three columns ``Favorite colors / Red``,
``Favorite colors / Green`` and ``Favorite colors / Blue``, each containing
``Oui`` or ``Non``.
* A matrix question ``Satisfaction`` with rows *Dashboards* and *Customer
relationship* produces two columns ``Satisfaction / Dashboards`` and
``Satisfaction / Customer relationship``, each containing the selected
option (e.g. *Not satisfied at all*).
Other question types keep their standard single-column export.
Bug Tracker
===========
Bugs are tracked on `Gitea Issues <https://git.elabore.coop/Elabore/survey-tools/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://git.elabore.coop/Elabore/survey-tools/issues/new?body=module:%20survey_xlsx_expand_multiple_choice%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Elabore
Contributors
~~~~~~~~~~~~
* `Elabore <https://www.elabore.coop>`_
* Quentin Mondot
Maintainers
~~~~~~~~~~~
This module is part of the `Elabore/survey-tools <https://git.elabore.coop/Elabore/survey-tools/tree/18.0/survey_xlsx_expand_multiple_choice>`_ project on git.elabore.coop.
You are welcome to contribute.

View File

@@ -0,0 +1,3 @@
# Copyright 2025 Elabore
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import report

View File

@@ -0,0 +1,16 @@
# Copyright 2025 Elabore
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Survey XLSX - Expand Multiple Choice",
"summary": """
Expands multiple_choice questions into one Oui/Non column per option, and
matrix questions into one column per row (value = selected option)""",
"version": "18.0.1.0.0",
"license": "AGPL-3",
"installable": True,
"application": False,
"author": "Elabore",
"website": "https://elabore.coop",
"depends": ["survey_xlsx"], # WARNING : besoin des hooks créés dans cette PR pour fonctionner : https://github.com/elabore-coop/survey/pull/1
}

View File

@@ -0,0 +1,3 @@
* `Elabore <https://www.elabore.coop>`_
* Quentin Mondot

View File

@@ -0,0 +1,23 @@
This module improves the **Survey Results XLSX export** provided by
``survey_xlsx`` for questions that can hold several answers.
By default such questions are exported as a single column containing every
selected value joined together, which is hard to analyse in a spreadsheet.
This module splits them into dedicated columns:
* **Multiple choice** questions (*multiple answers allowed*): one column per
possible answer, with ``Oui`` / ``Non`` as value.
* **Matrix** questions: one column per matrix row, with the selected option
as value.
.. warning::
This module relies on report extension hooks that are **not part of the
standard** ``survey_xlsx`` yet. They are introduced by this pull request:
https://github.com/elabore-coop/survey/pull/1
You must run a ``survey_xlsx`` that includes these hooks (the PR branch,
until it is merged upstream). Installed against a plain ``survey_xlsx``,
this module installs without error but the export **silently falls back**
to the default one-column-per-question behaviour.

View File

@@ -0,0 +1,17 @@
Export the results of a survey as usual:
#. Go to *Surveys* and open a survey.
#. Print the *Survey Results XLSX* report.
In the generated spreadsheet:
* A multiple choice question ``Favorite colors`` with options *Red*, *Green*
and *Blue* produces three columns ``Favorite colors / Red``,
``Favorite colors / Green`` and ``Favorite colors / Blue``, each containing
``Oui`` or ``Non``.
* A matrix question ``Satisfaction`` with rows *Dashboards* and *Customer
relationship* produces two columns ``Satisfaction / Dashboards`` and
``Satisfaction / Customer relationship``, each containing the selected
option (e.g. *Not satisfied at all*).
Other question types keep their standard single-column export.

View File

@@ -0,0 +1,3 @@
# Copyright 2025 Elabore
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import report_survey_xlsx

View File

@@ -0,0 +1,70 @@
# Copyright 2025 Elabore
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models
class ReportSurveyXlsx(models.AbstractModel):
_inherit = "report.survey.xlsx"
def _write_question_header(self, sheet, question, cols, bold):
if question.question_type == "multiple_choice":
for answer in question.suggested_answer_ids:
col_key = f"question_{question.id}_answer_{answer.id}"
sheet.write(
0,
cols[col_key],
f"{question.title} / {answer.value}",
bold,
)
return
if question.question_type == "matrix":
for row in question.matrix_row_ids:
col_key = f"question_{question.id}_row_{row.id}"
sheet.write(
0,
cols[col_key],
f"{question.title} / {row.value}",
bold,
)
return
return super()._write_question_header(sheet, question, cols, bold)
def _process_user_answer(self, data, user_input_id, user_answer, cols):
question = user_answer.question_id
if question.question_type == "multiple_choice":
if user_answer.skipped:
return
col_key = f"question_{question.id}_answer_{user_answer.suggested_answer_id.id}"
if col_key not in cols:
return
data[user_input_id][cols[col_key]] = ["Oui"]
return
if question.question_type == "matrix":
if user_answer.skipped:
return
col_key = f"question_{question.id}_row_{user_answer.matrix_row_id.id}"
if col_key not in cols:
return
data[user_input_id][cols[col_key]].append(
user_answer.suggested_answer_id.value
)
return
return super()._process_user_answer(data, user_input_id, user_answer, cols)
def _post_process_user_input(self, data, user_input, cols):
super()._post_process_user_input(data, user_input, cols)
answered_mc = set()
for line in user_input.user_input_line_ids:
if line.question_id.question_type == "multiple_choice" and not line.skipped:
answered_mc.add(line.question_id.id)
for question in user_input.survey_id.question_ids:
if question.question_type != "multiple_choice":
continue
if question.id not in answered_mc:
continue
for answer in question.suggested_answer_ids:
col_key = f"question_{question.id}_answer_{answer.id}"
if col_key in cols:
col_idx = cols[col_key]
if col_idx not in data[user_input.id]:
data[user_input.id][col_idx] = ["Non"]

View File

@@ -0,0 +1,466 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="survey-xlsx-expand-multiple-choice">
<h1>Survey XLSX - Expand Multiple Choice</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:2ec53fabb2863ebd536f204a7cb4fa4833a634a5711a9e325ed64f50a4c3c4b6
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/elabore-coop/survey-tools/tree/18.0/survey_xlsx_expand_multiple_choice"><img alt="elabore-coop/survey-tools" src="https://img.shields.io/badge/github-elabore--coop%2Fsurvey--tools-lightgray.png?logo=github" /></a></p>
<p>This module improves the <strong>Survey Results XLSX export</strong> provided by
<tt class="docutils literal">survey_xlsx</tt> for questions that can hold several answers.</p>
<p>By default such questions are exported as a single column containing every
selected value joined together, which is hard to analyse in a spreadsheet.
This module splits them into dedicated columns:</p>
<ul class="simple">
<li><strong>Multiple choice</strong> questions (<em>multiple answers allowed</em>): one column per
possible answer, with <tt class="docutils literal">Oui</tt> / <tt class="docutils literal">Non</tt> as value.</li>
<li><strong>Matrix</strong> questions: one column per matrix row, with the selected option
as value.</li>
</ul>
<div class="admonition warning">
<p class="first admonition-title">Warning</p>
<p>This module relies on report extension hooks that are <strong>not part of the
standard</strong> <tt class="docutils literal">survey_xlsx</tt> yet. They are introduced by this pull request:</p>
<p><a class="reference external" href="https://github.com/elabore-coop/survey/pull/1">https://github.com/elabore-coop/survey/pull/1</a></p>
<p class="last">You must run a <tt class="docutils literal">survey_xlsx</tt> that includes these hooks (the PR branch,
until it is merged upstream). Installed against a plain <tt class="docutils literal">survey_xlsx</tt>,
this module installs without error but the export <strong>silently falls back</strong>
to the default one-column-per-question behaviour.</p>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<p>Export the results of a survey as usual:</p>
<ol class="arabic simple">
<li>Go to <em>Surveys</em> and open a survey.</li>
<li>Print the <em>Survey Results XLSX</em> report.</li>
</ol>
<p>In the generated spreadsheet:</p>
<ul class="simple">
<li>A multiple choice question <tt class="docutils literal">Favorite colors</tt> with options <em>Red</em>, <em>Green</em>
and <em>Blue</em> produces three columns <tt class="docutils literal">Favorite colors / Red</tt>,
<tt class="docutils literal">Favorite colors / Green</tt> and <tt class="docutils literal">Favorite colors / Blue</tt>, each containing
<tt class="docutils literal">Oui</tt> or <tt class="docutils literal">Non</tt>.</li>
<li>A matrix question <tt class="docutils literal">Satisfaction</tt> with rows <em>Dashboards</em> and <em>Customer
relationship</em> produces two columns <tt class="docutils literal">Satisfaction / Dashboards</tt> and
<tt class="docutils literal">Satisfaction / Customer relationship</tt>, each containing the selected
option (e.g. <em>Not satisfied at all</em>).</li>
</ul>
<p>Other question types keep their standard single-column export.</p>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/elabore-coop/survey-tools/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/elabore-coop/survey-tools/issues/new?body=module:%20survey_xlsx_expand_multiple_choice%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-3">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-4">Authors</a></h3>
<ul class="simple">
<li>Elabore</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
<ul class="simple">
<li><a class="reference external" href="https://www.elabore.coop">Elabore</a><ul>
<li>Quentin Mondot</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h3>
<p>This module is part of the <a class="reference external" href="https://github.com/elabore-coop/survey-tools/tree/18.0/survey_xlsx_expand_multiple_choice">elabore-coop/survey-tools</a> project on GitHub.</p>
<p>You are welcome to contribute.</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,3 @@
# Copyright 2025 Elabore
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_report

View File

@@ -0,0 +1,148 @@
# Copyright 2025 Elabore
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import io
import openpyxl
from odoo.addons.survey.tests import common
class TestExpandMultipleChoice(common.TestSurveyCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.mc_question = cls._add_question(
cls,
page=cls.page_0,
name="Favorite colors",
qtype="multiple_choice",
labels=[
{"value": "Red"},
{"value": "Green"},
{"value": "Blue"},
],
survey_id=cls.survey.id,
sequence=10,
)
cls.answer_red = cls.mc_question.suggested_answer_ids[0]
cls.answer_green = cls.mc_question.suggested_answer_ids[1]
cls.answer_blue = cls.mc_question.suggested_answer_ids[2]
# Input 1: selects Red and Green
input1 = cls._add_answer(cls, cls.survey, cls.survey_manager.partner_id)
cls._add_answer_line(cls, cls.mc_question, input1, cls.answer_red.id)
cls._add_answer_line(cls, cls.mc_question, input1, cls.answer_green.id)
input1._mark_done()
# Input 2: selects Blue only
input2 = cls._add_answer(cls, cls.survey, False, email="test2@example.com")
cls._add_answer_line(cls, cls.mc_question, input2, cls.answer_blue.id)
input2._mark_done()
# Matrix question (one choice per row): satisfaction grid
cls.matrix_question = cls._add_question(
cls,
page=cls.page_0,
name="Satisfaction",
qtype="matrix",
matrix_subtype="simple",
labels=[
{"value": "Pas du tout satisfait"},
{"value": "Satisfait"},
],
labels_2=[
{"value": "Tableaux de bord"},
{"value": "Relation client"},
],
survey_id=cls.survey.id,
sequence=20,
)
cls.col_unhappy = cls.matrix_question.suggested_answer_ids[0]
cls.col_happy = cls.matrix_question.suggested_answer_ids[1]
cls.row_dashboard = cls.matrix_question.matrix_row_ids[0]
cls.row_client = cls.matrix_question.matrix_row_ids[1]
# input1 answers the matrix: dashboard -> unhappy, client -> happy
cls._add_answer_line(
cls,
cls.matrix_question,
input1,
cls.col_unhappy.id,
answer_value_row=cls.row_dashboard.id,
)
cls._add_answer_line(
cls,
cls.matrix_question,
input1,
cls.col_happy.id,
answer_value_row=cls.row_client.id,
)
def _get_sheet(self):
report = self.env.ref("survey_xlsx.report_survey_xlsx")
rep = self.env["ir.actions.report"]._render(report, self.survey.ids, {})
wb = openpyxl.load_workbook(io.BytesIO(rep[0]))
return wb.worksheets[0]
def _find_col(self, sheet, header):
for col in range(1, sheet.max_column + 1):
if sheet.cell(1, col).value == header:
return col
return None
def test_headers(self):
sheet = self._get_sheet()
self.assertIsNotNone(
self._find_col(sheet, "Favorite colors / Red")
)
self.assertIsNotNone(
self._find_col(sheet, "Favorite colors / Green")
)
self.assertIsNotNone(
self._find_col(sheet, "Favorite colors / Blue")
)
def test_values(self):
sheet = self._get_sheet()
col_red = self._find_col(sheet, "Favorite colors / Red")
col_green = self._find_col(sheet, "Favorite colors / Green")
col_blue = self._find_col(sheet, "Favorite colors / Blue")
# Collect rows by (red, green, blue) values
rows = set()
for row in range(2, sheet.max_row + 1):
rows.add((
sheet.cell(row, col_red).value,
sheet.cell(row, col_green).value,
sheet.cell(row, col_blue).value,
))
self.assertIn(("Oui", "Oui", "Non"), rows)
self.assertIn(("Non", "Non", "Oui"), rows)
def test_matrix_headers(self):
sheet = self._get_sheet()
self.assertIsNotNone(
self._find_col(sheet, "Satisfaction / Tableaux de bord")
)
self.assertIsNotNone(
self._find_col(sheet, "Satisfaction / Relation client")
)
# No per-column expansion for matrices
self.assertIsNone(
self._find_col(sheet, "Satisfaction / Pas du tout satisfait")
)
def test_matrix_values(self):
sheet = self._get_sheet()
col_dashboard = self._find_col(sheet, "Satisfaction / Tableaux de bord")
col_client = self._find_col(sheet, "Satisfaction / Relation client")
values = set()
for row in range(2, sheet.max_row + 1):
values.add((
sheet.cell(row, col_dashboard).value,
sheet.cell(row, col_client).value,
))
self.assertIn(("Pas du tout satisfait", "Satisfait"), values)

View File

@@ -0,0 +1,87 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
=================================
Survey XLSX - Extra Fields Bridge
=================================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:8baa47af03e5b4b4e70ca6db224a5ac3e73aa66f287c42053a9a8f631efd10c2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/gitea-Elabore%2Fsurvey--tools-lightgray.png
:target: https://git.elabore.coop/Elabore/survey-tools/tree/18.0/survey_xlsx_extra_fields
:alt: Elabore/survey-tools
|badge1| |badge2| |badge3|
This is a **bridge module** between ``survey_xlsx_expand_multiple_choice``
and ``survey_extra_fields``.
``survey_extra_fields`` adds a *File* question type, whose answers are
uploaded attachments that cannot be represented in a spreadsheet cell. This
module excludes those *File* questions from the **Survey Results XLSX
export**: they get no column at all, instead of an unusable one.
It installs automatically (``auto_install``) as soon as both
``survey_xlsx_expand_multiple_choice`` and ``survey_extra_fields`` are
installed, and is uninstalled when either of them is removed. There is
nothing to configure.
.. warning::
The exclusion is implemented through the report extension hooks provided
by ``survey_xlsx_expand_multiple_choice``, which itself relies on hooks
added to ``survey_xlsx`` by this pull request:
https://github.com/elabore-coop/survey/pull/1
Without those hooks, *File* questions are not filtered out of the export.
**Table of contents**
.. contents::
:local:
Bug Tracker
===========
Bugs are tracked on `Gitea Issues <https://git.elabore.coop/Elabore/survey-tools/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://git.elabore.coop/Elabore/survey-tools/issues/new?body=module:%20survey_xlsx_extra_fields%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Elabore
Contributors
~~~~~~~~~~~~
* `Elabore <https://www.elabore.coop>`_
* Quentin Mondot
Maintainers
~~~~~~~~~~~
This module is part of the `Elabore/survey-tools <https://git.elabore.coop/Elabore/survey-tools/tree/18.0/survey_xlsx_extra_fields>`_ project on git.elabore.coop.
You are welcome to contribute.

View File

@@ -0,0 +1 @@
from . import report

View File

@@ -0,0 +1,19 @@
# Copyright 2025 Elabore
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Survey XLSX - Extra Fields Bridge",
"summary": """
Excludes 'file' question types from the survey XLSX export""",
"version": "18.0.1.0.0",
"license": "AGPL-3",
"installable": True,
"application": False,
"auto_install": True,
"author": "Elabore",
"website": "https://elabore.coop",
"depends": [
"survey_xlsx_expand_multiple_choice",
"survey_extra_fields",
],
}

View File

@@ -0,0 +1,3 @@
* `Elabore <https://www.elabore.coop>`_
* Quentin Mondot

View File

@@ -0,0 +1,22 @@
This is a **bridge module** between ``survey_xlsx_expand_multiple_choice``
and ``survey_extra_fields``.
``survey_extra_fields`` adds a *File* question type, whose answers are
uploaded attachments that cannot be represented in a spreadsheet cell. This
module excludes those *File* questions from the **Survey Results XLSX
export**: they get no column at all, instead of an unusable one.
It installs automatically (``auto_install``) as soon as both
``survey_xlsx_expand_multiple_choice`` and ``survey_extra_fields`` are
installed, and is uninstalled when either of them is removed. There is
nothing to configure.
.. warning::
The exclusion is implemented through the report extension hooks provided
by ``survey_xlsx_expand_multiple_choice``, which itself relies on hooks
added to ``survey_xlsx`` by this pull request:
https://github.com/elabore-coop/survey/pull/1
Without those hooks, *File* questions are not filtered out of the export.

View File

@@ -0,0 +1 @@
from . import report_survey_xlsx

View File

@@ -0,0 +1,15 @@
# Copyright 2025 Elabore
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models
class ReportSurveyXlsx(models.AbstractModel):
_inherit = "report.survey.xlsx"
def _write_question_header(self, sheet, question, cols, bold):
# "file" questions store uploaded attachments that cannot be rendered
# in a spreadsheet cell: skip them so no column is created. Without a
# column, _process_user_answer ignores their answers automatically.
if question.question_type == "file":
return
return super()._write_question_header(sheet, question, cols, bold)

View File

@@ -0,0 +1,442 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="survey-xlsx-extra-fields-bridge">
<h1>Survey XLSX - Extra Fields Bridge</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:8baa47af03e5b4b4e70ca6db224a5ac3e73aa66f287c42053a9a8f631efd10c2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/elabore-coop/survey-tools/tree/18.0/survey_xlsx_extra_fields"><img alt="elabore-coop/survey-tools" src="https://img.shields.io/badge/github-elabore--coop%2Fsurvey--tools-lightgray.png?logo=github" /></a></p>
<p>This is a <strong>bridge module</strong> between <tt class="docutils literal">survey_xlsx_expand_multiple_choice</tt>
and <tt class="docutils literal">survey_extra_fields</tt>.</p>
<p><tt class="docutils literal">survey_extra_fields</tt> adds a <em>File</em> question type, whose answers are
uploaded attachments that cannot be represented in a spreadsheet cell. This
module excludes those <em>File</em> questions from the <strong>Survey Results XLSX
export</strong>: they get no column at all, instead of an unusable one.</p>
<p>It installs automatically (<tt class="docutils literal">auto_install</tt>) as soon as both
<tt class="docutils literal">survey_xlsx_expand_multiple_choice</tt> and <tt class="docutils literal">survey_extra_fields</tt> are
installed, and is uninstalled when either of them is removed. There is
nothing to configure.</p>
<div class="admonition warning">
<p class="first admonition-title">Warning</p>
<p>The exclusion is implemented through the report extension hooks provided
by <tt class="docutils literal">survey_xlsx_expand_multiple_choice</tt>, which itself relies on hooks
added to <tt class="docutils literal">survey_xlsx</tt> by this pull request:</p>
<p><a class="reference external" href="https://github.com/elabore-coop/survey/pull/1">https://github.com/elabore-coop/survey/pull/1</a></p>
<p class="last">Without those hooks, <em>File</em> questions are not filtered out of the export.</p>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-1">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-2">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-3">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-4">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-5">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-1">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/elabore-coop/survey-tools/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/elabore-coop/survey-tools/issues/new?body=module:%20survey_xlsx_extra_fields%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-2">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-3">Authors</a></h3>
<ul class="simple">
<li>Elabore</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-4">Contributors</a></h3>
<ul class="simple">
<li><a class="reference external" href="https://www.elabore.coop">Elabore</a><ul>
<li>Quentin Mondot</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h3>
<p>This module is part of the <a class="reference external" href="https://github.com/elabore-coop/survey-tools/tree/18.0/survey_xlsx_extra_fields">elabore-coop/survey-tools</a> project on GitHub.</p>
<p>You are welcome to contribute.</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1 @@
from . import test_report

View File

@@ -0,0 +1,55 @@
# Copyright 2025 Elabore
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import io
import openpyxl
from odoo.addons.survey.tests import common
class TestExcludeFileQuestion(common.TestSurveyCommon):
def setUp(self):
super().setUp()
self.text_question = self._add_question(
self.page_0,
"Your name",
"char_box",
survey_id=self.survey.id,
)
self.file_question = self._add_question(
self.page_0,
"Upload your document",
"file",
constr_mandatory=False,
survey_id=self.survey.id,
)
answer = self._add_answer(self.survey, False, email="test@example.com")
self._add_answer_line(self.text_question, answer, "Alice")
self.env["survey.user_input.line"].create({
"user_input_id": answer.id,
"question_id": self.file_question.id,
"answer_type": "file",
"skipped": False,
"value_file": "ZmFrZQ==",
"value_file_fname": "doc.pdf",
})
answer._mark_done()
def _get_sheet(self):
report = self.env.ref("survey_xlsx.report_survey_xlsx")
rep = self.env["ir.actions.report"]._render(report, self.survey.ids, {})
wb = openpyxl.load_workbook(io.BytesIO(rep[0]))
return wb.worksheets[0]
def _find_col(self, sheet, header):
for col in range(1, sheet.max_column + 1):
if sheet.cell(1, col).value == header:
return col
return None
def test_file_question_excluded(self):
sheet = self._get_sheet()
# Regular questions are still exported
self.assertIsNotNone(self._find_col(sheet, "Your name"))
# File questions get no column at all
self.assertIsNone(self._find_col(sheet, "Upload your document"))