Repository
OCA/l10n-usa · module folder · Try on Runboat
Module version
1.0.12
Category
Accounting/Localizations
Folder size
3.84 MB
License
LGPL-3
Application
No
Auto-installable
No
Website
https://github.com/OCA/l10n-usa
Last tracking update
2026-07-22 17:15:08
Authors
Odoo Community Association (OCA), Binhex
Maintainers
Odoo Community Association (OCA), Binhex
Committers
OCA-git-bot, oca-ci, Carlos R. Rodriguez
Odoo dependencies
Python dependencies
None
System dependencies
None
Required by
None
Description
Hybrid US Sales Tax engine for Odoo: resolves the combined state + county +
city + district rate for a ZIP code, applies it to sale orders and invoices,
and keeps a full audit trail of every calculation.

Resolution order: local rate database first, external API providers as
fallback (configured independently in their own `l10n_us_sales_tax_provider_*`
modules — this module never imports their code directly). Tracks nexus per
state, taxability per product category, and caches external API responses
to control call volume.

Code Analysis

Views touched (29)
XML IDNameModelTypeStatus
res_config_settings_view_us_tax res.config.settings.us.tax res.config.settings form Inherits account.res_config_settings_view_form
view_account_move_us_tax account.move.us.tax.form account.move form Inherits account.view_move_form
view_product_template_us_tax product.template.us.tax.form product.template form Inherits product.product_template_form_view
view_sale_order_us_tax sale.order.us.tax.form sale.order form Inherits sale.view_order_form
view_sale_order_us_tax_list sale.order.us.tax.list sale.order list Inherits sale.view_quotation_tree_with_onboarding
view_us_tax_api_cache_list us.tax.api.cache.list us.tax.api.cache list New
view_us_tax_api_cache_search us.tax.api.cache.search us.tax.api.cache search New
view_us_tax_import_batch_form us.tax.import.batch.form us.tax.import.batch form New
view_us_tax_import_batch_list us.tax.import.batch.list us.tax.import.batch list New
view_us_tax_import_wizard_form us.tax.import.wizard.form us.tax.import.wizard form New
view_us_tax_jurisdiction_form us.tax.jurisdiction.form us.tax.jurisdiction form New
view_us_tax_jurisdiction_list us.tax.jurisdiction.list us.tax.jurisdiction list New
view_us_tax_jurisdiction_search us.tax.jurisdiction.search us.tax.jurisdiction search New
view_us_tax_log_form us.tax.calculation.log.form us.tax.calculation.log form New
view_us_tax_log_list us.tax.calculation.log.list us.tax.calculation.log list New
view_us_tax_log_search us.tax.calculation.log.search us.tax.calculation.log search New
view_us_tax_nexus_form us.tax.nexus.form us.tax.nexus form New
view_us_tax_nexus_list us.tax.nexus.list us.tax.nexus list New
view_us_tax_product_category_form us.tax.product.category.form us.tax.product.category form New
view_us_tax_product_category_list us.tax.product.category.list us.tax.product.category list New
view_us_tax_provider_form us.tax.provider.form us.tax.provider form New
view_us_tax_provider_list us.tax.provider.list us.tax.provider list New
view_us_tax_provider_search us.tax.provider.search us.tax.provider search New
view_us_tax_rate_form us.tax.rate.form us.tax.rate form New
view_us_tax_rate_list us.tax.rate.list us.tax.rate list New
view_us_tax_rule_form us.tax.rule.form us.tax.rule form New
view_us_tax_rule_list us.tax.rule.list us.tax.rule list New
view_us_tax_zip_mapping_list us.tax.zip.mapping.list us.tax.zip.mapping list New
view_us_tax_zip_mapping_search us.tax.zip.mapping.search us.tax.zip.mapping search New
HTTP endpoints (2)
Route(s)HandlerAuthTypeMethodsFlags
/api/v1/us_tax/calculate UsTaxController.calculate user json POST sudo
/api/v1/us_tax/rate_lookup UsTaxController.rate_lookup user json GET, POST sudo
Models touched (16)

