diff --git a/.badges/code_style-black-000000.svg b/.badges/code_style-black-000000.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_LmJhZGdlcy9jb2RlX3N0eWxlLWJsYWNrLTAwMDAwMC5zdmc=
--- /dev/null
+++ b/.badges/code_style-black-000000.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<svg xmlns="http://www.w3.org/2000/svg" width="114" 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="114" height="20" rx="3" fill="#fff" />
+  </mask>
+  <g mask="url(#anybadge_1)">
+    <path fill="#555" d="M0 0h72v20H0z" />
+    <path fill="#000000" d="M72 0h42v20H72z" />
+    <path fill="url(#b)" d="M0 0h114v20H0z" />
+  </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="94.0" y="15" fill="#010101" fill-opacity=".3">black</text>
+    <text x="93.0" y="14">black</text>
+  </g>
+</svg>
diff --git a/.badges/code_style-prettier-ff69b4.svg b/.badges/code_style-prettier-ff69b4.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_LmJhZGdlcy9jb2RlX3N0eWxlLXByZXR0aWVyLWZmNjliNC5zdmc=
--- /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/licence-AGPL--3-blue.svg b/.badges/licence-AGPL--3-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_LmJhZGdlcy9saWNlbmNlLUFHUEwtLTMtYmx1ZS5zdmc=
--- /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
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_LmJhZGdlcy9tYXR1cml0eS5zdmc=
--- /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
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_LmVkaXRvcmNvbmZpZw==
--- /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
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_LmVzbGludHJjLnltbA==
--- /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/.flake8 b/.flake8
new file mode 100644
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_LmZsYWtlOA==
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,5 @@
+[flake8]
+max-line-length = 88
+per-file-ignores=
+    __init__.py:F401
+    __manifest__.py:B018
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_LmdpdGxhYi1jaS55bWw=
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,3 @@
+include:
+  - project: xcg/ci-templates
+    file: /odoo/16.0/gitlab-ci.yaml
diff --git a/.hgignore b/.hgignore
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_LmhnaWdub3Jl..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_LmhnaWdub3Jl 100644
--- a/.hgignore
+++ b/.hgignore
@@ -1,24 +1,3 @@
 syntax: glob
