# Fusion Accounting - Tax & Tax Unit Extensions # Deferred date propagation in tax computations and tax unit management from odoo import api, fields, models, Command, _ from odoo.exceptions import ValidationError class FusionAccountTax(models.Model): """Extends the tax engine to carry deferred revenue/expense dates through the base-line and tax-line computation pipeline.""" _inherit = "account.tax" def _prepare_base_line_for_taxes_computation(self, record, **kwargs): """Inject deferred period dates into the base-line dictionary.""" vals = super()._prepare_base_line_for_taxes_computation(record, **kwargs) vals['deferred_start_date'] = self._get_base_line_field_value_from_record( record, 'deferred_start_date', kwargs, False, ) vals['deferred_end_date'] = self._get_base_line_field_value_from_record( record, 'deferred_end_date', kwargs, False, ) return vals def _prepare_tax_line_for_taxes_computation(self, record, **kwargs): """Inject deferred period dates into the tax-line dictionary.""" vals = super()._prepare_tax_line_for_taxes_computation(record, **kwargs) vals['deferred_start_date'] = self._get_base_line_field_value_from_record( record, 'deferred_start_date', kwargs, False, ) vals['deferred_end_date'] = self._get_base_line_field_value_from_record( record, 'deferred_end_date', kwargs, False, ) return vals def _prepare_base_line_grouping_key(self, base_line): """Include deferred dates in the grouping key so lines with different deferral periods are not merged.""" grp_key = super()._prepare_base_line_grouping_key(base_line) grp_key['deferred_start_date'] = base_line['deferred_start_date'] grp_key['deferred_end_date'] = base_line['deferred_end_date'] return grp_key def _prepare_base_line_tax_repartition_grouping_key( self, base_line, base_line_grouping_key, tax_data, tax_rep_data, ): """Propagate deferred dates into the repartition grouping key only when the account is deferral-compatible and the tax repartition line does not participate in tax closing.""" grp_key = super()._prepare_base_line_tax_repartition_grouping_key( base_line, base_line_grouping_key, tax_data, tax_rep_data, ) source_record = base_line['record'] is_deferral_eligible = ( isinstance(source_record, models.Model) and source_record._name == 'account.move.line' and source_record._has_deferred_compatible_account() and base_line['deferred_start_date'] and base_line['deferred_end_date'] and not tax_rep_data['tax_rep'].use_in_tax_closing ) if is_deferral_eligible: grp_key['deferred_start_date'] = base_line['deferred_start_date'] grp_key['deferred_end_date'] = base_line['deferred_end_date'] else: grp_key['deferred_start_date'] = False grp_key['deferred_end_date'] = False return grp_key def _prepare_tax_line_repartition_grouping_key(self, tax_line): """Mirror deferred dates from the tax line into its repartition grouping key.""" grp_key = super()._prepare_tax_line_repartition_grouping_key(tax_line) grp_key['deferred_start_date'] = tax_line['deferred_start_date'] grp_key['deferred_end_date'] = tax_line['deferred_end_date'] return grp_key class FusionTaxUnit(models.Model): """A tax unit groups multiple companies for consolidated tax reporting. Manages fiscal position synchronisation and horizontal-group linkage to generic tax reports.""" _name = "account.tax.unit" _description = "Tax Unit" name = fields.Char(string="Name", required=True) country_id = fields.Many2one( comodel_name='res.country', string="Country", required=True, help="Jurisdiction under which this unit's consolidated tax returns are filed.", ) vat = fields.Char( string="Tax ID", required=True, help="VAT identification number used when submitting the unit's declaration.", ) company_ids = fields.Many2many( comodel_name='res.company', string="Companies", required=True, help="Member companies grouped under this unit.", ) main_company_id = fields.Many2one( comodel_name='res.company', string="Main Company", required=True, help="The reporting entity responsible for filing and payment.", ) fpos_synced = fields.Boolean( string="Fiscal Positions Synchronised", compute='_compute_fiscal_position_completion', help="Indicates whether fiscal positions exist for every member company.", ) # ---- CRUD Overrides ---- def create(self, vals_list): """After creation, set up horizontal groups on the generic tax reports so this unit appears in multi-company views.""" records = super().create(vals_list) h_groups = self.env['account.report.horizontal.group'].create([ { 'name': unit.name, 'rule_ids': [ Command.create({ 'field_name': 'company_id', 'domain': f"[('account_tax_unit_ids', 'in', {unit.id})]", }), ], } for unit in records ]) # Link horizontal groups to all relevant tax reports report_refs = [ 'account.generic_tax_report', 'account.generic_tax_report_account_tax', 'account.generic_tax_report_tax_account', 'fusion_accounting.generic_ec_sales_report', ] for ref_str in report_refs: tax_rpt = self.env.ref(ref_str) tax_rpt.horizontal_group_ids |= h_groups # Also attach to country-specific variants base_generic = self.env.ref('account.generic_tax_report') for unit in records: matching_variants = base_generic.variant_report_ids.filtered( lambda v: v.country_id == unit.country_id ) matching_variants.write({ 'horizontal_group_ids': [Command.link(hg.id) for hg in h_groups], }) return records def unlink(self): """Clean up fiscal positions before deletion.""" self._get_tax_unit_fiscal_positions( companies=self.env['res.company'].search([]), ).unlink() return super().unlink() # ---- Computed Fields ---- @api.depends('company_ids') def _compute_fiscal_position_completion(self): """Check whether every member company has a synchronised fiscal position mapping all other members' taxes to no-tax.""" for unit in self: is_synced = True for company in unit.company_ids: origin = company._origin if isinstance(company.id, models.NewId) else company fp = unit._get_tax_unit_fiscal_positions(companies=origin) partners_with_fp = ( self.env['res.company'] .search([]) .with_company(origin) .partner_id .filtered(lambda p: p.property_account_position_id == fp) if fp else self.env['res.partner'] ) is_synced = partners_with_fp == (unit.company_ids - origin).partner_id if not is_synced: break unit.fpos_synced = is_synced # ---- Fiscal Position Management ---- def _get_tax_unit_fiscal_positions(self, companies, create_or_refresh=False): """Retrieve (or create) fiscal positions for each company in the unit. When *create_or_refresh* is True, positions are upserted with mappings that zero-out all company taxes. :param companies: Companies to process. :param create_or_refresh: If True, create/update the fiscal positions. :returns: Recordset of fiscal positions. """ fp_set = self.env['account.fiscal.position'].with_context( allowed_company_ids=self.env.user.company_ids.ids, ) for unit in self: for comp in companies: xml_ref = f'account.tax_unit_{unit.id}_fp_{comp.id}' existing = self.env.ref(xml_ref, raise_if_not_found=False) if create_or_refresh: company_taxes = self.env['account.tax'].with_context( allowed_company_ids=self.env.user.company_ids.ids, ).search(self.env['account.tax']._check_company_domain(comp)) fp_data = { 'xml_id': xml_ref, 'values': { 'name': unit.name, 'company_id': comp.id, 'tax_ids': [Command.clear()] + [ Command.create({'tax_src_id': t.id}) for t in company_taxes ], }, } existing = fp_set._load_records([fp_data]) if existing: fp_set += existing return fp_set def action_sync_unit_fiscal_positions(self): """Remove existing unit fiscal positions and recreate them with up-to-date tax mappings for all member companies.""" self._get_tax_unit_fiscal_positions( companies=self.env['res.company'].search([]), ).unlink() for unit in self: for comp in unit.company_ids: fp = unit._get_tax_unit_fiscal_positions( companies=comp, create_or_refresh=True, ) (unit.company_ids - comp).with_company(comp).partner_id.property_account_position_id = fp # ---- Constraints ---- @api.constrains('country_id', 'company_ids') def _validate_companies_country(self): """All member companies must share the same currency and each company may only belong to one unit per country.""" for unit in self: currencies_seen = set() for comp in unit.company_ids: currencies_seen.add(comp.currency_id) other_units_same_country = any( u != unit and u.country_id == unit.country_id for u in comp.account_tax_unit_ids ) if other_units_same_country: raise ValidationError(_( "Company %(company)s already belongs to a tax unit in " "%(country)s. Each company can only participate in one " "tax unit per country.", company=comp.name, country=unit.country_id.name, )) if len(currencies_seen) > 1: raise ValidationError( _("All companies within a tax unit must share the same primary currency.") ) @api.constrains('company_ids', 'main_company_id') def _validate_main_company(self): """The designated main company must be among the unit's members.""" for unit in self: if unit.main_company_id not in unit.company_ids: raise ValidationError( _("The main company must be a member of the tax unit.") ) @api.constrains('company_ids') def _validate_companies(self): """A tax unit requires at least two member companies.""" for unit in self: if len(unit.company_ids) < 2: raise ValidationError( _("A tax unit requires a minimum of two companies. " "Consider deleting the unit instead.") ) @api.constrains('country_id', 'vat') def _validate_vat(self): """Validate the VAT number against the unit's country.""" for unit in self: if not unit.vat: continue detected_code = self.env['res.partner']._run_vat_test( unit.vat, unit.country_id, ) if detected_code and detected_code != unit.country_id.code.lower(): raise ValidationError( _("The country derived from the VAT number does not match " "the country configured on this tax unit.") ) if not detected_code: unit_label = _("tax unit [%s]", unit.name) err_msg = self.env['res.partner']._build_vat_error_message( unit.country_id.code.lower(), unit.vat, unit_label, ) raise ValidationError(err_msg) # ---- Onchange ---- @api.onchange('company_ids') def _onchange_company_ids(self): """Auto-select the first company as main when the current main is removed from the member list.""" if self.main_company_id not in self.company_ids and self.company_ids: self.main_company_id = self.company_ids[0]._origin elif not self.company_ids: self.main_company_id = False