New fields (2)
  • us_tax_calculated_at Datetime
    string='Tax Calculated At'
  • us_tax_source Char
    help='Source used for the last US tax calculation.' string='Tax Source'
Public methods (1)
  • action_calculate_us_tax(self)
    Manual trigger — recalculate US tax for this invoice.

New fields (1)
  • us_tax_category_id Many2one → us.tax.product.category
    help='Fiscal classification for US Sales Tax calculation. If empty, defaults to TANGIBLE GOODS.' string='US Tax Category' args: 'us.tax.product.category'
Public methods (0)

No public methods.

New fields (5)
  • us_tax_cache_ttl_hours Integer
    config_parameter='l10n_us_tax.cache_ttl_hours' default=720 help='Hours to keep API responses in cache. Default: 720 (30 days).' string='Cache TTL (hours)'
  • us_tax_confidence_threshold Float
    config_parameter='l10n_us_tax.confidence_threshold' default=0.7 help='Minimum ZIP-to-jurisdiction confidence to use local data (0.0–1.0).' string='ZIP Confidence Threshold'
  • us_tax_engine_active Boolean
    config_parameter='l10n_us_tax.engine_active' help='Activate hybrid US Sales Tax calculation for US customer addresses.' string='Enable US Sales Tax Engine'
  • us_tax_engine_mode Selection
    config_parameter='l10n_us_tax.engine_mode' default='hybrid' help='How the engine selects the tax rate source.' string='Engine Mode' args: [('local', 'Local Database Only'), ('api', 'External API Only'), ('hybrid', 'Hybrid (Local First, API Fallback)')]
  • us_tax_fail_policy Selection
    config_parameter='l10n_us_tax.fail_policy' default='warn' help='What happens when all configured API providers fail.' string='API Failure Policy' args: [('block', 'Block — prevent document confirmation'), ('warn', 'Warn — proceed with $0 tax + warning'), ('last_cache', 'Use Last Cache — use expired cache if available'), ('manual', 'Manual — let user enter rate')]
Public methods (0)

No public methods.

New fields (2)
  • us_tax_calculated_at Datetime
    string='Tax Calculated At'
  • us_tax_source Char
    help='Source used for the last US tax calculation.' string='Tax Source'
Public methods (2)
  • action_calculate_us_tax(self)
    Manual trigger — recalculate US tax for this order.
  • action_confirm(self)
    Auto-calculate tax on confirmation if engine is active.

New fields (15)
  • active Boolean
    default=True
  • address_hash Char
    index=True required=True
  • cached_at Datetime
    default=fields.Datetime.now
  • city_rate Float
    digits=(5, 4)
  • county_rate Float
    digits=(5, 4)
  • district_rate Float
    digits=(5, 4)
  • expires_at Datetime
    index=True
  • provider_id Many2one → us.tax.provider
    ondelete='set null' args: 'us.tax.provider'
  • request_payload Text
  • response_payload Text
  • source_date Date
    help='Rate effective date reported by provider.'
  • state_id Many2one → res.country.state
    index=True args: 'res.country.state'
  • state_rate Float
    digits=(5, 4)
  • total_rate Float
    digits=(5, 4)
  • zip Char
    index=True size=5
Public methods (4)
  • build_hash(self, zip_code, state_code, product_category_code, date_month)
    @api.model
    Build a deterministic cache key.
  • create_or_update(self, address_hash, provider_id, rates, ttl_hours, request_payload=None, response_payload=None, zip_code=None, state_id=None)
    @api.model
    Upsert a cache entry.
  • get_valid_entry(self, address_hash, provider_id=None)
    @api.model
    Return a non-expired cache entry or None.
  • purge_expired(self)
    @api.model
    Called by cron to archive expired entries.

