[freeside-commits] branch master updated. 4d9e2d0980eb542aec39997b7bd92130b3a60676

Mitch Jackson mitch at freeside.biz
Tue Dec 18 14:57:59 PST 2018


The branch, master has been updated
       via  4d9e2d0980eb542aec39997b7bd92130b3a60676 (commit)
       via  4ee944740d805eb46be09f45a213f0c279c56d8c (commit)
       via  f743de125f5daf7a7243aa4f2c72e5a87a770456 (commit)
      from  88678677483b169d035d623cc1a07606dff6b046 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 4d9e2d0980eb542aec39997b7bd92130b3a60676
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Tue Dec 18 17:47:57 2018 -0500

    RT# 80488 Allow city select for tax_district_method=wa_sales

diff --git a/httemplate/elements/city.html b/httemplate/elements/city.html
index 05250fef5..3c5e91782 100644
--- a/httemplate/elements/city.html
+++ b/httemplate/elements/city.html
@@ -153,9 +153,7 @@ my %opt = @_;
 my $pre = $opt{'prefix'};
 
 my $conf = new FS::Conf;
-# Using tax_district_method implies that there's not a preloaded city/county
-# tax district table.
-my $disable_select = 1 if $conf->config('tax_district_method');
+my $disable_select = 0;
 
 $opt{'disable_empty'} = 1 unless exists($opt{'disable_empty'});
 

commit 4ee944740d805eb46be09f45a213f0c279c56d8c
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Tue Dec 18 17:45:18 2018 -0500

    RT# 80488 Find tax districts for WA addresses using official API

diff --git a/FS/FS/Misc/Geo.pm b/FS/FS/Misc/Geo.pm
index 6b3d6ca71..96ce0764b 100644
--- a/FS/FS/Misc/Geo.pm
+++ b/FS/FS/Misc/Geo.pm
@@ -14,6 +14,7 @@ use Data::Dumper;
 use FS::Conf;
 use FS::Log;
 use Locale::Country;
