odoo-addon-account-dashboard-banner 16.0.1.0.1.1__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 (29) hide show
  1. odoo/addons/account_dashboard_banner/README.rst +151 -0
  2. odoo/addons/account_dashboard_banner/__init__.py +2 -0
  3. odoo/addons/account_dashboard_banner/__manifest__.py +29 -0
  4. odoo/addons/account_dashboard_banner/i18n/account_dashboard_banner.pot +259 -0
  5. odoo/addons/account_dashboard_banner/i18n/fr.po +287 -0
  6. odoo/addons/account_dashboard_banner/i18n/it.po +274 -0
  7. odoo/addons/account_dashboard_banner/models/__init__.py +1 -0
  8. odoo/addons/account_dashboard_banner/models/account_dashboard_banner_cell.py +331 -0
  9. odoo/addons/account_dashboard_banner/post_install.py +28 -0
  10. odoo/addons/account_dashboard_banner/readme/CONFIGURE.md +33 -0
  11. odoo/addons/account_dashboard_banner/readme/CONTRIBUTORS.md +1 -0
  12. odoo/addons/account_dashboard_banner/readme/DESCRIPTION.md +13 -0
  13. odoo/addons/account_dashboard_banner/readme/USAGE.md +3 -0
  14. odoo/addons/account_dashboard_banner/security/ir.model.access.csv +4 -0
  15. odoo/addons/account_dashboard_banner/static/description/account_dashboard_banner.png +0 -0
  16. odoo/addons/account_dashboard_banner/static/description/banner_cell_config.png +0 -0
  17. odoo/addons/account_dashboard_banner/static/description/cell_form_with_warning.png +0 -0
  18. odoo/addons/account_dashboard_banner/static/description/icon.png +0 -0
  19. odoo/addons/account_dashboard_banner/static/description/index.html +483 -0
  20. odoo/addons/account_dashboard_banner/static/src/views/account_dashboard_kanban_banner.esm.js +47 -0
  21. odoo/addons/account_dashboard_banner/static/src/views/account_dashboard_kanban_banner.xml +49 -0
  22. odoo/addons/account_dashboard_banner/tests/__init__.py +1 -0
  23. odoo/addons/account_dashboard_banner/tests/test_banner.py +56 -0
  24. odoo/addons/account_dashboard_banner/views/account_dashboard_banner_cell.xml +92 -0
  25. odoo/addons/account_dashboard_banner/views/account_journal_dashboard.xml +19 -0
  26. odoo_addon_account_dashboard_banner-16.0.1.0.1.1.dist-info/METADATA +168 -0
  27. odoo_addon_account_dashboard_banner-16.0.1.0.1.1.dist-info/RECORD +29 -0
  28. odoo_addon_account_dashboard_banner-16.0.1.0.1.1.dist-info/WHEEL +5 -0
  29. odoo_addon_account_dashboard_banner-16.0.1.0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,331 @@