-**/*.pyc
-*.pyc
-*.pyo
-*.swp
-.tmp*
-*~
-*.egg-info
-dist/*
-build/*
-lib/*
-output/*
-*.orig
-*.log
-.settings/*
-storage/*
-.project
-.idea
-.pydevproject
-*.db
-.ropeproject/*
-.mob
 ./doc/_build
 ./doc/autotodo
@@ -23,5 +2,2 @@
 ./doc/_build
 ./doc/autotodo
-./doc/manifest
-pyproject.toml
-.isort.cfg
diff --git a/.prettierrc.yml b/.prettierrc.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_LnByZXR0aWVycmMueW1s
--- /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
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_LnlhbWxsaW50LnlhbWw=
--- /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
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_TkVXUy5yc3Q=..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_TkVXUy5yc3Q= 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,6 +1,10 @@
-=======
-History
-=======
+Changelog
+=========
+
+16.0.1.0.0
+----------
+
+* Migrate to Odoo 16.0
 
 13.0.1.0.0
 ----------
diff --git a/README.rst b/README.rst
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_UkVBRE1FLnJzdA==..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_UkVBRE1FLnJzdA== 100644
--- a/README.rst
+++ b/README.rst
@@ -1,3 +1,4 @@
+============
 Board Alerts
 ============
 
@@ -1,6 +2,21 @@
 Board Alerts
 ============
 
+.. |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
+.. |black| image:: .badges/code_style-black-000000.svg
+    :target: https://github.com/psf/black
+    :alt: Black
+.. |prettier| image:: .badges/code_style-prettier-ff69b4.svg
+    :target: https://github.com/prettier/prettier
+    :alt: Prettier
+
+|maturity| |license| |black| |prettier|
+
 Send emails at regular intervals to summarize the contents of a dashboard.
 
 
@@ -13,6 +29,9 @@
 * The administrator can adjust the email frequency.
 
 
+Usage
+=====
+
 Example use
 -----------
 
@@ -34,3 +53,14 @@
 - The administrator can adjust the email frequency:
 
   .. image:: doc_images/board_alerts_example_4.png
+
+
+Credits
+=======
+
+Authors
+-------
+
+- XCG Consulting, part of `Orbeet <https://orbeet.io>`_
+
+ - `Arthur Mayer <arthur.mayer@xcg-consulting.fr>`_
\ No newline at end of file
diff --git a/__init__.py b/__init__.py
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_X19pbml0X18ucHk=..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_X19pbml0X18ucHk= 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,3 +1,1 @@
-# flake8: noqa
-
 from . import controllers, models, wizard
diff --git a/__manifest__.py b/__manifest__.py
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_X19tYW5pZmVzdF9fLnB5..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_X19tYW5pZmVzdF9fLnB5 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -1,3 +1,2 @@
-# -*- coding: utf-8 -*-
 ##############################################################################
 #
@@ -2,7 +1,7 @@
 ##############################################################################
 #
-#    Board Alerts, for Odoo
-#    Copyright (C) 2013 XCG Consulting (http://odoo.consulting)
+#    Board alerts, for Odoo
+#    Copyright © 2021, 2022, 2023 XCG Consulting <https://xcg-consulting.fr>
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU Affero General Public License as
@@ -20,21 +19,11 @@
 ##############################################################################
 {
     "name": "Board alerts",
-    "description": """
-Board alerts
-============
-
-Send emails at regular intervals to summarize the contents of a dashboard.
-
-
-Create your own Odoo notifications
-----------------------------------
-
-* Create your own notifications based on the contents of your activity.
-* See your alerts in your dasboard.
-* Receive them automatically by email at regular intervals.
-* The administrator can adjust the email frequency.
-""",
-    "version": "13.0.1.0",
+    "summary": """
+        Send emails at regular intervals"
+        to summarize the contents of a dashboard.
+    """,
+    "license": "AGPL-3",
+    "version": "16.0.1.0.0",
     "category": "Tools",
     "author": "XCG Consulting",
@@ -39,5 +28,5 @@
     "category": "Tools",
     "author": "XCG Consulting",
-    "website": "http://odoo.consulting/",
-    "depends": ["board", "mail", "web"],
+    "website": "https://orbeet.io/",
+    "depends": ["web", "board", "mail", "spreadsheet_dashboard"],
     "data": [
@@ -43,4 +32,5 @@
     "data": [
+        "security/ir.model.access.csv",
         "data/alert_board.xml",
         "data/board_alerts_email_template.xml",
         "data/board_alerts_cron_task.xml",
@@ -44,6 +34,5 @@
         "data/alert_board.xml",
         "data/board_alerts_email_template.xml",
         "data/board_alerts_cron_task.xml",
-        "views/board_alerts_assets.xml",
         "wizard/board_alerts_dlg.xml",
     ],
@@ -48,5 +37,11 @@
         "wizard/board_alerts_dlg.xml",
     ],
-    "qweb": ["static/src/xml/alert_board.xml"],
+    "assets": {
+        "web.assets_backend": [
+            "/board_alerts/static/src/xml/alert_board.xml",
+            # "/board_alerts/static/src/scss/alert_board.scss",
+            "/board_alerts/static/src/js/alert_board.js",
+        ],
+    },
     "installable": True,
 }
diff --git a/controllers/add_to_alert_dashboard.py b/controllers/add_to_alert_dashboard.py
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_Y29udHJvbGxlcnMvYWRkX3RvX2FsZXJ0X2Rhc2hib2FyZC5weQ==..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_Y29udHJvbGxlcnMvYWRkX3RvX2FsZXJ0X2Rhc2hib2FyZC5weQ== 100644
--- a/controllers/add_to_alert_dashboard.py
+++ b/controllers/add_to_alert_dashboard.py
@@ -13,11 +13,8 @@
     """
 
     @route(route="/board/add_to_alert_dashboard", type="json", auth="user")
-    def add_to_dashboard(
-        self, action_id, context_to_save, domain, view_mode, name=""
-    ):
-        """Called when adding to the alert dashboard from the Odoo web client.
-        """
+    def add_to_dashboard(self, action_id, context_to_save, domain, view_mode, name=""):
+        """Called when adding to the alert dashboard from the Odoo web client."""
 
         action = request.env.ref("board_alerts.action_alert_board")
         if (
@@ -28,7 +25,7 @@
         ):
             # Maybe should check the content instead of model board.board ?
             view_id = action["views"][0][0]
-            board = request.env["board.board"].fields_view_get(view_id, "form")
+            board = request.env["board.board"].get_view(view_id, "form")
             if board and "arch" in board:
                 xml = ElementTree.fromstring(board["arch"])
                 column = xml.find("./board/column")
diff --git a/data/alert_board.xml b/data/alert_board.xml
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_ZGF0YS9hbGVydF9ib2FyZC54bWw=..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_ZGF0YS9hbGVydF9ib2FyZC54bWw= 100644
--- a/data/alert_board.xml
+++ b/data/alert_board.xml
@@ -1,3 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8" ?>
 <odoo>
     <data noupdate="1">
@@ -2,6 +2,5 @@
 <odoo>
     <data noupdate="1">
-
         <!-- Define the board the user can drag views into that will be used to send
             emails. Imitated from the definition of the "My Dashboard" board in odoo/addons/board/views/board_views.xml. -->
 
@@ -25,8 +24,11 @@
             <field name="view_id" ref="alert_board" />
         </record>
 
-        <menuitem id="menu_alert_board" parent="base.menu_board_root"
-            action="action_alert_board" sequence="6" />
-
+        <menuitem
+            id="menu_alert_board"
+            parent="spreadsheet_dashboard.spreadsheet_dashboard_menu_root"
+            action="action_alert_board"
+            sequence="126"
+        />
     </data>
 </odoo>
diff --git a/data/board_alerts_cron_task.xml b/data/board_alerts_cron_task.xml
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_ZGF0YS9ib2FyZF9hbGVydHNfY3Jvbl90YXNrLnhtbA==..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_ZGF0YS9ib2FyZF9hbGVydHNfY3Jvbl90YXNrLnhtbA== 100644
--- a/data/board_alerts_cron_task.xml
+++ b/data/board_alerts_cron_task.xml
@@ -1,3 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8" ?>
 <odoo>
     <data noupdate="1">
@@ -2,6 +2,5 @@
 <odoo>
     <data noupdate="1">
-
         <!-- Cron task to send board alert emails every morning at 07H00. -->
 
         <record id="board_alerts_cron_task" model="ir.cron">
@@ -10,5 +9,5 @@
             <field name="code">model.send_board_alerts()</field>
             <field name="interval_number">1</field>
             <field name="interval_type">days</field>
-            <field name="model_id" ref="model_res_users"/>
+            <field name="model_id" ref="model_res_users" />
             <field name="name">Board alerts - Daily emails</field>
@@ -14,5 +13,4 @@
             <field name="name">Board alerts - Daily emails</field>
-            <field name="nextcall"
-                eval="time.strftime('%Y-%m-%d 07:00:00', time.localtime(time.time() + 24 * 3600))" />
+            <field name="nextcall" eval="time.strftime('%Y-%m-%d 07:00:00')" />
             <field name="numbercall">-1</field>
         </record>
@@ -17,5 +15,4 @@
             <field name="numbercall">-1</field>
         </record>
-
     </data>
 </odoo>
diff --git a/data/board_alerts_email_template.xml b/data/board_alerts_email_template.xml
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_ZGF0YS9ib2FyZF9hbGVydHNfZW1haWxfdGVtcGxhdGUueG1s..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_ZGF0YS9ib2FyZF9hbGVydHNfZW1haWxfdGVtcGxhdGUueG1s 100644
--- a/data/board_alerts_email_template.xml
+++ b/data/board_alerts_email_template.xml
@@ -1,3 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8" ?>
 <odoo>
     <data noupdate="1">
@@ -2,6 +2,5 @@
 <odoo>
     <data noupdate="1">
-
         <!-- Default email template for board alert emails. -->
 
         <record id="board_alerts_mail_template" model="mail.template">
@@ -5,8 +4,10 @@
         <!-- Default email template for board alert emails. -->
 
         <record id="board_alerts_mail_template" model="mail.template">
-            <field name="body_html"><![CDATA[
+            <field
+                name="body_html"
+            ><![CDATA[
                 <p>Hello ${ object.name },</p>
                 <p>Here follow your alerts for today.</p>
                 <div>${ object.get_board_alert_contents() | safe }</div>
             ]]></field>
@@ -9,8 +10,10 @@
                 <p>Hello ${ object.name },</p>
                 <p>Here follow your alerts for today.</p>
                 <div>${ object.get_board_alert_contents() | safe }</div>
             ]]></field>
-            <field name="email_from">${ object.company_id.name or '' } &lt;${ object.company_id.email or '' }&gt;</field>
+            <field
+                name="email_from"
+            >${ object.company_id.name or '' } &lt;${ object.company_id.email or '' }&gt;</field>
             <field name="partner_to">${ object.partner_id.id }</field>
             <field name="email_to">${ object.email }</field>
             <field name="lang">${object.partner_id.lang}</field>
@@ -18,6 +21,5 @@
             <field name="name">Board alert email</field>
             <field name="subject">Today's alerts</field>
         </record>
-
     </data>
 </odoo>
diff --git a/doc/.badges b/doc/.badges
new file mode 120000
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_ZG9jLy5iYWRnZXM=
--- /dev/null
+++ b/doc/.badges
@@ -0,0 +1,1 @@
+../.badges
\ No newline at end of file
diff --git a/doc/Makefile b/doc/Makefile
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_ZG9jL01ha2VmaWxl..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_ZG9jL01ha2VmaWxl 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -1,3 +1,3 @@
-# Makefile for Sphinx documentation
+# Minimal makefile for Sphinx documentation
 #
 
@@ -2,10 +2,13 @@
 #
 
-# You can set these variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = sphinx-build
-PAPER         =
-BUILDDIR      = _build
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS     ?=
+SPHINXBUILD    ?= sphinx-build
+SOURCEDIR      = .
+BUILDDIR       = _build
+LANGUAGE       ?= en
+BUILDDIRSUFFIX =
 
 # User-friendly check for sphinx-build
 ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
@@ -9,6 +12,6 @@
 
 # User-friendly check for sphinx-build
 ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
-	$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
 endif
 
@@ -13,5 +16,4 @@
 endif
 
-project:=$(shell basename $(shell readlink -f ..))
-branch:=$(shell hg identify --branch)
+.PHONY: help Makefile gettext clean
 
@@ -17,10 +19,3 @@
 
-# Internal variables.
-PAPEROPT_a4     = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-# the i18n builder cannot share the environment and doctrees with the others
-I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help
+# Put it first so that "make" without argument is like "make help".
 help:
@@ -26,27 +21,3 @@
 help:
-	@echo "Please use \`make <target>' where <target> is one of"
-	@echo "  html       to make standalone HTML files"
-	@echo "  dirhtml    to make HTML files named index.html in directories"
-	@echo "  singlehtml to make a single large HTML file"
-	@echo "  pickle     to make pickle files"
-	@echo "  json       to make JSON files"
-	@echo "  htmlhelp   to make HTML files and a HTML help project"
-	@echo "  qthelp     to make HTML files and a qthelp project"
-	@echo "  applehelp  to make an Apple Help Book"
-	@echo "  devhelp    to make HTML files and a Devhelp project"
-	@echo "  epub       to make an epub"
-	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
-	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
-	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
-	@echo "  text       to make text files"
-	@echo "  man        to make manual pages"
-	@echo "  texinfo    to make Texinfo files"
-	@echo "  info       to make Texinfo files and run them through makeinfo"
-	@echo "  gettext    to make PO message catalogs"
-	@echo "  changes    to make an overview of all changed/added/deprecated items"
-	@echo "  xml        to make Docutils-native XML files"
-	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
-	@echo "  linkcheck  to check all external links for integrity"
-	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
-	@echo "  coverage   to run coverage check of the documentation (if enabled)"
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)/$(LANGUAGE)$(BUILDDIRSUFFIX)" $(SPHINXOPTS) $(O)
 
@@ -52,4 +23,3 @@
 
-.PHONY: clean
 clean:
 	rm -rf $(BUILDDIR)/*
@@ -54,69 +24,4 @@
 clean:
 	rm -rf $(BUILDDIR)/*
-	rm -f autotodo manifest
-
-.PHONY: html
-html: autotodo manifest
-	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-.PHONY: dirhtml
-dirhtml: autotodo manifest
-	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-.PHONY: singlehtml
-singlehtml: autotodo manifest
-	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
-	@echo
-	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-.PHONY: pickle
-pickle:
-	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
-	@echo
-	@echo "Build finished; now you can process the pickle files."
-
-.PHONY: json
-json:
-	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
-	@echo
-	@echo "Build finished; now you can process the JSON files."
-
-.PHONY: htmlhelp
-htmlhelp: autotodo manifest
-	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
-	@echo
-	@echo "Build finished; now you can run HTML Help Workshop with the" \
-	      ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-.PHONY: epub
-epub:
-	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
-	@echo
-	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-.PHONY: latex
-latex:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo
-	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
-	@echo "Run \`make' in that directory to run these through (pdf)latex" \
-	      "(use \`make latexpdf' here to do that automatically)."
-
-.PHONY: latexpdf
-latexpdf:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through pdflatex..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-.PHONY: latexpdfja
-latexpdfja:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through platex and dvipdfmx..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+	rm -f autotodo
 
@@ -122,74 +27,5 @@
 
-.PHONY: text
-text:
-	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
-	@echo
-	@echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-.PHONY: man
-man:
-	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
-	@echo
-	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-.PHONY: texinfo
-texinfo:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo
-	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
-	@echo "Run \`make' in that directory to run these through makeinfo" \
-	      "(use \`make info' here to do that automatically)."
-
-.PHONY: info
-info:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo "Running Texinfo files through makeinfo..."
-	make -C $(BUILDDIR)/texinfo info
-	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
-
-.PHONY: gettext
-gettext:
-	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
-	@echo
-	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
-
-.PHONY: changes
-changes:
-	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
-	@echo
-	@echo "The overview file is in $(BUILDDIR)/changes."
-
-.PHONY: linkcheck
-linkcheck:
-	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
-	@echo
-	@echo "Link check complete; look for any errors in the above output " \
-	      "or in $(BUILDDIR)/linkcheck/output.txt."
-
-.PHONY: doctest
-doctest:
-	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
-	@echo "Testing of doctests in the sources finished, look at the " \
-	      "results in $(BUILDDIR)/doctest/output.txt."
-
-.PHONY: coverage
-coverage:
-	$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
-	@echo "Testing of coverage in the sources finished, look at the " \
-	      "results in $(BUILDDIR)/coverage/python.txt."
-
-.PHONY: xml
-xml:
-	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
-	@echo
-	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
-
-.PHONY: pseudoxml
-pseudoxml:
-	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
-	@echo
-	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
-
-autotodo: autotodo.py
+# depends on autotodo.py but that does not work well with the catch-all below
+autotodo:
 	@./autotodo.py ../ .py TODO,FIXME,XXX
 
@@ -194,4 +30,13 @@
 	@./autotodo.py ../ .py TODO,FIXME,XXX
 
-manifest: ../__manifest__.py manifest.py
-	@./manifest.py
+gettext: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)$(BUILDDIRSUFFIX)" $(SPHINXOPTS) $(O)
+
+# Use to update *.po files
+update_locale: Makefile gettext
+	@sphinx-intl update -p "$(BUILDDIR)$(BUILDDIRSUFFIX)/gettext" -l $(LANGUAGE)
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile autotodo
+	@$(SPHINXBUILD) -b $@ "$(SOURCEDIR)" "$(BUILDDIR)/$@/$(LANGUAGE)$(BUILDDIRSUFFIX)" -d "$(BUILDDIR)$(BUILDDIRSUFFIX)/doctrees/$(LANGUAGE)" $(SPHINXOPTS) -D language=$(LANGUAGE) $(O)
diff --git a/doc/autotodo.py b/doc/autotodo.py
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_ZG9jL2F1dG90b2RvLnB5..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_ZG9jL2F1dG90b2RvLnB5 100755
--- a/doc/autotodo.py
+++ b/doc/autotodo.py
@@ -2,7 +2,7 @@
 ##############################################################################
 #
 #    OpenERP, Open Source Management Solution
-#    Copyright (C) 2014, 2018 XCG Consulting
+#    Copyright © 2014, 2018, 2022, 2023 XCG Consulting
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU Affero General Public License as
@@ -22,6 +22,7 @@
 import os
 import os.path
 import sys
+from collections.abc import Mapping
 
 
 def main():
@@ -33,4 +34,5 @@
     exts = sys.argv[2].split(",")
     tags = sys.argv[3].split(",")
     todolist = {tag: [] for tag in tags}
+    path_file_length: Mapping[str, int] = {}
 
@@ -36,6 +38,6 @@
 
-    for root, dirs, files in os.walk(folder):
-        scan_folder((exts, tags, todolist), root, files)
-    create_autotodo(folder, todolist)
+    for root, _dirs, files in os.walk(folder):
+        scan_folder((exts, tags, todolist, path_file_length), root, files)
+    create_autotodo(folder, todolist, path_file_length)
 
 
@@ -40,6 +42,6 @@
 
 
-def write_info(f, infos, folder):
+def write_info(f, infos, folder, path_file_length: Mapping[str, int]):
     # Check sphinx version for lineno-start support
 
     import sphinx
@@ -52,10 +54,8 @@
     for i in infos:
         path = i[0]
         line = i[1]
-        lines = (line - 3, line + 4)
-        class_name = ":class:`%s`" % os.path.basename(
-            os.path.splitext(path)[0]
-        )
+        lines = (line - 3, min(line + 4, path_file_length[path]))
+        class_name = ":class:`%s`" % os.path.basename(os.path.splitext(path)[0])
         f.write(
             "%s\n"
             "%s\n\n"
@@ -71,7 +71,7 @@
                 path,
                 lines[0],
                 lines[1],
-                line,
+                4,
             )
         )
         if lineno_start:
@@ -79,5 +79,5 @@
         f.write("\n")
 
 
-def create_autotodo(folder, todolist):
+def create_autotodo(folder, todolist, path_file_length: Mapping[str, int]):
     with open("autotodo", "w+") as f:
@@ -83,3 +83,3 @@
     with open("autotodo", "w+") as f:
-        for tag, info in todolist.items():
+        for tag, info in list(todolist.items()):
             f.write("%s\n%s\n\n" % (tag, "=" * len(tag)))
@@ -85,5 +85,5 @@
             f.write("%s\n%s\n\n" % (tag, "=" * len(tag)))
-            write_info(f, info, folder)
+            write_info(f, info, folder, path_file_length)
 
 
 def scan_folder(data_tuple, dirname, names):
@@ -87,8 +87,7 @@
 
 
 def scan_folder(data_tuple, dirname, names):
-    (exts, tags, res) = data_tuple
-    file_info = {}
+    (exts, tags, res, path_file_length) = data_tuple
     for name in names:
         (root, ext) = os.path.splitext(name)
         if ext in exts:
@@ -92,9 +91,11 @@
     for name in names:
         (root, ext) = os.path.splitext(name)
         if ext in exts:
-            file_info = scan_file(os.path.join(dirname, name), tags)
-            for tag, info in file_info.items():
+            path = os.path.join(dirname, name)
+            file_info, length = scan_file(path, tags)
+            path_file_length[path] = length
+            for tag, info in list(file_info.items()):
                 if info:
                     res[tag].extend(info)
 
 
@@ -97,11 +98,12 @@
                 if info:
                     res[tag].extend(info)
 
 
-def scan_file(filename, tags):
-    res = {tag: [] for tag in tags}
+def scan_file(filename, tags) -> tuple[dict[str, list[tuple[str, int, str]]], int]:
+    res: dict[str, list[tuple[str, int, str]]] = {tag: [] for tag in tags}
+    line_num: int = 0
     with open(filename, "r") as f:
         for line_num, line in enumerate(f):
             for tag in tags:
                 if tag in line:
                     res[tag].append((filename, line_num, line[:-1].strip()))
@@ -103,9 +105,9 @@
     with open(filename, "r") as f:
         for line_num, line in enumerate(f):
             for tag in tags:
                 if tag in line:
                     res[tag].append((filename, line_num, line[:-1].strip()))
-    return res
+    return res, line_num
 
 
 if __name__ == "__main__":
diff --git a/doc/board_alerts.models.rst b/doc/board_alerts.models.rst
deleted file mode 100644
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_ZG9jL2JvYXJkX2FsZXJ0cy5tb2RlbHMucnN0..0000000000000000000000000000000000000000
--- a/doc/board_alerts.models.rst
+++ /dev/null
@@ -1,22 +0,0 @@
-odoo.addons.board_alerts.models package
-=======================================
-
-Submodules
-----------
-
-odoo.addons.board_alerts.models.board_alerts module
----------------------------------------------------
-
-.. automodule:: odoo.addons.board_alerts.models.board_alerts
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-
-Module contents
----------------
-
-.. automodule:: odoo.addons.board_alerts.models
-    :members:
-    :undoc-members:
-    :show-inheritance:
diff --git a/doc/board_alerts.rst b/doc/board_alerts.rst
deleted file mode 100644
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_ZG9jL2JvYXJkX2FsZXJ0cy5yc3Q=..0000000000000000000000000000000000000000
--- a/doc/board_alerts.rst
+++ /dev/null
@@ -1,18 +0,0 @@
-board_alerts package
-====================
-
-Subpackages
------------
-
-.. toctree::
-
-    board_alerts.models
-    board_alerts.wizard
-
-Module contents
----------------
-
-.. automodule:: odoo.addons.board_alerts
-    :members:
-    :undoc-members:
-    :show-inheritance:
diff --git a/doc/board_alerts.wizard.rst b/doc/board_alerts.wizard.rst
deleted file mode 100644
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_ZG9jL2JvYXJkX2FsZXJ0cy53aXphcmQucnN0..0000000000000000000000000000000000000000
--- a/doc/board_alerts.wizard.rst
+++ /dev/null
@@ -1,22 +0,0 @@
-odoo.addons.board_alerts.wizard package
-=======================================
-
-Submodules
-----------
-
-odoo.addons.board_alerts.wizard.board_alerts_dlg module
--------------------------------------------------------
-
-.. automodule:: odoo.addons.board_alerts.wizard.board_alerts_dlg
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-
-Module contents
----------------
-
-.. automodule:: odoo.addons.board_alerts.wizard
-    :members:
-    :undoc-members:
-    :show-inheritance:
diff --git a/doc/conf.py b/doc/conf.py
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_ZG9jL2NvbmYucHk=..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_ZG9jL2NvbmYucHk= 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -11,6 +11,10 @@
 import configparser
 import os
 import sys
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as import_version
+
+from odoo_scripts.config import Configuration
 
 import odoo
 
@@ -30,7 +34,9 @@
     "sphinx.ext.intersphinx",
     "sphinx.ext.todo",
     "sphinx.ext.coverage",
-    "sphinxodoo.ext.autodoc",
+    "sphinx.ext.graphviz",
+    "sphinx.ext.viewcode",
+    "sphinxodooautodoc.ext.autodoc",
 ]
 
 # Add any paths that contain templates here, relative to this directory.
@@ -46,7 +52,16 @@
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
-#
+
+# The full version, including alpha/beta/rc tags.
+try:
+    release = import_version("odoo-addon-board_alerts")
+    # The short X.Y version.
+    version = ".".join(release.split(".")[:2])
+except PackageNotFoundError:
+    # No version number if not installed
+    pass
+
 with open(os.path.join("..", "__manifest__.py"), "r") as f:
     read_data = f.read()
 d = ast.literal_eval(read_data)
@@ -50,10 +65,6 @@
 with open(os.path.join("..", "__manifest__.py"), "r") as f:
     read_data = f.read()
 d = ast.literal_eval(read_data)
-# The full version, including alpha/beta/rc tags.
-release = d["version"]
-# The short X.Y version.
-version = ".".join(release.split(".")[:4])
 
 # General information about the project.
 project = d["name"]
@@ -57,7 +68,7 @@
 
 # General information about the project.
 project = d["name"]
-copyright = "2016, XCG Consulting"
+copyright = "2021, 2022, 2023 XCG Consulting"
 author = d["author"]
 module_nospace = project.replace(" ", "")
 module_description = d.get("summary", "")
@@ -70,6 +81,8 @@
 # Usually you set "language" from the command line for these cases.
 language = None
 
+locale_dirs = ["locale"]
+
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 exclude_patterns = ["_build"]
@@ -90,7 +103,7 @@
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ["doc_images"]
+html_static_path = ["_static"]
 
 # Output file base name for HTML help builder.
 htmlhelp_basename = "%sdoc" % module_nospace
@@ -116,9 +129,7 @@
 
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
-man_pages = [
-    (master_doc, module_lowercase, "%s Documentation" % project, [author], 1)
-]
+man_pages = [(master_doc, module_lowercase, "%s Documentation" % project, [author], 1)]
 
 # -- Options for Texinfo output -------------------------------------------
 
@@ -141,7 +152,5 @@
 intersphinx_mapping = {"https://docs.python.org/3/": None}
 
 
-#
-# odoo-sphinx-autodoc
-#
+# -- Options for sphinx-odoo-autodoc extension ----------------------------
 
@@ -147,6 +156,6 @@
 
-# sphinxodoo_addons: List of addons name to load (if empty, no addon will be
+# sphinxodooautodoc_addons : List of addons name to load (if empty, no addon will be
 # loaded)
 this_module = os.path.basename(
     os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 )
@@ -149,9 +158,9 @@
 # loaded)
 this_module = os.path.basename(
     os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 )
-sphinxodoo_addons = [this_module]
-# sphinxodoo_root_path : Path of the Odoo root directory
-sphinxodoo_root_path = os.path.dirname(
+sphinxodooautodoc_addons = [this_module]
+# sphinxodooautodoc_root_path : Path of the Odoo root directory
+sphinxodooautodoc_root_path = os.path.dirname(
     os.path.dirname(os.path.abspath(odoo.__file__))
 )
@@ -156,6 +165,6 @@
     os.path.dirname(os.path.abspath(odoo.__file__))
 )
-# sphinxodoo_addons_path : List of paths were Odoo addons to load are located
+# sphinxodooautodoc_addons_path : List of paths were Odoo addons to load are located
 c = None
 # find setup file of superproject, if any
 directory = os.path.dirname(os.getenv("PWD"))
@@ -164,7 +173,10 @@
     if os.path.isfile(setup_path):
         c = configparser.ConfigParser()
         c.read(setup_path)
-        if not c.has_section("odoo_scripts"):
+        if c.has_section("odoo_scripts"):
+            # reload with odoo_scripts
+            c = Configuration(directory)
+        else:
             c = None
     if not c:
         if os.path.dirname(directory) != directory:
@@ -172,6 +184,6 @@
         else:
             directory = None
 
-sphinxodoo_addons_path = list()
+sphinxodooautodoc_addons_path = []
 
 if c:
@@ -176,8 +188,5 @@
 
 if c:
-    addon_dirs = set(
-        os.path.dirname(path)
-        for path in c.get("odoo_scripts", "modules").split()
-    )
+    addon_dirs = set(os.path.dirname(path) for path in c.modules)
 
     for line in addon_dirs:
@@ -182,3 +191,12 @@
 
     for line in addon_dirs:
-        sphinxodoo_addons_path.append(os.path.join(directory, line))
+        sphinxodooautodoc_addons_path.append(os.path.join(directory, line))
+else:
+    # add this directory top dir
+    sphinxodooautodoc_addons_path.append(
+        os.path.dirname(os.path.dirname(os.getenv("PWD")))
+    )
+    other_addons = os.getenv("ODOO_ADDONS_PATH", default=None)
+    if other_addons:
+        for addon_path in other_addons.split(","):
+            sphinxodooautodoc_addons_path.append(addon_path)
diff --git a/doc/index.rst b/doc/index.rst
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_ZG9jL2luZGV4LnJzdA==..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_ZG9jL2luZGV4LnJzdA== 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -1,5 +1,15 @@
-.. include:: manifest
+.. TEMPLATE
+.. |coverage| image:: .badges/coverage.svg
+    :target: https://orus.io/xcg/template/odoo_modules/-/pipelines?ref=branch/16.0
+    :alt: Coverage report
+.. |pylint| image:: .badges/pylint.svg
+    :target: https://orus.io/xcg/template/odoo_modules/-/pipelines?ref=branch/16.0
+    :alt: pylint score
+
+|coverage| |pylint|
+
+.. include:: README.rst
 
 Contents:
 
 .. toctree::
@@ -2,6 +12,6 @@
 
 Contents:
 
 .. toctree::
-   :maxdepth: 2
+   :maxdepth: 4
 
@@ -7,5 +17,4 @@
 
-   README
    modules
    NEWS
    TODO
@@ -16,4 +25,3 @@
 * :ref:`genindex`
 * :ref:`modindex`
 * :ref:`search`
-
diff --git a/doc/manifest.py b/doc/manifest.py
deleted file mode 100755
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_ZG9jL21hbmlmZXN0LnB5..0000000000000000000000000000000000000000
--- a/doc/manifest.py
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/usr/bin/env python3
-###############################################################################
-#
-#    OpenERP, Open Source Management Solution
-#    Copyright (C) 2016, 2018 XCG Consulting (http://www.xcg-consulting.fr/)
-#
-#    This program 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.
-#
-#    This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-###############################################################################
-import os
-
-from odoo.modules import load_information_from_description_file
-
-
-def main():
-    module = os.path.basename(
-        os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
-    )
-    d = load_information_from_description_file(module)
-    with open("manifest", "w") as out:
-        manifest_content = (
-            d["description"]
-            if "description" in d
-            else d["summary"]
-            if "summary" in d
-            else ""
-        )
-        out.write(manifest_content)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/doc/modules.rst b/doc/modules.rst
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_ZG9jL21vZHVsZXMucnN0..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_ZG9jL21vZHVsZXMucnN0 100644
--- a/doc/modules.rst
+++ b/doc/modules.rst
@@ -1,6 +1,15 @@
-board_alerts
-============
+.. TEMPLATE This file is generated by running `sphinx-apidoc -o doc . -f`
+   Then add the full path to the module package and functions by using something like (need zsh to work due to the substitution)
+   module_name=$(basename $PWD)
+   find doc \( -name "${module_name}*.rst" -or -name modules.rst \) -exec sed -i -e "s,${module_name:gs/_/\\\\\\?_/},odoo.addons.\0,g" {} +
+   find doc -name "${module_name}*.rst" -printf "%f\n" | xargs -I{} mv doc/{} doc/odoo.addons.{}
+   Titles need to be manually updated after the previous command.
+   Then add the new files to mercurial with something like:
+   hg add doc/modules.rst doc/odoo.addons.${module_name}*.rst
+
+odoo_module
+===========
 
 .. toctree::
    :maxdepth: 4
 
@@ -3,5 +12,5 @@
 
 .. toctree::
    :maxdepth: 4
 
-   board_alerts
+   odoo_module
diff --git a/i18n/fr.po b/i18n/fr.po
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_aTE4bi9mci5wbw==..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_aTE4bi9mci5wbw== 100644
--- a/i18n/fr.po
+++ b/i18n/fr.po
@@ -6,8 +6,8 @@
 msgstr ""
 "Project-Id-Version: Odoo Server 13.0\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-06-12 07:40+0000\n"
-"PO-Revision-Date: 2020-06-12 09:42+0200\n"
+"POT-Creation-Date: 2024-01-04 09:09+0000\n"
+"PO-Revision-Date: 2024-01-04 09:16+0000\n"
 "Last-Translator: Houzéfa Abbasbhay <houzefa.abba@xcg-consulting.fr>\n"
 "Language-Team: XCG Consulting\n"
 "Language: fr\n"
@@ -15,7 +15,7 @@
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
-"X-Generator: Poedit 2.0.6\n"
+"X-Generator: Poedit 3.4.2\n"
 
 #. module: board_alerts
 #: model:mail.template,body_html:board_alerts.board_alerts_mail_template
@@ -34,5 +34,5 @@
 "            "
 
 #. module: board_alerts
-#. openerp-web
+#. odoo-javascript
 #: code:addons/board_alerts/static/src/js/alert_board.js:0
@@ -38,6 +38,7 @@
 #: code:addons/board_alerts/static/src/js/alert_board.js:0
-#, python-format
-msgid "'%s' added to alert dashboard"
+#, fuzzy, python-format
+#| msgid "'%s' added to alert dashboard"
+msgid "\"%s\" added to alert dashboard"
 msgstr "'%s' ajouté au tableau de bord des alertes"
 
 #. module: board_alerts
@@ -41,10 +42,10 @@
 msgstr "'%s' ajouté au tableau de bord des alertes"
 
 #. module: board_alerts
-#. openerp-web
+#. odoo-javascript
 #: code:addons/board_alerts/static/src/xml/alert_board.xml:0
 #, python-format
 msgid "Add"
 msgstr "Ajouter"
 
 #. module: board_alerts
@@ -45,13 +46,13 @@
 #: code:addons/board_alerts/static/src/xml/alert_board.xml:0
 #, python-format
 msgid "Add"
 msgstr "Ajouter"
 
 #. module: board_alerts
-#. openerp-web
+#. odoo-javascript
 #: code:addons/board_alerts/static/src/xml/alert_board.xml:0
 #, python-format
 msgid "Add to my alert dashboard"
 msgstr "Ajouter à mon tableau d'alertes"
 
 #. module: board_alerts
@@ -52,9 +53,14 @@
 #: code:addons/board_alerts/static/src/xml/alert_board.xml:0
 #, python-format
 msgid "Add to my alert dashboard"
 msgstr "Ajouter à mon tableau d'alertes"
 
 #. module: board_alerts
+#: model:mail.template,name:board_alerts.board_alerts_mail_template
+msgid "Board alert email"
+msgstr "E-mail d'alerte du tableau de bord"
+
+#. module: board_alerts
 #: model:ir.model,name:board_alerts.model_board_alerts_dlg
 msgid "Board alert sender dialog box"
 msgstr "Boîte de dialogue d'envoi d'alertes de tableau de bord"
@@ -62,7 +68,6 @@
 #. module: board_alerts
 #: model:ir.actions.server,name:board_alerts.board_alerts_cron_task_ir_actions_server
 #: model:ir.cron,cron_name:board_alerts.board_alerts_cron_task
-#: model:ir.cron,name:board_alerts.board_alerts_cron_task
 msgid "Board alerts - Daily emails"
 msgstr "Tableau de bord d'alertes - Emails quotidiens"
 
@@ -72,7 +77,7 @@
 msgstr "Annuler"
 
 #. module: board_alerts
-#. openerp-web
+#. odoo-javascript
 #: code:addons/board_alerts/static/src/js/alert_board.js:0
 #, python-format
 msgid "Could not add filter to alert dashboard"
@@ -99,6 +104,7 @@
 msgstr "ID"
 
 #. module: board_alerts
+#. odoo-python
 #: code:addons/board_alerts/models/board_alerts.py:0
 #, python-format
 msgid "Lang %s not found"
@@ -120,6 +126,7 @@
 msgstr "Modifié le"
 
 #. module: board_alerts
+#. odoo-python
 #: code:addons/board_alerts/models/board_alerts.py:0
 #: model:ir.actions.act_window,name:board_alerts.action_alert_board
 #: model:ir.ui.menu,name:board_alerts.menu_alert_board
@@ -129,9 +136,10 @@
 msgstr "Mes alertes"
 
 #. module: board_alerts
+#. odoo-python
 #: code:addons/board_alerts/models/board_alerts.py:0
 #, python-format
 msgid "No"
 msgstr "Non"
 
 #. module: board_alerts
@@ -132,10 +140,10 @@
 #: code:addons/board_alerts/models/board_alerts.py:0
 #, python-format
 msgid "No"
 msgstr "Non"
 
 #. module: board_alerts
-#. openerp-web
+#. odoo-javascript
 #: code:addons/board_alerts/static/src/js/alert_board.js:0
 #, python-format
 msgid "Please refresh your browser for the changes to take effect."
@@ -155,8 +163,8 @@
 
 #. module: board_alerts
 #: model:ir.model,name:board_alerts.model_res_users
-msgid "Users"
-msgstr "Utilisateurs"
+msgid "User"
+msgstr "Utilisateur"
 
 #. module: board_alerts
 #: model_terms:ir.ui.view,arch_db:board_alerts.board_alerts_dlg
@@ -168,6 +176,7 @@
 "les alertes des tableaux\"."
 
 #. module: board_alerts
+#. odoo-python
 #: code:addons/board_alerts/models/board_alerts.py:0
 #, python-format
 msgid "Yes"
diff --git a/models/__init__.py b/models/__init__.py
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_bW9kZWxzL19faW5pdF9fLnB5..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_bW9kZWxzL19faW5pdF9fLnB5 100644
--- a/models/__init__.py
+++ b/models/__init__.py
@@ -1,3 +1,1 @@
-# flake8: noqa
-
 from . import board_alerts
diff --git a/models/board_alerts.py b/models/board_alerts.py
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_bW9kZWxzL2JvYXJkX2FsZXJ0cy5weQ==..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_bW9kZWxzL2JvYXJkX2FsZXJ0cy5weQ== 100644
--- a/models/board_alerts.py
+++ b/models/board_alerts.py
@@ -32,7 +32,6 @@
 
     @api.model
     def send_board_alerts(self):
-        """Find users and send them their board alerts.
-        """
+        """Find users and send them their board alerts."""
 
         # Get our email template, referenced by its XML ID.
@@ -37,8 +36,6 @@
 
         # Get our email template, referenced by its XML ID.
-        email_template = self.sudo().env.ref(
-            "board_alerts.board_alerts_mail_template"
-        )
+        email_template = self.sudo().env.ref("board_alerts.board_alerts_mail_template")
 
         # Loop through all users; send them an email.
         for user in self.sudo().search([]):
@@ -50,9 +47,9 @@
 
             # Fill the context to avoid computing contents twice.
             (
-                email_template.with_context(
-                    board_alert_contents=contents
-                ).send_mail(user.id)
+                email_template.with_context(board_alert_contents=contents).send_mail(
+                    user.id
+                )
             )
 
     def get_board_alert_contents(self):
@@ -83,9 +80,7 @@
         board_view = self.env.ref("board_alerts.alert_board")
 
         # Set up the link that will be inserted in emails.
-        board_link = (
-            self.env["ir.config_parameter"].sudo().get_param("web.base.url")
-        )
+        board_link = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
         if board_link:
             board_link += "/?db=%s#action=%s" % (
                 self.env.cr.dbname,
@@ -93,7 +88,7 @@
             )
 
         # Get the "custom view" representing the board.
-        board = self.env["board.board"].fields_view_get(view_id=board_view.id)
+        board = self.env["board.board"].get_view(view_id=board_view.id)
 
         to_send = []
 
@@ -132,7 +127,7 @@
                 )
                 .id
             )
-            act_view = act_model.with_context(act_context).fields_view_get(
+            act_view = act_model.with_context(act_context).get_view(
                 view_id=act_view_id, view_type=view_type
             )
 
@@ -194,12 +189,10 @@
         for data_title, data in data_list:
             frame = etree.SubElement(root, "div")
             frame.attrib["style"] = (
-                "border: 1px solid LightGray;"
-                "margin-top: 8px;"
-                "padding: 8px;"
+                "border: 1px solid LightGray;" "margin-top: 8px;" "padding: 8px;"
             )
 
             title = etree.SubElement(frame, "h3")
             title.text = data_title or ""
 
             table = etree.SubElement(frame, "table")
@@ -200,12 +193,10 @@
             )
 
             title = etree.SubElement(frame, "h3")
             title.text = data_title or ""
 
             table = etree.SubElement(frame, "table")
-            table.attrib["style"] = (
-                "border-collapse: collapse;" "border-spacing: 2px;"
-            )
+            table.attrib["style"] = "border-collapse: collapse;" "border-spacing: 2px;"
 
             first_record = True
 
@@ -236,11 +227,7 @@
 
         # The user object only has a "lang" selection key; find the actual
         # language object.
-        lang = (
-            self.sudo()
-            .env["res.lang"]
-            .search([("code", "=", self.lang)], limit=1)
-        )
+        lang = self.sudo().env["res.lang"].search([("code", "=", self.lang)], limit=1)
         if not lang:
             raise exceptions.UserError(_("Lang %s not found") % self.lang)
 
@@ -251,8 +238,7 @@
                 "allowed_company_ids": self.company_ids.ids,
                 # ---
                 "date_format": lang.date_format,
-                "datetime_format": "%s %s"
-                % (lang.date_format, lang.time_format),
+                "datetime_format": "%s %s" % (lang.date_format, lang.time_format),
                 "lang": self.lang,
                 "tz": self.tz,
                 "uid": self.id,
@@ -337,8 +323,7 @@
         return content or ""
 
     def _get_object_name(self, content):
-        """Call the "name_get" function of the specified Odoo record set.
-        """
+        """Call the "name_get" function of the specified Odoo record set."""
 
         # 0: first element of the returned list.
         # 1: second element of the (ID, name) tuple.
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_cHlwcm9qZWN0LnRvbWw=
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,78 @@
+[project]
+name = "odoo-addon-board_alerts"
+dynamic = ["version"]
+readme = "README.rst"
+requires-python = "~=3.10"
+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.10",
+  "Framework :: Odoo",
+  "Framework :: Odoo :: 16.0",
+  "License :: OSI Approved :: GNU Affero General Public License v3"
+]
+dependencies = ["odoo==16.0.*"]
+
+[project.optional-dependencies]
+doc = ["sphinx", "sphinx-odoo-autodoc", "odoo-scripts"]
+test = []
+
+[project.urls]
+repository = "https://orus.io/xcg/odoo-modules/board_alerts"
+changelog = "https://orus.io/xcg/odoo-module/board_alerts/-/blob/branch/16.0/NEWS.rst"
+
+[build-system]
+requires = ["hatchling >=1.19", "hatch-vcs"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build]
+exclude = [
+  "/doc/",
+  "/.editorconfig",
+  "/.eslintrc.yml",
+  "/.flake8",
+  "/.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/board_alerts"
+
+[tool.hatch.version]
+source = "vcs"
+
+[tool.black]
+target = 3.10
+
+[tool.isort]
+py_version = 310
+profile = "black"
+known_odoo = ['odoo']
+known_odoo_addons = ['odoo.addons']
+sections = [
+  'FUTURE',
+  'STDLIB',
+  'THIRDPARTY',
+  'ODOO',
+  'ODOO_ADDONS',
+  'FIRSTPARTY',
+  'LOCALFOLDER'
+]
diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv
new file mode 100644
index 0000000000000000000000000000000000000000..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_c2VjdXJpdHkvaXIubW9kZWwuYWNjZXNzLmNzdg==
--- /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
+access_board_alerts_dlg,board_alerts_dlg,model_board_alerts_dlg,base.group_user,1,1,1,1
diff --git a/static/src/js/alert_board.js b/static/src/js/alert_board.js
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_c3RhdGljL3NyYy9qcy9hbGVydF9ib2FyZC5qcw==..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_c3RhdGljL3NyYy9qcy9hbGVydF9ib2FyZC5qcw== 100644
--- a/static/src/js/alert_board.js
+++ b/static/src/js/alert_board.js
@@ -1,3 +1,13 @@
+/** @odoo-module **/
+
+import {Dropdown} from "@web/core/dropdown/dropdown";
+import {registry} from "@web/core/registry";
+import {useAutofocus, useService} from "@web/core/utils/hooks";
+import {sprintf} from "@web/core/utils/strings";
+
+const {Component, useState} = owl;
+const favoriteMenuRegistry = registry.category("favoriteMenu");
+
 /* Alert board. */
 
 /* Taken from the default "My board" dashboard.
@@ -6,17 +16,9 @@
  * Adapted to add an "Add to my alert board" menu command.
  */
 