New fields (27)
  • calculated_at Datetime
    default=fields.Datetime.now index=True
  • calculated_by Many2one → res.users
    default=<expr> ondelete='set null' args: 'res.users'
  • city_rate Float
    digits=(5, 4)
  • county_rate Float
    digits=(5, 4)
  • currency_id Many2one → res.currency
    default=<expr> args: 'res.currency'
  • district_rate Float
    digits=(5, 4)
  • document_ref Char
    compute='_compute_document_ref' help='Human-readable reference.' store=False
  • error_message Text
  • full_address Char
  • nexus_applied Boolean
  • partner_id Many2one → res.partner
    ondelete='set null' args: 'res.partner'
  • product_category_id Many2one → us.tax.product.category
    ondelete='set null' args: 'us.tax.product.category'
  • provider_id Many2one → us.tax.provider
    ondelete='set null' args: 'us.tax.provider'
  • request_payload Text
  • res_id Integer
    index=True
  • res_model Char
    help='sale.order or account.move' index=True
  • response_payload Text
    help='Sanitized — no API keys stored.'
  • shipping_city Char
  • shipping_county Char
  • shipping_state_id Many2one → res.country.state
    args: 'res.country.state'
  • shipping_zip Char
    size=5
  • source Selection
    required=True args: [('local', 'Local Database'), ('api', 'External API'), ('cache', 'Cached Result'), ('manual', 'Manual Rate'), ('exempt_nexus', 'Exempt — No Nexus'), ('exempt_rule', 'Exempt — Tax Rule'), ('error', 'Error')]
  • state_rate Float
    digits=(5, 4)
  • status Selection
    default='success' args: [('success', 'Success'), ('error', 'Error'), ('skipped', 'Skipped')]
  • tax_amount Monetary
    currency_field='currency_id'
  • taxable_amount Monetary
    currency_field='currency_id'
  • total_rate Float
    digits=(5, 4)
Public methods (3)
  • log_calculation(self, payload)
    @api.model
    Create an audit log entry. payload is a dict of field values. Uses sudo() because audit logs are system records — any user who triggers a tax calculation should have their action logged regardless of their group permissions on this model.
  • unlink(self)
  • write(self, vals)

New fields (0)

No new fields.

Public methods (2)
  • calculate_for_invoice(self, move)
    @api.model
    Calculate and apply US Sales Tax for an account.move record.
  • calculate_for_sale_order(self, order)
    @api.model
    Calculate and apply US Sales Tax for a sale.order record.

New fields (14)
  • can_rollback Boolean
    default=True
  • effective_date Date
    help='Rates effective date from the imported source.'
  • error_log Text
  • file_name Char
  • finished_at Datetime
  • imported_by Many2one → res.users
    default=<expr> ondelete='set null' args: 'res.users'
  • name Char
    compute='_compute_name' store=True
  • records_created Integer
    default=0
  • records_skipped Integer
    default=0
  • records_updated Integer
    default=0
  • source Char
    help='florida_dor, texas_open_data, manual_csv, api_ziptax' required=True
  • started_at Datetime
    default=fields.Datetime.now
  • state_id Many2one → res.country.state
    domain=[('country_id.code', '=', 'US')] string='State Covered' args: 'res.country.state'
  • status Selection
    default='pending' args: [('pending', 'Pending'), ('running', 'Running'), ('done', 'Done'), ('failed', 'Failed')]
Public methods (1)
  • action_rollback(self)
    Archive all rates created by this batch.

New fields (7)
  • delimiter Char
    default=',' size=1
  • effective_date Date
    default=fields.Date.today required=True
  • file_data Binary
    required=True string='File'
  • file_name Char
  • preview_html Html
    readonly=True
  • source Selection
    default='generic_csv' required=True args: [('florida_dor', 'Florida DOR (Master Address List)'), ('texas_open_data', 'Texas Open Data'), ('generic_csv', 'Generic CSV (any state)')]
  • state_id Many2one → res.country.state
    domain=[('country_id.code', '=', 'US')] required=True args: 'res.country.state'
Public methods (2)
  • action_import(self)
    Execute import using the appropriate importer.
  • action_preview(self)
    Show first 5 rows as preview before committing import.

