From 72ac948ff84a8ec94401242c42f3951c76b08bf5 Mon Sep 17 00:00:00 2001
From: benjamin <benjamin@le-filament.com>
Date: Mon, 15 Jul 2024 12:39:51 +0200
Subject: [PATCH] [IMP] detail view for customer and employee timesheet

---
 .pre-commit-config.yaml             |   1 +
 __manifest__.py                     |   2 +
 models/lefilament_tdb.py            | 185 ++++++++++++++++++++++++++++
 static/src/css/lefilament_tdb.css   |  10 +-
 static/src/js/dashboard_detail.js   |  61 +++++++++
 static/src/js/dashboard_overview.js |   4 -
 templates/dashboard_detail.xml      | 151 +++++++++++++++++++++++
 templates/lefilament_dashboard.xml  |   3 +-
 views/menus.xml                     |   7 ++
 9 files changed, 417 insertions(+), 7 deletions(-)
 create mode 100644 static/src/js/dashboard_detail.js
 create mode 100644 templates/dashboard_detail.xml

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 10acf1e..3c66a01 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -59,6 +59,7 @@ repos:
     hooks:
       - id: prettier
         name: prettier (with plugin-xml)
+        exclude: ^templates/
         additional_dependencies:
           - "prettier@2.7.1"
           - "@prettier/plugin-xml@2.2.0"
diff --git a/__manifest__.py b/__manifest__.py
index 4523b8c..94f1afe 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -15,6 +15,7 @@
         "security/ir.model.access.csv",
         # datas
         # templates
+        "templates/dashboard_detail.xml",
         "templates/lefilament_dashboard.xml",
         # views
         "views/account_bank_statement_line.xml",
@@ -29,6 +30,7 @@
         "web._assets_frontend_helpers": [],
         "web.assets_frontend": [],
         "web.assets_backend": [
+            "lefilament_tdb/static/src/js/dashboard_detail.js",
             "lefilament_tdb/static/src/js/dashboard_overview.js",
             "lefilament_tdb/static/src/css/lefilament_tdb.css",
         ],
diff --git a/models/lefilament_tdb.py b/models/lefilament_tdb.py
index 18fce4d..aab398b 100644
--- a/models/lefilament_tdb.py
+++ b/models/lefilament_tdb.py
@@ -2,7 +2,9 @@
 # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
 
 from datetime import date
+
 from dateutil.relativedelta import relativedelta
+from psycopg2.extensions import AsIs
 
 from odoo import api, fields, models
 from odoo.tools.safe_eval import safe_eval
@@ -291,3 +293,186 @@ class LeFilamentTdb(models.Model):
         res["company_id"] = self.env.company
 
         return res