-odoo.define('board_alerts.AddToAlertBoardMenu', function(require) {
-    "use strict";
-
-    var ActionManager = require('web.ActionManager');
-    var Context = require('web.Context');
-    var core = require('web.core');
-    var Domain = require('web.Domain');
-    var favorites_submenus_registry = require('web.favorites_submenus_registry');
-    var pyUtils = require('web.py_utils');
-    var Widget = require('web.Widget');
-
-    var _t = core._t;
-    var QWeb = core.qweb;
+export class AddToAlertBoard extends Component {
+    setup() {
+        this.notification = useService("notification");
+        this.rpc = useService("rpc");
+        this.state = useState({name: this.env.config.getDisplayName()});
 
@@ -22,28 +24,4 @@
 
-    var AddToAlertBoardMenu = Widget.extend({
-    events: _.extend({}, Widget.prototype.events, {
-    'click .o_add_to_alert_board.o_menu_header': '_onAlertMenuHeaderClick',
-    'click .o_add_to_alert_board_confirm_button': '_onAddToAlertBoardConfirmButtonClick',
-    'click .o_add_to_alert_board_input': '_onAddToAlertBoardInputClick',
-    'keyup .o_add_to_alert_board_input': '_onAlertKeyUp',
-    }),
-    /**
-     * @override
-     * @param {Object} params
-     * @param {Object} params.action an ir.actions description
-     */
-    init: function(parent, params) {
-        this._super(parent);
-        this.action = params.action;
-        this.isOpen = false;
-    },
-    /**
-     * @override
-     */
-    start: function() {
-        if (this.action.id && this.action.type === 'ir.actions.act_window') {
-            this._render();
-        }
-        return this._super.apply(this, arguments);
-    },
+        useAutofocus();
+    }
 
@@ -49,5 +27,5 @@
 
-    //--------------------------------------------------------------------------
-    // Public
-    //--------------------------------------------------------------------------
+    //---------------------------------------------------------------------
+    // Protected
+    //---------------------------------------------------------------------
 
@@ -53,14 +31,14 @@
 
-    /**
-     * Closes the menu and render it.
-     * 
-     */
-    closeMenu: function() {
-        this.isOpen = false;
-        this._render();
-    },
-
-    //--------------------------------------------------------------------------
-    // Private
-    //--------------------------------------------------------------------------
+    async addToAlertBoard() {
+        const {domain, globalContext} = this.env.searchModel;
+        const {comparison, context, groupBys, orderBy} =
+            this.env.searchModel.getPreFavoriteValues();
+        const contextToSave = {
+            ...globalContext,
+            ...context,
+            comparison,
+            orderedBy: orderBy,
+            group_by: groupBys,
+            dashboard_merge_domains_contexts: false,
+        };
 
@@ -66,42 +44,9 @@
 
-    /**
-     * This is the main function for actually saving the dashboard. This method is supposed to call
-     * the route /board/add_to_alert_dashboard with proper information.
-     * 
-     * @private
-     * @returns {Promise}
-     */
-    _addToAlertBoard: function() {
-        var self = this;
-        var searchQuery;
-        // TO DO: for now the domains in query are evaluated.
-        // This should be changed I think.
-        this.trigger_up('get_search_query', {
-            callback: function(query) {
-                searchQuery = query;
-            }
-        });
-        // TO DO: replace direct reference to action manager, controller, and currentAction in code below
-
-        // AAB: trigger_up an event that will be intercepted by the controller,
-        // as soon as the controller is the parent of the control panel
-        var actionManager = this.findAncestor(function(ancestor) {
-            return ancestor instanceof ActionManager;
-        });
-        var controller = actionManager.getCurrentController();
-
-        var context = new Context(this.action.context);
-        context.add(searchQuery.context);
-        context.add({
-        group_by: searchQuery.groupBy,
-        orderedBy: searchQuery.orderedBy,
-        });
-
-        this.trigger_up('get_controller_query_params', {
-            callback: function(controllerQueryParams) {
-                var queryContext = controllerQueryParams.context;
-                var allContext = _.extend(_.omit(controllerQueryParams, ['context']), queryContext);
-                context.add(allContext);
-            }
+        const result = await this.rpc("/board/add_to_alert_dashboard", {
+            action_id: this.env.config.actionId || false,
+            context_to_save: contextToSave,
+            domain,
+            name: this.state.name,
+            view_mode: this.env.config.viewType,
         });
 
@@ -106,11 +51,24 @@
         });
 
-        var domain = new Domain(this.action.domain || []);
-        domain = Domain.prototype.normalizeArray(domain.toArray().concat(searchQuery.domain));
-
-        var evalutatedContext = pyUtils.eval('context', context);
-        for ( var key in evalutatedContext) {
-            if (evalutatedContext.hasOwnProperty(key) && /^search_default_/.test(key)) {
-                delete evalutatedContext[key];
-            }
+        if (result) {
+            this.notification.add(
+                this.env._t(
+                    "Please refresh your browser for the changes to take effect."
+                ),
+                {
+                    title: sprintf(
+                        this.env._t(`"%s" added to alert dashboard`),
+                        this.state.name
+                    ),
+                    type: "warning",
+                }
+            );
+            this.state.name = this.env.config.getDisplayName();
+        } else {
+            this.notification.add(
+                this.env._t("Could not add filter to alert dashboard"),
+                {
+                    type: "danger",
+                }
+            );
         }
@@ -116,7 +74,3 @@
         }
-        evalutatedContext.dashboard_merge_domains_contexts = false;
-
-        var name = this.$input.val();
-
-        this.closeMenu();
+    }
 
@@ -122,46 +76,3 @@
 
-        return self._rpc({
-        route: '/board/add_to_alert_dashboard',
-        params: {
-        action_id: self.action.id || false,
-        context_to_save: evalutatedContext,
-        domain: domain,
-        view_mode: controller.viewType,
-        name: name,
-        },
-        }).then(function(r) {
-            if (r) {
-                self.do_notify(_.str.sprintf(_t("'%s' added to alert dashboard"), name), _t('Please refresh your browser for the changes to take effect.'));
-            } else {
-                self.do_warn(_t("Could not add filter to alert dashboard"));
-            }
-        });
-    },
-    /**
-     * Renders and focuses the unique input if it is visible.
-     * 
-     * @private
-     */
-    _render: function() {
-        var $el = QWeb.render('AddToAlertBoardMenu', {
-            widget: this
-        });
-        this._replaceElement($el);
-        if (this.isOpen) {
-            this.$input = this.$('.o_add_to_alert_board_input');
-            this.$input.val(this.action.name);
-            this.$input.focus();
-        }
-    },
-    /**
-     * Hides and displays the submenu which allows adding custom filters.
-     * 
-     * @private
-     */
-    _toggleMenu: function() {
-        this.isOpen = !this.isOpen;
-        this._render();
-    },
-
-    //--------------------------------------------------------------------------
+    //---------------------------------------------------------------------
     // Handlers
@@ -167,4 +78,4 @@
     // Handlers
-    //--------------------------------------------------------------------------
+    //---------------------------------------------------------------------
 
     /**
@@ -169,23 +80,4 @@
 
     /**
-     * @private
-     * @param {jQueryEvent} event
-     */
-    _onAddToAlertBoardInputClick: function(event) {
-        event.preventDefault();
-        event.stopPropagation();
-        this.$input.focus();
-    },
-    /**
-     * @private
-     * @param {jQueryEvent} event
-     */
-    _onAddToAlertBoardConfirmButtonClick: function(event) {
-        event.preventDefault();
-        event.stopPropagation();
-        this._addToAlertBoard();
-    },
-    /**
-     * @private
-     * @param {jQueryEvent} event
+     * @param {KeyboardEvent} ev
      */
@@ -191,5 +83,6 @@
      */
-    _onAlertKeyUp: function(event) {
-        if (event.which === $.ui.keyCode.ENTER) {
-            this._addToAlertBoard();
+    onInputKeydown(ev) {
+        if (ev.key === "Enter") {
+            ev.preventDefault();
+            this.addToAlertBoard();
         }
@@ -195,12 +88,7 @@
         }
-    },
-    /**
-     * @private
-     * @param {jQueryEvent} event
-     */
-    _onAlertMenuHeaderClick: function(event) {
-        event.preventDefault();
-        event.stopPropagation();
-        this._toggleMenu();
-    },
+    }
+}
+
+AddToAlertBoard.template = "board_alerts.AddToAlertBoard";
+AddToAlertBoard.components = {Dropdown};
 
@@ -206,3 +94,8 @@
 
-    });
+export const addToAlertBoardItem = {
+    Component: AddToAlertBoard,
+    groupNumber: 4,
+    isDisplayed: ({config}) =>
+        config.actionType === "ir.actions.act_window" && config.actionId,
+};
 
@@ -208,6 +101,2 @@
 
-    // Use prio=11 here so this comes right after the base "add_to_board_menu" which has prio=10.
-    favorites_submenus_registry.add('add_to_alert_board_menu', AddToAlertBoardMenu, 11);
-
-    return AddToAlertBoardMenu;
-});
+favoriteMenuRegistry.add("add-to-board-alerts", addToAlertBoardItem, {sequence: 11});
diff --git a/static/src/xml/alert_board.xml b/static/src/xml/alert_board.xml
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_c3RhdGljL3NyYy94bWwvYWxlcnRfYm9hcmQueG1s..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_c3RhdGljL3NyYy94bWwvYWxlcnRfYm9hcmQueG1s 100644
--- a/static/src/xml/alert_board.xml
+++ b/static/src/xml/alert_board.xml
@@ -1,1 +1,2 @@
+<?xml version="1.0" encoding="UTF-8" ?>
 <!-- QWeb template for the alert board. -->