New fields (13)
  • active Boolean
    default=True
  • child_ids One2many → us.tax.jurisdiction
    args: 'us.tax.jurisdiction', 'parent_id'
  • city Char
    index=True
  • complete_name Char
    compute='_compute_complete_name' recursive=True store=True
  • county Char
    index=True
  • district_code Char
  • name Char
    index=True required=True
  • parent_id Many2one → us.tax.jurisdiction
    index=True ondelete='restrict' string='Parent Jurisdiction' args: 'us.tax.jurisdiction'
  • parent_path Char
    index=True
  • rate_ids One2many → us.tax.rate
    args: 'us.tax.rate', 'jurisdiction_id'
  • state_id Many2one → res.country.state
    domain=[('country_id.code', '=', 'US')] index=True required=True args: 'res.country.state'
  • type Selection
    index=True required=True args: [('state', 'State'), ('county', 'County'), ('city', 'City'), ('district', 'Special District')]
  • zip_mapping_ids One2many → us.tax.zip.mapping
    args: 'us.tax.zip.mapping', 'jurisdiction_id'
Public methods (0)

No public methods.

New fields (7)
  • active Boolean
    default=True
  • company_id Many2one → res.company
    default=<expr> required=True args: 'res.company'
  • notes Text
  • start_date Date
    help='Date when economic nexus was established in this state.'
  • state_id Many2one → res.country.state
    domain=[('country_id.code', '=', 'US')] required=True args: 'res.country.state'
  • threshold_amount Float
    help='Economic nexus threshold in USD (informational for MVP).'
  • threshold_transactions Integer
    help='Transaction count threshold (informational for MVP).'
Public methods (1)
  • has_nexus(self, company_id, state_id)
    @api.model
    Check if a company has active nexus in a given state.

New fields (5)
  • active Boolean
    default=True
  • code Char
    help='Technical code: TANGIBLE, SERVICE, FOOD, MEDICINE, DIGITAL, SHIPPING, EXEMPT' index=True required=True size=32
  • default_taxable Boolean
    default=True help='Taxable by default. Override per state via Tax Rules.'
  • description Text
    translate=True
  • name Char
    required=True translate=True
Public methods (0)

No public methods.

New fields (17)
  • account_id_param Char
    help='ir.config_parameter key for Account ID (if required).'
  • active Boolean
    default=False
  • api_key_param Char
    help='ir.config_parameter key for API Key.'
  • calls_this_month Integer
    default=0 help='Calls made this month. Reset by monthly cron.' readonly=True
  • code Char
    help="Unique provider code (e.g. 'local'; external providers define their own code in their own addon)." index=True required=True size=32
  • description Text
  • is_paid Boolean
    default=False help='Mark if this provider requires payment after trial/free tier ends.'
  • limit_alert_sent Boolean
    default=False help='Whether the 80% warning has been sent this month.'
  • limit_status Selection
    compute='_compute_limit_status' args: [('ok', 'OK'), ('warning', 'Warning (>80%)'), ('exceeded', 'Limit Exceeded'), ('unlimited', 'Unlimited')]
  • limit_usage_pct Float
    compute='_compute_limit_status' help='Percentage of monthly limit used.'
  • monthly_call_limit Integer
    default=0 help='Monthly API call limit. 0 = unlimited (paid/enterprise plans).'
  • name Char
    required=True
  • priority Integer
    default=10 help='Lower number = higher priority in fallback chain. Engine tries providers in ascending priority order.'
  • sandbox_mode Boolean
    default=True help='Use sandbox/test endpoints. Disable for production.'
  • supports_address_level Boolean
    default=False help='Provider can resolve by full address (not just ZIP).'
  • supports_zip_level Boolean
    default=True help='Provider can resolve by ZIP code.'
  • timeout Integer
    default=5 help='API request timeout in seconds.'
