From 1fa0ea32bb85638a9de7ff4e6db912eff5cc39b4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9mi=20-=20Le=20Filament?= <remi@le-filament.com>
Date: Fri, 16 May 2025 14:20:32 +0200
Subject: [PATCH] [MIG] Migration of dashboard to OWL in v18

---
 __manifest__.py                               |  15 +-
 models/lefilament_tdb.py                      | 256 +++++++++++-
 static/src/components/dashboard_detail.esm.js | 105 +++++
 static/src/components/dashboard_detail.xml    | 193 +++++++++
 .../src/components/dashboard_overview.esm.js  | 345 +++++++++++++++
 static/src/components/dashboard_overview.xml  | 394 ++++++++++++++++++
 static/src/components/lefilament_tdb.css      |  43 ++
 views/lefilament_dashboard.xml                |  11 +
 views/menus.xml                               |  21 +
 views/res_company.xml                         |   1 +
 10 files changed, 1369 insertions(+), 15 deletions(-)
 create mode 100644 static/src/components/dashboard_detail.esm.js
 create mode 100644 static/src/components/dashboard_detail.xml
 create mode 100644 static/src/components/dashboard_overview.esm.js
 create mode 100644 static/src/components/dashboard_overview.xml
 create mode 100644 static/src/components/lefilament_tdb.css

diff --git a/__manifest__.py b/__manifest__.py
index 002e6a2..5b503d3 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -8,7 +8,13 @@
     "website": "https://le-filament.com",
     "version": "18.0.1.0.0",
     "license": "AGPL-3",
-    "depends": ["account_usability", "crm", "hr_expense", "sale"],
+    "depends": [
+        "account_usability",
+        "crm",
+        "hr_expense",
+        "hr_holidays",
+        "lefilament_sales",
+    ],
     "data": [
         # security
         "security/lefilament_dashboard_security.xml",
@@ -21,4 +27,11 @@
         # menus
         "views/menus.xml",
     ],
+    "assets": {
+        "web.assets_backend": [
+            "lefilament_tdb/static/src/components/*.js",
+            "lefilament_tdb/static/src/components/*.xml",
+            "lefilament_tdb/static/src/components/lefilament_tdb.css",
+        ],
+    },
 }
diff --git a/models/lefilament_tdb.py b/models/lefilament_tdb.py
index 8dc6326..69a16a3 100644
--- a/models/lefilament_tdb.py
+++ b/models/lefilament_tdb.py
@@ -4,9 +4,9 @@
 from datetime import date
 
 from dateutil.relativedelta import relativedelta
+from psycopg2._psycopg import AsIs
 
 from odoo import api, fields, models
-from odoo.tools.safe_eval import safe_eval
 
 
 class LeFilamentTdb(models.Model):
@@ -147,19 +147,6 @@ class LeFilamentTdb(models.Model):
     # ------------------------------------------------------
     # Action buttons
     # ------------------------------------------------------
-    @api.model
-    def open_detail(self, target_model, name=None, domain=None, views=None):
-        action = {
-            "name": name or target_model,
-            "type": "ir.actions.act_window",
-            "res_model": target_model,
-        }
-        if domain:
-            action["domain"] = safe_eval(domain)
-        if views:
-            action["views"] = safe_eval(views)
-
-        return action
 
     # ------------------------------------------------------
     # Business function
@@ -167,3 +154,244 @@ class LeFilamentTdb(models.Model):
     def get_month_values(self):
         for data in self:
             data._compute_dashboard_values()
