""" 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"))) )