Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
1
account_dashboard_banner/models/__init__.py
Executable file
1
account_dashboard_banner/models/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
from . import account_dashboard_banner_cell
|
||||
328
account_dashboard_banner/models/account_dashboard_banner_cell.py
Executable file
328
account_dashboard_banner/models/account_dashboard_banner_cell.py
Executable file
@@ -0,0 +1,328 @@
|
||||
# Copyright 2025 Akretion France (https://www.akretion.com/)
|
||||
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import date_utils
|
||||
from odoo.tools.misc import format_amount, format_date
|
||||
|
||||
|
||||
class AccountDashboardBannerCell(models.Model):
|
||||
_name = "account.dashboard.banner.cell"
|
||||
_description = "Accounting Dashboard Banner Cell"
|
||||
_order = "sequence, id"
|
||||
|
||||
sequence = fields.Integer()
|
||||
cell_type = fields.Selection(
|
||||
[
|
||||
("income_fiscalyear", "Fiscal Year-to-date Income"),
|
||||
("income_year", "Year-to-date Income"),
|
||||
("income_quarter", "Quarter-to-date Income"),
|
||||
("income_month", "Month-to-date Income"),
|
||||
("liquidity", "Liquidity"),
|
||||
("customer_debt", "Customer Debt"),
|
||||
("customer_overdue", "Customer Overdue"),
|
||||
("supplier_debt", "Supplier Debt"),
|
||||
# for lock dates, the key matches exactly the field name on res.company
|
||||
("tax_lock_date", "Tax Return Lock Date"),
|
||||
("sale_lock_date", "Sales Lock Date"),
|
||||
("purchase_lock_date", "Purchase Lock Date"),
|
||||
("fiscalyear_lock_date", "Global Lock Date"),
|
||||
("hard_lock_date", "Hard Lock Date"),
|
||||
],
|
||||
required=True,
|
||||
)
|
||||
custom_label = fields.Char()
|
||||
custom_tooltip = fields.Char()
|
||||
warn = fields.Boolean(string="Warning")
|
||||
warn_lock_date_days = fields.Integer(
|
||||
compute="_compute_warn_fields", store=True, readonly=False, precompute=True
|
||||
)
|
||||
warn_min = fields.Float(string="Minimum")
|
||||
warn_max = fields.Float(string="Maximum")
|
||||
warn_type_show = fields.Boolean(
|
||||
compute="_compute_warn_fields", store=True, precompute=True
|
||||
)
|
||||
warn_type = fields.Selection(
|
||||
[
|
||||
("under", "Under Minimum"),
|
||||
("above", "Above Maximum"),
|
||||
("outside", "Under Minimum or Above Maximum"),
|
||||
("inside", "Between Minimum and Maximum"),
|
||||
],
|
||||
default="under",
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"warn_lock_date_days_positive",
|
||||
"CHECK(warn_lock_date_days >= 0)",
|
||||
"Warn if lock date is older than N days must be positive or null.",
|
||||
)
|
||||
]
|
||||
|
||||
@api.constrains("warn_min", "warn_max", "warn_type", "warn", "cell_type")
|
||||
def _check_warn_config(self):
|
||||
for cell in self:
|
||||
if (
|
||||
cell.cell_type
|
||||
and not cell.cell_type.endswith("_lock_date")
|
||||
and cell.warn
|
||||
and cell.warn_type in ("outside", "inside")
|
||||
and cell.warn_max <= cell.warn_min
|
||||
):
|
||||
cell_type2label = dict(
|
||||
self.fields_get("cell_type", "selection")["cell_type"]["selection"]
|
||||
)
|
||||
raise ValidationError(
|
||||
_(
|
||||
"On cell '%(cell_type)s' with warning enabled, "
|
||||
"the minimum (%(warn_min)s) must be under "
|
||||
"the maximum (%(warn_max)s).",
|
||||
cell_type=cell_type2label[cell.cell_type],
|
||||
warn_min=cell.warn_min,
|
||||
warn_max=cell.warn_max,
|
||||
)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _default_warn_lock_date_days(self, cell_type):
|
||||
defaultmap = {
|
||||
"tax_lock_date": 61, # 2 months
|
||||
"sale_lock_date": 35, # 1 month + a few days
|
||||
"purchase_lock_date": 61,
|
||||
"fiscalyear_lock_date": 61, # 2 months
|
||||
"hard_lock_date": 520, # FY final closing, 1 year + 5 months
|
||||
}
|
||||
return defaultmap.get(cell_type)
|
||||
|
||||
@api.depends("cell_type", "warn")
|
||||
def _compute_warn_fields(self):
|
||||
for cell in self:
|
||||
warn_type_show = False
|
||||
warn_lock_date_days = 0
|
||||
if cell.cell_type and cell.warn:
|
||||
if cell.cell_type.endswith("_lock_date"):
|
||||
warn_lock_date_days = self._default_warn_lock_date_days(
|
||||
cell.cell_type
|
||||
)
|
||||
else:
|
||||
warn_type_show = True
|
||||
cell.warn_type_show = warn_type_show
|
||||
cell.warn_lock_date_days = warn_lock_date_days
|
||||
|
||||
@api.model
|
||||
def get_banner_data(self):
|
||||
"""This is the method called by the JS code that displays the banner"""
|
||||
company = self.env.company
|
||||
return self._prepare_banner_data(company)
|
||||
|
||||
def _prepare_speedy(self, company):
|
||||
lock_date_fields = [
|
||||
"tax_lock_date",
|
||||
"sale_lock_date",
|
||||
"purchase_lock_date",
|
||||
"fiscalyear_lock_date",
|
||||
"hard_lock_date",
|
||||
]
|
||||
speedy = {
|
||||
"cell_type2label": dict(
|
||||
self.fields_get("cell_type", "selection")["cell_type"]["selection"]
|
||||
),
|
||||
"lock_date2help": dict(
|
||||
(key, value["help"])
|
||||
for (key, value) in company.fields_get(lock_date_fields, "help").items()
|
||||
),
|
||||
"today": fields.Date.context_today(self),
|
||||
}
|
||||
return speedy
|
||||
|
||||
@api.model
|
||||
def _prepare_banner_data(self, company):
|
||||
# The order in this list will be the display order in the banner
|
||||
# In fact, it's not a list but a dict. I tried to make it work by returning
|
||||
# a list but it seems OWL only accepts dicts (I always get errors on lists)
|
||||
cells = self.search([])
|
||||
speedy = cells._prepare_speedy(company)
|
||||
res = {}
|
||||
seq = 0
|
||||
for cell in cells:
|
||||
seq += 1
|
||||
cell_data = cell._prepare_cell_data(company, speedy)
|
||||
cell._update_cell_warn(cell_data)
|
||||
res[seq] = cell_data
|
||||
# from pprint import pprint
|
||||
# pprint(res)
|
||||
return res
|
||||
|
||||
def _prepare_cell_data_liquidity(self, company, speedy):
|
||||
self.ensure_one()
|
||||
journals = self.env["account.journal"].search(
|
||||
[
|
||||
("company_id", "=", company.id),
|
||||
("type", "in", ("bank", "cash", "credit")),
|
||||
("default_account_id", "!=", False),
|
||||
]
|
||||
)
|
||||
accounts = journals.default_account_id
|
||||
return (accounts, 1, False, False)
|
||||
|
||||
def _prepare_cell_data_supplier_debt(self, company, speedy):
|
||||
accounts = (
|
||||
self.env["res.partner"]
|
||||
._fields["property_account_payable_id"]
|
||||
.get_company_dependent_fallback(self.env["res.partner"])
|
||||
)
|
||||
return (accounts, -1, False, False)
|
||||
|
||||
def _prepare_cell_data_income(self, company, speedy):
|
||||
cell_type = self.cell_type
|
||||
accounts = self.env["account.account"].search(
|
||||
[
|
||||
("company_ids", "in", [company.id]),
|
||||
("account_type", "in", ("income", "income_other")),
|
||||
]
|
||||
)
|
||||
if cell_type == "income_fiscalyear":
|
||||
start_date, end_date = date_utils.get_fiscal_year(
|
||||
speedy["today"],
|
||||
day=company.fiscalyear_last_day,
|
||||
month=int(company.fiscalyear_last_month),
|
||||
)
|
||||
elif cell_type == "income_month":
|
||||
start_date = speedy["today"] + relativedelta(day=1)
|
||||
elif cell_type == "income_year":
|
||||
start_date = speedy["today"] + relativedelta(day=1, month=1)
|
||||
elif cell_type == "income_quarter":
|
||||
month_start_quarter = 3 * ((speedy["today"].month - 1) // 3) + 1
|
||||
start_date = speedy["today"] + relativedelta(
|
||||
day=1, month=month_start_quarter
|
||||
)
|
||||
specific_domain = [("date", ">=", start_date)]
|
||||
specific_tooltip = _("from %s") % format_date(self.env, start_date)
|
||||
return (accounts, -1, specific_domain, specific_tooltip)
|
||||
|
||||
def _prepare_cell_data_customer_debt(self, company, speedy):
|
||||
accounts = (
|
||||
self.env["res.partner"]
|
||||
._fields["property_account_receivable_id"]
|
||||
.get_company_dependent_fallback(self.env["res.partner"])
|
||||
)
|
||||
if hasattr(company, "account_default_pos_receivable_account_id"):
|
||||
accounts |= company.account_default_pos_receivable_account_id
|
||||
return (accounts, 1, False, False)
|
||||
|
||||
def _prepare_cell_data_customer_overdue(self, company, speedy):
|
||||
accounts, sign, specific_domain, specific_tooltip = (
|
||||
self._prepare_cell_data_customer_debt(company, speedy)
|
||||
)
|
||||
specific_domain = expression.OR(
|
||||
[
|
||||
[("date_maturity", "=", False)],
|
||||
[("date_maturity", "<", speedy["today"])],
|
||||
[
|
||||
("date_maturity", "=", speedy["today"]),
|
||||
("journal_id.type", "!=", "sale"),
|
||||
],
|
||||
]
|
||||
)
|
||||
specific_tooltip = _("with due date before %s") % format_date(
|
||||
self.env, speedy["today"]
|
||||
)
|
||||
return (accounts, sign, specific_domain, specific_tooltip)
|
||||
|
||||
def _prepare_cell_data(self, company, speedy):
|
||||
"""Inherit this method to change the computation of a cell type"""
|
||||
self.ensure_one()
|
||||
cell_type = self.cell_type
|
||||
value = raw_value = tooltip = warn = False
|
||||
if cell_type.endswith("lock_date"):
|
||||
raw_value = company[cell_type]
|
||||
value = raw_value and format_date(self.env, raw_value)
|
||||
tooltip = speedy["lock_date2help"][cell_type]
|
||||
if self.warn:
|
||||
if not raw_value:
|
||||
warn = True
|
||||
elif raw_value < speedy["today"] - relativedelta(
|
||||
days=self.warn_lock_date_days
|
||||
):
|
||||
warn = True
|
||||
else:
|
||||
accounts = False
|
||||
if hasattr(self, f"_prepare_cell_data_{cell_type}"):
|
||||
specific_method = getattr(self, f"_prepare_cell_data_{cell_type}")
|
||||
accounts, sign, specific_domain, specific_tooltip = specific_method(
|
||||
company, speedy
|
||||
)
|
||||
elif cell_type.startswith("income_"):
|
||||
accounts, sign, specific_domain, specific_tooltip = (
|
||||
self._prepare_cell_data_income(company, speedy)
|
||||
)
|
||||
if accounts:
|
||||
domain = (specific_domain or []) + [
|
||||
("company_id", "=", company.id),
|
||||
("account_id", "in", accounts.ids),
|
||||
("date", "<=", speedy["today"]),
|
||||
("parent_state", "=", "posted"),
|
||||
]
|
||||
rg_res = self.env["account.move.line"]._read_group(
|
||||
domain, aggregates=["balance:sum"]
|
||||
)
|
||||
assert sign in (1, -1)
|
||||
raw_value = rg_res and rg_res[0][0] * sign or 0
|
||||
value = format_amount(self.env, raw_value, company.currency_id)
|
||||
tooltip = _(
|
||||
"Balance of account(s) %(account_codes)s%(specific)s.",
|
||||
account_codes=", ".join(accounts.mapped("code")),
|
||||
specific=specific_tooltip and f" {specific_tooltip}" or "",
|
||||
)
|
||||
res = {
|
||||
"cell_type": cell_type,
|
||||
"label": self.custom_label or speedy["cell_type2label"][cell_type],
|
||||
"raw_value": raw_value,
|
||||
"value": value or _("None"),
|
||||
"tooltip": self.custom_tooltip or tooltip,
|
||||
"warn": warn,
|
||||
}
|
||||
return res
|
||||
|
||||
def _update_cell_warn(self, cell_data):
|
||||
self.ensure_one()
|
||||
if (
|
||||
not cell_data.get("warn")
|
||||
and self.warn
|
||||
and self.warn_type
|
||||
and isinstance(cell_data["raw_value"], (int | float))
|
||||
):
|
||||
raw_value = cell_data["raw_value"]
|
||||
if (
|
||||
(self.warn_type == "under" and raw_value < self.warn_min)
|
||||
or (self.warn_type == "above" and raw_value > self.warn_max)
|
||||
or (
|
||||
self.warn_type == "outside"
|
||||
and (raw_value < self.warn_min or raw_value > self.warn_max)
|
||||
)
|
||||
or (
|
||||
self.warn_type == "inside"
|
||||
and raw_value > self.warn_min
|
||||
and raw_value < self.warn_max
|
||||
)
|
||||
):
|
||||
cell_data["warn"] = True
|
||||
|
||||
@api.depends("cell_type", "custom_label")
|
||||
def _compute_display_name(self):
|
||||
type2name = dict(
|
||||
self.fields_get("cell_type", "selection")["cell_type"]["selection"]
|
||||
)
|
||||
for cell in self:
|
||||
display_name = "-"
|
||||
if cell.custom_label:
|
||||
display_name = cell.custom_label
|
||||
elif cell.cell_type:
|
||||
display_name = type2name[cell.cell_type]
|
||||
cell.display_name = display_name
|
||||
Reference in New Issue
Block a user