+
+    @staticmethod
+    def _format_monetary(amount):
+        return f"{amount:,}".replace(",", " ") + " €"
+
+    @api.model
+    def dashboard_detail_values(self, date_start=None, date_end=None):
+        customer = self._customer_detail(date_start, date_end)
+        employee_time = self._employee_time(date_start, date_end)
+        return {
+            "customer": customer,
+            "customer_totals": self._compute_totals(customer),
+            "employee_time": employee_time,
+            "employee_time_totals": self._compute_totals(employee_time),
+        }
+
+    def _customer_detail(self, date_start=None, date_end=None):
+        clause = "where 1=1 "
+        if date_start:
+            clause += f"and line_date >= '{date_start}' "
+        if date_end:
+            clause += f"and line_date <= '{date_end}' "
+
+        query = """
+        select
+            customer as "Client",
+            sum(prod) as "Imput.",
+            sum(invoiced + purchase + expense)::int as "Balance Prod",
+            sum(invoiced + purchase + expense)/NULLIF(sum(prod), 0) as "Taux Horaire",
+            sum(invoiced + invoiced_mco) as "Tot. Fact.",
+            sum(invoiced)::int as "Fact. Prod",
+            sum(invoiced_mco)::int as "Fact. Maint",
+            sum(purchase)::int as "Achats",
+            sum(expense)::int as "NdF"
+        from
+        (
+            -- Sélection des heures
+            select
+                aal.date as line_date,
+                p.name as customer,
+                -- contact != Filament et projet != Maintenance et pas flagué vacances
+                case when aal.partner_id != 1 or aal.partner_id is null
+                    and aal.holiday_id is null and project_id != 19
+                    then unit_amount else 0 end as prod,
+                0 as invoiced,
+                0 as invoiced_mco,
+                0 as purchase,
+                0 as expense
+            from
+                account_analytic_line aal
+            left join
+                res_partner p on aal.partner_id = p.id
+            left join
+                hr_leave l on aal.holiday_id = l.id
+            left join
+                hr_leave_type lt on l.holiday_status_id = lt.id
+            where
+                aal.project_id is not null
+                and aal.date <= CURRENT_DATE
+                and (lt.active is true or lt.active is null)
+                and partner_id != 1
+
+          -- Sélection du facturé hors maintenance
+          union all
+            select
+                aml.date as line_date,
+                p.name as customer,
+                0 as prod,
+                (aml.credit - aml.debit) as invoiced,
+                0 as invoiced_mco,
+                0 as purchase,
+                0 as expense
+            from account_move_line aml
+            left join account_move i on aml.move_id = i.id
+            left join res_partner p on i.beneficiary_id = p.id
+            where
+                i.move_type in ('out_invoice', 'out_refund')
+                and i.state = 'posted'
+                and aml.product_id is not null
+                and aml.product_id not in (33, 34, 50, 51, 61, 62)
+
+          -- Sélection du facturé Maintenance
+          union all
+            select
+                aml.date as line_date,
+                p.name as customer,
+                0 as prod,
+                0 as invoiced,
+                (aml.credit - aml.debit) as invoiced_mco,
+                0 as purchase,
+                0 as expense
+            from account_move_line aml
+            left join account_move i on aml.move_id = i.id
+            left join res_partner p on i.beneficiary_id = p.id
+            where
+                i.move_type in ('out_invoice', 'out_refund')
+                and i.state = 'posted'
+                and aml.product_id is not null
+                and aml.product_id in (33, 34, 50, 51, 61, 62)
+
+          -- Sélection des charges
+          union all
+            select
+                aal.date as line_date,
+                p.name as customer,
+                0 as prod,
+                0 as invoiced,
+                0 as invoiced_mco,
+                amount as purchase,
+                0 as expense
+            from account_analytic_line aal
+            left join account_move_line aml on aal.move_line_id = aml.id
+            left join account_move i on aml.move_id = i.id
+            left join account_analytic_account a on aal.account_id = a.id
+            left join res_partner p on a.partner_id = p.id
+            where
+                (a.plan_id is null or a.plan_id = 1)
+                and i.state = 'posted'
+                and aml.journal_id = 2
+
+          -- Sélection des NDF
+          union all
+            select
+                aal.date as line_date,
+                p.name as customer,
+                0 as prod,
+                0 as invoiced,
+                0 as invoiced_mco,
+                0 as purchase,
+                amount as expense
+            from account_analytic_line aal
+            left join account_move_line aml on aal.move_line_id = aml.id
+            left join account_move i on aml.move_id = i.id
+            left join account_analytic_account a on aal.account_id = a.id
+            left join res_partner p on a.partner_id = p.id
+            where
+                (a.plan_id is null or a.plan_id = 1)
+                and i.state = 'posted'
+                and aml.journal_id = 9
+        ) query
+        %s
+        group by
+            customer
+        order by
+            sum(invoiced) desc
+        """
+        self.env.cr.execute(query, (AsIs(clause),))
+        return self.env.cr.dictfetchall()
+
+    def _employee_time(self, date_start=None, date_end=None):
+        clause = ""
+        if date_start:
+            clause += f" and aal.date >= '{date_start}'"
+        if date_end:
+            clause += f" and aal.date <= '{date_end}'"
+
+        query = """
+        select
+            employee as "Employé",
+            sum(production) as "Prod",
+            sum(internal) as "Interne",
+            sum(revenue) as "CA"
+
+        from (
+            select
+                p.name as employee,
+                0 as production,
+                0 as internal,
+                sum(aal.amount) as revenue
+            from
+                account_analytic_line aal
+            left join
+                account_analytic_account aa on aal.account_id = aa.id
+            left join
+                account_analytic_plan aap on aa.plan_id = aap.id
+            left join
+                res_partner p on aa.partner_id = p.id
+            where
+                aap.id = 3
+                %s
+            group by p.name
+
+            union
+
+            select
+                e.name as employee,
+                sum(case when aal.partner_id != 1 or aal.partner_id is null
+                    and aal.holiday_id is null then unit_amount else 0 end)
+                    as production,
+                sum(case when aal.partner_id = 1 and aal.holiday_id is null
+                    then unit_amount else 0 end)
+                    as internal,
+                0 as revenue
+            from
+                account_analytic_line aal
+            left join
+                hr_employee e on aal.employee_id = e.id
+            left join
+                hr_leave l on aal.holiday_id = l.id
+            left join
+                hr_leave_type lt on l.holiday_status_id = lt.id
+            where
+                aal.project_id is not null
+                %s
+            group by e.name
+            ) query
+        group by employee
+        order by sum(production) desc
+        """
+        self.env.cr.execute(
+            query,
+            (
+                AsIs(clause),
+                AsIs(clause),
+            ),
+        )
+        return self.env.cr.dictfetchall()
+
+    @staticmethod
+    def _compute_totals(result):
+        if not isinstance(result, list) or not result:
+            return {}
+        last_result = result[-1]
+        totals = {}
+        for column in last_result.keys():
+            if isinstance(last_result.get(column), int | float):
+                totals.update(
+                    {
+                        column: sum(
+                            list(
+                                map(
+                                    lambda d: d.get(column) if d.get(column) else 0.0,
+                                    result,
+                                )
+                            )
+                        )
+                    }
+                )
+            else:
+                totals.update({column: False})
+        return totals
diff --git a/static/src/components/dashboard_detail.esm.js b/static/src/components/dashboard_detail.esm.js
new file mode 100644
index 0000000..8d7d4c7
--- /dev/null
+++ b/static/src/components/dashboard_detail.esm.js
@@ -0,0 +1,105 @@
+import {registry} from "@web/core/registry";
+import {useService} from "@web/core/utils/hooks";
+const {Component, useState, onWillStart} = owl;
+
+export class LFDetailsDashboard extends Component {
+    setup() {
+        this.state = useState({
+            date_start: null,
+            date_end: null,
+            customer: [],
+            customer_totals: {},
+            employee_time: [],
+            employee_time_totals: {},
+        });
+        this.orm = useService("orm");
+
+        onWillStart(async () => {
+            await this.getData();
+        });
+    }
+
+    async getData() {
+        const result = await this.orm.call(
+            "lefilament.dashboard",
+            "dashboard_detail_values",
+            [],
+            {
+                date_start: this.state.date_start || null,
+                date_end: this.state.date_end || null,
+            }
+        );
+        if (result) {
+            this.state.customer = result.customer || [];
+            this.state.customer_totals = result.customer_totals || {};
+            this.state.employee_time = result.employee_time || [];
+            this.state.employee_time_totals = result.employee_time_totals || {};
+        }
+    }
+
+    async updatePeriod(period) {
+        const date = new Date();
+        switch (period) {
+            case "this_month":
+                this.state.date_start = new Date(
+                    date.getFullYear(),
+                    date.getMonth(),
+                    1,
+                    12
+                )
+                    .toISOString()
+                    .split("T")[0];
+                this.state.date_end = new Date(
+                    date.getFullYear(),
+                    date.getMonth() + 1,
+                    0,
+                    12
+                )
+                    .toISOString()
+                    .split("T")[0];
+                break;
+            case "last_month":
+                this.state.date_start = new Date(
+                    date.getFullYear(),
+                    date.getMonth() - 1,
+                    1,
+                    12
+                )
+                    .toISOString()
+                    .split("T")[0];
+                this.state.date_end = new Date(
+                    date.getFullYear(),
+                    date.getMonth(),
+                    0,
+                    12
+                )
+                    .toISOString()
+                    .split("T")[0];
+                break;
+            case "this_year":
+                this.state.date_start = new Date(date.getFullYear(), 0, 1, 12)
+                    .toISOString()
+                    .split("T")[0];
+                this.state.date_end = new Date(date.getFullYear() + 1, 0, 0, 12)
+                    .toISOString()
+                    .split("T")[0];
+                break;
+            case "last_year":
+                this.state.date_start = new Date(date.getFullYear() - 1, 0, 1, 12)
+                    .toISOString()
+                    .split("T")[0];
+                this.state.date_end = new Date(date.getFullYear(), 0, 0, 12)
+                    .toISOString()
+                    .split("T")[0];
+                break;
+            default:
+                this.state.date_start = null;
+                this.state.date_end = null;
+        }
+        await this.getData();
+    }
+}
+
+LFDetailsDashboard.template = "lefilament_tdb.LFDetailsDashboard";
+
+registry.category("actions").add("lefilament_tdb.dashboard_detail", LFDetailsDashboard);
diff --git a/static/src/components/dashboard_detail.xml b/static/src/components/dashboard_detail.xml
new file mode 100644
index 0000000..3ff7165
--- /dev/null
+++ b/static/src/components/dashboard_detail.xml
@@ -0,0 +1,193 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<templates xml:space="preserve">
+    <t t-name="lefilament_tdb.LFDetailsDashboard" owl="1">
+        <div id="dashboard_detail_values">
+            <t t-call="lefilament_tdb.dashboard_detail_values" />
+        </div>
+    </t>
+    <t t-name="lefilament_tdb.dashboard_detail_values" owl="1">
+        <div class="vh-100 overflow-auto bg-muted">
+            <div class="row m-3">
+                <div class="col-lg-12">
+                    <div class="row">
+                        <div class="col m-0 p-0">
+                            <div class="shadow-sm border m-2 p-4 bg-white">
+                                <div
+                                    class="d-flex align-items-center justify-content-between"
+                                >
+                                    <h1 class="text-primary fw-bold">Rapport Annuel</h1>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="row">
+                        <div class="col m-0 p-0">
+                            <div class="shadow-sm border m-2 p-4 bg-white">
+                                <div
+                                    class="d-flex align-items-center justify-content-between"
+                                >
+                                    <div
+                                        class="btn-group"
+                                        role="group"
+                                        aria-label="Dates buttons"
+                                    >
+                                        <button
+                                            type="object"
+                                            class="btn btn-outline-primary button-period"
+                                            name="this_month"
+                                            t-on-click="() => this.updatePeriod('this_month')"
+                                        >
+                                            Ce mois-ci
+                                        </button>
+                                        <button
+                                            type="object"
+                                            class="btn btn-outline-primary button-period"
+                                            name="last_month"
+                                            t-on-click="() => this.updatePeriod('last_month')"
+                                        >
+                                            Le mois dernier
+                                        </button>
+                                        <button
+                                            type="object"
+                                            class="btn btn-outline-primary button-period"
+                                            name="this_year"
+                                            t-on-click="() => this.updatePeriod('this_year')"
+                                        >
+                                            Cette année
+                                        </button>
+                                        <button
+                                            type="object"
+                                            class="btn btn-outline-primary button-period"
+                                            name="last_year"
+                                            t-on-click="() => this.updatePeriod('last_year')"
+                                        >
+                                            L'année dernière
+                                        </button>
+                                        <button
+                                            type="object"
+                                            class="btn btn-outline-primary button-period"
+                                            name="all"
+                                            t-on-click="() => this.updatePeriod('all')"
+                                        >
+                                            Tout
+                                        </button>
+                                    </div>
+                                    <div class="d-flex align-items-center">
+                                        <div
+                                            class="d-flex align-items-center me-2 px-2"
+                                        >
+                                            <label
+                                                for="date_start"
+                                                class="form-label date-label px-2"
+                                            >
+                                                <h5>Du:</h5>
+                                            </label>
+                                            <input
+                                                type="date"
+                                                id="date_start"
+                                                class="form-control date-input"
+                                                t-model="state.date_start"
+                                            />
+                                        </div>
+                                        <div
+                                            class="d-flex align-items-center me-2 px-2"
+                                        >
+                                            <label
+                                                for="date_end"
+                                                class="form-label date-label px-2"
+                                            >
+                                                <h5>Au:</h5>
+                                            </label>
+                                            <input
+                                                type="date"
+                                                id="date_end"
+                                                class="form-control date-input"
+                                                t-model="state.date_end"
+                                            />
+                                        </div>
+                                    </div>
+                                    <button
+                                        class="btn btn-primary px-4"
+                                        t-on-click="getData"
+                                    >Mettre à jour</button>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="row dashboard_detail">
+                        <div class="col-12" t-if="state.date_start">
+                            <div class="mt-2 mb-4 display-6 text-center">
+                                Période :
+                                <t t-esc="state.date_start" /> -
+                                <t t-esc="state.date_end" />
+                            </div>
+                        </div>
+                        <div class="col-7" t-if="state.customer.length > 0">
+                            <h3 class="text-uppercase py-2 mb-0">Rentabilité client</h3>
+                            <hr class="mt-0" />
+                            <t t-call="lefilament_tdb.dashboard_detail_table">
+                                <t t-set="data" t-value="state.customer" />
+                                <t t-set="totals" t-value="state.customer_totals" />
+                            </t>
+                        </div>
+                        <div class="col-5" t-if="state.employee_time.length > 0">
+                            <h3 class="text-uppercase py-2 mb-0">Imputations</h3>
+                            <hr class="mt-0" />
+                            <t t-call="lefilament_tdb.dashboard_detail_table">
+                                <t t-set="data" t-value="state.employee_time" />
+                                <t t-log="data" />
+                                <t
+                                    t-set="totals"
+                                    t-value="state.employee_time_totals"
+                                />
+                            </t>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </t>
+
+    <!-- Table Template -->
+    <t t-name="lefilament_tdb.dashboard_detail_table">
+        <div
+            t-if="data.length > 0"
+            class="px-4 mb-5"
+            style="max-height: 600px; overflow: scroll; border: 1px solid #eee;"
+        >
+            <table class="table table-hover table-striped">
+                <thead>
+                    <tr class="bg-100">
+                        <th>#</th>
+                        <th t-foreach="data[0]" t-as="header" t-key="header_index">
+                            <t t-esc="header" />
+                        </th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr t-foreach="data" t-as="line" t-key="line_index">
+                        <td t-esc="line_index + 1" />
+                        <t t-foreach="line" t-as="v" t-key="v_index">
+                            <td class="text-end">
+                                <t t-esc="v_value" />
+                            </td>
+                        </t>
+                    </tr>
+                </tbody>
+                <tfoot t-if="totals">
+                    <tr class="bg-100">
+                        <th>Total</th>
+                        <th
+                            t-foreach="totals"
+                            t-as="footer"
+                            class="text-end"
+                            t-key="footer_index"
+                        >
+                            <t t-if="footer_value != false" t-esc="footer_value" />
+                        </th>
+                    </tr>
+                </tfoot>
+            </table>
+        </div>
+    </t>
+</templates>
diff --git a/static/src/components/dashboard_overview.esm.js b/static/src/components/dashboard_overview.esm.js
new file mode 100644
index 0000000..86c6ea3
--- /dev/null
+++ b/static/src/components/dashboard_overview.esm.js
@@ -0,0 +1,345 @@
+import {registry} from "@web/core/registry";
+import {useService} from "@web/core/utils/hooks";
+import {user} from "@web/core/user";
+const {Component, useState, onWillStart} = owl;
+
+export class LFDashboard extends Component {
+    setup() {
+        this.state = useState({});
+        this.orm = useService("orm");
+        this.actionService = useService("action");
+
+        onWillStart(async () => {
+            await this.getData();
+        });
+    }
+
+    async getData() {
+        const fiscal_year = new Date().getFullYear() + "-01-01";
+        const fiscal_year_next = new Date().getFullYear() + "-12-31";
+
+        const ca_target = await this.orm.searchRead(
+            "res.company",
+            [["id", "=", user.context.allowed_company_ids[0]]],
+            ["ca_target"]
+        );
+        const target = ca_target[0].ca_target;
+
+        const domain_invoice = [
+            ["invoice_date", ">=", fiscal_year],
+            ["invoice_date", "<=", fiscal_year_next],
+            ["move_type", "in", ["out_invoice", "out_refund"]],
+            ["state", "=", "posted"],
+        ];
+        const invoiced_rg = await this.orm.readGroup(
+            "account.move",
+            domain_invoice,
+            ["amount_untaxed_signed:sum"],
+            []
+        );
+        const invoiced_percentage =
+            (invoiced_rg[0].amount_untaxed_signed / target) * 100;
+        const toinvoice_percentage =
+            ((target - invoiced_rg[0].amount_untaxed_signed) / target) * 100;
+
+        const domain_order = [["invoice_status", "=", "to invoice"]];
+        const ordered_rg = await this.orm.readGroup(
+            "sale.order",
+            domain_order,
+            ["untaxed_amount_to_invoice:sum"],
+            []
+        );
+        const ordered_percentage =
+            (ordered_rg[0].untaxed_amount_to_invoice / target) * 100;
+
+        const crm_domain = [
+            "|",
+            ["date_deadline", "<=", fiscal_year_next],
+            ["date_deadline", "=", false],
+        ];
+        const pipe_rg = await this.orm.readGroup(
+            "crm.lead",
+            crm_domain,
+            ["prorated_revenue:sum"],
+            []
+        );
+        const crm_win_domain = [
+            ["probability", "=", 100],
+            "|",
+            ["date_deadline", "<=", fiscal_year_next],
+            ["date_deadline", "=", false],
+        ];
+        const pipe_win_rg = await this.orm.readGroup(
+            "crm.lead",
+            crm_win_domain,
+            ["prorated_revenue:sum"],
+            []
+        );
+        const pipe_win_percentage = (pipe_win_rg[0].prorated_revenue / target) * 100;
+
+        const ongoing =
+            invoiced_rg[0].amount_untaxed_signed +
+            ordered_rg[0].untaxed_amount_to_invoice +
+            pipe_win_rg[0].prorated_revenue;
+        const ongoing_percentage = (ongoing / target) * 100;
+
+        const last_date = await this.orm.searchRead(
+            "account.bank.statement.line",
+            [],
+            ["date"],
+            {limit: 1, order: "date desc"}
+        );
+        const date_maj = new Date(last_date[0].date).toLocaleDateString("fr-FR");
+
+        const available_cash_rg = await this.orm.readGroup(
+            "account.bank.statement.line",
+            [],
+            ["amount:sum"],
+            []
+        );
+        const cash_by_bank = await this.orm.readGroup(
+            "account.bank.statement.line",
+            [],
+            ["journal_id", "amount"],
+            ["journal_id"]
+        );
+
+        const domain_invoice_to_get = [
+            ["move_type", "in", ["out_invoice", "out_refund"]],
+            ["state", "=", "posted"],
+            ["payment_state", "in", ["not_paid", "partial", "in_payment"]],
+        ];
+        const to_get_rg = await this.orm.readGroup(
+            "account.move",
+            domain_invoice_to_get,
+            ["amount_untaxed_signed:sum"],
+            []
+        );
+        const domain_invoice_to_pay = [
+            ["move_type", "in", ["in_invoice", "in_refund"]],
+            ["state", "=", "posted"],
+            ["payment_state", "in", ["not_paid", "partial", "in_payment"]],
+        ];
+        const to_pay_rg = await this.orm.readGroup(
+            "account.move",
+            domain_invoice_to_pay,
+            ["amount_untaxed_signed:sum"],
+            []
+        );
+
+        const domain_bank_line = [["date", ">", fiscal_year]];
+        const cash_in_rg = await this.orm.readGroup(
+            "account.bank.statement.line",
+            [
+                ["amount", ">", 0],
+                ["date", ">", fiscal_year],
+            ],
+            ["amount:sum"],
+            []
+        );
+        const cash_out_rg = await this.orm.readGroup(
+            "account.bank.statement.line",
+            [
+                ["amount", "<", 0],
+                ["date", ">", fiscal_year],
+            ],
+            ["amount:sum"],
+            []
+        );
+        const variation = cash_in_rg[0].amount + cash_out_rg[0].amount;
+
+        this.state = {
+            domain_invoice: domain_invoice,
+            invoiced: invoiced_rg[0].amount_untaxed_signed.toLocaleString("fr-FR", {
+                style: "currency",
+                currency: "EUR",
+                maximumFractionDigits: 0,
+            }),
+            toinvoice: (target - invoiced_rg[0].amount_untaxed_signed).toLocaleString(
+                "fr-FR",
+                {style: "currency", currency: "EUR", maximumFractionDigits: 0}
+            ),
+            domain_order: domain_order,
+            ordered: ordered_rg[0].untaxed_amount_to_invoice.toLocaleString("fr-FR", {
+                style: "currency",
+                currency: "EUR",
+                maximumFractionDigits: 0,
+            }),
+            pipe_win: pipe_win_rg[0].prorated_revenue.toLocaleString("fr-FR", {
+                style: "currency",
+                currency: "EUR",
+                maximumFractionDigits: 0,
+            }),
+            pipe: pipe_rg[0].prorated_revenue.toLocaleString("fr-FR", {
+                style: "currency",
+                currency: "EUR",
+                maximumFractionDigits: 0,
+            }),
+            crm_domain: crm_domain,
+            ongoing: ongoing.toLocaleString("fr-FR", {
+                style: "currency",
+                currency: "EUR",
+                maximumFractionDigits: 0,
+            }),
+            target: target.toLocaleString("fr-FR", {
+                style: "currency",
+                currency: "EUR",
+                maximumFractionDigits: 0,
+            }),
+            invoiced_percentage: invoiced_percentage.toLocaleString("fr-FR", {
+                maximumFractionDigits: 0,
+            }),
+            toinvoice_percentage: toinvoice_percentage.toLocaleString("fr-FR", {
+                maximumFractionDigits: 0,
+            }),
+            ordered_percentage: ordered_percentage.toLocaleString("fr-FR", {
+                maximumFractionDigits: 0,
+            }),
+            pipe_win_percentage: pipe_win_percentage.toLocaleString("fr-FR", {
+                maximumFractionDigits: 0,
+            }),
+            ongoing_percentage: ongoing_percentage.toLocaleString("fr-FR", {
+                maximumFractionDigits: 0,
+            }),
+            date_maj: date_maj,
+            available_cash: available_cash_rg[0].amount.toLocaleString("fr-FR", {
+                style: "currency",
+                currency: "EUR",
+                maximumFractionDigits: 0,
+            }),
+            cash_by_bank: cash_by_bank,
+            domain_invoice_to_get: domain_invoice_to_get,
+            to_get: to_get_rg[0].amount_untaxed_signed.toLocaleString("fr-FR", {
+                style: "currency",
+                currency: "EUR",
+                maximumFractionDigits: 0,
+            }),
+            domain_invoice_to_pay: domain_invoice_to_pay,
+            to_pay: to_pay_rg[0].amount_untaxed_signed.toLocaleString("fr-FR", {
+                style: "currency",
+                currency: "EUR",
+                maximumFractionDigits: 0,
+            }),
+            domain_bank_line: domain_bank_line,
+            variation: variation.toLocaleString("fr-FR", {
+                style: "currency",
+                currency: "EUR",
+                maximumFractionDigits: 0,
+            }),
+            cash_in: cash_in_rg[0].amount.toLocaleString("fr-FR", {
+                style: "currency",
+                currency: "EUR",
+                maximumFractionDigits: 0,
+            }),
+            cash_out: cash_out_rg[0].amount.toLocaleString("fr-FR", {
+                style: "currency",
+                currency: "EUR",
+                maximumFractionDigits: 0,
+            }),
+        };
+    }
+
+    async updateView() {
+        this.actionService.doAction({
+            type: "ir.actions.client",
+            tag: "soft_reload",
+        });
+    }
+
+    async viewInvoices() {
+        this.actionService.doAction({
+            type: "ir.actions.act_window",
+            name: "Facturé cette année",
+            res_model: "account.move",
+            domain: this.state.domain_invoice,
+            views: [
+                [0, "list"],
+                [0, "pivot"],
+                [0, "graph"],
+                [0, "form"],
+            ],
+        });
+    }
+
+    async viewOrders() {
+        this.actionService.doAction({
+            type: "ir.actions.act_window",
+            name: "Commandes en cours",
+            res_model: "sale.order",
+            domain: this.state.domain_order,
+            views: [
+                [0, "list"],
+                [0, "pivot"],
+                [0, "graph"],
+                [0, "form"],
+            ],
+        });
+    }
+
+    async viewPipe() {
+        this.actionService.doAction({
+            type: "ir.actions.act_window",
+            name: "Pipe",
+            res_model: "crm.lead",
+            domain: this.state.crm_domain,
+            views: [
+                [0, "kanban"],
+                [0, "list"],
+                [0, "pivot"],
+                [0, "graph"],
+                [0, "form"],
+            ],
+        });
+    }
+
+    async viewBankStatements() {
+        this.actionService.doAction("account.action_bank_statement_tree");
+    }
+
+    async viewToGetInvoices() {
+        this.actionService.doAction({
+            type: "ir.actions.act_window",
+            name: "Factures à encaisser",
+            res_model: "account.move",
+            domain: this.state.domain_invoice_to_get,
+            views: [
+                [0, "list"],
+                [0, "pivot"],
+                [0, "graph"],
+                [0, "form"],
+            ],
+        });
+    }
+
+    async viewToPayInvoices() {
+        this.actionService.doAction({
+            type: "ir.actions.act_window",
+            name: "Factures à payer",
+            res_model: "account.move",
+            domain: this.state.domain_invoice_to_pay,
+            views: [
+                [0, "list"],
+                [0, "pivot"],
+                [0, "graph"],
+                [0, "form"],
+            ],
+        });
+    }
+
+    async viewBankLines() {
+        this.actionService.doAction({
+            type: "ir.actions.act_window",
+            name: "Trésorerie cette année",
+            res_model: "account.bank.statement.line",
+            domain: this.state.domain_bank_line,
+            views: [
+                [0, "list"],
+                [0, "form"],
+            ],
+        });
+    }
+}
+
+LFDashboard.template = "lefilament_tdb.LFDashboard";
+
+registry.category("actions").add("lefilament_tdb.dashboard_overview", LFDashboard);
diff --git a/static/src/components/dashboard_overview.xml b/static/src/components/dashboard_overview.xml
new file mode 100644
index 0000000..3b64b71
--- /dev/null
+++ b/static/src/components/dashboard_overview.xml
@@ -0,0 +1,394 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<templates xml:space="preserve">
+    <t t-name="lefilament_tdb.LFDashboard" owl="1">
+        <div class="vh-100 overflow-auto bg-muted">
+            <div class="row m-3">
+                <div class="col-lg-12">
+                    <div class="row">
+                        <div class="col m-0 p-0">
+                            <div class="shadow-sm border m-2 p-4 bg-white">
+                                <div
+                                    class="d-flex align-items-center justify-content-between"
+                                >
+                                    <h1 class="text-primary fw-bold">Rapport Annuel</h1>
+                                    <button
+                                        type="object"
+                                        class="btn"
+                                        id="update_view"
+                                        t-on-click="updateView"
+                                    >
+                                        <i class="fa fa-refresh" />
+                                    </button>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="row lefilament_dashboard">
+                        <!-- Column 1 -->
+                        <div class="col-12 col-md-6 col-lg-4">
+                            <h3
+                                class="text-uppercase fw-bolder o_horizontal_separator mb-4"
+                            >En Cours</h3>
+                            <!-- Facturé -->
+                            <a
+                                class="card mb32"
+                                t-on-click="viewInvoices"
+                                type="action"
+                            >
+                                <div class="card-body">
+                                    <h5 class="card-title">Facturé</h5>
+                                    <p class="display-6">
+                                        <t t-esc="state.invoiced" />
+                                    </p>
+                                </div>
+                            </a>
+                            <!-- Commandes -->
+                            <a class="card mb32" t-on-click="viewOrders" type="action">
+                                <div class="card-body">
+                                    <h5 class="card-title">Commandes en cours</h5>
+                                    <p class="display-6" t-esc="state.ordered" />
+                                </div>
+                            </a>
+                            <!-- Pipe -->
+                            <a class="card mb32" t-on-click="viewPipe" type="action">
+                                <div class="card-body">
+                                    <h5 class="card-title">Pipe</h5>
+                                    <p class="display-6" t-esc="state.pipe" />
+                                </div>
+                            </a>
+                        </div>
+
+                        <!-- Column 2 -->
+                        <div class="col-12 col-md-6 col-lg-4">
+                            <h3
+                                class="text-uppercase fw-bolder o_horizontal_separator mb-4"
+                            >Objectif</h3>
+                            <div class="card">
+                                <div class="card-body">
+                                    <p class="display-5 mb16" t-esc="state.target" />
+                                    <div class="progress" style="height: 60px;">
+                                        <div
+                                            class="progress-bar progress-bar-striped bg-success"
+                                            role="progressbar"
+                                            t-att-aria-valuenow="state.invoiced_percentage"
+                                            aria-valuemin="0"
+                                            aria-valuemax="100"
+                                            t-attf-style="width: {{state.invoiced_percentage}}%; font-size: 14px; font-weight: 600;"
+                                        >
+                                            <t t-esc="state.invoiced_percentage" /> %
+                                        </div>
+                                    </div>
+                                    <table class="table-legend mt16">
+                                            <tr>
+                                            <td
+                                                style="background-color: #8ED8A2; width: 20px;"
+                                                class="progress-bar-striped"
+                                            />
+                                                <td>Facturé</td>
+                                                <td class="nb" t-esc="state.invoiced" />
+                                                <td class="nb"><t
+                                                    t-esc="state.invoiced_percentage"
+                                                /> %</td>
+                                            </tr>
+                                        <tr>
+                                            <td
+                                                style="background-color: #F6DCA2; width: 20px;"
+                                                class="progress-bar-striped"
+                                            />
+                                            <td>Commandes</td>
+                                            <td class="nb" t-esc="state.ordered" />
+                                            <td class="nb"><t
+                                                    t-esc="state.ordered_percentage"
+                                                /> %</td>
+                                        </tr>
+                                        <tr>
+                                            <td
+                                                style="background-color: #F6CCA2; width: 20px;"
+                                                class="progress-bar-striped"
+                                            />
+                                            <td>Pipe Gagné</td>
+                                            <td class="nb" t-esc="state.pipe_win" />
+                                            <td class="nb"><t
+                                                    t-esc="state.pipe_win_percentage"
+                                                /> %</td>
+                                            </tr>
+                                        <tr class="table-legend-total">
+                                                <td style="width: 20px;" />
+                                            <td>Total</td>
+                                            <td class="nb" t-esc="state.ongoing" />
+                                            <td class="nb"><t
+                                                    t-esc="state.ongoing_percentage"
+                                                /> %</td>
+                                            </tr>
+                                        <tr>
+                                            <td
+                                                style="background-color: #eee; width: 20px;"
+                                                class="progress-bar-striped"
+                                            />
+                                            <td>À facturer</td>
+                                            <td class="nb" t-esc="state.toinvoice" />
+                                            <td class="nb">
+                                                    <t
+                                                    t-esc="state.toinvoice_percentage"
+                                                /> %
+                                                </td>
+                                            </tr>
+                                        </table>
+                                </div>
+                            </div>
+                        </div>
+
+                        <!-- Column 3 -->
+                        <div class="col-12 col-md-6 col-lg-4">
+                            <h3
+                                class="text-uppercase fw-bolder o_horizontal_separator mb-4"
+                            >Trésorerie</h3>
+                            <!-- Banques -->
+                            <div class="card mb32">
+                                <div class="card-body">
+                                    <ul
+                                        class="nav nav-tabs"
+                                        id="cash-tab"
+                                        role="tablist"
+                                    >
+                                        <li class="nav-item" role="presentation">
+                                        <button
+                                                class="nav-link active"
+                                                id="cash-tab"
+                                                data-bs-toggle="tab"
+                                                data-bs-target="#cash"
+                                                type="button"
+                                                role="tab"
+                                                aria-controls="cash"
+                                                aria-selected="true"
+                                            >
+                                            Trésorerie
+                                        </button>
+                                        </li>
+                                        <li class="nav-item" role="presentation">
+                                        <button
+                                                class="nav-link"
+                                                id="bank-tab"
+                                                data-bs-toggle="tab"
+                                                data-bs-target="#bank"
+                                                type="button"
+                                                role="tab"
+                                                aria-controls="bank"
+                                                aria-selected="false"
+                                            >Banques
+                                        </button>
+                                        </li>
+                                    </ul>
+                                    <div class="tab-content mt16" id="cashContent">
+                                        <div
+                                            class="tab-pane fade show active"
+                                            id="cash"
+                                            role="tabpanel"
+                                            aria-labelledby="cash-tab"
+                                        >
+                                            <a
+                                                type="action"
+                                                t-on-click="viewBankStatements"
+                                            >
+                                                <p class="card-maj">
+                                                    Denière mise à jour le <t
+                                                        t-esc="state.date_maj"
+                                                    /></p>
+                                                <p
+                                                    class="display-6"
+                                                    t-esc="state.available_cash"
+                                                />
+                                            </a>
+                                        </div>
+                                        <div
+                                            class="tab-pane fade"
+                                            id="bank"
+                                            role="tabpanel"
+                                            aria-labelledby="bank-tab"
+                                        >
+                                        <table
+                                                class="table table-striped table-sm table-bordered table-hover"
+                                            >
+                                            <t
+                                                    t-foreach="state.cash_by_bank"
+                                                    t-as="bank"
+                                                    t-key="bank_index"
+                                                >
+                                            <tr>
+                                                <td t-esc="bank['journal_id'][1]" />
+                                                <td class="text-right">
+                                                    <t t-esc="bank['amount']" />
+                                                </td>
+                                            </tr>
+                                            </t>
+                                        </table>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <!-- To paid -->
+                            <div class="card mb32">
+                                        <div class="card-body">
+                                            <ul
+                                        class="nav nav-tabs"
+                                        id="cash-tab"
+                                        role="tablist"
+                                    >
+                                                <li
+                                            class="nav-item"
+                                            role="presentation"
+                                        >
+                                                    <button
+                                                class="nav-link active"
+                                                id="cust_to_pay-tab"
+                                                data-bs-toggle="tab"
+                                                data-bs-target="#cust_to_pay"
+                                                type="button"
+                                                role="tab"
+                                                aria-controls="cust_to_pay"
+                                                aria-selected="true"
+                                            >
+                                                        Facturé non encaissé
+                                                    </button>
+                                                </li>
+                                                <li
+                                            class="nav-item"
+                                            role="presentation"
+                                        >
+                                                    <button
+                                                class="nav-link"
+                                                id="provider_to_pay-tab"
+                                                data-bs-toggle="tab"
+                                                data-bs-target="#provider_to_pay"
+                                                type="button"
+                                                role="tab"
+                                                aria-controls="provider_to_pay"
+                                                aria-selected="false"
+                                            >Fournisseurs
+                                                    </button>
+                                                </li>
+                                            </ul>
+                                            <div
+                                        class="tab-content mt16"
+                                        id="toPayContent"
+                                    >
+                                                <div
+                                            class="tab-pane fade show active"
+                                            id="cust_to_pay"
+                                            role="tabpanel"
+                                            aria-labelledby="cust_to_pay-tab"
+                                        >
+                                                    <a
+                                                t-on-click="viewToGetInvoices"
+                                                type="action"
+                                            >
+                                                        <p
+                                                    class="display-6"
+                                                    t-esc="state.to_get"
+                                                />
+                                                    </a>
+                                                </div>
+                                                <div
+                                            class="tab-pane fade"
+                                            id="provider_to_pay"
+                                            role="tabpanel"
+                                            aria-labelledby="provider_to_pay-tab"
+                                        >
+                                                    <a
+                                                t-on-click="viewToPayInvoices"
+                                                type="action"
+                                            >
+                                                        <p
+                                                    class="display-6"
+                                                    t-esc="state.to_pay"
+                                                />
+                                                    </a>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+
+                            <!-- Variation -->
+                            <div class="card mb32">
+                                <div class="card-body">
+                                    <ul
+                                        class="nav nav-tabs"
+                                        id="variation-tab"
+                                        role="tablist"
+                                    >
+                                        <li class="nav-item" role="presentation">
+                                        <button
+                                                class="nav-link active"
+                                                id="variation-tab"
+                                                data-bs-toggle="tab"
+                                                data-bs-target="#variation"
+                                                type="button"
+                                                role="tab"
+                                                aria-controls="variation"
+                                                aria-selected="true"
+                                            >
+                                            Variation
+                                        </button>
+                                        </li>
+                                        <li class="nav-item" role="presentation">
+                                        <button
+                                                class="nav-link"
+                                                id="inout-tab"
+                                                data-bs-toggle="tab"
+                                                data-bs-target="#inout"
+                                                type="button"
+                                                role="tab"
+                                                aria-controls="inout"
+                                                aria-selected="false"
+                                            >Entrées/Sorties
+                                        </button>
+                                        </li>
+                                    </ul>
+                                    <div class="tab-content mt16" id="variationContent">
+                                        <div
+                                            class="tab-pane fade show active"
+                                            id="variation"
+                                            role="tabpanel"
+                                            aria-labelledby="variation-tab"
+                                        >
+                                            <a t-on-click="viewBankLines" type="action">
+                                                <p
+                                                    class="display-6"
+                                                    t-esc="state.variation"
+                                                />
+                                            </a>
+                                        </div>
+                                        <div
+                                            class="tab-pane fade"
+                                            id="inout"
+                                            role="tabpanel"
+                                            aria-labelledby="inout-tab"
+                                        >
+                                            <div class="row">
+                                                <div class="col-6">
+                                                <p><strong>Encaissé</strong></p>
+                                                    <p
+                                                        class="display-6"
+                                                        t-esc="state.cash_in"
+                                                    />
+                                                </div>
+                                                <div class="col-6">
+                                                <p><strong>Décaissé</strong></p>
+                                                    <p
+                                                        class="display-6"
+                                                        t-esc="state.cash_out"
+                                                    />
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </t>
+</templates>
diff --git a/static/src/components/lefilament_tdb.css b/static/src/components/lefilament_tdb.css
new file mode 100644
index 0000000..07acd84
--- /dev/null
+++ b/static/src/components/lefilament_tdb.css
@@ -0,0 +1,43 @@
+.lefilament_dashboard {
+    padding-top: 40px;
+    padding-bottom: 40px;
+    background-color: #fefefe;
+}
+.lefilament_dashboard .display-6,
+.lefilament_dashboard .card-title {
+    color: #495057;
+}
+.lefilament_dashboard a.dashboard_view {
+    cursor: pointer;
+}
+
+p.card-maj {
+    font-size: 10px;
+    font-style: italic;
+    font-weight: 300;
+}
+
+/* Legende ProgressBar */
+.table-legend {
+    width: 100%;
+    max-width: 100%;
+    margin: 10px 0;
+    border-collapse: separate;
+    border-spacing: 8px;
+    font-size: 11px;
+}
+.table-legend .nb {
+    text-align: right;
+}
+.table-legend-total td {
+    border-top: 1px solid #eee;
+    border-bottom: 1px solid #eee;
+    font-weight: 700;
+    padding: 2px 0;
+}
+
+.dashboard_detail {
+    padding-top: 10px;
+    padding-bottom: 40px;
+    background-color: #fefefe;
+}
diff --git a/views/lefilament_dashboard.xml b/views/lefilament_dashboard.xml
index c7869ac..2582f5c 100644
--- a/views/lefilament_dashboard.xml
+++ b/views/lefilament_dashboard.xml
@@ -138,6 +138,17 @@
     <record id="lefilament_dashboard_action" model="ir.actions.act_window">
         <field name="name">Dashboard - Le Filament</field>
         <field name="res_model">lefilament.dashboard</field>
