odoo-addon-mis-builder 16.0.5.2.3.1__py3-none-any.whl → 16.0.5.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. odoo/addons/mis_builder/README.rst +1 -1
  2. odoo/addons/mis_builder/__manifest__.py +2 -1
  3. odoo/addons/mis_builder/i18n/ca.po +91 -0
  4. odoo/addons/mis_builder/i18n/de.po +91 -0
  5. odoo/addons/mis_builder/i18n/el.po +91 -0
  6. odoo/addons/mis_builder/i18n/el_GR.po +91 -0
  7. odoo/addons/mis_builder/i18n/es.po +91 -0
  8. odoo/addons/mis_builder/i18n/fr.po +91 -0
  9. odoo/addons/mis_builder/i18n/hr.po +91 -0
  10. odoo/addons/mis_builder/i18n/it.po +91 -0
  11. odoo/addons/mis_builder/i18n/mis_builder.pot +97 -0
  12. odoo/addons/mis_builder/i18n/nl.po +91 -0
  13. odoo/addons/mis_builder/i18n/nl_NL.po +91 -0
  14. odoo/addons/mis_builder/i18n/pt.po +91 -0
  15. odoo/addons/mis_builder/i18n/pt_BR.po +91 -0
  16. odoo/addons/mis_builder/i18n/sv.po +91 -0
  17. odoo/addons/mis_builder/i18n/tr.po +91 -0
  18. odoo/addons/mis_builder/models/__init__.py +1 -0
  19. odoo/addons/mis_builder/models/kpimatrix.py +39 -4
  20. odoo/addons/mis_builder/models/mis_report_instance.py +71 -1
  21. odoo/addons/mis_builder/models/mis_report_instance_annotation.py +113 -0
  22. odoo/addons/mis_builder/report/mis_report_instance_qweb.xml +23 -0
  23. odoo/addons/mis_builder/report/mis_report_instance_xlsx.py +11 -1
  24. odoo/addons/mis_builder/security/ir.model.access.csv +2 -0
  25. odoo/addons/mis_builder/security/res_groups.xml +17 -0
  26. odoo/addons/mis_builder/static/description/index.html +1 -1
  27. odoo/addons/mis_builder/static/src/components/mis_report_widget.css +41 -0
  28. odoo/addons/mis_builder/static/src/components/mis_report_widget.esm.js +109 -4
  29. odoo/addons/mis_builder/static/src/components/mis_report_widget.xml +104 -13
  30. odoo/addons/mis_builder/static/src/css/report.css +22 -0
  31. odoo/addons/mis_builder/tests/__init__.py +1 -0
  32. odoo/addons/mis_builder/tests/test_mis_report_instance_annotation.py +154 -0
  33. odoo/addons/mis_builder/views/mis_report_instance.xml +1 -0
  34. {odoo_addon_mis_builder-16.0.5.2.3.1.dist-info → odoo_addon_mis_builder-16.0.5.4.0.dist-info}/METADATA +2 -2
  35. {odoo_addon_mis_builder-16.0.5.2.3.1.dist-info → odoo_addon_mis_builder-16.0.5.4.0.dist-info}/RECORD +37 -34
  36. {odoo_addon_mis_builder-16.0.5.2.3.1.dist-info → odoo_addon_mis_builder-16.0.5.4.0.dist-info}/WHEEL +0 -0
  37. {odoo_addon_mis_builder-16.0.5.2.3.1.dist-info → odoo_addon_mis_builder-16.0.5.4.0.dist-info}/top_level.txt +0 -0
@@ -23,6 +23,10 @@
23
23
  .oe_mis_builder_content {
24
24
  }
25
25
 
26
+ .oe_mis_builder_report_wide_sheet {
27
+ max-width: 95% !important;
28
+ }
29
+
26
30
  /* style for the control panel (search box and buttons) */
27
31
 
28
32
  .oe_mis_builder_cp {
@@ -44,6 +48,11 @@
44
48
  max-width: 1280px;
45
49
  }
46
50
 
51
+ .oe_mis_builder_cp_right_top_right {
52
+ display: flex;
53
+ flex-direction: row;
54
+ }
55
+
47
56
  .oe_mis_builder_cp_right_top {
48
57
  display: flex;
49
58
  flex-direction: row;
@@ -65,3 +74,35 @@
65
74
  flex-grow: 1;
66
75
  justify-content: flex-end;
67
76
  }
