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