From 3551a0e5b16a2cc2c6821cad495f2a049e708dc1 Mon Sep 17 00:00:00 2001 From: Chris Chapman Date: Mon, 19 Aug 2024 08:36:58 -0600 Subject: [PATCH] Second draft of boundary/tax code/tax rule importing - support for boundary data with tax codes (only) - dynamic resolution of tax rule and tax code for sale and invoice - reporting code stored on tax line as text - cascade deletes onto tax code lines - Added jurasdiction names for Utah --- __init__.py | 3 + account.py | 70 +++++++- sale.py | 2 - scripts/import_boundaries.py | 285 +++++++++++++++++++++-------- scripts/jurisdictions.csv | 336 +++++++++++++++++++++++++++++++++++ tax.py | 79 ++++++-- tax.xml | 11 ++ view/tax_line_form.xml | 8 + view/tax_line_tree.xml | 7 + 9 files changed, 708 insertions(+), 93 deletions(-) create mode 100644 scripts/jurisdictions.csv create mode 100644 view/tax_line_form.xml create mode 100644 view/tax_line_tree.xml diff --git a/__init__.py b/__init__.py index 870fa16..c5f1a65 100644 --- a/__init__.py +++ b/__init__.py @@ -17,10 +17,13 @@ def register(): tax.Tax, tax.TaxBoundary, tax.TaxCode, + tax.TaxCodeLine, + tax.TaxLine, tax.TaxRule, module='account_us_sstp', type_='model') Pool.register( account.InvoiceLine, + account.InvoiceTax, module='account_us_sstp', type_='model', depends=['account_invoice']) Pool.register( diff --git a/account.py b/account.py index 41db7c6..930783f 100644 --- a/account.py +++ b/account.py @@ -8,7 +8,7 @@ class InvoiceLine(metaclass=PoolMeta): __name__ = 'account.invoice.line' @fields.depends( - '_parent_invoice.party', 'party', 'invoice', 'tax_date', + '_parent_invoice.party', 'party', 'invoice', '_parent_invoice.accounting_date', '_parent_invoice.invoice_date', '_parent_invoice.invoice_address') def on_change_product(self): @@ -16,12 +16,12 @@ def on_change_product(self): Date = pool.get('ir.date') Boundary = pool.get('account.tax.boundary') - if self.invoice and self.invoice.tax_date: - tax_date = self.invoice.tax_date - elif self.tax_date: + if self.tax_date: tax_date = self.tax_date - elif self.taxes_date: - tax_date = self.taxes_date + elif self.invoice and self.invoice.tax_date: + tax_date = self.invoice.tax_date + else: + tax_date = Date.today() if self.invoice and self.invoice.invoice_address: a = self.invoice.invoice_address @@ -67,3 +67,61 @@ def on_change_product(self): party.customer_tax_rule = boundary.rule return super().on_change_product() + +class InvoiceTax(metaclass=PoolMeta): + __name__ = 'account.invoice.tax' + + def get_move_lines(self): + lines = super().get_move_lines() + + pool = Pool() + Date = pool.get('ir.date') + Boundary = pool.get('account.tax.boundary') + + if self.invoice and self.invoice.tax_date: + tax_date = self.invoice.tax_date + else: + tax_date = Date.today() + + if self.invoice and self.invoice.invoice_address: + a = self.invoice.invoice_address + + pattern = r'(\d{5})-?(\d{4})?$' + match = re.match(pattern, a.postal_code) + if match: + zipcode, zipext = match.groups() + + try: + boundary, = Boundary.search([ + ('start_date', '<=', tax_date), + ['OR', [ + ('end_date', '>=', tax_date) + ], [ + ('end_date', '=', None) + ], + ], + ('authority.country', '=', a.country), + ('authority.subdivision', '=', a.subdivision), + ['OR', [ + ('type', '=', '4'), + ('zipcode_low', '<=', zipcode), + ('zipcode_high', '>=', zipcode), + ('zipext_low', '<=', zipext), + ('zipext_high', '>=', zipext), + ], [ + ('type', '=', 'Z'), + ('zipcode_low', '<=', zipcode), + ('zipcode_high', '>=', zipcode), + ], + ] + ], limit=1, order=[('type', 'DESC')]) + except ValueError: + boundary = None + + if boundary and boundary.code: + for line in lines: + for tax_line in line.tax_lines: + if tax_line.type == 'tax': + tax_line.code = boundary.code.code + + return lines diff --git a/sale.py b/sale.py index 620ee04..85d37c1 100644 --- a/sale.py +++ b/sale.py @@ -48,11 +48,9 @@ def compute_taxes(self, party): ] ], limit=1, order=[('type', 'DESC')]) except ValueError: - print("Could not find a matching tax rule for %s" % party) boundary = None if boundary and boundary.rule: - print('using boundary rule %s' % boundary.rule.rec_name) if party and not party.customer_tax_rule: party.customer_tax_rule = boundary.rule diff --git a/scripts/import_boundaries.py b/scripts/import_boundaries.py index cd961a0..22fcf0d 100755 --- a/scripts/import_boundaries.py +++ b/scripts/import_boundaries.py @@ -4,6 +4,7 @@ from __future__ import print_function import csv +from collections import defaultdict import datetime as dt import os import sys @@ -19,7 +20,7 @@ import zipfile from argparse import ArgumentParser from io import BytesIO, TextIOWrapper -#from itertools import batched +from itertools import batched try: from progressbar import ETA, Bar, ProgressBar, SimpleProgress @@ -122,57 +123,56 @@ def fetch(code): def get_places(code): Place = Model.get('census.place') return {p.code_fips: p for p in Place.find([ - ('subdivision.code', '=', 'US-%s' % code) + ('subdivision.code', '=', code) ])} -def get_tax_codes(code): - return {c.code_ser: c} +class TaxRuleCollector: -def import_boundaries(code, boundaries): - sys.stderr.write('Importing boundaries') - sys.stderr.flush() - Boundary = Model.get('account.tax.boundary') - Tax = Model.get('account.tax') - TaxRule = Model.get('account.tax.rule') - TaxRuleLine = Model.get('account.tax.rule') + def __init__(self, code, places): + self.places = places + self.rules = {} + self.tax_sets = {} + self.generic_taxes = {} - places = get_places(code) + self.Tax = Model.get('account.tax') + self.TaxRule = Model.get('account.tax.rule') + self.TaxRuleLine = Model.get('account.tax.rule') - def get_rule(jurisdiction, authority): + def get_rule(self, jurisdiction, authority): code_fips = jurisdiction.code_fips - rule = rules.get(code_fips) + rule = self.rules.get(code_fips) if not rule: try: - rule, = TaxRule.find([ + rule, = self.TaxRule.find([ + ('authority', '=', authority), ('jurisdiction.code_fips', '=', code_fips), ]) except ValueError: return - rules[code_fips] = rule + self.rules[code_fips] = rule return rule - rules = {} - def get_taxes(jurisdiction, authority): + def get_taxes(self, jurisdiction, authority): code_fips = jurisdiction.code_fips - taxes = tax_sets.get(code_fips) + taxes = self.tax_sets.get(code_fips) if not taxes: try: - taxes = Tax.find([ - ('jurisdiction.code_fips', '=', code_fips), + taxes = self.Tax.find([ ('authority', '=', authority), + ('jurisdiction.code_fips', '=', code_fips), ('type', '=', 'none'), + ('parent', '=', None), ]) except ValueError: return [] - tax_sets[code_fips] = taxes + self.tax_sets[code_fips] = taxes return taxes - tax_sets = {} - def get_generic_tax(tax): - generic_tax = generic_taxes.get(tax.id) + def get_generic_tax(self, tax): + generic_tax = self.generic_taxes.get(tax.id) if not generic_tax: try: - generic_tax, = Tax.find([ + generic_tax, = self.Tax.find([ ('authority', '=', None), ('group', '=', tax.group), ('company', '=', tax.company), @@ -183,54 +183,205 @@ def get_generic_tax(tax): ]) except ValueError: sys.exit("Error could not find generic tax for %s" % tax.name) - generic_taxes[tax.id] = generic_tax + self.generic_taxes[tax.id] = generic_tax return generic_tax - generic_taxes = {} - f = TextIOWrapper(BytesIO(boundaries), encoding='utf-8') - reader = csv.DictReader(f, fieldnames=_fieldnames, - restkey='special_districts') - records = [] - for row in _progress(reader): - jurisdiction = places.get(row['fips_place_code'], - places[row['fips_county_code']]) - authority = places[row['fips_state_code']] - start_date = dt.datetime.strptime(row['start_date'], '%Y%m%d').date() - end_date = dt.datetime.strptime(row['end_date'], '%Y%m%d').date() - end_date = None if end_date == dt.date.max else end_date + def collect(self, row): + jurisdiction = self.places.get(row['fips_place_code'], + self.places[row['fips_county_code']]) + authority = self.places[row['fips_state_code']] - if end_date != None: - continue #TODO: for now only load current records + rule = self.get_rule(jurisdiction, authority) - rule = get_rule(jurisdiction, authority) if not rule: name = '%s, %s Retail' % (jurisdiction.name, jurisdiction.subdivision.code) - rule = TaxRule( + rule = self.TaxRule( name=name, jurisdiction=jurisdiction, authority=authority) + for code_fips in ['fips_state_indicator', 'fips_county_code', 'fips_place_code']: if all(c == '0' for c in row[code_fips]): continue - place = places.get(row[code_fips]) + place = self.places.get(row[code_fips]) if place: - taxes = get_taxes(place, authority) - for tax in taxes: + for tax in self.get_taxes(place, authority): + origin_tax = self.get_generic_tax(tax) + line = rule.lines.new() - line.start_date = start_date - line.end_date = end_date line.group = tax.group + line.origin_tax = origin_tax line.tax = tax - line.origin_tax = get_generic_tax(tax) line.to_country = line.from_country = place.country line.to_subdivision = place.subdivision if tax.sourcing == 'intrastate': line.from_subdivision = place.subdivision else: line.from_subdivision = None + + for sd in batched(row['special_districts'], n=3): + pass + rule.save() + return rule + +class TaxCodeCollector: + + def __init__(self, code, places): + self.tax_codes = {} + self.places = places + + authority = None + try: + authority, = [v for v in places.values() if v.parent == None] + except: + sys.exit("\nError could not find a state authority for the code: %s" % code) + self.authority = authority + + with open(os.path.join(os.path.dirname(__file__), + 'jurisdictions.csv'), newline='') as csvfile: + reader = csv.DictReader(csvfile, fieldnames=['code', 'code_tax', 'name']) + self.names = {r['code_tax']: r['name'] for r in reader if r['code'] == code} + + TaxCode = Model.get('account.tax.code') + root = TaxCode(name="%s Streamlined Sales Tax Report" % self.authority.subdivision.name, + code='SSTR-%s' % self.authority.subdivision.code, + authority=self.authority) + root.save() + + taxable_sales = root.childs.new() + taxable_sales.name = "Taxable Sales" + taxable_sales.code = 'A' + taxable_sales.authority = self.authority + taxable_sales.save() + + total_sales = taxable_sales.childs.new() + total_sales.name = "Total Sales" + total_sales.code = '1' + total_sales.authority = self.authority + total_sales.save() + + exemptions = taxable_sales.childs.new() + exemptions.name = "Exemptions and Deductions" + exemptions.code = '2' + exemptions.authority = self.authority + exemptions.save() + + for name in ['Agriculture', 'Direct Pay', 'Government Exemption Organizations', + 'Manufacturing', 'Resale', 'Other']: + subcode = exemptions.childs.new() + subcode.name = name + subcode.authority = self.authority + subcode.save() + + total_tax = root.childs.new() + total_tax.name = "Total Tax Due" + total_tax.code = 'B' + total_tax.authority = self.authority + total_tax.save() + + self.total_sales = total_sales + self.total_tax = total_tax + + + def get_tax_code(self, code_tax): + tax_code = self.tax_codes.get(code_tax) + if not tax_code: + TaxCode = Model.get('account.tax.code') + try: + tax_code, = TaxCode.find([ + ('authority', '=', self.authority), + ('code', '=', code_tax), + ]) + except ValueError: + return + self.tax_codes[code_tax] = tax_code + return tax_code + + def collect(self, row): + code_tax = row['composite_ser_code'] + if not code_tax or all(c == '0' for c in code_tax): + return + + tax_code = self.get_tax_code(code_tax) + if not tax_code: + name = self.names.get(code_tax) + if not name: + print('Could not find jurisdiction name for %s' % code_tax) + name = code_tax + + tax_code = self.total_tax.childs.new() + tax_code.name = name + tax_code.code = code_tax + tax_code.authority = self.authority + + tax_code.save() + return tax_code + +def import_(code, boundaries): + sys.stderr.write('Importing') + sys.stderr.flush() + Boundary = Model.get('account.tax.boundary') + + places = get_places(code) + code_collector = TaxCodeCollector(code, places) + rule_collector = TaxRuleCollector(code, places) + + _seen = defaultdict(set) + def seen(rule, code=None): + if code: + if _seen.get(code) and rule in _seen[code]: + return True + _seen[code].add(rule) + return False + else: + if _seen.get(rule): + return True + _seen[rule].add(1) + return False + + _taxes = set() + def setup_tax_lines(code, tax, amount='tax'): + for op, type_ in zip(['+', '-'], ['invoice', 'credit']): + line = code.lines.new() + line.operator = op + line.tax = tax + line.amount = amount + line.type = type_ + + f = TextIOWrapper(BytesIO(boundaries), encoding='utf-8') + reader = csv.DictReader(f, fieldnames=_fieldnames, + restkey='special_districts') + records = [] + for row in _progress(reader): + authority = places[row['fips_state_code']] + start_date = dt.datetime.strptime(row['start_date'], '%Y%m%d').date() + end_date = dt.datetime.strptime(row['end_date'], '%Y%m%d').date() + end_date = None if end_date == dt.date.max else end_date + + tax_code = code_collector.collect(row) + rule = rule_collector.collect(row) + + if tax_code and not seen(rule, code=tax_code): + for line in rule.lines: + for tax in line.tax.childs: + taxes = [line.tax for line in tax_code.lines] + if tax not in taxes: + setup_tax_lines(tax_code, tax) + _taxes.add(tax) + tax_code.save() + elif not tax_code and not seen(rule): + for line in rule.lines: + for tax in line.tax.childs: + total_tax = code_collector.total_tax + taxes = [line.tax for line in total_tax.lines] + if not tax in taxes: + setup_tax_lines(total_tax, tax) + _taxes.add(tax) + total_tax.save() + records.append(Boundary( type=row['record_type'], @@ -242,32 +393,23 @@ def get_generic_tax(tax): zipcode_high=row['zipcode_high'], zipext_high=row['zipext_high'], rule=rule, + code=tax_code, )) if reader.line_num % 10000 == 0: Boundary.save(records) records = [] - #for sd in batched(row['special_districts'], n=3): - # pass - Boundary.save(records) - print('.', file=sys.stderr) -def update_tax_codes(code): - pass + total_sales = code_collector.total_sales + for tax in _taxes: + # only the state-level bases are needed + if tax.jurisdiction == code_collector.authority: + setup_tax_lines(total_sales, tax, amount='base') + total_sales.save() -def get_tax_rules(code): - TaxRule = Model.get('account.tax.rule') - return {(r.county_fips, r.place_fips): r for r in TaxRule.find([ - ('authority.subdivision.code', '=', 'US-%s' % code) - ])} - -def update_tax_rules(code): - TaxRule = Model.get('account.tax.rule') - -def update_tax_rules_interstate(codes): - pass + print('.', file=sys.stderr) _fieldnames = ['record_type', 'start_date', 'end_date', 'address_range_low', 'address_range_high', 'odd_even_indicator', 'street_predirectional', @@ -277,12 +419,6 @@ def update_tax_rules_interstate(codes): 'composite_ser_code', 'fips_state_code', 'fips_state_indicator','fips_county_code', 'fips_place_code', 'fips_place_class_code', 'longitude', 'latitude'] -#_fieldnames.extend([k + str(i) for i in range(1, 21) for k in [ -# 'special_tax_district_code_source_', -# 'special_tax_district_code_', -# 'special_tax_district_authority_' -# ]]) - def main(database, codes, config_file=None): config.set_trytond(database, config_file=config_file) do_import(codes) @@ -294,9 +430,8 @@ def do_import(codes): code = code.upper() clean_boundaries('US-%s' % code) clean_tax_rules('US-%s' % code) - #clean_tax_codes('US-%s' % code) - boundaries = fetch(code) - import_boundaries(code, boundaries) + clean_tax_codes('US-%s' % code) + import_('US-%s' % code, fetch(code)) def run(): diff --git a/scripts/jurisdictions.csv b/scripts/jurisdictions.csv new file mode 100644 index 0000000..446913e --- /dev/null +++ b/scripts/jurisdictions.csv @@ -0,0 +1,336 @@ +US-UT,01000,Beaver County +US-UT,01002,Beaver City +US-UT,01008,Milford +US-UT,01009,Minersville +US-UT,01500,UIPA Min Mt - Beaver Co +US-UT,01501,UIPA Min Mt - Beaver City +US-UT,01502,UIPA Min Mt - Milford +US-UT,02000,Box Elder County +US-UT,02004,Bear River +US-UT,02017,Brigham +US-UT,02025,Corinne +US-UT,02032,Deweyville +US-UT,02035,Elwood +US-UT,02041,Fielding +US-UT,02044,Garland +US-UT,02054,Honeyville +US-UT,02057,Howell +US-UT,02069,Mantua +US-UT,02086,Perry +US-UT,02090,Plymouth +US-UT,02092,Portage +US-UT,02100,Snowville +US-UT,02113,Tremonton +US-UT,02120,Willard +US-UT,02500,UIPA GS - Box Elder Co +US-UT,02501,UIPA GS - Brigham City +US-UT,02502,UIPA GS - Garland +US-UT,02503,UIPA GS - Tremonton +US-UT,03000,Cache County +US-UT,03001,Amalga +US-UT,03014,Clarkston +US-UT,03017,Cornish +US-UT,03032,Hyde Park +US-UT,03033,Hyrum +US-UT,03036,Lewiston +US-UT,03038,Logan +US-UT,03041,Mendon +US-UT,03044,Millville +US-UT,03047,Newton +US-UT,03049,North Logan +US-UT,03053,Paradise +US-UT,03056,Providence +US-UT,03059,Richmond +US-UT,03060,River Heights +US-UT,03062,Smithfield +US-UT,03076,Wellsville +US-UT,03081,Trenton +US-UT,03098,Nibley +US-UT,03900,Cache Valley Transit +US-UT,04000,Carbon County +US-UT,04016,Helper +US-UT,04035,Price +US-UT,04040,Scofield +US-UT,04053,Wellington +US-UT,04058,East Carbon +US-UT,05000,Daggett County +US-UT,05002,Dutch John +US-UT,05006,Manila +US-UT,06000,Davis County +US-UT,06004,Bountiful +US-UT,06006,Centerville +US-UT,06008,Clearfield +US-UT,06010,Fruit Heights +US-UT,06017,Farmington +US-UT,06026,Kaysville +US-UT,06030,Layton +US-UT,06035,North Salt Lake +US-UT,06045,South Weber +US-UT,06048,Sunset +US-UT,06049,Syracuse +US-UT,06056,West Point +US-UT,06057,Woods Cross +US-UT,06059,Clinton +US-UT,06061,West Bountiful +US-UT,06300,Falcon Hill Davis +US-UT,06301,Falcon Hill Clearfield +US-UT,06302,Falcon Hill Sunset +US-UT,07000,Duchesne County +US-UT,07001,Altamont +US-UT,07008,Duchesne City +US-UT,07017,Myton +US-UT,07019,Roosevelt +US-UT,07020,Tabiona +US-UT,08000,Emery County +US-UT,08001,Castle Dale +US-UT,08003,Clawson +US-UT,08004,Cleveland +US-UT,08007,Elmo +US-UT,08008,Emery City +US-UT,08009,Ferron +US-UT,08011,Green River +US-UT,08012,Huntington +US-UT,08016,Orangeville +US-UT,09000,Garfield County +US-UT,09001,Antimony +US-UT,09002,Boulder +US-UT,09003,Bryce Canyon +US-UT,09004,Cannonville +US-UT,09005,Escalante +US-UT,09006,Hatch +US-UT,09008,Henrieville +US-UT,09011,Panguitch +US-UT,09015,Tropic +US-UT,10000,Grand County +US-UT,10005,Castle Valley +US-UT,10011,Moab +US-UT,11000,Iron County +US-UT,11003,Cedar City +US-UT,11005,Enoch +US-UT,11012,Kanarraville +US-UT,11018,Paragonah +US-UT,11019,Parowan +US-UT,11028,Brian Head +US-UT,11501,Inland Port Iron Springs +US-UT,12000,Juab County +US-UT,12009,Eureka +US-UT,12019,Levan +US-UT,12024,Mona +US-UT,12026,Nephi +US-UT,12030,Rocky Ridge Town +US-UT,12050,Santaquin South +US-UT,12500,UIPA Agri-Park +US-UT,13000,Kane County +US-UT,13001,Alton +US-UT,13002,Glendale +US-UT,13004,Kanab +US-UT,13007,Orderville +US-UT,13010,Big Water +US-UT,14000,Millard County +US-UT,14010,Delta +US-UT,14014,Fillmore +US-UT,14023,Hinckley +US-UT,14024,Holden +US-UT,14026,Kanosh +US-UT,14028,Leamington +US-UT,14030,Lynndyl +US-UT,14034,Meadow +US-UT,14037,Oak City +US-UT,14040,Scipio +US-UT,15000,Morgan County +US-UT,15007,Morgan City +US-UT,16000,Piute County +US-UT,16003,Circleville +US-UT,16005,Junction +US-UT,16006,Kingston +US-UT,16007,Marysvale +US-UT,17000,Rich County +US-UT,17001,Garden City +US-UT,17002,Laketown +US-UT,17005,Randolph +US-UT,17010,Woodruff +US-UT,18000,Salt Lake County +US-UT,18003,Alta +US-UT,18010,Brighton +US-UT,18019,Bluffdale +US-UT,18020,Cottonwood Heights +US-UT,18039,Draper +US-UT,18060,Herriman +US-UT,18065,Holladay +US-UT,18093,Midvale +US-UT,18094,Millcreek +US-UT,18096,Murray +US-UT,18118,Riverton +US-UT,18122,Salt Lake City +US-UT,18131,Sandy +US-UT,18138,South Jordan +US-UT,18139,South Salt Lake +US-UT,18142,Taylorsville +US-UT,18155,West Jordan +US-UT,18167,West Valley City +US-UT,18300,Utah Data Center SL Co +US-UT,18301,MIDA Sundance - SLC +US-UT,18401,Copperton Township +US-UT,18402,Emigration Canyon Township +US-UT,18403,Kearns Township +US-UT,18404,Magna Township +US-UT,18405,White City Township +US-UT,18501,Inland Port Salt Lake City +US-UT,18502,Inland Port West Valley City +US-UT,18503,Inland Port Magna +US-UT,18504,Inland Port Salt Lake County +US-UT,18601,SLC Convention Hotel +US-UT,18701,Salt Lake City HTRZ +US-UT,18702,Sandy HTRZ +US-UT,18703,South Salt Lake HTRZ +US-UT,19000,San Juan County +US-UT,19002,Blanding +US-UT,19004,Bluff +US-UT,19009,Monticello +US-UT,20000,Sanpete County +US-UT,20004,Centerfield +US-UT,20008,Ephraim +US-UT,20009,Fairview +US-UT,20010,Fayette +US-UT,20011,Fountain Green +US-UT,20014,Gunnison +US-UT,20020,Manti +US-UT,20021,Mayfield +US-UT,20023,Moroni +US-UT,20024,Mt. Pleasant +US-UT,20031,Spring City +US-UT,20032,Sterling +US-UT,20033,Wales +US-UT,21000,Sevier County +US-UT,21001,Annabella +US-UT,21002,Aurora +US-UT,21007,Central Valley +US-UT,21014,Elsinore +US-UT,21018,Glenwood +US-UT,21025,Joseph +US-UT,21029,Koosharem +US-UT,21031,Monroe +US-UT,21033,Redmond +US-UT,21034,Richfield +US-UT,21035,Salina +US-UT,21038,Sigurd +US-UT,22000,Summit County +US-UT,22006,Coalville +US-UT,22013,Francis +US-UT,22017,Henefer +US-UT,22022,Kamas +US-UT,22029,Oakley +US-UT,22030,Park City +US-UT,22900,Snyderville Basin Tr Dist +US-UT,23000,Tooele County +US-UT,23018,Erda +US-UT,23020,Erda City West +US-UT,23023,Grantsville +US-UT,23031,Lakepoint City +US-UT,23035,Lakepoint Transit +US-UT,23046,Stockton +US-UT,23048,Tooele City +US-UT,23050,Vernon +US-UT,23052,Wendover +US-UT,23056,Rush Valley +US-UT,23065,Lincoln +US-UT,23066,Stansbury Park +US-UT,23301,Dugway Proving Grounds +US-UT,23500,UIPA Tooele Valley +US-UT,23501,UIPA Twenty Wells +US-UT,24000,Uintah County +US-UT,24014,Naples +US-UT,24024,Vernal +US-UT,24028,Ballard +US-UT,25000,Utah County +US-UT,25001,Alpine +US-UT,25002,American Fork +US-UT,25010,Bluffdale South +US-UT,25019,Cedar Fort +US-UT,25029,Draper City South +US-UT,25030,Eagle Mountain +US-UT,25035,Fairfield +US-UT,25038,Genola +US-UT,25043,Goshen +US-UT,25066,Lehi +US-UT,25070,Lindon +US-UT,25073,Mapleton +US-UT,25083,Orem +US-UT,25085,Payson +US-UT,25088,Pleasant Grove +US-UT,25090,Provo +US-UT,25096,Salem +US-UT,25097,Santaquin +US-UT,25098,Saratoga Springs +US-UT,25099,Highland +US-UT,25103,Spanish Fork +US-UT,25106,Springville +US-UT,25117,Vineyard +US-UT,25123,Cedar Hills +US-UT,25124,Elk Ridge +US-UT,25125,Woodland Hills +US-UT,25300,Utah Data Center Utah Co +US-UT,25301,MIDA Sundance - Ut Co +US-UT,25500,UIPA Verk - Ut Co +US-UT,25501,UIPA Verk - Spanish ForkĀ  +US-UT,25702,Vineyard HTRZ +US-UT,25800,ULA Utah County +US-UT,25801,ULA Genola +US-UT,25802,ULA Lehi +US-UT,25803,ULA Lindon +US-UT,25804,ULA Provo +US-UT,25805,ULA Vineyard +US-UT,26000,Wasatch County +US-UT,26003,Charleston +US-UT,26005,Daniel +US-UT,26008,Heber +US-UT,26009,Independence +US-UT,26010,Interlaken +US-UT,26011,Midway +US-UT,26013,Park City East +US-UT,26014,Wallsburg +US-UT,26020,Hideout +US-UT,26300,Military Recreation - Wasatch +US-UT,26301,Military Recreation - Hideout +US-UT,26302,Military Recreation - MWR Hotel +US-UT,26303,Military Recreation - GAEC PID +US-UT,27000,Washington County +US-UT,27002,Apple Valley +US-UT,27005,Enterprise +US-UT,27008,Hurricane +US-UT,27010,Ivins +US-UT,27011,La Verkin +US-UT,27012,Leeds +US-UT,27015,New Harmony +US-UT,27019,Rockville +US-UT,27020,St George +US-UT,27021,Santa Clara +US-UT,27023,Springdale +US-UT,27024,Toquerville +US-UT,27026,Virgin +US-UT,27027,Washington City +US-UT,27035,Hildale +US-UT,28000,Wayne County +US-UT,28001,Bicknell +US-UT,28005,Hanksville +US-UT,28007,Loa +US-UT,28008,Lyman +US-UT,28010,Torrey +US-UT,29000,Weber County +US-UT,29012,Farr West +US-UT,29016,Harrisville +US-UT,29018,Hooper +US-UT,29019,Huntsville +US-UT,29022,Marriott-Slaterville +US-UT,29026,North Ogden +US-UT,29027,Ogden +US-UT,29030,Plain City +US-UT,29031,Pleasant View +US-UT,29036,Riverdale +US-UT,29037,Roy +US-UT,29040,South Ogden +US-UT,29043,Uintah +US-UT,29049,Washington Terrace +US-UT,29051,West Haven +US-UT,29300,Falcon Hill Riverdale +US-UT,29301,Falcon Hill Roy diff --git a/tax.py b/tax.py index 3ceb6e8..8978106 100644 --- a/tax.py +++ b/tax.py @@ -1,6 +1,5 @@ from trytond.model import ( - DeactivableMixin, MatchMixin, ModelSQL, ModelView, fields, - sequence_ordered, tree) + MatchMixin, ModelSQL, ModelView, fields) from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval from trytond.transaction import Transaction @@ -44,6 +43,22 @@ def copy(cls, taxes, default=None): default.setdefault('authority', None) return super().copy(taxes, default=default) + @classmethod + def _amount_where(cls, tax_line, move_line, move): + where = super()._amount_where(tax_line, move_line, move) + + context = Transaction().context + code_id = context.get('code') + amount = context.get('amount') + + if code_id and amount == 'tax': + TaxCode = Pool().get('account.tax.code') + code = TaxCode(code_id) + return where & (tax_line.code == code.code) + else: + return where + + class TaxBoundary(ModelView, ModelSQL, MatchMixin): "Tax Boundary" __name__ = 'account.tax.boundary' @@ -51,18 +66,33 @@ class TaxBoundary(ModelView, ModelSQL, MatchMixin): ('A', 'Address'), ('Z', 'ZIP Code'), ('4', 'ZIP+4 Code'), - ], "Boundary Type") - start_date = fields.Date("Starting Date") + ], "Boundary Type", required=True) + start_date = fields.Date("Starting Date", required=True) end_date = fields.Date("End Date") - zipcode_low = fields.Char("ZIP Code Low", size=5) - zipcode_high = fields.Char("ZIP Code High", size=5) - zipext_low = fields.Char("ZIP+4 Code Low", size=4) - zipext_high = fields.Char("ZIP+4 Code High", size=4) + zipcode_low = fields.Char("ZIP Code Low", size=5, states={ + 'required': Eval('type').in_(['Z', '4']), + }) + zipcode_high = fields.Char("ZIP Code High", size=5, states={ + 'required': Eval('type').in_(['Z', '4']), + }) + zipext_low = fields.Char("ZIP+4 Code Low", size=4, states={ + 'required': Eval('type') == '4', + }) + zipext_high = fields.Char("ZIP+4 Code High", size=4, states={ + 'required': Eval('type') == '4', + }) authority = fields.Many2One('census.place', "Authority", - domain=[('parent', '=', None)], + domain=[('parent', '=', None)], required=True, help="The entity that administers this tax boundary") rule = fields.Many2One('account.tax.rule', "Tax Rule", - domain=[('authority', '=', Eval('authority', -1))], + domain=[ + ('authority', '=', Eval('authority', -1)) + ], + ondelete='RESTRICT', required=True) + code = fields.Many2One('account.tax.code', "Tax Code", + domain=[ + ('authority', '=', Eval('authority', -1)) + ], ondelete='RESTRICT') class TaxCode(metaclass=PoolMeta): @@ -72,6 +102,35 @@ class TaxCode(metaclass=PoolMeta): domain=[('parent', '=', None)], help="The entity that administers this tax code") + +class TaxCodeLine(metaclass=PoolMeta): + "Tax Code Line" + __name__ = 'account.tax.code.line' + + @classmethod + def __setup__(cls): + super().__setup__() + cls.tax.context['code'] = Eval('code') + cls.tax.depends.add('code') + cls.tax.context['amount'] = Eval('amount') + cls.tax.depends.add('amount') + cls.code.ondelete = 'CASCADE' + + @property + def _line_domain(self): + domain = super()._line_domain + domain.append(['OR', + [('code', '=', self.code.code)], + [('type', '=', 'base')], + ]) + return domain + + +class TaxLine(metaclass=PoolMeta): + "Tax Line" + __name__ = 'account.tax.line' + code = fields.Char("Reporting Code") + class TaxRule(metaclass=PoolMeta): __name__ = 'account.tax.rule' diff --git a/tax.xml b/tax.xml index 01682c3..944d1ab 100644 --- a/tax.xml +++ b/tax.xml @@ -36,5 +36,16 @@ tax_code_list + + account.tax.line + + tax_line_form + + + account.tax.line + + tax_line_tree + + diff --git a/view/tax_line_form.xml b/view/tax_line_form.xml new file mode 100644 index 0000000..9dc906d --- /dev/null +++ b/view/tax_line_form.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/view/tax_line_tree.xml b/view/tax_line_tree.xml new file mode 100644 index 0000000..088cdc9 --- /dev/null +++ b/view/tax_line_tree.xml @@ -0,0 +1,7 @@ + + + + + + +