+        <field name="path">dashboard-lf-list</field>
         <field name="view_mode">list,graph,form,pivot</field>
     </record>
+    <record id="le_filament_dashboard_overview_action" model="ir.actions.client">
+        <field name="name">Rapport Annuel</field>
+        <field name="path">dashboard-lf-year</field>
+        <field name="tag">lefilament_tdb.dashboard_overview</field>
+    </record>
+    <record id="le_filament_dashboard_detail_action" model="ir.actions.client">
+        <field name="name">Détail</field>
+        <field name="path">dashboard-lf-detail</field>
+        <field name="tag">lefilament_tdb.dashboard_detail</field>
+    </record>
 </odoo>
diff --git a/views/menus.xml b/views/menus.xml
index 91f611f..424ab94 100644
--- a/views/menus.xml
+++ b/views/menus.xml
@@ -9,6 +9,27 @@
         groups="group_dashboard"
         web_icon="lefilament_tdb,static/description/icon_menu.png"
     />
+    <menuitem
+        id="lefilament_dashboard_report"
+        parent="lefilament_dashboard_menu"
+        name="Rapports"
+        sequence="1"
+    />
+
+    <menuitem
+        id="lefilament_dashboard_report_year"
+        parent="lefilament_dashboard_report"
+        name="Annuel"
+        sequence="1"
+        action="lefilament_tdb.le_filament_dashboard_overview_action"
+    />
+    <menuitem
+        id="lefilament_dashboard_report_detail"
+        parent="lefilament_dashboard_report"
+        name="Détail"
+        sequence="1"
+        action="lefilament_tdb.le_filament_dashboard_detail_action"
+    />
 
     <menuitem
         id="lefilament_dashboard_conf"
diff --git a/views/res_company.xml b/views/res_company.xml
index 8bea63c..b5d3c77 100644
--- a/views/res_company.xml
+++ b/views/res_company.xml
@@ -27,6 +27,7 @@
     <record id="lefilament_dashboard_variables_action" model="ir.actions.act_window">
         <field name="name">Variables Dashboard</field>
         <field name="res_model">res.company</field>
+        <field name="path">company-variables</field>
         <field name="res_id">1</field>
         <field name="view_mode">form</field>
     </record>
-- 
GitLab