TIP: You can type at any time to perform a new search.
US Sales Tax Engine
l10n_us_sales_tax_engine · OCA/l10n-usa
- 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 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_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 (16)
New fields (2)
-
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.
New fields (1)
-
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.
New fields (5)
-
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.
New fields (2)
-
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.
New fields (15)
-
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.
New fields (27)
-
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'), ('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)
New fields (0)
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.
New fields (14)
-
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.
New fields (7)
-
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.
New fields (13)
-
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.
New fields (7)
-
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.
New fields (5)
-
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.
New fields (17)
-
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.
New fields (15)
-
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.
New fields (8)
-
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)
New fields (10)
-
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