+use XML::LibXML;
 
 FS::UID->install_callback( sub {
   $conf = new FS::Conf;
@@ -141,102 +142,155 @@ sub get_district {
   &$method($location);
 }
 
-sub wa_sales {
-  my $location = shift;
-  my $error = '';
-  return '' if $location->{state} ne 'WA';
 
-  my $return = { %$location };
-  $return->{'exempt_amount'} = 0.00;
+=head2 wa_sales location_hash
 
-  my $url = 'http://webgis2.dor.wa.gov/TaxRateLookup_AGS/TaxReport.aspx';
-  my $ua = new LWP::UserAgent;
+Expects output of location_hash() as parameter
+
+Dies on error, or if tax rate cannot be found using given address
+
+Query the WA State Dept of Revenue API with an address, and return
+tax district information for that address.
+
+Documentation for the API can be found here:
+
+L<https://dor.wa.gov/find-taxes-rates/retail-sales-tax/destination-based-sales-tax-and-streamlined-sales-tax/wa-sales-tax-rate-lookup-url-interface>
+
+This API does not return consistent usable county names, as the county
+name may include appreviations or labels referring to PTBA (public transport
+benefit area) or CEZ (community empowerment zone).  It's recommended to use
+the tool wa_tax_rate_update to fully populate the city/county/districts for
+WA state every financial quarter.
+
+Returns a hashref with the following keys:
 
-  my $delim = '<|>'; # yes, <|>
-  my $year  = (localtime)[5] + 1900;
-  my $month = (localtime)[4] + 1;
-  my @zip = split('-', $location->{zip});
-
-  my @args = (
-    'TaxType=S',  #sales; 'P' = property
-    'Src=0',      #does something complicated
-    'TAXABLE=',
-    'Addr='.uri_escape($location->{address1}),
-    'City='.uri_escape($location->{city}),
-    'Zip='.$zip[0],
-    'Zip1='.($zip[1] || ''), #optional
-    'Year='.$year,
-    'SYear='.$year,
-    'Month='.$month,
-    'EMon='.$month,
+  - district        the wa state tax district id
+  - tax             the combined total tax rate, as a percentage
+  - city            the API rate name
+  - county          The API address PTBA
+  - state           WA
+  - country         US
+  - exempt_amount   0
+
+=cut
+
+sub wa_sales {
+  my $location_hash = shift;
+
+  # Return without die() when called with pointless context
+  return
+    unless $location_hash
+        && ref $location_hash
+        && $location_hash->{state} eq 'WA'
+        && $location_hash->{address1}
+        && $location_hash->{zip}
+        && $location_hash->{city};
+
+  my $log = FS::Log->new('wa_sales');
+
+  warn "wa_sales() called with location_hash:\n".Dumper( $location_hash)."\n"
+    if $DEBUG;
+
+  my $api_url = 'http://webgis.dor.wa.gov/webapi/AddressRates.aspx';
+  my @api_response_codes = (
+    'The address was found',
+    'The address was not found, but the ZIP+4 was located.',
+    'The address was updated and found, the user should validate the address record',
+    'The address was updated and Zip+4 located, the user should validate the address record',
+    'The address was corrected and found, the user should validate the address record',
+    'Neither the address or ZIP+4 was found, but the 5-digit ZIP was located.',
+    'The address, ZIP+4, and ZIP could not be found.',
+    'Invalid Latitude/Longitude',
+    'Internal error'
   );
-  
-  my $query_string = join($delim, @args );
-  $url .= "?$query_string";
-  warn "\nrequest:  $url\n\n" if $DEBUG > 1;
 
-  my $res = $ua->request( GET( "$url?$query_string" ) );
+  my %get_query = (
+    output => 'xml',
+    addr   => $location_hash->{address1},
+    city   => $location_hash->{city},
+    zip    => substr( $location_hash->{zip}, 0, 5 ),
+  );
+  my $get_string = join '&' => (
+    map{ sprintf "%s=%s", $_, uri_escape( $get_query{$_} ) }
+    keys %get_query
+  );
 
-  warn $res->as_string
-  if $DEBUG > 2;
+  my $prepared_url = "${api_url}?$get_string";
 
-  if ($res->code ne '200') {
-    $error = $res->message;
-  }
+  warn "API call to URL: $prepared_url\n"
+    if $DEBUG;
 
-  my $content = $res->content;
-  my $p = new HTML::TokeParser \$content;
-  my $js = '';
-  while ( my $t = $p->get_tag('script') ) {
-    my $u = $p->get_token; #either enclosed text or the </script> tag
-    if ( $u->[0] eq 'T' and $u->[1] =~ /tblSales/ ) {
-      $js = $u->[1];
-      last;
-    }
+  my $dom;
+  local $@;
+  eval { $dom = XML::LibXML->load_xml( location => $prepared_url ); };
+  if ( $@ ) {
+    my $error =
+      sprintf "Problem parsing XML from API URL(%s): %s",
+      $prepared_url, $@;
+    $log->error( $error );
+    die $error;
   }
-  if ( $js ) { #found it
-    # strip down to the quoted string, which contains escaped single quotes.
-    $js =~ s/.*\('tblSales'\);c.innerHTML='//s;
-    $js =~ s/(?<!\\)'.*//s; # (?<!\\) means "not preceded by a backslash"
-    warn "\n\n  innerHTML:\n$js\n\n" if $DEBUG > 2;
-
-    $p = new HTML::TokeParser \$js;
-    TD: while ( my $td = $p->get_tag('td') ) {
-      while ( my $u = $p->get_token ) {
-        next TD if $u->[0] eq 'E' and $u->[1] eq 'td';
-        next if $u->[0] ne 'T'; # skip non-text
-        my $text = $u->[1];
-
-        if ( lc($text) eq 'location code' ) {
-          $p->get_tag('td'); # skip to the next column
-          undef $u;
-          $u = $p->get_token until ($u->[0] || '') eq 'T'; # and then skip non-text
-          $return->{'district'} = $u->[1];
-        }
-        elsif ( lc($text) eq 'total tax rate' ) {
-          $p->get_tag('td');
-          undef $u;
-          $u = $p->get_token until ($u->[0] || '') eq 'T';
-          $return->{'tax'} = $u->[1];
-        }
-      } # get_token
-    } # TD
-
-    # just to make sure
-    if ( $return->{'district'} =~ /^\d+$/ and $return->{'tax'} =~ /^.\d+$/ ) {
-      $return->{'tax'} *= 100; #percentage
-      warn Dumper($return) if $DEBUG > 1;
-      return $return;
-    }
-    else {
-      $error = 'district code/tax rate not found';
-    }
+
+  my ($res_root)        = $dom->findnodes('/response');
+  my ($res_addressline) = $dom->findnodes('/response/addressline');
+  my ($res_rate)        = $dom->findnodes('/response/rate');
+
+  my $res_code = $res_root->getAttribute('code')
+    if $res_root;
+
+  unless (
+       ref $res_root
+    && ref $res_addressline
+    && ref $res_rate
+    && $res_code <= 5
+    && $res_root->getAttribute('rate') > 0
+  ) {
+    my $error =
+      sprintf
+        "Problem querying WA DOR tax district - " .
+        "code( %s %s ) " .
+        "address( %s ) " .
+        "url( %s )",
+          $res_code || 'n/a',
+          $res_code ? $api_response_codes[$res_code] : 'n/a',
+          $location_hash->{address1},
+          $prepared_url;
+      $log->error( $error );
+      die "$error\n";
   }
-  else {
-    $error = "failed to parse document";
+
+  my %response = (
+    exempt_amount => 0,
+    state         => 'WA',
+    country       => 'US',
+    district      => $res_root->getAttribute('loccode'),
+    tax           => $res_root->getAttribute('rate') * 100,
+    county        => uc $res_addressline->getAttribute('ptba'),
+    city          => uc $res_rate->getAttribute('name')
+  );
+
+  $response{county} =~ s/ PTBA//i;
+
+  if ( $DEBUG ) {
+    warn "XML document: $dom\n";
+    warn "API parsed response: ".Dumper( \%response )."\n";
   }
 
-  die "WA tax district lookup error: $error";
+  my $info_message =
+    sprintf
+      "Tax district(%s) selected for address(%s %s %s %s)",
+      $response{district},
+      $location_hash->{address1},
+      $location_hash->{city},
+      $location_hash->{state},
+      $location_hash->{zip};
+
+  $log->info( $info_message );
+  warn "$info_message\n"
+    if $DEBUG;
+
+  \%response;
+
 }
 
 ###### USPS Standardization ######

commit f743de125f5daf7a7243aa4f2c72e5a87a770456
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Tue Dec 18 03:20:20 2018 -0500

    RT# 80488 Utility to populate WA tax district table

diff --git a/bin/wa_tax_rate_update b/bin/wa_tax_rate_update
index d4a4b52e4..c50a77771 100644
--- a/bin/wa_tax_rate_update
+++ b/bin/wa_tax_rate_update
@@ -9,118 +9,355 @@ wa_tax_rate_update
 Tool to update city/district sales tax rates in I<cust_main_county> from 
 the Washington State Department of Revenue website.
 
-This does not handle address standardization or geocoding addresses to 
-Washington tax district codes.  That logic is still in FS::Misc::Geo,
-and relies on a heinous screen-scraping of the interactive search tool.
-This script just updates the cust_main_county records that already exist
-with the latest quarterly tax rates.
+Creates, or updates, a L<FS::cust_main_county> row for every tax district
+in Washington state.  Some cities have different tax rates based on the
+address, within the city.  Because of this, some cities may have multiple
+rows defined.
+
+When a Washington state address is inserted or changed in L<FS::cust_location>,
+a job is queued for FS::geocode_Mixin::process_district_update, to ask the
+Washington state API which tax district to use for this address.
 
 Options:
 
--c <taxclass>: operate only on records with the named tax class.  If not 
-specified, this operates on records with null tax class.
+-f <filename>: Skip downloading, and process the given excel file
+
+-c <taxclass>: Updated or create records within given tax class,
+   If not specified, taxclass will be set as NULL
+
+-t <taxname>: Updated or created records will be set to the given tax name.
+  If not specified, conf value 'tax_district_taxname' will be used
+
+-y <year>: Specify year for tax table - defaults to current year
+
+-q <quarter>: Specify quarter for tax table - defaults to current quarter
+
+=head1 Washington State Department of Revenue Resources
+
+The state of Washington makes data files available via their public website.
+It's possible the availability or format of these files may change.  As of now,
+the only data file that contains both city and county names is published in
+XLSX format.
+
+=item WA Dept of Revenue
+
+https://dor.wa.gov
+
+=item Data file downloads
+
+https://dor.wa.gov/find-taxes-rates/sales-and-use-tax-rates/downloadable-database
+
+=item XLSX file example
 
--t <taxname>: operate only on records with that tax name.  If not specified,
-it operates on records where the tax name is either null or 'Tax'.
+https://dor.wa.gov/sites/default/files/legacy/Docs/forms/ExcsTx/LocSalUseTx/ExcelLocalSlsUserates_19_Q1.xlsx
+
+=item CSV file example
+
+https://dor.wa.gov/sites/default/files/legacy/downloads/Add_DataRates2018Q4.zip
+
+
+=item Address lookup API tool
+
+http://webgis.dor.wa.gov/webapi/AddressRates.aspx?output=xml&addr=410 Terry Ave. North&city=&zip=98100
 
 =cut
 
-use FS::Record qw(qsearch qsearchs dbh);
-use FS::cust_main_county;
-use FS::UID qw(adminsuidsetup);
+use strict;
+use warnings;
+
+our $VERSION = '0.02'; # Make Getopt:Std happy
+
+use Data::Dumper;
 use DateTime;
-use LWP::UserAgent;
 use File::Temp 'tempdir';
 use File::Slurp qw(read_file write_file);
-use Text::CSV;
 use Getopt::Std;
+use LWP::UserAgent;
+use Spreadsheet::XLSX;
 
-getopts('c:t:');
-my $user = shift or die usage();
-
-# download the update file
-my $now = DateTime->now;
-my $yr = $now->year;
-my $qt = $now->quarter;
-my $file = "Rates${yr}Q${qt}.zip";
-my $url = 'http://dor.wa.gov/downloads/Add_Data/'.$file;
-my $dir = tempdir();
-chdir($dir);
-my $ua = LWP::UserAgent->new;
-warn "Downloading $url...\n";
-my $response = $ua->get($url);
-if ( ! $response->is_success ) {
-  die $response->status_line;
-}
-write_file($file, $response->decoded_content);
+use FS::cust_main_county;
+use FS::Log;
+use FS::Record qw(qsearch qsearchs dbh);
+use FS::UID qw(adminsuidsetup);
+
+my %opts;
+getopts( 'c:t:y:q:f:', \%opts );
+my $user = shift
+  or die HELP_MESSAGE();
+
+adminsuidsetup( $user )
+  or die "bad username '$user'\n";
+
+my $temp_dir = tempdir();
+my $log = FS::Log->new('wa_tax_rate_update');
+
+$opts{t} ||= FS::Conf->new->config('tax_district_taxname');
+
+log_info_and_warn( "Set taxclass=$opts{c}" ) if $opts{c};
+log_info_and_warn( "Set taxname=$opts{t}"  ) if $opts{t};
+
+my $xls_fn = $opts{f} || fetch_xlsx_file();
+my $tax_districts = parse_xlsx_file( $xls_fn );
+
+update_cust_main_county( $tax_districts );
+
+# print Dumper( \%opts );
+# for ( 0..5 ) {
+#   print Dumper( $tax_districts->[$_] );
+# }
+
+log_info_and_warn( "Finished" );
+exit;
+
+
+sub update_cust_main_county {
+  my $tax_districts = shift;
+
+  #
+  # Working from an assumption tax tables may be loaded multiple times,
+  # each with a different tax class,
+  #  re: comments on FS::geocode_Mixin::process_district_update
+  #
+
+  my %cust_main_county =
+    map { $_->district => $_ }
+    qsearch(
+      cust_main_county => {
+        district => { op => '!=', value => undef },
+        state    => 'WA',
+        source   => 'wa_sales',
+        taxclass => $opts{c},
+      }
+    );
+
+  # Work within a SQL transaction
+  local $FS::UID::AutoCommit = 0;
+
+  my $insert_count = 0;
+  my $update_count = 0;
+
+  for my $district ( @$tax_districts ) {
+    if ( my $row = $cust_main_county{ $district->{district} } ) {
+
+      $row->city( uc $district->{city} );
+      $row->county( uc $district->{county} );
+      $row->taxclass( $opts{c} || undef );
+      $row->taxname( $opts{t} || undef );
+      $row->tax( $district->{tax_combined} * 100 );
+
+      if ( my $error = $row->replace ) {
+        dbh->rollback;
+        local $FS::UID::AutoCommit = 1;
+        log_error_and_die(
+          sprintf
+            "Error updating cust_main_county row %s for district %s: %s",
+            $row->taxnum,
+            $district->{district},
+            $error
+        );
+      }
+
+      $update_count++;
+
+    } else {
+      my $row = FS::cust_main_county->new({
+        district => $district->{district},
+        city     => uc $district->{city},
+        county   => uc $district->{county},
+        state    => 'WA',
+        country  => 'US',
+        taxclass => $opts{c} || undef,
+        taxname  => $opts{t} || undef,
+        tax      => $district->{tax_combined} * 100,
+        source   => 'wa_sales',
+      });
+
+      if ( my $error = $row->insert ) {
+        dbh->rollback;
+        local $FS::UID::AutoCommit = 1;
+        log_error_and_die(
+          sprintf
+            "Error inserting cust_main_county row for district %s: %s",
+            $district->{district},
+            $error
+        );
+      }
+
+      $cust_main_county{ $district->{district} } = $row;
+      $insert_count++;
+    }
+  }
+
+  dbh->commit;
+
+  local $FS::UID::AutoCommit = 1;
+  log_info_and_warn(
+    sprintf
+      "WA tax table update completed. Inserted %s rows, updated %s rows",
+      $insert_count,
+      $update_count
+  );
 
-# parse it
-system('unzip', $file);
-$file =~ s/\.zip$/.csv/;
-if (! -f $file) {
-  die "$file not found in zip archive.\n";
 }
-open my $fh, '<', $file
-  or die "couldn't open $file: $!\n";
-my $csv = Text::CSV->new;
-my $header = $csv->getline($fh);
-$csv->column_names(@$header);
-# columns we care about are headed 'Code' and 'Rate'
-
-# connect to the DB
-adminsuidsetup($user) or die "bad username '$user'\n";
-$FS::UID::AutoCommit = 0;
-
-$opt_c ||= ''; # taxclass
-$opt_t ||= ''; # taxname
-my $total_changed = 0;
-my $total_skipped = 0;
-while ( !$csv->eof ) {
-  my $line = $csv->getline_hr($fh);
-  my $district = $line->{Code} or next;
-  $district = sprintf('%04d', $district);
-  my $tax = sprintf('%.1f', $line->{Rate} * 100);
-  my $changed = 0;
-  my $skipped = 0;
-  # find all rates in WA
-  my @rates = qsearch('cust_main_county', {
-      country   => 'US',
-      state     => 'WA', # this is specific to WA
-      district  => $district,
-      taxclass  => $opt_c,
-      taxname   => $opt_t,
-      tax       => { op => '>', value => '0' },
-  });
-  if ($opt_t eq '') {
-    push @rates, qsearch('cust_main_county', {
-      country   => 'US',
-      state     => 'WA', # this is specific to WA
-      district  => $district,
-      taxclass  => $opt_c,
-      taxname   => 'Tax',
-      tax       => { op => '>', value => '0' },
-    });
+
+sub parse_xlsx_file {
+  my $parse_fn = shift;
+
+  # About the file format:
+  #
+  # The current spreadsheet contains the following @columns.
+  # Rows 1 and 2 are a marquee header
+  # Row 3 is the column labels.  We will test these to detect
+  #   changes in the data format
+  # Rows 4+ are the tax district data
+  #
+  # The "city" column is being parsed from "Location"
+
+  my @columns = qw( city county district tax_local tax_state tax_combined );
+
+  log_error_and_die( "Unable to access XLSX file: $parse_fn" )
+    unless -r $parse_fn;
+
+  my $xls_parser = Spreadsheet::XLSX->new( $parse_fn )
+    or log_error_and_die( "Error parsing XLSX file: $!" );
+
+  my $sheet = $xls_parser->{Worksheet}->[0]
+    or log_error_and_die(" Unable to access worksheet 1 in XLSX file" );
+
+  my $cells = $sheet->{Cells}
+    or log_error_and_die( "Unable to read cells in XLSX file" );
+
+  # Read the column labels and verify
+  my %labels =
+    map{ $columns[$_] => $cells->[2][$_]->{Val} }
+    0 .. scalar(@columns)-1;
+
+  my %expected_labels = (
+    city         => 'Location',
+    county       => 'County',
+    district     => 'Location Code',
+    tax_local    => 'Local Rate',
+    tax_state    => 'State Rate',
+    tax_combined => 'Combined Sales Tax',
+  );
+
+  if (
+    my @error_labels =
+      grep { lc $labels{$_} ne lc $expected_labels{$_} }
+      @columns
+  ) {
+    my $error = "Error parsing XLS file - ".
+                "Data format may have been updated with WA DOR! ";
+    $error .= "Expected column $expected_labels{$_}, found $labels{$_}! "
+      for @error_labels;
+    log_error_and_die( $error );
   }
-  foreach my $rate (@rates) {
-    if ( $rate->tax == $tax ) {
-      $skipped++;
+
+  # Parse the rows into an array of hashes
+  my @districts;
+  for my $row ( 3..$sheet->{MaxRow} ) {
+    my %district = (
+      map { $columns[$_] => $cells->[$row][$_]->{Val} }
+      0 .. scalar(@columns)-1
+    );
+
+    if (
+         $district{city}
+      && $district{county}
+      && $district{district}     =~ /^\d+$/
+      && $district{tax_local}    =~ /^\d?\.\d+$/
+      && $district{tax_state}    =~ /^\d?\.\d+$/
+      && $district{tax_combined} =~ /^\d?\.\d+$/
+    ) {
+
+      # For some reason, city may contain line breaks!
+      $district{city} =~ s/[\r\n]//g;
+
+      push @districts, \%district;
     } else {
-      $rate->set('tax', $tax);
-      my $error = $rate->replace;
-      die "error updating district $district: $error\n" if $error;
-      $changed++;
+      log_warn_and_warn(
+        "Non-usable row found in spreadsheet:\n" . Dumper( \%district )
+      );
     }
+
   }
-  print "$district: updated $changed, skipped $skipped\n"
-    if $changed or $skipped;
-  $total_changed += $changed;
-  $total_skipped += $skipped;
+
+  log_error_and_die( "No \@districts found in data file!" )
+    unless @districts;
+
+  log_info_and_warn(
+    sprintf "Parsed %s districts from data file", scalar @districts
+  );
+
+  \@districts;
+}
+
+sub fetch_xlsx_file {
+  # Download file to temporary storage, return filename
+
+  my $url_base = 'https://dor.wa.gov'.
+                 '/sites/default/files/legacy/Docs/forms/ExcsTx/LocSalUseTx';
+
+  my $year = $opts{y}    || DateTime->now->year;
+  my $quarter = $opts{q} || DateTime->now->quarter;
+  $year = substr( $year, 2, 2 ) if $year >= 1000;
+  my $fn = sprintf( 'ExcelLocalSlsUserates_%s_Q%s.xlsx', $year, $quarter );
+  my $url = "$url_base/$fn";
+
+  my $write_fn = "$temp_dir/$fn";
+
+  log_info_and_warn( "Begin download from url: $url" );
+
+  my $ua = LWP::UserAgent->new;
+  my $res = $ua->get( $url );
+
+  log_error_and_die( "Download error: ".$res->status_line )
+    unless $res->is_success;
+
+  local $@;
+  eval { write_file( $write_fn, $res->decoded_content ); };
+  log_error_and_die( "Problem writing download to disk: $@" )
+    if $@;
+
+  log_info_and_warn( "Temporary file: $write_fn" );
+  $write_fn;
+}
+
+
+sub HELP_MESSAGE {
+  print "
+    Tool to update city/district sales tax rates in I<cust_main_county> from
+    the Washington State Department of Revenue website.
+
+    Usage: [-f filename] [-c taxclass] [-t taxname] [-y year] [-q quarter]
+
+    Optional Options:
+      -f filename   Skip download, and process the specified filename
+      -c taxclass   Update or create records within this taxclass
+                    defaults as NULL
+      -t taxname    Apply tax name value to created or updated records
+                    defaults as conf value 'tax_district_taxname'
+      -y year       Year for data file download
+      -q quarter    Quarter of data file to download
+
+  ";
+  exit;
+}
+
+sub log_info_and_warn {
+  my $log_message = shift;
+  warn "$log_message\n";
+  $log->info( $log_message );
+}
+
+sub log_warn_and_warn {
+  my $log_message = shift;
+  warn "$log_message\n";
+  $log->warn( $log_message );
 }
-print "Updated $total_changed tax rates.\nSkipped $total_skipped unchanged rates.\n";
-dbh->commit;
 
-sub usage {
-  "usage:
-  wa_tax_rate_update [ -c taxclass ] [ -t taxname ] user
-";
+sub log_error_and_die {
+  my $log_message = shift;
+  $log->error( $log_message );
+  die( "$log_message\n" );
 }

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Misc/Geo.pm             | 222 +++++++++++++---------
 bin/wa_tax_rate_update        | 425 ++++++++++++++++++++++++++++++++----------
 httemplate/elements/city.html |   4 +-
 3 files changed, 470 insertions(+), 181 deletions(-)




More information about the freeside-commits mailing list