Files
Odoo-18.0-20251222/account_dashboard_banner/models/account_dashboard_banner_cell.py
tocmo0nlord adbe430761
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled
Initial commit: Odoo 18.0-20251222 extra-addons
2026-03-13 20:43:25 +00:00

329 lines
13 KiB
Python
Executable File

# 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