+
+    @api.model
+    def dashboard_detail_values(self, date_start=None, date_end=None):
+        return {
+            "customer": self._customer_detail(date_start, date_end),
+            "employee_time": self._employee_time(date_start, date_end),
+            "date_start": date_start,
+            "date_end": date_end,
+        }
+
+    @api.model
+    def dashboard_detail_values_template(self, date_start=None, date_end=None):
+        return self.env["ir.ui.view"]._render_template(
+            "lefilament_tdb.dashboard_detail_values",
+            self.dashboard_detail_values(date_start, date_end),
+        )
+
+    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
+                (aal.plan_id is null or aal.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
+                (aal.plan_id is null or aal.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),))
+
+        result = self.env.cr.dictfetchall()
+        return result
+
+    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
+            e.name as "Personne",
+            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 "Prod",
+            sum(case when aal.partner_id = 1 and aal.holiday_id is null then unit_amount else 0 end) as "Interne"
+        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
+            and (lt.active is true or lt.active is null)
+            %s
+        group by
+            e.name
+        order by
+            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) desc
+        """
+        self.env.cr.execute(query, (AsIs(clause),))
+
+        result = self.env.cr.dictfetchall()
+        return result
diff --git a/static/src/css/lefilament_tdb.css b/static/src/css/lefilament_tdb.css
index a2457f7..07acd84 100644
--- a/static/src/css/lefilament_tdb.css
+++ b/static/src/css/lefilament_tdb.css
@@ -3,14 +3,14 @@
     padding-bottom: 40px;
     background-color: #fefefe;
 }
-.lefilament_dashboard .display-6, .lefilament_dashboard .card-title {
+.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;
@@ -35,3 +35,9 @@ p.card-maj {
     font-weight: 700;
     padding: 2px 0;
 }
+
+.dashboard_detail {
+    padding-top: 10px;
+    padding-bottom: 40px;
+    background-color: #fefefe;
+}
diff --git a/static/src/js/dashboard_detail.js b/static/src/js/dashboard_detail.js
new file mode 100644
index 0000000..53ba803
--- /dev/null
+++ b/static/src/js/dashboard_detail.js
@@ -0,0 +1,61 @@
+odoo.define("lefilament_tdb.dashboard_detail", function (require) {
+    "use strict";
+
+    const qweb = require("web.qweb");
+    const viewRegistry = require("web.view_registry");
+
+    const Controller = qweb.Controller.extend({
+        events: _.extend({}, qweb.Controller.prototype.events, {
+            "click .button-period": "_onClickButtonPeriod",
+            "click #button-custom-date": "_onClickButtonCustomDate",
+        }),
+
+        async _onClickButtonPeriod(e) {
+            e.preventDefault();
+            var $target = $(e.currentTarget);
+            var data = $target.data();
+            var dashboard_detail_values = this.$el.find("#dashboard_detail_values");
+
+            await this._rpc({
+                model: "lefilament.dashboard",
+                method: "dashboard_detail_values_template",
+                args: [],
+                kwargs: {
+                    date_start: data.dateStart || null,
+                    date_end: data.dateEnd || null,
+                },
+            }).then(function (result) {
+                dashboard_detail_values.html(result);
+            });
+        },
+
+        async _onClickButtonCustomDate(e) {
+            e.preventDefault();
+            var dashboard_detail_values = this.$el.find("#dashboard_detail_values");
+            var date_start = this.$el.find("#custom-date-start")[0];
+            var date_end = this.$el.find("#custom-date-end")[0];
+
+            await this._rpc({
+                model: "lefilament.dashboard",
+                method: "dashboard_detail_values_template",
+                args: [],
+                kwargs: {
+                    date_start: date_start.value || null,
+                    date_end: date_end.value || null,
+                },
+            }).then(function (result) {
+                dashboard_detail_values.html(result);
+            });
+        },
+    });
+
+    const DashboardDetail = qweb.View.extend({
+        withSearchBar: false,
+        searchMenuTypes: [],
+        config: _.extend({}, qweb.View.prototype.config, {
+            Controller: Controller,
+        }),
+    });
+
+    viewRegistry.add("dashboard_detail", DashboardDetail);
+});
diff --git a/static/src/js/dashboard_overview.js b/static/src/js/dashboard_overview.js
index 1402a50..1f7d699 100644
--- a/static/src/js/dashboard_overview.js
+++ b/static/src/js/dashboard_overview.js
@@ -1,12 +1,9 @@
 odoo.define("lefilament_tdb.dashboard_overview", function (require) {
     "use strict";
 
-    var core = require("web.core");
     const qweb = require("web.qweb");
     const viewRegistry = require("web.view_registry");
 
-    var QWeb = core.qweb;
-
     const Controller = qweb.Controller.extend({
         events: _.extend({}, qweb.Controller.prototype.events, {
             "click #update_view": "_onClickUpdateView",
@@ -38,7 +35,6 @@ odoo.define("lefilament_tdb.dashboard_overview", function (require) {
                 }
             });
         },
-
     });
 
     const DashboardOverview = qweb.View.extend({
diff --git a/templates/dashboard_detail.xml b/templates/dashboard_detail.xml
new file mode 100644
index 0000000..ba63bea
--- /dev/null
+++ b/templates/dashboard_detail.xml
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+    <!-- Template layout -->
+    <record id="lefilament_dashboard_detail" model="ir.ui.view">
+        <field name="name">lefilament.dashboard.detail</field>
+        <field name="type">qweb</field>
+        <field name="model">lefilament.dashboard</field>
+        <field name="arch" type="xml">
+            <qweb js_class="dashboard_detail">
+                <nav class="o_qweb_cp_buttons">
+                    <div class="btn-group" role="group" aria-label="Dates buttons">
+                        <button
+                            type="object"
+                            class="btn btn-outline-primary button-period"
+                            name="this_month"
+                            t-att-data-date-start="datetime.date.today().replace(day=1).strftime('%Y-%m-%d')"
+                            t-att-data-date-end="(datetime.date.today().replace(day=1) + relativedelta(months=1, days=-1)).strftime('%Y-%m-%d')"
+                        >
+                            Ce mois-ci
+                        </button>
+                        <button
+                            type="object"
+                            class="btn btn-outline-primary button-period"
+                            name="last_month"
+                            t-att-data-date-start="(datetime.date.today().replace(day=1) + relativedelta(months=-1)).strftime('%Y-%m-%d')"
+                            t-att-data-date-end="(datetime.date.today().replace(day=1) + relativedelta(days=-1)).strftime('%Y-%m-%d')"
+                        >
+                            Le mois dernier
+                        </button>
+                        <button
+                            type="object"
+                            class="btn btn-outline-primary button-period"
+                            name="this_year"
+                            t-att-data-date-start="datetime.date.today().replace(day=1, month=1).strftime('%Y-%m-%d')"
+                            t-att-data-date-end="datetime.date.today().replace(day=31, month=12).strftime('%Y-%m-%d')"
+                        >
+                            Cette année
+                        </button>
+                        <button
+                            type="object"
+                            class="btn btn-outline-primary button-period"
+                            name="last_year"
+                            t-att-data-date-start="(datetime.date.today().replace(day=1, month=1) + relativedelta(years=-1)).strftime('%Y-%m-%d')"
+                            t-att-data-date-end="(datetime.date.today().replace(day=31, month=12) + relativedelta(years=-1)).strftime('%Y-%m-%d')"
+                        >
+                            L'année dernière
+                        </button>
+                        <button
+                            type="object"
+                            class="btn btn-outline-primary button-period"
+                            name="all"
+                            data-date-start=""
+                            data-date-end=""
+                        >
+                            Tout
+                        </button>
+                    </div>
+
+                    <div class="btn-group ms-5" role="group">
+                        <input type="date" class="form-control text-center" id="custom-date-start" />
+                        <input type="date" class="form-control text-center" id="custom-date-end" />
+                        <button
+                            type="object"
+                            id="button-custom-date"
+                            class="btn btn-outline-primary"
+                        >
+                            Mettre à jour
+                        </button>
+                    </div>
+                </nav>
+                <div id="dashboard_detail_values">
+                    <t t-value="records.dashboard_detail_values()" t-set="init_data" />
+                    <t t-value="init_data.get('customer')" t-set="customer" />
+                    <t t-value="init_data.get('employee_time')" t-set="employee_time" />
+                    <t t-call="lefilament_tdb.dashboard_detail_values" />
+                </div>
+            </qweb>
+        </field>
+    </record>
+
+    <!-- Dashboard Values -->
+    <template id="dashboard_detail_values" name="dashboard_detail_values">
+        <div class="dashboard_detail">
+            <div class="container-fluid">
+                <div class="row">
+                    <div class="col-12">
+                        <div class="mt-2 mb-4 display-6 text-center">
+                            Période :
+                            <t t-out="date_start" t-options="{'widget': 'date', 'format': 'dd/MM/YYYY'}" /> -
+                            <t t-out="date_end" t-options="{'widget': 'date', 'format': 'dd/MM/YYYY'}" />
+                        </div>
+                    </div>
+                    <div class="col-7">
+                        <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="customer" />
+                        </t>
+                    </div>
+                    <div class="col-5">
+                        <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="employee_time" />
+                        </t>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </template>
+
+    <!-- Table Template -->
+    <template id="dashboard_detail_table" name="dashboard_detail_table">
+        <div t-if="data" 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].keys()" t-as="header">
+                            <t t-out="header" />
+                        </th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr t-foreach="data" t-as="line">
+                        <td t-out="line_index + 1" />
+                        <t t-foreach="line.values()" t-as="v">
+                            <td t-att-class="'text-end' if isinstance(v, (int, float)) else ''">
+                                <t t-if="isinstance(v, (int, float))">
+                                    <t t-out="v" t-options="{'widget': 'float', 'precision': 0}" />
+                                </t>
+                                <t t-else="">
+                                    <t t-out="v" />
+                                </t>
+                            </td>
+                        </t>
+                    </tr>
+                </tbody>
+            </table>
+        </div>
+    </template>
+
+    <!-- Action -->
+    <record id="le_filament_dashboard_detail_action" model="ir.actions.act_window">
+        <field name="name">Détail</field>
+        <field name="res_model">lefilament.dashboard</field>
+        <field name="view_mode">qweb</field>
+        <field name="view_id" ref="lefilament_dashboard_detail"/>
+        <field name="context">{'active_test': False}</field>
+    </record>
+</odoo>
diff --git a/templates/lefilament_dashboard.xml b/templates/lefilament_dashboard.xml
index 0e3db35..3e451a7 100644
--- a/templates/lefilament_dashboard.xml
+++ b/templates/lefilament_dashboard.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8" ?>
 <odoo>
     <record id="lefilament_dashboard_overview_form" model="ir.ui.view">
-        <field name="name">lefilament.dashboardt.overview</field>
+        <field name="name">lefilament.dashboard.overview</field>
         <field name="type">qweb</field>
         <field name="model">lefilament.dashboard</field>
         <field name="arch" type="xml">
@@ -535,6 +535,7 @@
         <field name="name">Rapport Annuel</field>
         <field name="res_model">lefilament.dashboard</field>
         <field name="view_mode">qweb</field>
+        <field name="view_id" ref="lefilament_dashboard_overview_form" />
         <field name="context">{'active_test': False}</field>
     </record>
 </odoo>
diff --git a/views/menus.xml b/views/menus.xml
index 5bdc593..90d346f 100644
--- a/views/menus.xml
+++ b/views/menus.xml
@@ -24,6 +24,13 @@
         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"
         parent="lefilament_dashboard_menu"
-- 
GitLab