@@ -1,19 +2,26 @@
 <!-- QWeb template for the alert board. -->
-
-<template>
-
-    <!-- Include a link in the "Favorites" menu to add to the alert dashboard. Same
-        as the default "Add to my dashboard" link defined in
-        odoo/addons/board/static/src/xml/board.xml. -->
-    <div t-name="AddToAlertBoardMenu">
-        <button type="button" class="dropdown-item o_add_to_alert_board o_menu_header">Add to my alert dashboard</button>
-        <div t-if="widget.isOpen" class="dropdown-item-text o_add_to_alert_board">
-            <input class="o_input o_add_to_alert_board_input" type="text" />
-        </div>
-        <div t-if="widget.isOpen" class="dropdown-item-text o_add_to_alert_board">
-            <button type="button"
-                class="btn btn-primary o_add_to_alert_board_confirm_button">Add</button>
-        </div>
-    </div>
-
-</template>
+<templates xml:space="preserve">
+    <t t-name="board_alerts.AddToAlertBoard" owl="1">
+        <Dropdown class="'o_add_to_board'">
+            <t t-set-slot="toggler">Add to my alert dashboard</t>
+            <div class="px-3 py-2">
+                <input
+                    type="text"
+                    class="o_input"
+                    t-ref="autofocus"
+                    t-model.trim="state.name"
+                    t-on-keydown="onInputKeydown"
+                />
+            </div>
+            <div class="px-3 py-2">
+                <button
+                    type="button"
+                    class="btn btn-primary"
+                    t-on-click="addToAlertBoard"
+                >
+                    Add
+                </button>
+            </div>
+        </Dropdown>
+    </t>
+</templates>
diff --git a/views/board_alerts_assets.xml b/views/board_alerts_assets.xml
deleted file mode 100644
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_dmlld3MvYm9hcmRfYWxlcnRzX2Fzc2V0cy54bWw=..0000000000000000000000000000000000000000
--- a/views/board_alerts_assets.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-
-    <!-- Include assets used by alert boards. -->
-
-    <template id="assets_backend" name="board_alerts assets"
-        inherit_id="web.assets_backend">
-        <xpath expr="." position="inside">
-            <link rel="stylesheet" type="text/scss"
-                href="/board_alerts/static/src/scss/alert_board.scss" />
-            <script type="text/javascript" src="/board_alerts/static/src/js/alert_board.js"></script>
-        </xpath>
-    </template>
-
-</odoo>
diff --git a/wizard/__init__.py b/wizard/__init__.py
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_d2l6YXJkL19faW5pdF9fLnB5..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_d2l6YXJkL19faW5pdF9fLnB5 100644
--- a/wizard/__init__.py
+++ b/wizard/__init__.py
@@ -1,3 +1,1 @@
-# flake8: noqa
-
 from . import board_alerts_dlg