Public methods (6)
  • action_reset_monthly_counter(self)
  • action_validate_credentials(self)
  • get_api_key(self)
    Safely retrieve API key from ir.config_parameter.
  • increment_call_counter(self)
    Increment monthly call counter and send alert if threshold reached.
  • is_limit_reached(self)
    Return True if this provider has reached its monthly call limit.
  • is_limit_warning(self)
    Return True if this provider is at ≥80% of its monthly limit.

New fields (15)
  • active Boolean
    default=True
  • city_rate Float
    default=0.0 digits=(5, 4)
  • county_rate Float
    default=0.0 digits=(5, 4)
  • district_rate Float
    default=0.0 digits=(5, 4)
  • effective_date Date
    index=True required=True
  • end_date Date
    help='Leave empty if this rate is currently in effect.' index=True
  • import_batch_id Many2one → us.tax.import.batch
    args: 'us.tax.import.batch'
  • jurisdiction_id Many2one → us.tax.jurisdiction
    index=True ondelete='restrict' required=True args: 'us.tax.jurisdiction'
  • notes Text
  • product_tax_category_id Many2one → us.tax.product.category
    help='Leave empty to apply to all categories.' args: 'us.tax.product.category'
  • source Char
    default='manual' help='Origin: e.g. florida_dor, manual, or the code of an external provider module.'
  • source_url Char
    string='Source URL'
  • state_id Many2one
    index=True related='jurisdiction_id.state_id' store=True
  • state_rate Float
    default=0.0 digits=(5, 4)
  • total_rate Float
    compute='_compute_total_rate' digits=(5, 4) help='Sum of all rate components.' store=True
Public methods (1)
  • get_rate_for_date(self, jurisdiction_id, date, product_category_id=None)
    @api.model
    Return the rate valid on a given date for a jurisdiction. Preference: specific product category > generic (null category). A category-specific rate is tried first when one is requested; if none exists, falls back to the generic rate rather than returning nothing — a jurisdiction is rarely imported with a rate row for every category.

New fields (8)
  • active Boolean
    default=True
  • effective_date Date
  • end_date Date
  • exemption_reason Char
    help='Legal or regulatory basis for exemption.'
  • product_tax_category_id Many2one → us.tax.product.category
    ondelete='restrict' required=True args: 'us.tax.product.category'
  • rate_override Float
    digits=(5, 4) help='If set, use this rate instead of the standard jurisdiction rate.'
  • state_id Many2one → res.country.state
    domain=[('country_id.code', '=', 'US')] help='Leave empty to apply to all US states.' args: 'res.country.state'
  • taxable Boolean
    default=True help='Is this category taxable in this state?'
Public methods (1)
  • is_taxable(self, state_id, product_category_id, date=None)
    @api.model
    Check if a product category is taxable in a state on a given date. Returns: (taxable: bool, rate_override: float | None)

New fields (10)
  • city Char
  • confidence Float
    default=1.0 help='Confidence 0.0-1.0. Highest wins when ZIP has multiple jurisdictions.'
  • county Char
  • import_batch_id Many2one → us.tax.import.batch
    args: 'us.tax.import.batch'
  • imported_at Datetime
    default=fields.Datetime.now
  • jurisdiction_id Many2one → us.tax.jurisdiction
    index=True ondelete='restrict' required=True args: 'us.tax.jurisdiction'
  • source Char
    default='manual' help='Data origin: florida_dor, texas_open_data, api_ziptax, manual'
  • state_id Many2one → res.country.state
    domain=[('country_id.code', '=', 'US')] index=True required=True args: 'res.country.state'
  • zip Char
    index=True required=True size=5 string='ZIP Code'
  • zip4 Char
    size=4 string='ZIP+4'
Public methods (1)
  • get_best_jurisdiction(self, zip_code, state_code=None, confidence_min=0.7)
    @api.model
    Return the best jurisdiction for a ZIP code. Args: zip_code: 5-digit ZIP string state_code: Optional 2-letter state code to narrow down confidence_min: Minimum confidence threshold Returns: us.tax.jurisdiction record or empty recordset