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