diff --git a/wizard/board_alerts_dlg.py b/wizard/board_alerts_dlg.py
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_d2l6YXJkL2JvYXJkX2FsZXJ0c19kbGcucHk=..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_d2l6YXJkL2JvYXJkX2FsZXJ0c19kbGcucHk= 100644
--- a/wizard/board_alerts_dlg.py
+++ b/wizard/board_alerts_dlg.py
@@ -10,8 +10,7 @@
     _description = "Board alert sender dialog box"
 
     def send_board_alerts(self):
-        """Send board alerts then show emails.
-        """
+        """Send board alerts then show emails."""
 
         self.env["res.users"].send_board_alerts()
 
diff --git a/wizard/board_alerts_dlg.xml b/wizard/board_alerts_dlg.xml
index e2623afff2ec70097927cb98e2dc22e2e44f48a4_d2l6YXJkL2JvYXJkX2FsZXJ0c19kbGcueG1s..1d0c80b8baa395de2b880e3cccc3b03cfb3468b0_d2l6YXJkL2JvYXJkX2FsZXJ0c19kbGcueG1s 100644
--- a/wizard/board_alerts_dlg.xml
+++ b/wizard/board_alerts_dlg.xml
@@ -1,2 +1,2 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8" ?>
 <odoo>
@@ -2,5 +2,4 @@
 <odoo>
