Initial commit
This commit is contained in:
258
Fusion Accounting/models/external_tax_provider.py
Normal file
258
Fusion Accounting/models/external_tax_provider.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
Fusion Accounting - External Tax Provider (Abstract)
|
||||
=====================================================
|
||||
|
||||
Defines an abstract interface for external tax calculation services such as
|
||||
Avalara AvaTax, Vertex, TaxJar, or any custom tax API. Concrete providers
|
||||
inherit this model and implement the core calculation and voiding methods.
|
||||
|
||||
The provider model stores connection credentials and exposes a registry of
|
||||
active providers per company so that invoice and order workflows can delegate
|
||||
tax computation transparently.
|
||||
|
||||
Copyright (c) Nexa Systems Inc. - All rights reserved.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionExternalTaxProvider(models.Model):
|
||||
"""Abstract base for external tax calculation providers.
|
||||
|
||||
Each concrete provider (AvaTax, Vertex, etc.) inherits this model
|
||||
and implements :meth:`calculate_tax` and :meth:`void_transaction`.
|
||||
Only one provider may be active per company at any time.
|
||||
"""
|
||||
|
||||
_name = "fusion.external.tax.provider"
|
||||
_description = "Fusion External Tax Provider"
|
||||
_order = "sequence, name"
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Fields
|
||||
# -------------------------------------------------------------------------
|
||||
name = fields.Char(
|
||||
string="Provider Name",
|
||||
required=True,
|
||||
help="Human-readable label for this tax provider configuration.",
|
||||
)
|
||||
code = fields.Char(
|
||||
string="Provider Code",
|
||||
required=True,
|
||||
help="Short technical identifier for the provider type (e.g. 'avatax', 'vertex').",
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string="Sequence",
|
||||
default=10,
|
||||
help="Ordering priority when multiple providers are defined.",
|
||||
)
|
||||
provider_type = fields.Selection(
|
||||
selection=[('generic', 'Generic')],
|
||||
string="Provider Type",
|
||||
default='generic',
|
||||
required=True,
|
||||
help="Discriminator used to load provider-specific configuration views.",
|
||||
)
|
||||
api_key = fields.Char(
|
||||
string="API Key",
|
||||
groups="account.group_account_manager",
|
||||
help="Authentication key for the external tax service. "
|
||||
"Stored encrypted; only visible to accounting managers.",
|
||||
)
|
||||
api_url = fields.Char(
|
||||
string="API URL",
|
||||
help="Base URL of the tax service endpoint.",
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name='res.company',
|
||||
string="Company",
|
||||
required=True,
|
||||
default=lambda self: self.env.company,
|
||||
help="Company to which this provider configuration belongs.",
|
||||
)
|
||||
is_active = fields.Boolean(
|
||||
string="Active",
|
||||
default=False,
|
||||
help="Only one provider may be active per company. "
|
||||
"Activating this provider will deactivate others for the same company.",
|
||||
)
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
('draft', 'Not Configured'),
|
||||
('test', 'Test Passed'),
|
||||
('error', 'Connection Error'),
|
||||
],
|
||||
string="Connection State",
|
||||
default='draft',
|
||||
readonly=True,
|
||||
copy=False,
|
||||
help="Reflects the result of the most recent connection test.",
|
||||
)
|
||||
last_test_message = fields.Text(
|
||||
string="Last Test Result",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
help="Diagnostic message from the most recent connection test.",
|
||||
)
|
||||
log_requests = fields.Boolean(
|
||||
string="Log API Requests",
|
||||
default=False,
|
||||
help="When enabled, all API requests and responses are written to the server log "
|
||||
"at DEBUG level. Useful for troubleshooting but may expose sensitive data.",
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# SQL Constraints
|
||||
# -------------------------------------------------------------------------
|
||||
_sql_constraints = [
|
||||
(
|
||||
'unique_code_per_company',
|
||||
'UNIQUE(code, company_id)',
|
||||
'Only one provider configuration per code is allowed per company.',
|
||||
),
|
||||
]
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Constraint: single active provider per company
|
||||
# -------------------------------------------------------------------------
|
||||
@api.constrains('is_active', 'company_id')
|
||||
def _check_single_active_provider(self):
|
||||
"""Ensure at most one provider is active for each company."""
|
||||
for provider in self.filtered('is_active'):
|
||||
siblings = self.search([
|
||||
('company_id', '=', provider.company_id.id),
|
||||
('is_active', '=', True),
|
||||
('id', '!=', provider.id),
|
||||
])
|
||||
if siblings:
|
||||
raise ValidationError(_(
|
||||
"Only one external tax provider may be active per company. "
|
||||
"Provider '%(existing)s' is already active for %(company)s.",
|
||||
existing=siblings[0].name,
|
||||
company=provider.company_id.name,
|
||||
))
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Public API
|
||||
# -------------------------------------------------------------------------
|
||||
@api.model
|
||||
def get_provider(self, company=None):
|
||||
"""Return the active external tax provider for the given company.
|
||||
|
||||
:param company: ``res.company`` record or ``None`` for the current company.
|
||||
:returns: A single ``fusion.external.tax.provider`` record or an empty recordset.
|
||||
"""
|
||||
target_company = company or self.env.company
|
||||
return self.search([
|
||||
('company_id', '=', target_company.id),
|
||||
('is_active', '=', True),
|
||||
], limit=1)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Abstract Methods (to be implemented by concrete providers)
|
||||
# -------------------------------------------------------------------------
|
||||
def calculate_tax(self, order_lines):
|
||||
"""Compute tax amounts for a collection of order/invoice lines.
|
||||
|
||||
Concrete providers must override this method and return a list of
|
||||
dictionaries with at least the following keys per input line:
|
||||
|
||||
* ``line_id`` - The ``id`` of the originating ``account.move.line``.
|
||||
* ``tax_amount`` - The computed tax amount in document currency.
|
||||
* ``tax_details`` - A list of dicts ``{tax_name, tax_rate, tax_amount, jurisdiction}``.
|
||||
* ``doc_code`` - An external document reference for later void/commit.
|
||||
|
||||
:param order_lines: Recordset of ``account.move.line`` (or compatible)
|
||||
containing the products, quantities, and addresses.
|
||||
:returns: ``list[dict]`` as described above.
|
||||
:raises UserError: When the provider encounters a non-recoverable error.
|
||||
"""
|
||||
raise UserError(_(
|
||||
"The external tax provider '%(name)s' does not implement tax calculation. "
|
||||
"Please configure a concrete provider such as AvaTax.",
|
||||
name=self.name,
|
||||
))
|
||||
|
||||
def void_transaction(self, doc_code, doc_type='SalesInvoice'):
|
||||
"""Void (cancel) a previously committed tax transaction.
|
||||
|
||||
:param doc_code: The external document code returned by :meth:`calculate_tax`.
|
||||
:param doc_type: The transaction type (default ``'SalesInvoice'``).
|
||||
:returns: ``True`` on success.
|
||||
:raises UserError: When the void operation fails.
|
||||
"""
|
||||
raise UserError(_(
|
||||
"The external tax provider '%(name)s' does not implement transaction voiding.",
|
||||
name=self.name,
|
||||
))
|
||||
|
||||
def test_connection(self):
|
||||
"""Verify connectivity and credentials with the external service.
|
||||
|
||||
Concrete providers should override this to perform an actual API ping
|
||||
and update :attr:`state` and :attr:`last_test_message` accordingly.
|
||||
|
||||
:returns: ``True`` if the test succeeds.
|
||||
"""
|
||||
raise UserError(_(
|
||||
"The external tax provider '%(name)s' does not implement a connection test.",
|
||||
name=self.name,
|
||||
))
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Actions
|
||||
# -------------------------------------------------------------------------
|
||||
def action_test_connection(self):
|
||||
"""Button action: run the connection test and display the result."""
|
||||
self.ensure_one()
|
||||
try:
|
||||
self.test_connection()
|
||||
self.write({
|
||||
'state': 'test',
|
||||
'last_test_message': _("Connection successful."),
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _("Connection Test"),
|
||||
'message': _("Connection to '%s' succeeded.", self.name),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
except Exception as exc:
|
||||
self.write({
|
||||
'state': 'error',
|
||||
'last_test_message': str(exc),
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _("Connection Test Failed"),
|
||||
'message': str(exc),
|
||||
'type': 'danger',
|
||||
'sticky': True,
|
||||
},
|
||||
}
|
||||
|
||||
def action_activate(self):
|
||||
"""Activate this provider and deactivate all others for the same company."""
|
||||
self.ensure_one()
|
||||
self.search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('is_active', '=', True),
|
||||
('id', '!=', self.id),
|
||||
]).write({'is_active': False})
|
||||
self.is_active = True
|
||||
|
||||
def action_deactivate(self):
|
||||
"""Deactivate this provider."""
|
||||
self.ensure_one()
|
||||
self.is_active = False
|
||||
Reference in New Issue
Block a user