Initial commit of account_us_wa_salestax for 11.0

This commit is contained in:
Jared Kipe
2018-09-12 14:01:55 -07:00
parent 0822c98f3f
commit 59788ff95f
6 changed files with 196 additions and 0 deletions

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,13 @@
{'name': 'US WA State SalesTax API',
'version': '10.0.1.0.0',
'category': 'Tools',
'depends': ['account',
],
'author': 'Hibou Corp.',
'license': 'AGPL-3',
'website': 'https://hibou.io/',
'data': ['views/account_fiscal_position_view.xml',
],
'installable': True,
'application': False,
}

View File

@@ -0,0 +1,2 @@
from . import account_fiscal_position
from . import wa_tax_request

View File

@@ -0,0 +1,78 @@
from odoo import api, fields, models
from odoo.exceptions import ValidationError
from .wa_tax_request import WATaxRequest
class AccountFiscalPosition(models.Model):
_inherit = 'account.fiscal.position'
is_us_wa = fields.Boolean(string='Use WA State API')
wa_base_tax_id = fields.Many2one('account.tax', string='WA Base/Error Tax')
@api.multi
def map_tax(self, taxes, product=None, partner=None):
if not taxes or not self.is_us_wa or partner is None:
return super(AccountFiscalPosition, self).map_tax(taxes)
AccountTax = self.env['account.tax'].sudo()
result = AccountTax.browse()
for tax in taxes:
# step 1: If we were to save the location code on the partner we might not have to do this
request = WATaxRequest()
res = request.get_rate(partner)
wa_tax = None
if not request.is_success(res):
# Cache.
wa_tax = AccountTax.search([
('wa_location_zips', 'like', '%' + partner.zip + '%'),
('amount_type', '=', 'percent'),
('type_tax_use', '=', 'sale')], limit=1)
if not wa_tax:
result |= self.wa_base_tax_id
continue
# step 2: Find or create tax
if not wa_tax:
wa_tax = AccountTax.search([
('wa_location_code', '=', res['location_code']),
('amount', '=', res['rate']),
('amount_type', '=', 'percent'),
('type_tax_use', '=', 'sale')], limit=1)
if not wa_tax:
wa_tax = AccountTax.create({
'name': '%s - WA Tax %s %%' % (res['location_code'], res['rate']),
'wa_location_code': res['location_code'],
'amount': res['rate'],
'amount_type': 'percent',
'type_tax_use': 'sale',
'account_id': self.wa_base_tax_id.account_id.id,
'refund_account_id': self.wa_base_tax_id.refund_account_id.id
})
if not wa_tax.wa_location_zips:
wa_tax.wa_location_zips = partner.zip
elif not wa_tax.wa_location_zips.find(partner.zip) >= 0:
zips = wa_tax.wa_location_zips.split(',')
zips.append(partner.zip)
wa_tax.wa_location_zips = zips.append(',')
# step 3: Find or create mapping
tax_line = self.tax_ids.filtered(lambda x: x.tax_src_id.id == tax.id and x.tax_dest_id.id == wa_tax.id)
if not tax_line:
tax_line = self.env['account.fiscal.position.tax'].sudo().create({
'position_id': self.id,
'tax_src_id': tax.id,
'tax_dest_id': wa_tax.id,
})
result |= tax_line.tax_dest_id
return result
class AccountTax(models.Model):
_inherit = 'account.tax'
wa_location_code = fields.Integer('WA Location Code')
wa_location_zips = fields.Char('WA Location ZIPs', default='')

View File

@@ -0,0 +1,74 @@
from urllib.request import urlopen, quote
from urllib.error import HTTPError
from ssl import _create_unverified_context
from logging import getLogger
from odoo.exceptions import ValidationError
_logger = getLogger(__name__)
class WATaxRequest(object):
def __init__(self):
pass
def get_rate(self, partner):
# https://webgis.dor.wa.gov/webapi/addressrates.aspx/?output=text\&addr=test\&city=Marysville\&zip=98270
if not all((partner.street, partner.city, partner.zip)):
raise ValidationError('WATaxRequest impossible without Street, City and ZIP.')
url = 'https://webgis.dor.wa.gov/webapi/addressrates.aspx?output=text&addr=' + quote(partner.street) + \
'&city=' + quote(partner.city) + '&zip=' + quote(partner.zip)
_logger.info(url)
try:
response = urlopen(url, context=_create_unverified_context())
response_body = response.read()
_logger.info(response_body)
except HTTPError as e:
_logger.warn('Error on request: ' + str(e))
response_body = ''
return self._parse_rate(response_body)
def is_success(self, result):
'''
ADDRESS = 0,
LATLON = 0,
PLUS4 = 1,
ADDRESS_STANARDIZED = 2,
PLUS4_STANARDIZED = 3,
ADDRESS_CHANGED = 4,
ZIPCODE = 5,
ADDRESS_NOT_FOUND = 6,
LATLON_NOT_FOUND = 7,
POI = 8,
ERROR = 9
internal parse_error = 100
'''
if 'result_code' not in result or result['result_code'] >= 9 or result['result_code'] < 0:
return False
return True
def _parse_rate(self, response_body):
# 'LocationCode=1704 Rate=0.100 ResultCode=0'
# {
# 'result_code': 0,
# 'location_code': '1704',
# 'rate': '10.00',
# }
res = {'result_code': 100}
if len(response_body) > 200:
# this likely means that they returned an HTML page
return res
body_parts = response_body.decode().split(' ')
for part in body_parts:
if part.find('ResultCode=') >= 0:
res['result_code'] = int(part[len('ResultCode='):])
elif part.find('Rate=') >= 0:
res['rate'] = '%.2f' % (float(part[len('Rate='):]) * 100.0)
elif part.find('LocationCode=') >= 0:
res['location_code'] = part[len('LocationCode='):]
elif part.find('debughint=') >= 0:
res['debug_hint'] = part[len('debughint='):]
return res

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_position_us_wa_inherit_from_view" model="ir.ui.view">
<field name="name">account.fiscal.position.form.inherit</field>
<field name="model">account.fiscal.position</field>
<field name="inherit_id" ref="account.view_account_position_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='active']" position="after">
<field name="is_us_wa"/>
<field name="wa_base_tax_id" attrs="{'invisible': [('is_us_wa', '=', False)]}" />
</xpath>
</field>
</record>
<record id="view_tax_form" model="ir.ui.view">
<field name="name">account.tax.form.inherit</field>
<field name="model">account.tax</field>
<field name="inherit_id" ref="account.view_tax_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='description']" position="after">
<field name="wa_location_code" />
<field name="wa_location_zips" />
</xpath>
</field>
</record>
</odoo>