-
     <!-- Dialog shown before manually sending board alert emails, to have some
         kind of confirmation. -->
 
@@ -9,6 +8,5 @@
         <field name="model">board_alerts_dlg</field>
         <field name="arch" type="xml">
             <form>
-
                 <footer>
                     <separator
@@ -13,4 +11,5 @@
                 <footer>
                     <separator
-                        string="Warning! Emails will be sent when clicking on the &quot;Send board alerts&quot; button." />
+                        string="Warning! Emails will be sent when clicking on the &quot;Send board alerts&quot; button."
+                    />
 
@@ -16,5 +15,9 @@
 
-                    <button string="Send board alerts" name="send_board_alerts"
-                        type="object" class="oe_highlight" />
+                    <button
+                        string="Send board alerts"
+                        name="send_board_alerts"
+                        type="object"
+                        class="oe_highlight"
+                    />
                     <button string="Cancel" class="oe_link" special="cancel" />
                 </footer>
@@ -19,6 +22,5 @@
                     <button string="Cancel" class="oe_link" special="cancel" />
                 </footer>
-
             </form>
         </field>
     </record>
@@ -32,7 +34,11 @@
         <field name="target">new</field>
     </record>
 
-    <menuitem id="menu_board_alerts_dlg" action="action_board_alerts_dlg"
-        parent="base.menu_reporting_config" sequence="10" groups="base.group_no_one" />
-
+    <menuitem
+        id="menu_board_alerts_dlg"
+        action="action_board_alerts_dlg"
+        parent="spreadsheet_dashboard.spreadsheet_dashboard_menu_configuration"
+        sequence="20"
+        groups="base.group_no_one"
+    />
 </odoo>