US Sales Tax Engine
l10n_us_sales_tax_engine · OCA/l10n-usa
-
Security25/25No findings
-
Activity25/25Last human commit 3 months ago
-
Required by20/252 modules depend on it
-
Maturity5/25In 1 Odoo series since 18.0
Heuristic over the facts below, not an audit · How it's computed
Migration considerations
UsTaxApiCache— Migrating to 19.0: `_sql_constraints` is ignored and its constraints are silently not created: declare `models.Constraint` attributes instead. migration-sql-constraints · sourceUsTaxJurisdiction— Migrating to 19.0: `_sql_constraints` is ignored and its constraints are silently not created: declare `models.Constraint` attributes instead. migration-sql-constraints · sourceUsTaxNexus— Migrating to 19.0: `_sql_constraints` is ignored and its constraints are silently not created: declare `models.Constraint` attributes instead. migration-sql-constraints · sourceUsTaxProductCategory— Migrating to 19.0: `_sql_constraints` is ignored and its constraints are silently not created: declare `models.Constraint` attributes instead. migration-sql-constraints · sourceUsTaxProvider— Migrating to 19.0: `_sql_constraints` is ignored and its constraints are silently not created: declare `models.Constraint` attributes instead. migration-sql-constraints · sourceUsTaxZipMapping— Migrating to 19.0: `_sql_constraints` is ignored and its constraints are silently not created: declare `models.Constraint` attributes instead. migration-sql-constraints · source
Migration review checklist, not a compatibility verdict. "Migrating to X.0" applies once you upgrade to that version; "Since X.0" means this version already ignores or rejects that code. Methodology
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.
Dependencies
Code analysis
Extracted from the source by best-effort static analysis (no code is executed); details may be incomplete.
Views touched 30
| XML ID | Name | Model | Type | Status |
|---|---|---|---|---|
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_res_partner_us_tax_form |
res.partner.us.tax.form | res.partner | form | Inherits base.view_partner_form |
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) | Handler | Auth | Type | Methods | Flags |
|---|---|---|---|---|---|
/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 17
-
us_tax_calculated_atDatetimestring='Tax Calculated At' -
us_tax_sourceCharhelp='Source used for the last US tax calculation.'string='Tax Source'
-
action_calculate_us_tax(self)Manual trigger — recalculate US tax for this invoice.
-
us_tax_category_idMany2one → us.tax.product.categoryhelp='Fiscal classification for US Sales Tax calculation. If empty, defaults to TANGIBLE GOODS.'string='US Tax Category' args: 'us.tax.product.category'
No public methods.
-
us_tax_cache_ttl_hoursIntegerconfig_parameter='l10n_us_tax.cache_ttl_hours'default=720help='Hours to keep API responses in cache. Default: 720 (30 days).'string='Cache TTL (hours)' -
us_tax_confidence_thresholdFloatconfig_parameter='l10n_us_tax.confidence_threshold'default=0.7help='Minimum ZIP-to-jurisdiction confidence to use local data (0.0–1.0).'string='ZIP Confidence Threshold' -
us_tax_engine_activeBooleanconfig_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_modeSelectionconfig_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_policySelectionconfig_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')]
No public methods.
-
us_tax_exemptBooleanhelp='When checked, this partner is exempt from US Sales Tax on all orders. Attach the exemption certificate document to this record.'string='US Tax Exempt' -
us_tax_exemption_codeCharhelp='Category of exemption (e.g. RESALE, AGRICULTURE, GOVERNMENT, NONPROFIT).'string='Exemption Code' -
us_tax_exemption_numberCharhelp='Official exemption certificate number issued by the state authority.'string='Exemption Certificate #'
No public methods.
-
us_tax_based_on_shippingBooleandefault=Truehelp='When checked (default), US Sales Tax is calculated using the shipping address. Uncheck to use the billing/invoice address instead.'string='Tax Based on Shipping Address' -
us_tax_calculated_atDatetimestring='Tax Calculated At' -
us_tax_sourceCharhelp='Source used for the last US tax calculation.'string='Tax Source'
-
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.
-
activeBooleandefault=True -
address_hashCharindex=Truerequired=True -
cached_atDatetimedefault=fields.Datetime.now -
city_rateFloatdigits=(5, 4) -
county_rateFloatdigits=(5, 4) -
district_rateFloatdigits=(5, 4) -
expires_atDatetimeindex=True -
provider_idMany2one → us.tax.providerondelete='set null' args: 'us.tax.provider' -
request_payloadText -
response_payloadText -
source_dateDatehelp='Rate effective date reported by provider.' -
state_idMany2one → res.country.stateindex=True args: 'res.country.state' -
state_rateFloatdigits=(5, 4) -
total_rateFloatdigits=(5, 4) -
zipCharindex=Truesize=5
-
build_hash(self, zip_code, state_code, product_category_code, date_month)@api.modelBuild 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.modelUpsert a cache entry. -
get_valid_entry(self, address_hash, provider_id=None)@api.modelReturn a non-expired cache entry or None. -
purge_expired(self)@api.modelCalled by cron to archive expired entries.
-
calculated_atDatetimedefault=fields.Datetime.nowindex=True -
calculated_byMany2one → res.usersdefault=<expr>ondelete='set null' args: 'res.users' -
city_rateFloatdigits=(5, 4) -
county_rateFloatdigits=(5, 4) -
currency_idMany2one → res.currencydefault=<expr> args: 'res.currency' -
district_rateFloatdigits=(5, 4) -
document_refCharcompute='_compute_document_ref'help='Human-readable reference.'store=False -
error_messageText -
full_addressChar -
nexus_appliedBoolean -
partner_idMany2one → res.partnerondelete='set null' args: 'res.partner' -
product_category_idMany2one → us.tax.product.categoryondelete='set null' args: 'us.tax.product.category' -
provider_idMany2one → us.tax.providerondelete='set null' args: 'us.tax.provider' -
request_payloadText -
res_idIntegerindex=True -
res_modelCharhelp='sale.order or account.move'index=True -
response_payloadTexthelp='Sanitized — no API keys stored.' -
shipping_cityChar -
shipping_countyChar -
shipping_state_idMany2one → res.country.stateargs: 'res.country.state' -
shipping_zipCharsize=5 -
sourceSelectionrequired=True args: [('local', 'Local Database'), ('api', 'External API'), ('cache', 'Cached Result'), ('manual', 'Manual Rate'), ('exempt_nexus', 'Exempt — No Nexus'), ('exempt_rule', 'Exempt — Tax Rule'), ('exempt_partner', 'Exempt — Partner Certificate'), ('error', 'Error')] -
state_rateFloatdigits=(5, 4) -
statusSelectiondefault='success' args: [('success', 'Success'), ('error', 'Error'), ('skipped', 'Skipped')] -
tax_amountMonetarycurrency_field='currency_id' -
taxable_amountMonetarycurrency_field='currency_id' -
total_rateFloatdigits=(5, 4)
-
log_calculation(self, payload)@api.modelCreate 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)
No new fields.
Public methods (2)-
calculate_for_invoice(self, move)@api.modelCalculate and apply US Sales Tax for an account.move record. -
calculate_for_sale_order(self, order)@api.modelCalculate and apply US Sales Tax for a sale.order record.
-
can_rollbackBooleandefault=True -
effective_dateDatehelp='Rates effective date from the imported source.' -
error_logText -
file_nameChar -
finished_atDatetime -
imported_byMany2one → res.usersdefault=<expr>ondelete='set null' args: 'res.users' -
nameCharcompute='_compute_name'store=True -
records_createdIntegerdefault=0 -
records_skippedIntegerdefault=0 -
records_updatedIntegerdefault=0 -
sourceCharhelp='florida_dor, texas_open_data, manual_csv, api_ziptax'required=True -
started_atDatetimedefault=fields.Datetime.now -
state_idMany2one → res.country.statedomain=[('country_id.code', '=', 'US')]string='State Covered' args: 'res.country.state' -
statusSelectiondefault='pending' args: [('pending', 'Pending'), ('running', 'Running'), ('done', 'Done'), ('failed', 'Failed')]
-
action_rollback(self)Archive all rates created by this batch.
-
delimiterChardefault=','size=1 -
effective_dateDatedefault=fields.Date.todayrequired=True -
file_dataBinaryrequired=Truestring='File' -
file_nameChar -
preview_htmlHtmlreadonly=True -
sourceSelectiondefault='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_idMany2one → res.country.statedomain=[('country_id.code', '=', 'US')]required=True args: 'res.country.state'
-
action_import(self)Execute import using the appropriate importer. -
action_preview(self)Show first 5 rows as preview before committing import.
-
activeBooleandefault=True -
child_idsOne2many → us.tax.jurisdictionargs: 'us.tax.jurisdiction', 'parent_id' -
cityCharindex=True -
complete_nameCharcompute='_compute_complete_name'recursive=Truestore=True -
countyCharindex=True -
district_codeChar -
nameCharindex=Truerequired=True -
parent_idMany2one → us.tax.jurisdictionindex=Trueondelete='restrict'string='Parent Jurisdiction' args: 'us.tax.jurisdiction' -
parent_pathCharindex=True -
rate_idsOne2many → us.tax.rateargs: 'us.tax.rate', 'jurisdiction_id' -
state_idMany2one → res.country.statedomain=[('country_id.code', '=', 'US')]index=Truerequired=True args: 'res.country.state' -
typeSelectionindex=Truerequired=True args: [('state', 'State'), ('county', 'County'), ('city', 'City'), ('district', 'Special District')] -
zip_mapping_idsOne2many → us.tax.zip.mappingargs: 'us.tax.zip.mapping', 'jurisdiction_id'
No public methods.
-
activeBooleandefault=True -
company_idMany2one → res.companydefault=<expr>required=True args: 'res.company' -
notesText -
start_dateDatehelp='Date when economic nexus was established in this state.' -
state_idMany2one → res.country.statedomain=[('country_id.code', '=', 'US')]required=True args: 'res.country.state' -
threshold_amountFloathelp='Economic nexus threshold in USD (informational for MVP).' -
threshold_transactionsIntegerhelp='Transaction count threshold (informational for MVP).'
-
has_nexus(self, company_id, state_id)@api.modelCheck if a company has active nexus in a given state.
-
activeBooleandefault=True -
codeCharhelp='Technical code: TANGIBLE, SERVICE, FOOD, MEDICINE, DIGITAL, SHIPPING, EXEMPT'index=Truerequired=Truesize=32 -
default_taxableBooleandefault=Truehelp='Taxable by default. Override per state via Tax Rules.' -
descriptionTexttranslate=True -
nameCharrequired=Truetranslate=True
No public methods.
-
account_id_paramCharhelp='ir.config_parameter key for Account ID (if required).' -
activeBooleandefault=False -
api_key_paramCharhelp='ir.config_parameter key for API Key.' -
calls_this_monthIntegerdefault=0help='Calls made this month. Reset by monthly cron.'readonly=True -
codeCharhelp="Unique provider code (e.g. 'local'; external providers define their own code in their own addon)."index=Truerequired=Truesize=32 -
descriptionText -
is_paidBooleandefault=Falsehelp='Mark if this provider requires payment after trial/free tier ends.' -
limit_alert_sentBooleandefault=Falsehelp='Whether the 80% warning has been sent this month.' -
limit_statusSelectioncompute='_compute_limit_status' args: [('ok', 'OK'), ('warning', 'Warning (>80%)'), ('exceeded', 'Limit Exceeded'), ('unlimited', 'Unlimited')] -
limit_usage_pctFloatcompute='_compute_limit_status'help='Percentage of monthly limit used.' -
monthly_call_limitIntegerdefault=0help='Monthly API call limit. 0 = unlimited (paid/enterprise plans).' -
nameCharrequired=True -
priorityIntegerdefault=10help='Lower number = higher priority in fallback chain. Engine tries providers in ascending priority order.' -
sandbox_modeBooleandefault=Truehelp='Use sandbox/test endpoints. Disable for production.' -
supports_address_levelBooleandefault=Falsehelp='Provider can resolve by full address (not just ZIP).' -
supports_zip_levelBooleandefault=Truehelp='Provider can resolve by ZIP code.' -
timeoutIntegerdefault=5help='API request timeout in seconds.'
-
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.
-
activeBooleandefault=True -
city_rateFloatdefault=0.0digits=(5, 4) -
county_rateFloatdefault=0.0digits=(5, 4) -
district_rateFloatdefault=0.0digits=(5, 4) -
effective_dateDateindex=Truerequired=True -
end_dateDatehelp='Leave empty if this rate is currently in effect.'index=True -
import_batch_idMany2one → us.tax.import.batchargs: 'us.tax.import.batch' -
jurisdiction_idMany2one → us.tax.jurisdictionindex=Trueondelete='restrict'required=True args: 'us.tax.jurisdiction' -
notesText -
product_tax_category_idMany2one → us.tax.product.categoryhelp='Leave empty to apply to all categories.' args: 'us.tax.product.category' -
sourceChardefault='manual'help='Origin: e.g. florida_dor, manual, or the code of an external provider module.' -
source_urlCharstring='Source URL' -
state_idMany2oneindex=Truerelated='jurisdiction_id.state_id'store=True -
state_rateFloatdefault=0.0digits=(5, 4) -
total_rateFloatcompute='_compute_total_rate'digits=(5, 4)help='Sum of all rate components.'store=True
-
get_rate_for_date(self, jurisdiction_id, date, product_category_id=None)@api.modelReturn 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.
-
activeBooleandefault=True -
effective_dateDate -
end_dateDate -
exemption_reasonCharhelp='Legal or regulatory basis for exemption.' -
product_tax_category_idMany2one → us.tax.product.categoryondelete='restrict'required=True args: 'us.tax.product.category' -
rate_overrideFloatdigits=(5, 4)help='If set, use this rate instead of the standard jurisdiction rate.' -
state_idMany2one → res.country.statedomain=[('country_id.code', '=', 'US')]help='Leave empty to apply to all US states.' args: 'res.country.state' -
taxableBooleandefault=Truehelp='Is this category taxable in this state?'
-
is_taxable(self, state_id, product_category_id, date=None)@api.modelCheck if a product category is taxable in a state on a given date. Returns: (taxable: bool, rate_override: float | None)
-
cityChar -
confidenceFloatdefault=1.0help='Confidence 0.0-1.0. Highest wins when ZIP has multiple jurisdictions.' -
countyChar -
import_batch_idMany2one → us.tax.import.batchargs: 'us.tax.import.batch' -
imported_atDatetimedefault=fields.Datetime.now -
jurisdiction_idMany2one → us.tax.jurisdictionindex=Trueondelete='restrict'required=True args: 'us.tax.jurisdiction' -
sourceChardefault='manual'help='Data origin: florida_dor, texas_open_data, api_ziptax, manual' -
state_idMany2one → res.country.statedomain=[('country_id.code', '=', 'US')]index=Truerequired=True args: 'res.country.state' -
zipCharindex=Truerequired=Truesize=5string='ZIP Code' -
zip4Charsize=4string='ZIP+4'
-
get_best_jurisdiction(self, zip_code, state_code=None, confidence_min=0.7)@api.modelReturn 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