77
+
78
+ .oe_mis_builder_dropdown {
79
+ overflow: visible !important;
80
+ }
81
+
82
+ .oe_mis_builder_footnote {
83
+ font-size: 80%;
84
+ color: red;
85
+ position: relative;
86
+ bottom: 1ex;
87
+ width: 1em;
88
+ display: inline-block;
89
+ padding-right: 1px;
90
+ }
91
+
92
+ .oe_mis_builder_footnote_table {
93
+ list-style: none;
94
+ white-space: pre-wrap;
95
+ display: inline-block;
96
+
97
+ td {
98
+ vertical-align: top;
99
+ }
100
+ }
101
+
102
+ .oe_mis_builder_footnote_div {
103
+ padding-top: 1em;
104
+ }
105
+
106
+ .oe_mis_builder_menu_disabled {
107
+ color: gainsboro;
108
+ }
@@ -1,13 +1,15 @@
1
1
  /** @odoo-module **/
2
2
 
3
- import {Component, onWillStart, useState, useSubEnv} from "@odoo/owl";
4
- import {useBus, useService} from "@web/core/utils/hooks";
3
+ import Dialog from "web.Dialog";
4
+ import {Component, onMounted, onWillStart, useState, useSubEnv} from "@odoo/owl";
5
5
  import {DatePicker} from "@web/core/datepicker/datepicker";
6
6
  import {FilterMenu} from "@web/search/filter_menu/filter_menu";
7
7
  import {SearchBar} from "@web/search/search_bar/search_bar";
8
8
  import {SearchModel} from "@web/search/search_model";
9
9
  import {parseDate} from "@web/core/l10n/dates";
10
+ import {qweb} from "web.core";
10
11
  import {registry} from "@web/core/registry";
12
+ import {useBus, useService} from "@web/core/utils/hooks";
11
13
 
12
14
  export class MisReportWidget extends Component {
13
15
  setup() {
@@ -18,8 +20,10 @@ export class MisReportWidget extends Component {
18
20
  this.view = useService("view");
19
21
  this.JSON = JSON;
20
22
  this.state = useState({
21
- mis_report_data: {header: [], body: []},
23
+ mis_report_data: {header: [], body: [], notes: {}},
22
24
  pivot_date: null,
25
+ can_edit_annotation: false,
26
+ can_read_annotation: false,
23
27
  });
24
28
  this.searchModel = new SearchModel(this.env, {
25
29
  user: this.user,
@@ -32,6 +36,8 @@ export class MisReportWidget extends Component {
32
36
  this.refresh();
33
37
  });
34
38
  onWillStart(this.willStart);
39
+
40
+ onMounted(this._onMounted);
35
41
  }
36
42
 
37
43
  // Lifecycle
@@ -46,6 +52,9 @@ export class MisReportWidget extends Component {
46
52
  "widget_search_view_id",
47
53
  "pivot_date",
48
54
  "widget_show_pivot_date",
55
+ "user_can_read_annotation",
56
+ "user_can_edit_annotation",
57
+ "wide_display_by_default",
49
58
  ],
50
59
  {context: this.context}
51
60
  );
@@ -64,8 +73,16 @@ export class MisReportWidget extends Component {
64
73
  });
65
74
  }
66
75
 
76
+ this.wide_display = result.wide_display_by_default;
77
+
67
78
  // Compute the report
68
79
  this.refresh();
80
+ this.state.can_edit_annotation = result.user_can_edit_annotation;
81
+ this.state.can_read_annotation = result.user_can_read_annotation;
82
+ }
83
+
84
+ async _onMounted() {
85
+ this.resize_sheet();
69
86
  }
70
87
 