1
+ # Copyright 2025 Akretion France (https://www.akretion.com/)
2
+ # @author: Alexis de Lattre <alexis.delattre@akretion.com>
3
+ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
4
+
5
+ from dateutil.relativedelta import relativedelta
6
+
7
+ from odoo import _, api, fields, models
8
+ from odoo.exceptions import ValidationError
9
+ from odoo.tools import date_utils
10
+ from odoo.tools.misc import format_amount, format_date
11
+
12
+
13
+ class AccountDashboardBannerCell(models.Model):
14
+ _name = "account.dashboard.banner.cell"
15
+ _description = "Accounting Dashboard Banner Cell"
16
+ _order = "sequence, id"
17
+
18
+ sequence = fields.Integer()
19
+ cell_type = fields.Selection(
20
+ [
21
+ ("income_fiscalyear", "Fiscal Year-to-date Income"),
22
+ ("income_year", "Year-to-date Income"),
23
+ ("income_quarter", "Quarter-to-date Income"),
24
+ ("income_month", "Month-to-date Income"),
25
+ ("liquidity", "Liquidity"),
26
+ ("customer_debt", "Customer Debt"),
27
+ ("customer_overdue", "Customer Overdue"),
28
+ ("supplier_debt", "Supplier Debt"),
29
+ # for lock dates, the key matches exactly the field name on res.company
30
+ ("tax_lock_date", "Tax Return Lock Date"),
31
+ ("period_lock_date", "Journals Entries Lock Date"),
32
+ ("fiscalyear_lock_date", "All Users Lock Date"),
33
+ ],
34
+ required=True,
35
+ )
36
+ custom_label = fields.Char()
37
+ custom_tooltip = fields.Char()
38
+ warn = fields.Boolean(string="Warning")
39
+ warn_lock_date_days = fields.Integer(
40
+ compute="_compute_warn_fields", store=True, readonly=False, precompute=True
41
+ )
42
+ warn_min = fields.Float(string="Minimum")
43
+ warn_max = fields.Float(string="Maximum")
44
+ warn_type_show = fields.Boolean(
45
+ compute="_compute_warn_fields", store=True, precompute=True
46
+ )
47
+ warn_type = fields.Selection(
48
+ [
49
+ ("under", "Under Minimum"),
50
+ ("above", "Above Maximum"),
51
+ ("outside", "Under Minimum or Above Maximum"),
52
+ ("inside", "Between Minimum and Maximum"),
53
+ ],
54
+ default="under",
55
+ )
56
+
57
+ _sql_constraints = [
58
+ (
59
+ "warn_lock_date_days_positive",
60
+ "CHECK(warn_lock_date_days >= 0)",
61
+ "Warn if lock date is older than N days must be positive or null.",
62
+ )
63
+ ]
64
+
65
+ @api.constrains("warn_min", "warn_max", "warn_type", "warn", "cell_type")
66
+ def _check_warn_config(self):
67
+ for cell in self:
68
+ if (
69
+ cell.cell_type
70
+ and not cell.cell_type.endswith("_lock_date")
71
+ and cell.warn
72
+ and cell.warn_type in ("outside", "inside")
73
+ and cell.warn_max <= cell.warn_min
74
+ ):
75
+ cell_type2label = dict(
76
+ self.fields_get("cell_type", "selection")["cell_type"]["selection"]
77
+ )
78
+ raise ValidationError(
79
+ _(
80
+ "On cell '%(cell_type)s' with warning enabled, "
81
+ "the minimum (%(warn_min)s) must be under "
82
+ "the maximum (%(warn_max)s).",
83
+ cell_type=cell_type2label[cell.cell_type],
84
+ warn_min=cell.warn_min,
85
+ warn_max=cell.warn_max,
86
+ )
87
+ )
88
+
89
+ @api.model
90
+ def _default_warn_lock_date_days(self, cell_type):
91
+ defaultmap = {
92
+ "tax_lock_date": 61, # 2 months
93
+ "period_lock_date": 61, # 2 months
94
+ "fiscalyear_lock_date": 520, # FY final closing, 1 year + 5 months
95
+ }
96
+ return defaultmap.get(cell_type)
97
+
98
+ @api.depends("cell_type", "warn")
99
+ def _compute_warn_fields(self):
100
+ for cell in self:
101
+ warn_type_show = False
102
+ warn_lock_date_days = 0
103
+ if cell.cell_type and cell.warn:
104
+ if cell.cell_type.endswith("_lock_date"):
105
+ warn_lock_date_days = self._default_warn_lock_date_days(
106
+ cell.cell_type
107
+ )
108
+ else:
109
+ warn_type_show = True
110
+ cell.warn_type_show = warn_type_show
111
+ cell.warn_lock_date_days = warn_lock_date_days
112
+
113
+ @api.model
114
+ def get_banner_data(self):
115
+ """This is the method called by the JS code that displays the banner"""
116
+ company = self.env.company
117
+ return self._prepare_banner_data(company)
118
+
119
+ def _prepare_speedy(self, company):
120
+ lock_date_fields = [
121
+ "tax_lock_date",
122
+ "period_lock_date",
123
+ "fiscalyear_lock_date",
124
+ ]
125
+ speedy = {
126
+ "cell_type2label": dict(
127
+ self.fields_get("cell_type", "selection")["cell_type"]["selection"]
128
+ ),
129
+ "lock_date2help": {
130
+ key: value["help"]
131
+ for (key, value) in company.fields_get(lock_date_fields, "help").items()
132
+ },
133
+ "today": fields.Date.context_today(self),
134
+ }
135
+ return speedy
136
+
137
+ @api.model
138
+ def _prepare_banner_data(self, company):
139
+ # The order in this list will be the display order in the banner
140
+ # In fact, it's not a list but a dict. I tried to make it work by returning
141
+ # a list but it seems OWL only accepts dicts (I always get errors on lists)
142
+ cells = self.search([])
143
+ speedy = cells._prepare_speedy(company)
144
+ res = {}
145
+ seq = 0
146
+ for cell in cells:
147
+ seq += 1
148
+ cell_data = cell._prepare_cell_data(company, speedy)
149
+ cell._update_cell_warn(cell_data)
150
+ res[seq] = cell_data
151
+ # from pprint import pprint
152
+ # pprint(res)
153
+ return res
154
+
155
+ def _prepare_cell_data_liquidity(self, company, speedy):
156
+ self.ensure_one()
157
+ journals = self.env["account.journal"].search(
158
+ [
159
+ ("company_id", "=", company.id),
160
+ ("type", "in", ("bank", "cash", "credit")),
161
+ ("default_account_id", "!=", False),
162
+ ]
163
+ )
164
+ accounts = journals.default_account_id
165
+ return (accounts, 1, False, False, False)
166
+
167
+ def _prepare_cell_data_supplier_debt(self, company, speedy):
168
+ accounts = self.env["ir.property"]._get(
169
+ "property_account_payable_id", "res.partner"
170
+ )
171
+ return (accounts, -1, False, False, False)
172
+
173
+ def _prepare_cell_data_income(self, company, speedy):
174
+ cell_type = self.cell_type
175
+ accounts = self.env["account.account"].search(
176
+ [
177
+ ("company_id", "=", company.id),
178
+ ("account_type", "in", ("income", "income_other")),
179
+ ]
180
+ )
181
+ if cell_type == "income_fiscalyear":
182
+ start_date, end_date = date_utils.get_fiscal_year(
183
+ speedy["today"],
184
+ day=company.fiscalyear_last_day,
185
+ month=int(company.fiscalyear_last_month),
186
+ )
187
+ elif cell_type == "income_month":
188
+ start_date = speedy["today"] + relativedelta(day=1)
189
+ elif cell_type == "income_year":
190
+ start_date = speedy["today"] + relativedelta(day=1, month=1)
191
+ elif cell_type == "income_quarter":
192
+ month_start_quarter = 3 * ((speedy["today"].month - 1) // 3) + 1
193
+ start_date = speedy["today"] + relativedelta(
194
+ day=1, month=month_start_quarter
195
+ )
196
+ specific_domain = [("date", ">=", start_date)]
197
+ specific_tooltip = _(
198
+ "Balance of account(s) {account_codes} since %s.",
199
+ format_date(self.env, start_date),
200
+ )
201
+ return (accounts, -1, specific_domain, False, specific_tooltip)
202
+
203
+ def _prepare_cell_data_customer_debt(self, company, speedy):
204
+ accounts = self.env["ir.property"]._get(
205
+ "property_account_receivable_id", "res.partner"
206
+ )
207
+ if (
208
+ hasattr(company, "account_default_pos_receivable_account_id")
209
+ and company.account_default_pos_receivable_account_id
210
+ ):
211
+ accounts |= company.account_default_pos_receivable_account_id
212
+ return (accounts, 1, False, False, False)
213
+
214
+ def _prepare_cell_data_customer_overdue(self, company, speedy):
215
+ (
216
+ accounts,
217
+ sign,
218
+ specific_domain,
219
+ specific_aggregate,
220
+ specific_tooltip,
221
+ ) = self._prepare_cell_data_customer_debt(company, speedy)
222
+ specific_domain = [("date_maturity", "<", speedy["today"])]
223
+ specific_aggregate = "amount_residual:sum"
224
+ specific_tooltip = _(
225
+ "Residual amount of account(s) {account_codes} with due date in the past."
226
+ )
227
+ return (accounts, sign, specific_domain, specific_aggregate, specific_tooltip)
228
+
229
+ def _prepare_cell_data(self, company, speedy):
230
+ """Inherit this method to change the computation of a cell type"""
231
+ self.ensure_one()
232
+ cell_type = self.cell_type
233
+ value = raw_value = tooltip = warn = False
234
+ if cell_type.endswith("lock_date"):
235
+ raw_value = company[cell_type]
236
+ value = raw_value and format_date(self.env, raw_value)
237
+ tooltip = speedy["lock_date2help"][cell_type]
238
+ if self.warn:
239
+ if not raw_value:
240
+ warn = True
241
+ elif raw_value < speedy["today"] - relativedelta(
242
+ days=self.warn_lock_date_days
243
+ ):
244
+ warn = True
245
+ else:
246
+ accounts = False
247
+ if hasattr(self, f"_prepare_cell_data_{cell_type}"):
248
+ specific_method = getattr(self, f"_prepare_cell_data_{cell_type}")
249
+ (
250
+ accounts,
251
+ sign,
252
+ specific_domain,
253
+ specific_aggregate,
254
+ specific_tooltip,
255
+ ) = specific_method(company, speedy)
256
+ elif cell_type.startswith("income_"):
257
+ (
258
+ accounts,
259
+ sign,
260
+ specific_domain,
261
+ specific_aggregate,
262
+ specific_tooltip,
263
+ ) = self._prepare_cell_data_income(company, speedy)
264
+ if accounts:
265
+ domain = (specific_domain or []) + [
266
+ ("company_id", "=", company.id),
267
+ ("account_id", "in", accounts.ids),
268
+ ("date", "<=", speedy["today"]),
269
+ ("parent_state", "=", "posted"),
270
+ ]
271
+ aggregate = specific_aggregate or "balance:sum"
272
+ rg_res = self.env["account.move.line"]._read_group(
273
+ domain, [aggregate], []
274
+ )
275
+ assert sign in (1, -1)
276
+ raw_value = (
277
+ rg_res and rg_res[0].get(aggregate.split(":")[0]) or 0
278
+ ) * sign
279
+ value = format_amount(self.env, raw_value, company.currency_id)
280
+ tooltip_src = specific_tooltip or _(
281
+ "Balance of account(s) {account_codes}."
282
+ )
283
+ tooltip = tooltip_src.format(
284
+ account_codes=", ".join(accounts.mapped("code"))
285
+ )
286
+ res = {
287
+ "cell_type": cell_type,
288
+ "label": self.custom_label or speedy["cell_type2label"][cell_type],
289
+ "raw_value": raw_value,
290
+ "value": value or _("None"),
291
+ "tooltip": self.custom_tooltip or tooltip,
292
+ "warn": warn,
293
+ }
294
+ return res
295
+
296
+ def _update_cell_warn(self, cell_data):
297
+ self.ensure_one()
298
+ if (
299
+ not cell_data.get("warn")
300
+ and self.warn
301
+ and self.warn_type
302
+ and isinstance(cell_data["raw_value"], (int | float))
303
+ ):
304
+ raw_value = cell_data["raw_value"]
305
+ if (
306
+ (self.warn_type == "under" and raw_value < self.warn_min)
307
+ or (self.warn_type == "above" and raw_value > self.warn_max)
308
+ or (
309
+ self.warn_type == "outside"
310
+ and (raw_value < self.warn_min or raw_value > self.warn_max)
311
+ )
312
+ or (
313
+ self.warn_type == "inside"
314
+ and raw_value > self.warn_min
315
+ and raw_value < self.warn_max
316
+ )
317
+ ):
318
+ cell_data["warn"] = True
319
+
320
+ @api.depends("cell_type", "custom_label")
321
+ def _compute_display_name(self):
322
+ type2name = dict(
323
+ self.fields_get("cell_type", "selection")["cell_type"]["selection"]
324
+ )
325
+ for cell in self:
326
+ display_name = "-"
327
+ if cell.custom_label:
328
+ display_name = cell.custom_label
329
+ elif cell.cell_type:
330
+ display_name = type2name[cell.cell_type]
331
+ cell.display_name = display_name
@@ -0,0 +1,28 @@
1
+ # Copyright 2025 Akretion France (https://www.akretion.com/)
2
+ # @author: Alexis de Lattre <alexis.delattre@akretion.com>
3
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4
+
5
+ # I create default cells via post-install script instead of
6
+ # data/account_dashboard_banner_cell.xml
7
+ # to avoid the problem when a user deletes a cell that has an XMLID
8
+ # and Odoo would re-create the cells when the module is reloaded
9
+
10
+ from odoo import SUPERUSER_ID, api
11
+
12
+
13
+ def create_default_account_dashboard_cells(cr, registry):
14
+ env = api.Environment(cr, SUPERUSER_ID, {})
15
+ vals_list = [
16
+ {"cell_type": "fiscalyear_lock_date", "sequence": 10, "warn": True},
17
+ {"cell_type": "income_fiscalyear", "sequence": 20},
18
+ {"cell_type": "customer_overdue", "sequence": 30},
19
+ {"cell_type": "customer_debt", "sequence": 40},
20
+ {"cell_type": "supplier_debt", "sequence": 50},
21
+ {
22
+ "cell_type": "liquidity",
23
+ "sequence": 60,
24
+ "warn": True,
25
+ "warn_type": "under",
26
+ },
27
+ ]
28
+ env["account.dashboard.banner.cell"].create(vals_list)
@@ -0,0 +1,33 @@
1
+ Go to the menu **Invoicing \> Configuration \> Dashboard \> Dashboard
2
+ Banner Cells**: in this menu, you can add or remove cells from the
3
+ banner, change the cell type, modify the order via drag-and-drop,
4
+ customize the labels if necessary.
5
+
6
+ ![Cell configuration menu](../static/description/banner_cell_config.png)
7
+
8
+ Many cell types are available:
9
+
10
+ - **Income** with 4 options: *Fiscal Year-to-date Income*, *Year-to-date
11
+ Income*, *Quarter-to-date Income* and *Month-to-date Income*. It
12
+ displays the period balance of the accounts with type *Income* and
13
+ *Other Income*.
14
+ - **Liquidity**: it display the ending balance of the accounts linked to
15
+ a bank or cash or credit journal.
16
+ - **Customer Debt**: it displays the ending balance of the default
17
+ *Account Receivable* and, if the point of sale is installed, the
18
+ intermediary account used for unidentified customers.
19
+ - **Customer Overdue**: same as the *Customer Debt*, but limited to
20
+ journal items with a due date in the past.
21
+ - **Supplier Debt**: it displays the ending balance of the default
22
+ *Account Payable*.
23
+ - **Lock dates**: all the lock dates are available: *Tax Return Lock
24
+ Date*, *Journals Entries Lock Date* and *All Users Lock Date*.
25
+
26
+ The module is designed to allow the modification of the computation of
27
+ the different cell types by inheriting the method
28
+ *\_prepare_cell_data_<cell_type>()* or *\_prepare_cell_data()*.
29
+ It is also easy for a developper to add more cell types.
30
+
31
+ It is possible to display a cell as a warning cell (yellow background color instead of light grey): click on the *Warning* option and customize the conditions to trigger the warning.
32
+
33
+ ![Cell form with warning](../static/description/cell_form_with_warning.png)
@@ -0,0 +1 @@
1
+ - Alexis de Lattre \<<alexis.delattre@akretion.com>\>
@@ -0,0 +1,13 @@
1
+ The development of this module started with a simple analysis: accountants
2
+ tend to forget to update the lock dates. Part of the problem lies in the
3
+ fact that the lock dates can be seen in the wizard to update the lock
4
+ dates, but accountants have no incentive to start this wizard regularly
5
+ to check the lock dates. The idea was to display the lock dates in a
6
+ banner at the top of the accounting dashboard, so that accountants
7
+ have the value of the lock dates in front of their eyes.
8
+
9
+ With such a banner in the accounting dashboard to display the lock
10
+ dates, there was a great temptation to display other interesting
11
+ information in that banner, such as some key figures of the accounting:
12
+ liquidity, turnover, customer overdue, etc. It gave birth to the idea to
13
+ have a configurable banner in the accounting dashboard!
@@ -0,0 +1,3 @@
1
+ Enjoy the accounting dashboard with the banner at the top:
2
+
3
+ ![Accounting dashboard with banner](../static/description/account_dashboard_banner.png)
@@ -0,0 +1,4 @@
1
+ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2
+ access_account_dashboard_banner_cell_manager,Full access on account.dashboard.banner.cell,model_account_dashboard_banner_cell,account.group_account_manager,1,1,1,1
3
+ access_account_dashboard_banner_cell_user,Read access on account.dashboard.banner.cell,model_account_dashboard_banner_cell,account.group_account_user,1,0,0,0
4
+ access_account_dashboard_banner_cell_auditor,Read access on account.dashboard.banner.cell,model_account_dashboard_banner_cell,account.group_account_readonly,1,0,0,0