# HG changeset patch
# User Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr>
# Date 1725629533 -7200
#      Fri Sep 06 15:32:13 2024 +0200
# Branch 17.0
# Node ID e0445096b8ee18d8321b0d67b51a076892d029f5
# Parent  a69c8a5278341db9b1806e2b72ea9c2b2af7360a
✨

diff --git a/.badges/code_style-prettier-ff69b4.svg b/.badges/code_style-prettier-ff69b4.svg
new file mode 100644
--- /dev/null
+++ b/.badges/code_style-prettier-ff69b4.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<svg xmlns="http://www.w3.org/2000/svg" width="129" height="20">
+  <linearGradient id="b" x2="0" y2="100%">
+    <stop offset="0" stop-color="#bbb" stop-opacity=".1" />
+    <stop offset="1" stop-opacity=".1" />
+  </linearGradient>
+  <mask id="anybadge_1">
+    <rect width="129" height="20" rx="3" fill="#fff" />
+  </mask>
+  <g mask="url(#anybadge_1)">
+    <path fill="#555" d="M0 0h72v20H0z" />
+    <path fill="#ff69b4" d="M72 0h57v20H72z" />
+    <path fill="url(#b)" d="M0 0h129v20H0z" />
+  </g>
+  <g
+    fill="#fff"
+    text-anchor="middle"
+    font-family="DejaVu Sans,Verdana,Geneva,sans-serif"
+    font-size="11"
+  >
+    <text x="37.0" y="15" fill="#010101" fill-opacity=".3">code style</text>
+    <text x="36.0" y="14">code style</text>
+  </g>
+  <g
+    fill="#fff"
+    text-anchor="middle"
+    font-family="DejaVu Sans,Verdana,Geneva,sans-serif"
+    font-size="11"
+  >
+    <text x="101.5" y="15" fill="#010101" fill-opacity=".3">prettier</text>
+    <text x="100.5" y="14">prettier</text>
+  </g>
+</svg>
diff --git a/.badges/code_style-ruff.svg b/.badges/code_style-ruff.svg
new file mode 100644
--- /dev/null
+++ b/.badges/code_style-ruff.svg
@@ -0,0 +1,49 @@
+<svg
+  xmlns="http://www.w3.org/2000/svg"
+  xmlns:xlink="http://www.w3.org/1999/xlink"
+  width="53"
+  height="20"
+  role="img"
+  aria-label="Ruff"
+>
+  <title>Ruff</title>
+  <linearGradient id="s" x2="0" y2="100%">
+    <stop offset="0" stop-color="#bbb" stop-opacity=".1" />
+    <stop offset="1" stop-opacity=".1" />
+  </linearGradient>
+  <clipPath id="r">
+    <rect width="53" height="20" rx="3" fill="#fff" />
+  </clipPath>
+  <g clip-path="url(#r)">
+    <rect width="20" height="20" fill="#555" />
+    <rect x="20" width="33" height="20" fill="#261230" />
+    <rect width="53" height="20" fill="url(#s)" />
+  </g>
+  <g
+    fill="#fff"
+    text-anchor="middle"
+    font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
+    text-rendering="geometricPrecision"
+    font-size="110"
+  >
+    <image
+      x="5"
+      y="3"
+      width="10"
+      height="14"
+      xlink:href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEwIiBoZWlnaHQ9IjYyMiIgdmlld0JveD0iMCAwIDUxMCA2MjIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yMDYuNzAxIDBDMjAwLjk2NCAwIDE5Ni4zMTQgNC42NDEzMSAxOTYuMzE0IDEwLjM2NjdWNDEuNDY2N0MxOTYuMzE0IDQ3LjE5MiAxOTEuNjYzIDUxLjgzMzMgMTg1LjkyNyA1MS44MzMzSDE1Ni44NDNDMTUxLjEwNyA1MS44MzMzIDE0Ni40NTYgNTYuNDc0NiAxNDYuNDU2IDYyLjJWMTQ1LjEzM0MxNDYuNDU2IDE1MC44NTkgMTQxLjgwNiAxNTUuNSAxMzYuMDY5IDE1NS41SDEwNi45ODZDMTAxLjI0OSAxNTUuNSA5Ni41OTg4IDE2MC4xNDEgOTYuNTk4OCAxNjUuODY3VjIyMi44ODNDOTYuNTk4OCAyMjguNjA5IDkxLjk0ODQgMjMzLjI1IDg2LjIxMTggMjMzLjI1SDU3LjEyODNDNTEuMzkxNyAyMzMuMjUgNDYuNzQxMyAyMzcuODkxIDQ2Ljc0MTMgMjQzLjYxN1YzMDAuNjMzQzQ2Ljc0MTMgMzA2LjM1OSA0Mi4wOTA5IDMxMSAzNi4zNTQ0IDMxMUgxMC4zODdDNC42NTA0IDMxMSAwIDMxNS42NDEgMCAzMjEuMzY3VjM1Mi40NjdDMCAzNTguMTkyIDQuNjUwNCAzNjIuODMzIDEwLjM4NyAzNjIuODMzSDE0NS40MThDMTUxLjE1NCAzNjIuODMzIDE1NS44MDQgMzY3LjQ3NSAxNTUuODA0IDM3My4yVjQzMC4yMTdDMTU1LjgwNCA0MzUuOTQyIDE1MS4xNTQgNDQwLjU4MyAxNDUuNDE4IDQ0MC41ODNIMTE2LjMzNEMxMTAuNTk3IDQ0MC41ODMgMTA1Ljk0NyA0NDUuMjI1IDEwNS45NDcgNDUwLjk1VjUwNy45NjdDMTA1Ljk0NyA1MTMuNjkyIDEwMS4yOTcgNTE4LjMzMyA5NS41NjAxIDUxOC4zMzNINjYuNDc2NkM2MC43NCA1MTguMzMzIDU2LjA4OTYgNTIyLjk3NSA1Ni4wODk2IDUyOC43VjYxMS42MzNDNTYuMDg5NiA2MTcuMzU5IDYwLjc0IDYyMiA2Ni40NzY2IDYyMkgxNDkuNTcyQzE1NS4zMDkgNjIyIDE1OS45NTkgNjE3LjM1OSAxNTkuOTU5IDYxMS42MzNWNTcwLjE2N0gyMDEuNTA3QzIwNy4yNDQgNTcwLjE2NyAyMTEuODk0IDU2NS41MjUgMjExLjg5NCA1NTkuOFY1MjguN0MyMTEuODk0IDUyMi45NzUgMjE2LjU0NCA1MTguMzMzIDIyMi4yODEgNTE4LjMzM0gyNTEuMzY1QzI1Ny4xMDEgNTE4LjMzMyAyNjEuNzUyIDUxMy42OTIgMjYxLjc1MiA1MDcuOTY3VjQ3Ni44NjdDMjYxLjc1MiA0NzEuMTQxIDI2Ni40MDIgNDY2LjUgMjcyLjEzOCA0NjYuNUgzMDEuMjIyQzMwNi45NTkgNDY2LjUgMzExLjYwOSA0NjEuODU5IDMxMS42MDkgNDU2LjEzM1Y0MjUuMDMzQzMxMS42MDkgNDE5LjMwOCAzMTYuMjU5IDQxNC42NjcgMzIxLjk5NiA0MTQuNjY3SDM1MS4wNzlDMzU2LjgxNiA0MTQuNjY3IDM2MS40NjYgNDEwLjAyNSAzNjEuNDY2IDQwNC4zVjM3My4yQzM2MS40NjYgMzY3LjQ3NSAzNjYuMTE3IDM2Mi44MzMgMzcxLjg1MyAzNjIuODMzSDQwMC45MzdDNDA2LjY3MyAzNjIuODMzIDQxMS4zMjQgMzU4LjE5MiA0MTEuMzI0IDM1Mi40NjdWMzIxLjM2N0M0MTEuMzI0IDMxNS42NDEgNDE1Ljk3NCAzMTEgNDIxLjcxMSAzMTFINDUwLjc5NEM0NTYuNTMxIDMxMSA0NjEuMTgxIDMwNi4zNTkgNDYxLjE4MSAzMDAuNjMzVjIxNy43QzQ2MS4xODEgMjExLjk3NSA0NTYuNTMxIDIwNy4zMzMgNDUwLjc5NCAyMDcuMzMzSDQyMC42NzJDNDE0LjkzNiAyMDcuMzMzIDQxMC4yODUgMjAyLjY5MiA0MTAuMjg1IDE5Ni45NjdWMTY1Ljg2N0M0MTAuMjg1IDE2MC4xNDEgNDE0LjkzNiAxNTUuNSA0MjAuNjcyIDE1NS41SDQ0OS43NTZDNDU1LjQ5MiAxNTUuNSA0NjAuMTQzIDE1MC44NTkgNDYwLjE0MyAxNDUuMTMzVjExNC4wMzNDNDYwLjE0MyAxMDguMzA4IDQ2NC43OTMgMTAzLjY2NyA0NzAuNTMgMTAzLjY2N0g0OTkuNjEzQzUwNS4zNSAxMDMuNjY3IDUxMCA5OS4wMjUzIDUxMCA5My4zVjEwLjM2NjdDNTEwIDQuNjQxMzIgNTA1LjM1IDAgNDk5LjYxMyAwSDIwNi43MDFaTTE2OC4yNjkgNDQwLjU4M0MxNjIuNTMyIDQ0MC41ODMgMTU3Ljg4MiA0NDUuMjI1IDE1Ny44ODIgNDUwLjk1VjUwNy45NjdDMTU3Ljg4MiA1MTMuNjkyIDE1My4yMzEgNTE4LjMzMyAxNDcuNDk1IDUxOC4zMzNIMTE4LjQxMUMxMTIuNjc1IDUxOC4zMzMgMTA4LjAyNCA1MjIuOTc1IDEwOC4wMjQgNTI4LjdWNTU5LjhDMTA4LjAyNCA1NjUuNTI1IDExMi42NzUgNTcwLjE2NyAxMTguNDExIDU3MC4xNjdIMTU5Ljk1OVY1MjguN0MxNTkuOTU5IDUyMi45NzUgMTY0LjYxIDUxOC4zMzMgMTcwLjM0NiA1MTguMzMzSDE5OS40M0MyMDUuMTY2IDUxOC4zMzMgMjA5LjgxNyA1MTMuNjkyIDIwOS44MTcgNTA3Ljk2N1Y0NzYuODY3QzIwOS44MTcgNDcxLjE0MSAyMTQuNDY3IDQ2Ni41IDIyMC4yMDQgNDY2LjVIMjQ5LjI4N0MyNTUuMDI0IDQ2Ni41IDI1OS42NzQgNDYxLjg1OSAyNTkuNjc0IDQ1Ni4xMzNWNDI1LjAzM0MyNTkuNjc0IDQxOS4zMDggMjY0LjMyNSA0MTQuNjY3IDI3MC4wNjEgNDE0LjY2N0gyOTkuMTQ1QzMwNC44ODEgNDE0LjY2NyAzMDkuNTMyIDQxMC4wMjUgMzA5LjUzMiA0MDQuM1YzNzMuMkMzMDkuNTMyIDM2Ny40NzUgMzE0LjE4MiAzNjIuODMzIDMxOS45MTkgMzYyLjgzM0gzNDkuMDAyQzM1NC43MzkgMzYyLjgzMyAzNTkuMzg5IDM1OC4xOTIgMzU5LjM4OSAzNTIuNDY3VjMyMS4zNjdDMzU5LjM4OSAzMTUuNjQxIDM2NC4wMzkgMzExIDM2OS43NzYgMzExSDM5OC44NTlDNDA0LjU5NiAzMTEgNDA5LjI0NiAzMDYuMzU5IDQwOS4yNDYgMzAwLjYzM1YyNjkuNTMzQzQwOS4yNDYgMjYzLjgwOCA0MDQuNTk2IDI1OS4xNjcgMzk4Ljg1OSAyNTkuMTY3SDMxOC44OEMzMTMuMTQzIDI1OS4xNjcgMzA4LjQ5MyAyNTQuNTI1IDMwOC40OTMgMjQ4LjhWMjE3LjdDMzA4LjQ5MyAyMTEuOTc1IDMxMy4xNDMgMjA3LjMzMyAzMTguODggMjA3LjMzM0gzNDcuOTYzQzM1My43IDIwNy4zMzMgMzU4LjM1IDIwMi42OTIgMzU4LjM1IDE5Ni45NjdWMTY1Ljg2N0MzNTguMzUgMTYwLjE0MSAzNjMuMDAxIDE1NS41IDM2OC43MzcgMTU1LjVIMzk3LjgyMUM0MDMuNTU3IDE1NS41IDQwOC4yMDggMTUwLjg1OSA0MDguMjA4IDE0NS4xMzNWMTE0LjAzM0M0MDguMjA4IDEwOC4zMDggNDEyLjg1OCAxMDMuNjY3IDQxOC41OTUgMTAzLjY2N0g0NDcuNjc4QzQ1My40MTUgMTAzLjY2NyA0NTguMDY1IDk5LjAyNTMgNDU4LjA2NSA5My4zVjYyLjJDNDU4LjA2NSA1Ni40NzQ2IDQ1My40MTUgNTEuODMzMyA0NDcuNjc4IDUxLjgzMzNIMjA4Ljc3OEMyMDMuMDQxIDUxLjgzMzMgMTk4LjM5MSA1Ni40NzQ2IDE5OC4zOTEgNjIuMlYxNDUuMTMzQzE5OC4zOTEgMTUwLjg1OSAxOTMuNzQxIDE1NS41IDE4OC4wMDQgMTU1LjVIMTU4LjkyMUMxNTMuMTg0IDE1NS41IDE0OC41MzQgMTYwLjE0MSAxNDguNTM0IDE2NS44NjdWMjIyLjg4M0MxNDguNTM0IDIyOC42MDkgMTQzLjg4MyAyMzMuMjUgMTM4LjE0NyAyMzMuMjVIMTA5LjA2M0MxMDMuMzI3IDIzMy4yNSA5OC42NzYyIDIzNy44OTEgOTguNjc2MiAyNDMuNjE3VjMwMC42MzNDOTguNjc2MiAzMDYuMzU5IDEwMy4zMjcgMzExIDEwOS4wNjMgMzExSDE5Ny4zNTJDMjAzLjA4OSAzMTEgMjA3LjczOSAzMTUuNjQxIDIwNy43MzkgMzIxLjM2N1Y0MzAuMjE3QzIwNy43MzkgNDM1Ljk0MiAyMDMuMDg5IDQ0MC41ODMgMTk3LjM1MiA0NDAuNTgzSDE2OC4yNjlaIiBmaWxsPSIjRDdGRjY0Ii8+PC9zdmc+"
+    />
+    <text
+      aria-hidden="true"
+      x="355"
+      y="150"
+      fill="#010101"
+      fill-opacity=".3"
+      transform="scale(.1)"
+      textLength="230"
+    >
+      Ruff
+    </text>
+    <text x="355" y="140" transform="scale(.1)" fill="#fff" textLength="230">Ruff</text>
+  </g>
+</svg>
diff --git a/.badges/licence-AGPL--3-blue.svg b/.badges/licence-AGPL--3-blue.svg
new file mode 100644
--- /dev/null
+++ b/.badges/licence-AGPL--3-blue.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<svg xmlns="http://www.w3.org/2000/svg" width="107" height="20">
+  <linearGradient id="b" x2="0" y2="100%">
+    <stop offset="0" stop-color="#bbb" stop-opacity=".1" />
+    <stop offset="1" stop-opacity=".1" />
+  </linearGradient>
+  <mask id="anybadge_1">
+    <rect width="107" height="20" rx="3" fill="#fff" />
+  </mask>
+  <g mask="url(#anybadge_1)">
+    <path fill="#555" d="M0 0h53v20H0z" />
+    <path fill="#0000FF" d="M53 0h54v20H53z" />
+    <path fill="url(#b)" d="M0 0h107v20H0z" />
+  </g>
+  <g
+    fill="#fff"
+    text-anchor="middle"
+    font-family="DejaVu Sans,Verdana,Geneva,sans-serif"
+    font-size="11"
+  >
+    <text x="27.5" y="15" fill="#010101" fill-opacity=".3">licence</text>
+    <text x="26.5" y="14">licence</text>
+  </g>
+  <g
+    fill="#fff"
+    text-anchor="middle"
+    font-family="DejaVu Sans,Verdana,Geneva,sans-serif"
+    font-size="11"
+  >
+    <text x="81.0" y="15" fill="#010101" fill-opacity=".3">AGPL-3</text>
+    <text x="80.0" y="14">AGPL-3</text>
+  </g>
+</svg>
diff --git a/.badges/maturity.svg b/.badges/maturity.svg
new file mode 100644
--- /dev/null
+++ b/.badges/maturity.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<svg xmlns="http://www.w3.org/2000/svg" width="103" height="20">
+  <linearGradient id="b" x2="0" y2="100%">
+    <stop offset="0" stop-color="#bbb" stop-opacity=".1" />
+    <stop offset="1" stop-opacity=".1" />
+  </linearGradient>
+  <mask id="anybadge_1">
+    <rect width="103" height="20" rx="3" fill="#fff" />
+  </mask>
+  <g mask="url(#anybadge_1)">
+    <path fill="#555" d="M0 0h61v20H0z" />
+    <path fill="#e05d44" d="M61 0h42v20H61z" />
+    <path fill="url(#b)" d="M0 0h103v20H0z" />
+  </g>
+  <g
+    fill="#fff"
+    text-anchor="middle"
+    font-family="DejaVu Sans,Verdana,Geneva,sans-serif"
+    font-size="11"
+  >
+    <text x="31.5" y="15" fill="#010101" fill-opacity=".3">maturity</text>
+    <text x="30.5" y="14">maturity</text>
+  </g>
+  <g
+    fill="#fff"
+    text-anchor="middle"
+    font-family="DejaVu Sans,Verdana,Geneva,sans-serif"
+    font-size="11"
+  >
+    <text x="83.0" y="15" fill="#010101" fill-opacity=".3">Alpha</text>
+    <text x="82.0" y="14">Alpha</text>
+  </g>
+</svg>
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,20 @@
+# Configuration for known file extensions
+[*.{css,htm,html,js,json,jsx,less,markdown,md,py,rst,sass,scss,toml,xml,yaml,yml}]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{json,yml,yaml,rst,markdown,md,toml}]
+indent_size = 2
+
+# Do not configure editor for libs
+[*/static/{lib,src/lib}/**]
+charset = unset
+end_of_line = unset
+indent_size = unset
+indent_style = unset
+insert_final_newline = false
+trim_trailing_whitespace = false
diff --git a/.eslintrc.yml b/.eslintrc.yml
new file mode 100644
--- /dev/null
+++ b/.eslintrc.yml
@@ -0,0 +1,187 @@
+env:
+  browser: true
+  es6: true
+
+# See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449
+parserOptions:
+  ecmaVersion: 2019
+
+overrides:
+  - files:
+      - "**/*.esm.js"
+    parserOptions:
+      sourceType: module
+
+# Globals available in Odoo that shouldn't produce errorings
+globals:
+  _: readonly
+  $: readonly
+  fuzzy: readonly
+  jQuery: readonly
+  moment: readonly
+  odoo: readonly
+  openerp: readonly
+  owl: readonly
+
+# Styling is handled by Prettier, so we only need to enable AST rules;
+# see https://github.com/OCA/maintainer-quality-tools/pull/618#issuecomment-558576890
+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
+  valid-jsdoc:
+    - warn
+    - prefer:
+        arg: param
+        argument: param
+        augments: extends
+        constructor: class
+        exception: throws
+        func: function
+        method: function
+        prop: property
+        return: returns
+        virtual: abstract
+        yield: yields
+      preferType:
+        array: Array
+        bool: Boolean
+        boolean: Boolean
+        number: Number
+        object: Object
+        str: String
+        string: String
+      requireParamDescription: false
+      requireReturn: false
+      requireReturnDescription: false
+      requireReturnType: false
+  valid-typeof: warn
+  yoda: warn
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,3 @@
+include:
+  - project: xcg/ci-templates
+    file: /odoo/17.0/gitlab-ci.yaml
diff --git a/.hgignore b/.hgignore
new file mode 100644
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,3 @@
+syntax: glob
+./doc/_build
+./doc/autotodo
diff --git a/.prettierrc.yml b/.prettierrc.yml
new file mode 100644
--- /dev/null
+++ b/.prettierrc.yml
@@ -0,0 +1,8 @@
+# Defaults for all prettier-supported languages.
+# Prettier will complete this with settings from .editorconfig file.
+bracketSpacing: false
+printWidth: 88
+proseWrap: always
+semi: true
+trailingComma: "es5"
+xmlWhitespaceSensitivity: "ignore"
diff --git a/.yamllint.yaml b/.yamllint.yaml
new file mode 100644
--- /dev/null
+++ b/.yamllint.yaml
@@ -0,0 +1,4 @@
+rules:
+  document-start: disable
+  indentation:
+    indent-sequences: true
diff --git a/NEWS.rst b/NEWS.rst
new file mode 100644
--- /dev/null
+++ b/NEWS.rst
@@ -0,0 +1,7 @@
+Changelog
+=========
+
+17.0.1.0.0
+----------
+
+Initial version.
diff --git a/README.rst b/README.rst
new file mode 100644
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,52 @@
+====================
+Template Odoo Module
+====================
+
+.. |maturity| image:: .badges/maturity.svg
+    :target: https://odoo-community.org/page/development-status
+    :alt: Alpha
+.. |license| image:: .badges/licence-AGPL--3-blue.svg
+    :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+    :alt: License: AGPL-3
+.. |ruff| image:: .badges/code_style-ruff.svg
+    :target: https://github.com/astral-sh/ruff
+    :alt: Ruff
+.. |prettier| image:: .badges/code_style-prettier-ff69b4.svg
+    :target: https://github.com/prettier/prettier
+    :alt: Prettier
+
+|maturity| |license| |ruff| |prettier|
+
+This module add a wizard to call force_storage.
+
+Configuration
+=============
+
+Usage
+=====
+
+Development
+===========
+
+Known Issues / Roadmap
+======================
+
+Bug Tracker
+===========
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+-------
+
+- XCG Consulting, part of `Orbeet <https://orbeet.io>`_
+
+  - `Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr>`_
+
+Maintainers
+-----------
+
+XCG Consulting
diff --git a/__init__.py b/__init__.py
new file mode 100644
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,1 @@
+from . import wizards
diff --git a/__manifest__.py b/__manifest__.py
new file mode 100644
--- /dev/null
+++ b/__manifest__.py
@@ -0,0 +1,30 @@
+##############################################################################
+#
+#    Force Storage Wizard, for Odoo
+#    Copyright © 2024 XCG Consulting <https://xcg-consulting.fr>
+#
+#    Force Storage Wizard is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    Force Storage Wizard is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with Template Odoo Module.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+{
+    "name": "Force Storage Wizard",
+    "license": "AGPL-3",
+    "summary": "This module adds a wizard to force storage on attachments",
+    "version": "17.0.1.0.0",
+    "category": "Technical",
+    "author": "XCG Consulting",
+    "website": "https://orbeet.io/",
+    "depends": ["base"],
+    "data": ["security/ir.model.access.csv", "wizards/force_storage_wizard.xml"],
+}
diff --git a/i18n/fr.po b/i18n/fr.po
new file mode 100644
--- /dev/null
+++ b/i18n/fr.po
@@ -0,0 +1,110 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# 	* base_force_storage_wizard
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 17.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-06 15:22+0200\n"
+"PO-Revision-Date: 2024-09-06 15:25+0200\n"
+"Last-Translator: Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr>\n"
+"Language-Team: \n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Generator: Poedit 3.2.2\n"
+
+#. module: base_force_storage_wizard
+#. odoo-python
+#: code:addons/base_force_storage_wizard/wizards/force_storage_wizard.py:0
+#, python-format
+msgid "- In database: %d."
+msgstr "– Dans la base de données : %d."
+
+#. module: base_force_storage_wizard
+#. odoo-python
+#: code:addons/base_force_storage_wizard/wizards/force_storage_wizard.py:0
+#, python-format
+msgid "- In fs storage “%(location)s”: %(count)d."
+msgstr "– Dans le fs storage « %(location)s » : %(count)d."
+
+#. module: base_force_storage_wizard
+#. odoo-python
+#: code:addons/base_force_storage_wizard/wizards/force_storage_wizard.py:0
+#, python-format
+msgid "- On disk: %d."
+msgstr "– Sur disque : %d."
+
+#. module: base_force_storage_wizard
+#: model_terms:ir.ui.view,arch_db:base_force_storage_wizard.force_storage_wizard_view
+msgid "Apply"
+msgstr "Appliquer"
+
+#. module: base_force_storage_wizard
+#: model_terms:ir.ui.view,arch_db:base_force_storage_wizard.force_storage_wizard_view
+msgid "Attachment Force Storage"
+msgstr "Forcer le stockage des pièces jointes"
+
+#. module: base_force_storage_wizard
+#: model_terms:ir.ui.view,arch_db:base_force_storage_wizard.force_storage_wizard_view
+msgid "Close"
+msgstr "Fermer"
+
+#. module: base_force_storage_wizard
+#: model:ir.model.fields,field_description:base_force_storage_wizard.field_force_storage_wizard__create_uid
+msgid "Created by"
+msgstr "Créé par"
+
+#. module: base_force_storage_wizard
+#: model:ir.model.fields,field_description:base_force_storage_wizard.field_force_storage_wizard__create_date
+msgid "Created on"
+msgstr "Créé le"
+
+#. module: base_force_storage_wizard
+#. odoo-python
+#: code:addons/base_force_storage_wizard/wizards/force_storage_wizard.py:0
+#, python-format
+msgid "Current storage:"
+msgstr "Statistiques du stockage :"
+
+#. module: base_force_storage_wizard
+#: model:ir.model.fields,field_description:base_force_storage_wizard.field_force_storage_wizard__display_name
+msgid "Display Name"
+msgstr "Nom d’affichage"
+
+#. module: base_force_storage_wizard
+#: model:ir.actions.act_window,name:base_force_storage_wizard.force_storage_wizard_action
+#: model:ir.model,name:base_force_storage_wizard.model_force_storage_wizard
+#: model:ir.ui.menu,name:base_force_storage_wizard.force_storage_wizard_menu
+msgid "Force Storage Wizard"
+msgstr "Assistant pour forcer le stockage"
+
+#. module: base_force_storage_wizard
+#: model:ir.model.fields,field_description:base_force_storage_wizard.field_force_storage_wizard__id
+msgid "ID"
+msgstr ""
+
+#. module: base_force_storage_wizard
+#: model:ir.model.fields,field_description:base_force_storage_wizard.field_force_storage_wizard__write_uid
+msgid "Last Updated by"
+msgstr "Dernière mise à jour par"
+
+#. module: base_force_storage_wizard
+#: model:ir.model.fields,field_description:base_force_storage_wizard.field_force_storage_wizard__write_date
+msgid "Last Updated on"
+msgstr "Dernière modification le"
+
+#. module: base_force_storage_wizard
+#: model:ir.model.fields,field_description:base_force_storage_wizard.field_force_storage_wizard__stats
+msgid "Stats"
+msgstr "Statistiques"
+
+#. module: base_force_storage_wizard
+#. odoo-python
+#: code:addons/base_force_storage_wizard/wizards/force_storage_wizard.py:0
+#, python-format
+msgid "URL in attachments: %d"
+msgstr "URL dans la table des pièces jointes : %d"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,94 @@
+[project]
+name = "odoo-addon-base_force_storage_wizard"
+dynamic = ["version"]
+readme = "README.rst"
+requires-python = "~=3.11"
+license = { file = "LICENSE", name = "GNU Affero General Public License v3" }
+keywords = ["odoo"]
+authors = [{ name = "XCG Consulting" }]
+classifiers = [
+  "Programming Language :: Python",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3.11",
+  "Framework :: Odoo",
+  "Framework :: Odoo :: 17.0",
+  "License :: OSI Approved :: GNU Affero General Public License v3"
+]
+dependencies = ["odoo==17.0.*"]
+
+[project.optional-dependencies]
+doc = ["sphinx", "sphinx-odoo-autodoc", "odoo-scripts"]
+test = []
+
+[project.urls]
+repository = "https://orus.io/xcg/odoo-modules/base_force_storage_wizard"
+changelog = "https://orus.io/xcg/odoo-modules/base_force_storage_wizard/-/blob/branch/17.0/NEWS.rst"
+
+[build-system]
+requires = ["hatchling >=1.19", "hatch-vcs"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build]
+exclude = [
+  "/doc/",
+  "/.editorconfig",
+  "/.eslintrc.yml",
+  "/.gitlab-ci.yml",
+  "/.hgignore",
+  "/.hgtags",
+  "/.prettierrc.yml",
+  "/.yamllint.yaml"
+]
+
+[tool.hatch.build.targets.wheel]
+include = [
+  "*.csv",
+  "/i18n/",
+  "/static/",
+  "README.rst",
+  "*.xml",
+  "*.py",
+  "*.svg",
+  "*.png"
+]
+
+[tool.hatch.build.targets.wheel.sources]
+"" = "odoo/addons/base_force_storage_wizard"
+
+[tool.hatch.version]
+source = "vcs"
+
+[tool.isort]
+section-order = [
+  "future",
+  "standard-library",
+  "third-party",
+  "odoo",
+  "odoo-addons",
+  "first-party",
+  "local-folder"
+]
+
+[tool.isort.sections]
+"odoo" = ["odoo"]
+"odoo-addons" = ["odoo.addons"]
+
+[tool.ruff.lint.mccabe]
+max-complexity = 16
+
+[tool.ruff]
+target-version = "py311"
+
+[tool.ruff.lint]
+extend-select = [
+  "B",
+  "C90",
+  "E501", # line too long (default 88)
+  "I", # isort
+  "UP", # pyupgrade
+
+]
+
+[tool.ruff.lint.per-file-ignores]
+"__init__.py" = ["F401", "I001"] # ignore unused and unsorted imports in __init__.py
+"__manifest__.py" = ["B018"] # useless expression
diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv
new file mode 100644
--- /dev/null
+++ b/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+base_force_storage_wizard.access_force_storage_wizard,access_force_storage_wizard,base_force_storage_wizard.model_force_storage_wizard,base.group_erp_manager,1,1,1,1
diff --git a/wizards/__init__.py b/wizards/__init__.py
new file mode 100644
--- /dev/null
+++ b/wizards/__init__.py
@@ -0,0 +1,1 @@
+from . import force_storage_wizard
diff --git a/wizards/force_storage_wizard.py b/wizards/force_storage_wizard.py
new file mode 100644
--- /dev/null
+++ b/wizards/force_storage_wizard.py
@@ -0,0 +1,79 @@
+##############################################################################
+#
+#    Force Storage Wizard, for Odoo
+#    Copyright © 2024 XCG Consulting <https://xcg-consulting.fr>
+#
+#    Force Storage Wizard is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    Force Storage Wizard is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with Template Odoo Module.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from odoo import _, fields, models
+
+
+class ForceStorageWizard(models.TransientModel):
+    """This wizard…"""
+
+    _name = "force_storage.wizard"
+    _description = "Force Storage Wizard"
+
+    def _default_stats(self):
+        if self.env.is_admin():
+            self.env.cr.execute(
+                "SELECT COUNT(*) FROM ir_attachment WHERE db_datas IS NOT NULL "
+                "AND type != 'url'"
+            )
+            (count,) = self.env.cr.fetchone()
+            message = [_("Current storage:"), _("- In database: %d.") % count]
+            # defined in OCA module fs_storage
+            if "fs.storage" in self.env:
+                self.env.cr.execute(
+                    "SELECT COUNT(*) FROM ir_attachment WHERE fs_storage_id IS NULL "
+                    "AND db_datas IS NULL AND type != 'url'"
+                )
+                (count,) = self.env.cr.fetchone()
+                message.append(_("- On disk: %d.", count))
+                for location in self.env["fs.storage"].search([]):
+                    self.env.cr.execute(
+                        "SELECT COUNT(*) FROM ir_attachment WHERE fs_storage_id = %s "
+                        "AND type != 'url'",
+                        (location.id,),
+                    )
+                    (count,) = self.env.cr.fetchone()
+                    message.append(
+                        _(
+                            "- In fs storage “%(location)s”: %(count)d.",
+                            count=count,
+                            location=location.name,
+                        )
+                    )
+            else:
+                self.env.cr.execute(
+                    "SELECT COUNT(*) FROM ir_attachment WHERE store_fname IS NOT NULL "
+                    "AND type != 'url'"
+                )
+                (count,) = self.env.cr.fetchone()
+                message.append(_("- On disk: %d.", count))
+
+            self.env.cr.execute("SELECT COUNT(*) FROM ir_attachment WHERE type = 'url'")
+            (count,) = self.env.cr.fetchone()
+            message.append("")
+            message.append(_("URL in attachments: %d", count))
+
+            return "\n".join(message)
+        return ""
+
+    stats = fields.Text(default=_default_stats, readonly=True)
+
+    def action_force_storage(self):
+        """Run force_storage on attachments"""
+        self.env["ir.attachment"].force_storage()
diff --git a/wizards/force_storage_wizard.xml b/wizards/force_storage_wizard.xml
new file mode 100644
--- /dev/null
+++ b/wizards/force_storage_wizard.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+    <record id="force_storage_wizard_view" model="ir.ui.view">
+        <field name="name">Force Storage Wizard</field>
+        <field name="model">force_storage.wizard</field>
+        <field name="arch" type="xml">
+            <form string="Attachment Force Storage">
+                <div role="info">
+                    <field name="stats" nolabel="1" />
+                </div>
+                <footer>
+                    <button
+                        name="action_force_storage"
+                        string="Apply"
+                        type="object"
+                        class="btn-primary"
+                    />
+                    <button
+                        string="Close"
+                        class="btn-secondary"
+                        special="cancel"
+                        data-hotkey="x"
+                    />
+                </footer>
+            </form>
+        </field>
+    </record>
+    <record id="force_storage_wizard_action" model="ir.actions.act_window">
+        <field name="name">Force Storage Wizard</field>
+        <field name="res_model">force_storage.wizard</field>
+        <field name="view_mode">form</field>
+        <field name="target">new</field>
+    </record>
+    <menuitem
+        id="force_storage_wizard_menu"
+        action="force_storage_wizard_action"
+        parent="base.next_id_9"
+    />
+</odoo>