71
88
  get showSearchBar() {
@@ -121,7 +138,7 @@ export class MisReportWidget extends Component {
121
138
  }
122
139
 
123
140
  async drilldown(event) {
124
- const drilldown = $(event.target).data("drilldown");
141
+ const drilldown = JSON.parse(event.target.dataset.drilldown);
125
142
  const action = await this.orm.call(
126
143
  "mis.report.instance",
127
144
  "drilldown",
@@ -140,6 +157,15 @@ export class MisReportWidget extends Component {
140
157
  );
141
158
  }
142
159
 
160
+ async refresh_annotation() {
161
+ this.state.mis_report_data.notes = await this.orm.call(
162
+ "mis.report.instance",
163
+ "get_notes_by_cell_id",
164
+ [this._instanceId()],
165
+ {context: this.context}
166
+ );
167
+ }
168
+
143
169
  async printPdf() {
144
170
  const action = await this.orm.call(
145
171
  "mis.report.instance",
@@ -170,10 +196,89 @@ export class MisReportWidget extends Component {
170
196
  this.action.doAction(action);
171
197
  }
172
198
 
199
+ async _remove_annotation(cell_id) {
200
+ await this.orm.call(
201
+ "mis.report.instance.annotation",
202
+ "remove_annotation",
203
+ [cell_id, this._instanceId()],
204
+ {context: this.context}
205
+ );
206
+ this.refresh_annotation();
207
+ }
208
+
209
+ async _save_annotation(cell_id) {
210
+ const text = document.querySelector(".o_mis_builder_annotation_text").value;
211
+ await this.orm.call(
212
+ "mis.report.instance.annotation",
213
+ "set_annotation",
214
+ [cell_id, this._instanceId(), text],
215
+ {context: this.context}
216
+ );
217
+ await this.refresh_annotation();
218
+ }
219
+
220
+ async annotate(event) {
221
+ const cell_id = event.target.dataset.cellId;
222
+ const note = this.state.mis_report_data.notes[cell_id];
223
+ const note_text = (note && note.text) || "";
224
+ var buttons = [
225
+ {
226
+ text: this.env._t("Save"),
227
+ classes: "btn-primary",
228
+ close: true,
229
+ click: this._save_annotation.bind(this, cell_id),
230
+ },
231
+ {
232
+ text: this.env._t("Cancel"),
233
+ close: true,
234
+ },
235
+ ];
236
+ if (typeof note !== "undefined") {
237
+ buttons.push({
238
+ text: this.env._t("Remove"),
239
+ classes: "btn-secondary",
240
+ close: true,
241
+ click: this._remove_annotation.bind(this, cell_id),
242
+ });
243
+ }
244
+
245
+ new Dialog(this, {
246
+ title: "Annotate",
247
+ size: "medium",
248
+ $content: $(
249
+ qweb.render("mis_builder.annotation_dialog", {
250
+ text: note_text,
251
+ })
252
+ ),
253
+ buttons: buttons,
254
+ }).open();
255
+ }
256
+
257
+ async remove_annotation(event) {
258
+ const cell_id = event.target.dataset.cellId;
259
+ this._remove_annotation(cell_id);
260
+ }
261
+
173
262
  onDateTimeChanged(ev) {
174
263
  this.state.pivot_date = ev;
175
264
  this.refresh();
176
265
  }
266
+
267
+ async toggle_wide_display() {
268
+ this.wide_display = !this.wide_display;
269
+ this.resize_sheet();
270
+ }
271
+
272
+ async resize_sheet() {
273
+ var sheet_element = document.getElementsByClassName("o_form_sheet")[0];
274
+ sheet_element.classList.toggle(
275
+ "oe_mis_builder_report_wide_sheet",
276
+ this.wide_display
277
+ );
278
+ var button_resize_element = document.getElementById("icon_resize");
279
+ button_resize_element.classList.toggle("fa-expand", !this.wide_display);
280
+ button_resize_element.classList.toggle("fa-compress", this.wide_display);
281
+ }
177
282
  }
178
283
 
179
284
  MisReportWidget.components = {FilterMenu, SearchBar, DatePicker};
@@ -4,10 +4,21 @@
4
4
  <t t-name="mis_builder.MisReportWidget" owl="1">
5
5
  <div class="oe_mis_builder_content">
6
6
  <t t-if="state.mis_report_data">
7
+ <t t-set="notes" t-value="state.mis_report_data.notes" />
7
8
  <div class="oe_mis_builder_cp">
8
9
  <div class="oe_mis_builder_cp_left">
9
10
  </div>
10
11
  <div class="oe_mis_builder_cp_right">
12
+ <div class="oe_mis_builder_cp_right_top_right">
13
+ <div class="oe_mis_builder_action_buttons">
14
+ <button
15
+ t-on-click="toggle_wide_display"
16
+ class="btn btn-secondary"
17
+ >
18
+ <i id="icon_resize" class="fa" />
19
+ </button>
20
+ </div>
21
+ </div>
11
22
  <div class="oe_mis_builder_cp_right_top">
12
23
  <SearchBar t-if="showSearchBar" />
13
24
  </div>
@@ -85,21 +96,65 @@
85
96
  t-as="cell"
86
97
  t-key="cell_index"
87
98
  t-att="{'style': cell.style, 'title': cell.val_c}"
88
- class="mis_builder_amount"
99
+ class="mis_builder_amount oe_mis_builder_dropdown"
89
100
  >
90
- <t t-if="cell.drilldown_arg">
91
- <a
92
- href="javascript:void(0)"
93
- class="mis_builder_drilldown"
94
- t-on-click="drilldown"
95
- t-att-data-drilldown="JSON.stringify(cell.drilldown_arg)"
96
- >
101
+ <div>
102
+ <t t-if="cell.drilldown_arg">
103
+ <a
104
+ href="javascript:void(0)"
105
+ class="mis_builder_drilldown"
106
+ t-on-click="drilldown"
107
+ t-att-data-drilldown="JSON.stringify(cell.drilldown_arg)"
108
+ >
109
+ <t t-esc="cell.val_r" />
110
+ </a>
111
+ </t>
112
+ <t t-else="">
97
113
  <t t-esc="cell.val_r" />
98
- </a>
99
- </t>
100
- <t t-if="!cell.drilldown_arg">
101
- <t t-esc="cell.val_r" />
102
- </t>
114
+ </t>
115
+ <span class="oe_mis_builder_footnote">
116
+ <div t-if="notes[cell.cell_id]">
117
+ <a
118
+ t-att-id="'note_'+notes[cell.cell_id].sequence"
119
+ t-out="notes[cell.cell_id] and notes[cell.cell_id].sequence"
120
+ t-att="{'title': notes[cell.cell_id].text}"
121
+ href="#footnotes"
122
+ />
123
+ </div>
124
+ </span>
125
+
126
+ <div id="dropdown_menu" class="btn-group">
127
+ <div
128
+ class="dropdown"
129
+ t-if="state.can_edit_annotation and cell.can_be_annotated"
130
+ >
131
+ <div
132
+ data-bs-toggle="dropdown"
133
+ t-attf-class="dropdown-toggle"
134
+ />
135
+
136
+ <div
137
+ class="dropdown-menu o_filter_menu"
138
+ role="menu"
139
+ >
140
+ <a
141
+ href="javascript:void(0)"
142
+ t-on-click="annotate"
143
+ t-att-data-cell-id="cell.cell_id"
144
+ role="menuitem"
145
+ class="dropdown-item js_tag"
146
+ >
147
+ Annotate
148
+ </a>
149
+ </div>
150
+ </div>
151
+ <!-- show menu as disabled -->
152
+ <div
153
+ t-else=""
154
+ class="dropdown-toggle oe_mis_builder_menu_disabled"
155
+ />
156
+ </div>
157
+ </div>
103
158
  </td>
104
159
  </tr>
105
160
  </tbody>
@@ -108,8 +163,44 @@
108
163
  </tfoot>
109
164
  </table>
110
165
  </div>
166
+ <!-- Adding notes -->
167
+ <div class="oe_mis_builder_footnote_div" id="footnotes">
168
+ <table class="oe_mis_builder_footnote_table">
169
+ <t
170
+ t-foreach="state.mis_report_data.notes"
171
+ t-as="cell_id"
172
+ t-key="cell_id"
173
+ >
174
+ <tr>
175
+ <td><a
176
+ t-out="notes[cell_id].sequence"
177
+ t-att-href="'#note_'+notes[cell_id].sequence"
178
+ />. </td>
179
+ <td><t t-out="notes[cell_id].text" /></td>
180
+ <td><i
181
+ href="javascript:void(0)"
182
+ t-on-click="remove_annotation"
183
+ t-att-data-cell-id="cell_id"
184
+ class="btn fa fa-trash-o"
185
+ t-if="state.can_edit_annotation"
186
+ /></td>
187
+ </tr>
188
+ </t>
189
+ </table>
190
+ </div>
111
191
  </t>
112
192
  </div>
113
193
  </t>
114
194
 
195
+ <t t-name="mis_builder.annotation_dialog">
196
+ <form role="form">
197
+ <textarea
198
+ class="o_mis_builder_annotation_text"
199
+ name="note"
200
+ rows='4'
201
+ placeholder="Insert note here"
202
+ ><t t-out="text" t-att-data-textnote="text" /></textarea>
203
+ </form>
204
+ </t>
205
+
115
206
  </templates>
@@ -44,3 +44,25 @@
44
44
  .mis_table .mis_cell.mis_amount {
45
45
  text-align: right;
46
46
  }
47
+ .oe_mis_builder_footnote {
48
+ font-size: 70%;
49
+ color: red;
50
+ position: relative;
51
+ bottom: 1ex;
52
+ width: 1em;
53
+ display: inline-block;
54
+ padding-right: 1px;
55
+ }
56
+ .oe_mis_builder_footnote_div {
57
+ padding-top: 1em;
58
+ }
59
+
60
+ .oe_mis_builder_footnote_table {
61
+ list-style: none;
62
+ white-space: pre-wrap;
63
+ display: inline-block;
64
+
65
+ td {
66
+ vertical-align: top;
67
+ }
68
+ }
@@ -14,3 +14,4 @@ from . import test_render
14
14
  from . import test_simple_array
15
15
  from . import test_target_move
16
16
  from . import test_utc_midnight
17
+ from . import test_mis_report_instance_annotation
@@ -0,0 +1,154 @@
1
+ # Copyright 2025 ACSONE SA/NV
2
+ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3
+
4
+ from odoo import Command
5
+ from odoo.tests.common import TransactionCase
6
+
7
+
8
+ class TestMisReportInstanceAnnotation(TransactionCase):
9
+ def setUp(self):
10
+ super().setUp()
11
+ self.report = self.env["mis.report"].create(
12
+ dict(
13
+ name="test report",
14
+ subkpi_ids=[
15
+ Command.create(
16
+ dict(
17
+ name="subkpi1_report2",
18
+ description="subkpi 1, report 2",
19
+ sequence=1,
20
+ )
21
+ ),
22
+ Command.create(
23
+ dict(
24
+ name="subkpi2_report2",
25
+ description="subkpi 2, report 2",
26
+ sequence=2,
27
+ ),
28
+ ),
29
+ ],
30
+ )
31
+ )
32
+
33
+ self.kpi = self.env["mis.report.kpi"].create(
34
+ dict(
35
+ report_id=self.report.id,
36
+ description="kpi 1",
37
+ name="k1",
38
+ multi=True,
39
+ expression_ids=[
40
+ Command.create(
41
+ dict(name="bale[200%]", subkpi_id=self.report.subkpi_ids[0].id),
42
+ ),
43
+ Command.create(
44
+ dict(name="balp[200%]", subkpi_id=self.report.subkpi_ids[1].id),
45
+ ),
46
+ ],
47
+ )
48
+ )
49
+
50
+ self.report_instance = self.env["mis.report.instance"].create(
51
+ dict(
52
+ name="test instance",
53
+ report_id=self.report.id,
54
+ company_id=self.env.ref("base.main_company").id,
55
+ period_ids=[
56
+ Command.create(
57
+ dict(
58
+ name="p1",
59
+ mode="fix",
60
+ manual_date_from="2013-01-01",
61
+ manual_date_to="2013-12-31",
62
+ sequence=1,
63
+ ),
64
+ ),
65
+ Command.create(
66
+ dict(
67
+ name="p2",
68
+ mode="fix",
69
+ manual_date_from="2014-01-01",
70
+ manual_date_to="2014-12-31",
71
+ sequence=2,
72
+ ),
73
+ ),
74
+ ],
75
+ )
76
+ )
77
+
78
+ def test_adding_note(self):
79
+ notes = self.report_instance.get_notes_by_cell_id()
80
+
81
+ self.assertEqual({}, notes)
82
+
83
+ # report with 4 cells, 2 periods and 2 subkpis
84
+ matrix = self.report_instance._compute_matrix()
85
+ cell_ids = [c.cell_id for row in matrix.iter_rows() for c in row.iter_cells()]
86
+ self.assertEqual(len(cell_ids), 4)
87
+
88
+ first_cell_id, second_cell_id, third_cell_id, _fourth_cell_id = cell_ids
89
+
90
+ # adding one note
91
+ self.env["mis.report.instance.annotation"].set_annotation(
92
+ first_cell_id, self.report_instance.id, "This is a note"
93
+ )
94
+ notes = self.report_instance.get_notes_by_cell_id()
95
+ self.assertDictEqual(
96
+ {first_cell_id: {"text": "This is a note", "sequence": 1}}, notes
97
+ )
98
+
99
+ # adding another note
100
+ self.env["mis.report.instance.annotation"].set_annotation(
101
+ third_cell_id, self.report_instance.id, "This is another note"
102
+ )
103
+ notes = self.report_instance.get_notes_by_cell_id()
104
+ self.assertDictEqual(
105
+ {
106
+ first_cell_id: {"text": "This is a note", "sequence": 1},
107
+ third_cell_id: {"text": "This is another note", "sequence": 2},
108
+ },
109
+ notes,
110
+ )
111
+
112
+ self.env["mis.report.instance.annotation"].set_annotation(
113
+ second_cell_id, self.report_instance.id, "This is third note"
114
+ )
115
+
116
+ notes = self.report_instance.get_notes_by_cell_id()
117
+ # Last note added should have a sequence of
118
+ # 2 since it is deplayed in the second cell
119
+ self.assertDictEqual(
120
+ {
121
+ first_cell_id: {"text": "This is a note", "sequence": 1},
122
+ second_cell_id: {"text": "This is third note", "sequence": 2},
123
+ third_cell_id: {"text": "This is another note", "sequence": 3},
124
+ },
125
+ notes,
126
+ )
127
+
128
+ def test_remove_note(self):
129
+ notes = self.report_instance.get_notes_by_cell_id()
130
+
131
+ self.assertEqual({}, notes)
132
+
133
+ # report with 4 cells, 2 periods and 2 subkpis
134
+ matrix = self.report_instance._compute_matrix()
135
+ cell_ids = [c.cell_id for row in matrix.iter_rows() for c in row.iter_cells()]
136
+ self.assertEqual(len(cell_ids), 4)
137
+
138
+ first_cell_id = cell_ids[0]
139
+
140
+ # adding one note
141
+ self.env["mis.report.instance.annotation"].set_annotation(
142
+ first_cell_id, self.report_instance.id, "This is a note"
143
+ )
144
+ notes = self.report_instance.get_notes_by_cell_id()
145
+ self.assertDictEqual(
146
+ {first_cell_id: {"text": "This is a note", "sequence": 1}}, notes
147
+ )
148
+
149
+ # remove note
150
+ self.env["mis.report.instance.annotation"].remove_annotation(
151
+ first_cell_id, self.report_instance.id
152
+ )
153
+ notes = self.report_instance.get_notes_by_cell_id()
154
+ self.assertEqual({}, notes)
@@ -195,6 +195,7 @@
195
195
  <field name="landscape_pdf" />
196
196
  <field name="no_auto_expand_accounts" />
197
197
  <field name="display_columns_description" />
198
+ <field name="wide_display_by_default" />
198
199
  </group>
199
200
  </page>
200
201
  <page string="Widget">
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: odoo-addon-mis_builder
3
- Version: 16.0.5.2.3.1
3
+ Version: 16.0.5.4.0
4
4
  Requires-Python: >=3.10
5
5
  Requires-Dist: odoo-addon-date_range>=16.0dev,<16.1dev
6
6
  Requires-Dist: odoo-addon-report_xlsx>=16.0dev,<16.1dev
@@ -25,7 +25,7 @@ MIS Builder
25
25
  !! This file is generated by oca-gen-addon-readme !!
26
26
  !! changes will be overwritten. !!
27
27
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
28
- !! source digest: sha256:578ec6e1eb6754314b8a51c950070f53d28faebcd67c3d7014c0a53d82cca432
28
+ !! source digest: sha256:eaf310c997ae9746cf01edf102baa7c519355121536c5a695b45470f6ab27dee
29
29
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
30
30
 
31
31
  .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png