163 lines
5.7 KiB
Python
163 lines
5.7 KiB
Python
"""
|
|
Fusion Accounting - Fiscal Categories
|
|
|
|
Provides a classification system for grouping general ledger accounts
|
|
into fiscal reporting categories (income, expense, asset, liability).
|
|
These categories drive structured fiscal reports and SAF-T exports,
|
|
allowing companies to map their chart of accounts onto standardised
|
|
government reporting taxonomies.
|
|
|
|
Original implementation by Nexa Systems Inc.
|
|
"""
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import ValidationError
|
|
|
|
|
|
class FusionFiscalCategory(models.Model):
|
|
"""
|
|
A fiscal reporting category that groups one or more GL accounts.
|
|
|
|
Each category carries a ``category_type`` that mirrors the four
|
|
fundamental pillars of double-entry bookkeeping. When a SAF-T or
|
|
Intrastat export is generated the accounts linked here determine
|
|
which transactions are included.
|
|
|
|
Uniqueness of ``code`` is enforced per company so that external
|
|
auditors can refer to categories unambiguously.
|
|
"""
|
|
|
|
_name = "fusion.fiscal.category"
|
|
_description = "Fiscal Category"
|
|
_order = "code, name"
|
|
_rec_name = "display_name"
|
|
|
|
# ------------------------------------------------------------------
|
|
# Fields
|
|
# ------------------------------------------------------------------
|
|
name = fields.Char(
|
|
string="Category Name",
|
|
required=True,
|
|
translate=True,
|
|
help="Human-readable label shown in reports and menus.",
|
|
)
|
|
code = fields.Char(
|
|
string="Code",
|
|
required=True,
|
|
help=(
|
|
"Short alphanumeric identifier used in fiscal exports "
|
|
"(e.g. SAF-T GroupingCode). Must be unique per company."
|
|
),
|
|
)
|
|
category_type = fields.Selection(
|
|
selection=[
|
|
("income", "Income"),
|
|
("expense", "Expense"),
|
|
("asset", "Asset"),
|
|
("liability", "Liability"),
|
|
],
|
|
string="Type",
|
|
required=True,
|
|
default="expense",
|
|
help="Determines the section of the fiscal report this category appears in.",
|
|
)
|
|
description = fields.Text(
|
|
string="Description",
|
|
translate=True,
|
|
help="Optional long description for internal documentation purposes.",
|
|
)
|
|
active = fields.Boolean(
|
|
string="Active",
|
|
default=True,
|
|
help="Archived categories are excluded from new exports but remain on historical records.",
|
|
)
|
|
company_id = fields.Many2one(
|
|
comodel_name="res.company",
|
|
string="Company",
|
|
required=True,
|
|
default=lambda self: self.env.company,
|
|
help="Company to which this fiscal category belongs.",
|
|
)
|
|
account_ids = fields.Many2many(
|
|
comodel_name="account.account",
|
|
relation="fusion_fiscal_category_account_rel",
|
|
column1="category_id",
|
|
column2="account_id",
|
|
string="Accounts",
|
|
help="General-ledger accounts assigned to this fiscal category.",
|
|
)
|
|
account_count = fields.Integer(
|
|
string="# Accounts",
|
|
compute="_compute_account_count",
|
|
store=False,
|
|
help="Number of accounts linked to this category.",
|
|
)
|
|
parent_id = fields.Many2one(
|
|
comodel_name="fusion.fiscal.category",
|
|
string="Parent Category",
|
|
index=True,
|
|
ondelete="restrict",
|
|
help="Optional parent for hierarchical fiscal taxonomies.",
|
|
)
|
|
child_ids = fields.One2many(
|
|
comodel_name="fusion.fiscal.category",
|
|
inverse_name="parent_id",
|
|
string="Sub-categories",
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# SQL constraints
|
|
# ------------------------------------------------------------------
|
|
_sql_constraints = [
|
|
(
|
|
"unique_code_per_company",
|
|
"UNIQUE(code, company_id)",
|
|
"The fiscal category code must be unique within each company.",
|
|
),
|
|
]
|
|
|
|
# ------------------------------------------------------------------
|
|
# Computed fields
|
|
# ------------------------------------------------------------------
|
|
@api.depends("account_ids")
|
|
def _compute_account_count(self):
|
|
"""Count the number of accounts linked to each category."""
|
|
for record in self:
|
|
record.account_count = len(record.account_ids)
|
|
|
|
@api.depends("name", "code")
|
|
def _compute_display_name(self):
|
|
"""Build a display name combining code and name for clarity."""
|
|
for record in self:
|
|
if record.code:
|
|
record.display_name = f"[{record.code}] {record.name}"
|
|
else:
|
|
record.display_name = record.name or ""
|
|
|
|
# ------------------------------------------------------------------
|
|
# Constraints
|
|
# ------------------------------------------------------------------
|
|
@api.constrains("parent_id")
|
|
def _check_parent_recursion(self):
|
|
"""Prevent circular parent-child references."""
|
|
if not self._check_recursion():
|
|
raise ValidationError(
|
|
_("A fiscal category cannot be its own ancestor. "
|
|
"Please choose a different parent.")
|
|
)
|
|
|
|
@api.constrains("account_ids", "company_id")
|
|
def _check_account_company(self):
|
|
"""Ensure all linked accounts belong to the same company."""
|
|
for record in self:
|
|
foreign = record.account_ids.filtered(
|
|
lambda a: a.company_id != record.company_id
|
|
)
|
|
if foreign:
|
|
raise ValidationError(
|
|
_("All linked accounts must belong to company '%(company)s'. "
|
|
"The following accounts belong to a different company: %(accounts)s",
|
|
company=record.company_id.name,
|
|
accounts=", ".join(foreign.mapped("code")))
|
|
)
|