diff --git a/automation_oca/__manifest__.py b/automation_oca/__manifest__.py index 80e8202..b994d5a 100644 --- a/automation_oca/__manifest__.py +++ b/automation_oca/__manifest__.py @@ -24,6 +24,7 @@ "assets": { "web.assets_backend": [ "automation_oca/static/src/**/*.js", + "automation_oca/static/src/**/*.xml", "automation_oca/static/src/**/*.scss", ], }, diff --git a/automation_oca/models/automation_configuration_activity.py b/automation_oca/models/automation_configuration_activity.py index f4f924e..1a21071 100644 --- a/automation_oca/models/automation_configuration_activity.py +++ b/automation_oca/models/automation_configuration_activity.py @@ -1,10 +1,14 @@ # Copyright 2024 Dixmit # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from collections import defaultdict + +import babel.dates from dateutil.relativedelta import relativedelta from odoo import api, fields, models from odoo.osv import expression +from odoo.tools import get_lang from odoo.tools.safe_eval import safe_eval @@ -77,6 +81,83 @@ class AutomationConfigurationActivity(models.Model): parent_position = fields.Integer( compute="_compute_parent_position", recursive=True, store=True ) + graph_data = fields.Json(compute="_compute_graph_data") + graph_done = fields.Integer(compute="_compute_total_graph_data") + graph_error = fields.Integer(compute="_compute_total_graph_data") + + @api.depends() + def _compute_graph_data(self): + total = self.env["automation.record.activity"].read_group( + [ + ("configuration_activity_id", "in", self.ids), + ("processed_on", ">=", fields.Date.today() + relativedelta(days=-14)), + ], + ["configuration_activity_id"], + ["configuration_activity_id", "processed_on:day"], + lazy=False, + ) + done = self.env["automation.record.activity"].read_group( + [ + ("configuration_activity_id", "in", self.ids), + ("processed_on", ">=", fields.Date.today() + relativedelta(days=-14)), + ("state", "=", "done"), + ], + ["configuration_activity_id"], + ["configuration_activity_id", "processed_on:day"], + lazy=False, + ) + now = fields.Datetime.now() + date_map = { + babel.dates.format_datetime( + now + relativedelta(days=i - 14), + format="dd MMM yyy", + tzinfo=self._context.get("tz", None), + locale=get_lang(self.env).code, + ): 0 + for i in range(0, 15) + } + result = defaultdict( + lambda: {"done": date_map.copy(), "error": date_map.copy()} + ) + for line in total: + result[line["configuration_activity_id"][0]]["error"][ + line["processed_on:day"] + ] += line["__count"] + for line in done: + result[line["configuration_activity_id"][0]]["done"][ + line["processed_on:day"] + ] += line["__count"] + result[line["configuration_activity_id"][0]]["error"][ + line["processed_on:day"] + ] -= line["__count"] + for record in self: + graph_info = dict(result[record.id]) + record.graph_data = { + "error": [ + {"x": key[:-5], "y": value, "name": key} + for (key, value) in graph_info["error"].items() + ], + "done": [ + {"x": key[:-5], "y": value, "name": key} + for (key, value) in graph_info["done"].items() + ], + } + + @api.depends() + def _compute_total_graph_data(self): + for record in self: + record.graph_done = self.env["automation.record.activity"].search_count( + [ + ("configuration_activity_id", "in", self.ids), + ("state", "=", "done"), + ] + ) + record.graph_error = self.env["automation.record.activity"].search_count( + [ + ("configuration_activity_id", "in", self.ids), + ("state", "in", ["expired", "rejected", "error", "cancel"]), + ] + ) @api.depends("trigger_interval", "trigger_interval_type") def _compute_trigger_interval_hours(self): diff --git a/automation_oca/static/src/fields/automation_activity.esm.js b/automation_oca/static/src/fields/automation_activity/automation_activity.esm.js similarity index 93% rename from automation_oca/static/src/fields/automation_activity.esm.js rename to automation_oca/static/src/fields/automation_activity/automation_activity.esm.js index 8b396f4..1d8149d 100644 --- a/automation_oca/static/src/fields/automation_activity.esm.js +++ b/automation_oca/static/src/fields/automation_activity/automation_activity.esm.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import {useOpenX2ManyRecord, useX2ManyCrud} from "@web/views/fields/relational_utils"; -import {AutomationKanbanRenderer} from "../views/automation_kanban/automation_kanban_renderer.esm"; +import {AutomationKanbanRenderer} from "../../views/automation_kanban/automation_kanban_renderer.esm"; import {X2ManyField} from "@web/views/fields/x2many/x2many_field"; import {registry} from "@web/core/registry"; diff --git a/automation_oca/static/src/fields/automation_graph/automation_graph.esm.js b/automation_oca/static/src/fields/automation_graph/automation_graph.esm.js new file mode 100644 index 0000000..b22906b --- /dev/null +++ b/automation_oca/static/src/fields/automation_graph/automation_graph.esm.js @@ -0,0 +1,104 @@ +/** @odoo-module **/ +/* global Chart*/ + +import {loadJS} from "@web/core/assets"; +import {registry} from "@web/core/registry"; +import {standardFieldProps} from "@web/views/fields/standard_field_props"; + +const {Component, onWillStart, useEffect, useRef} = owl; + +export class AutomationGraph extends Component { + setup() { + this.chart = null; + this.canvasRef = useRef("canvas"); + onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js")); + useEffect(() => { + this.renderChart(); + return () => { + if (this.chart) { + this.chart.destroy(); + } + }; + }); + } + _getChartConfig() { + return { + type: "line", + data: { + labels: this.props.value.done.map(function (pt) { + return pt.x; + }), + datasets: [ + { + backgroundColor: "#4CAF5080", + borderColor: "#4CAF50", + data: this.props.value.done, + fill: "start", + label: this.env._t("Done"), + borderWidth: 2, + }, + { + backgroundColor: "#F4433680", + borderColor: "#F44336", + data: this.props.value.error, + fill: "start", + label: this.env._t("Error"), + borderWidth: 2, + }, + ], + }, + options: { + legend: {display: false}, + + layout: { + padding: {left: 10, right: 10, top: 10, bottom: 10}, + }, + scales: { + yAxes: [ + { + type: "linear", + display: false, + ticks: { + beginAtZero: true, + }, + }, + ], + xAxes: [ + { + ticks: { + maxRotation: 0, + }, + }, + ], + }, + maintainAspectRatio: false, + elements: { + line: { + tension: 0.000001, + }, + }, + tooltips: { + intersect: false, + position: "nearest", + caretSize: 0, + borderWidth: 2, + }, + }, + }; + } + renderChart() { + if (this.chart) { + this.chart.destroy(); + } + var config = this._getChartConfig(); + this.chart = new Chart(this.canvasRef.el, config); + Chart.animationService.advance(); + } +} + +AutomationGraph.template = "automation_oca.AutomationGraph"; +AutomationGraph.props = { + ...standardFieldProps, +}; + +registry.category("fields").add("automation_graph", AutomationGraph); diff --git a/automation_oca/static/src/fields/automation_graph/automation_graph.xml b/automation_oca/static/src/fields/automation_graph/automation_graph.xml new file mode 100644 index 0000000..03212b5 --- /dev/null +++ b/automation_oca/static/src/fields/automation_graph/automation_graph.xml @@ -0,0 +1,10 @@ + + + + +
+ +
+
+ +
diff --git a/automation_oca/static/src/views/automation_kanban/automation_kanban.scss b/automation_oca/static/src/views/automation_kanban/automation_kanban.scss index 356ad74..2f2461b 100644 --- a/automation_oca/static/src/views/automation_kanban/automation_kanban.scss +++ b/automation_oca/static/src/views/automation_kanban/automation_kanban.scss @@ -65,6 +65,14 @@ right: 0px; } } + .o_automation_kanban_graph { + .o_automation_kpi_processed { + color: #4caf50; + } + .o_automation_kpi_error { + color: #f44336; + } + } .o_automation_kanban_child_add { .o_automation_kanban_child_add_title { padding: 2px; @@ -107,3 +115,6 @@ } } } +.o_field_automation_graph { + width: 100%; +} diff --git a/automation_oca/views/automation_configuration.xml b/automation_oca/views/automation_configuration.xml index 9ce419f..6dafc2d 100644 --- a/automation_oca/views/automation_configuration.xml +++ b/automation_oca/views/automation_configuration.xml @@ -233,8 +233,27 @@ -
- TO BE FILLED +
+
+ +
+
+

+ +

+
Processed
+

+ +

+
Error
+