specify Avalara tax product for per-line taxes, #73063
authorMark Wells <mark@freeside.biz>
Wed, 7 Dec 2016 23:27:49 +0000 (15:27 -0800)
committerMark Wells <mark@freeside.biz>
Wed, 7 Dec 2016 23:27:58 +0000 (15:27 -0800)
FS/FS/Schema.pm
FS/FS/TaxEngine/billsoft.pm
FS/FS/part_pkg.pm
FS/FS/part_pkg_taxproduct.pm
httemplate/browse/part_pkg.cgi
httemplate/edit/part_pkg.cgi
httemplate/elements/tr-part_pkg-taxproducts.html

index f8b82f4..0e41b1a 100644 (file)
@@ -3248,6 +3248,7 @@ sub tables_hashref {
         'adjourn_months', 'int',    'NULL', '', '', '',
         'contract_end_months','int','NULL', '', '', '',
         'change_to_pkgpart', 'int', 'NULL', '', '', '',
         'adjourn_months', 'int',    'NULL', '', '', '',
         'contract_end_months','int','NULL', '', '', '',
         'change_to_pkgpart', 'int', 'NULL', '', '', '',
+        'units_taxproductnum','int','NULL', '', '', '',
       ],
       'primary_key'  => 'pkgpart',
       'unique'       => [],
       ],
       'primary_key'  => 'pkgpart',
       'unique'       => [],
@@ -3265,6 +3266,10 @@ sub tables_hashref {
                           { columns    => [ 'taxproductnum' ],
                             table      => 'part_pkg_taxproduct',
                           },
                           { columns    => [ 'taxproductnum' ],
                             table      => 'part_pkg_taxproduct',
                           },
+                          { columns    => [ 'units_taxproductnum' ],
+                            table      => 'part_pkg_taxproduct',
+                            references => [ 'taxproductnum' ],
+                          },
                           { columns    => [ 'agentnum' ],
                             table      => 'agent',
                           },
                           { columns    => [ 'agentnum' ],
                             table      => 'agent',
                           },
index 69717a2..9147f5c 100644 (file)
@@ -188,8 +188,7 @@ sub create_batch {
     # cache some things
     my (%cust_pkg, %part_pkg, %cust_location, %classname);
     # keys are transaction codes (the first part of the taxproduct string)
     # cache some things
     my (%cust_pkg, %part_pkg, %cust_location, %classname);
     # keys are transaction codes (the first part of the taxproduct string)
-    # and then locationnums; for per-location taxes
-    my %sales;
+    my %all_tcodes;
 
     my @options = $self->conf->config('billsoft-taxconfig');
     
 
     my @options = $self->conf->config('billsoft-taxconfig');
     
@@ -239,8 +238,7 @@ sub create_batch {
 
         my $taxproduct = $self->part_pkg_taxproduct($part_pkg, $classnum)
           or next;
 
         my $taxproduct = $self->part_pkg_taxproduct($part_pkg, $classnum)
           or next;
-        my $tcode = substr($taxproduct, 0, 6);
-        my $scode = substr($taxproduct, 6, 6);
+        my ($tcode, $scode) = split(':', $taxproduct);
 
         # For CDRs, use the call termination site rather than setting
         # Termination fields to the service address.
 
         # For CDRs, use the call termination site rather than setting
         # Termination fields to the service address.
@@ -259,9 +257,6 @@ sub create_batch {
 
       } # while $cdr = $cdr_search->fetch
       
 
       } # while $cdr = $cdr_search->fetch
       
-      my $recur_tcode;
-      # now write lines for the non-CDR portion of the charges
-
       my $locationnum = $cust_pkg->locationnum;
 
       # use termination address for the service location
       my $locationnum = $cust_pkg->locationnum;
 
       # use termination address for the service location
@@ -284,13 +279,10 @@ sub create_batch {
         my $taxproduct = $self->part_pkg_taxproduct($part_pkg, $_);
         next unless $taxproduct;
 
         my $taxproduct = $self->part_pkg_taxproduct($part_pkg, $_);
         next unless $taxproduct;
 
-        my $tcode = substr($taxproduct, 0, 6);
-        my $scode = substr($taxproduct, 6, 6);
-        $sales{$tcode} ||= 0;
-        $recur_tcode = $tcode if $_ eq 'recur';
+        my ($tcode, $scode) = split(':', $taxproduct);
+        $all_tcodes{$tcode} ||= 1;
 
         my $price = $cust_bill_pkg->get($_);
 
         my $price = $cust_bill_pkg->get($_);
-        $sales{$tcode} += $price;
 
         $price -= $usage_total if $_ eq 'recur';
 
 
         $price -= $usage_total if $_ eq 'recur';
 
@@ -305,10 +297,9 @@ sub create_batch {
 
       } # foreach (setup, recur)
 
 
       } # foreach (setup, recur)
 
-      # S-code 21: taxes based on number of lines (E911, mostly)
-      # voip_cdr and voip_inbound packages know how to report this.  Not all 
-      # T-codes are eligible for this; only report it if the /21 taxproduct
-      # exists.
+      # taxes based on number of lines (E911, mostly)
+      # mostly S-code 21 but can be others, as they want to know about
+      # Centrex trunks, PBX extensions, etc.
       #
       # (note: the nomenclature of "service" and "transaction" codes is 
       # backward from the way most people would use the terms.  you'd think
       #
       # (note: the nomenclature of "service" and "transaction" codes is 
       # backward from the way most people would use the terms.  you'd think
@@ -318,25 +309,18 @@ sub create_batch {
       # to avoid confusion.)
 
       # XXX cache me
       # to avoid confusion.)
 
       # XXX cache me
-      # XXX this isn't precisely correct. Local exchange service on
-      # high-capacity trunks, Centrex, and PBX trunks are supposed to be
-      # reported as three separate implicit transactions: number of trunks,
-      # of outbound channels, of extensions.
-      # This is also true for VoIP PBX trunks. Come back to this.
-      if ( $recur_tcode ) {
-        my $lines_taxproduct = FS::part_pkg_taxproduct->count(
-          'data_vendor = \'billsoft\' and taxproduct = ?',
-          sprintf('%06d%06d', $recur_tcode, 21)
-        );
+      if ( my $lines_taxproduct = $part_pkg->units_taxproduct ) {
         my $lines = $cust_bill_pkg->units;
         my $lines = $cust_bill_pkg->units;
-
-        if ( $lines_taxproduct and $lines ) {
+        my $taxproduct = $lines_taxproduct->taxproduct;
+        my ($tcode, $scode) = split(':', $taxproduct);
+        $all_tcodes{$tcode} ||= 1;
+        if ( $lines ) {
           $csv->print_hr($fh, {
             %pkg_properties,
             %termination,
             RequestType       => 'CalcTaxes',
           $csv->print_hr($fh, {
             %pkg_properties,
             %termination,
             RequestType       => 'CalcTaxes',
-            TransactionType   => $recur_tcode,
-            ServiceType       => 21,
+            TransactionType   => $tcode,
+            ServiceType       => $scode,
             Charge            => 0,
             Lines             => $lines,
           } );
             Charge            => 0,
             Lines             => $lines,
           } );
@@ -345,12 +329,16 @@ sub create_batch {
 
     } # foreach my $cust_bill_pkg
 
 
     } # foreach my $cust_bill_pkg
 
-    foreach my $tcode (keys %sales) {
+    foreach my $tcode (keys %all_tcodes) {
 
 
-      # S-code 43: per-invoice tax (apparently this is a thing)
+      # S-code 43: per-invoice tax
+      # XXX not exactly correct; there's "Invoice Bundle" (7:94) and
+      # "Centrex Invoice" (7:623). Local Exchange service would benefit from
+      # more high-level selection of the tax properties. (Infer from the FCC
+      # reporting options?)
       my $invoice_taxproduct = FS::part_pkg_taxproduct->count(
         'data_vendor = \'billsoft\' and taxproduct = ?',
       my $invoice_taxproduct = FS::part_pkg_taxproduct->count(
         'data_vendor = \'billsoft\' and taxproduct = ?',
-        sprintf('%06d%06d', $tcode, 43)
+        $tcode . ':43'
       );
       if ( $invoice_taxproduct ) {
         $csv->print_hr($fh, {
       );
       if ( $invoice_taxproduct ) {
         $csv->print_hr($fh, {
index 35f178e..ae63487 100644 (file)
@@ -735,6 +735,7 @@ sub check {
     || $self->ut_floatn('pay_weight')
     || $self->ut_floatn('credit_weight')
     || $self->ut_numbern('taxproductnum')
     || $self->ut_floatn('pay_weight')
     || $self->ut_floatn('credit_weight')
     || $self->ut_numbern('taxproductnum')
+    || $self->ut_numbern('units_taxproductnum')
     || $self->ut_foreign_keyn('classnum',       'pkg_class', 'classnum')
     || $self->ut_foreign_keyn('addon_classnum', 'pkg_class', 'classnum')
     || $self->ut_foreign_keyn('taxproductnum',
     || $self->ut_foreign_keyn('classnum',       'pkg_class', 'classnum')
     || $self->ut_foreign_keyn('addon_classnum', 'pkg_class', 'classnum')
     || $self->ut_foreign_keyn('taxproductnum',
@@ -1731,6 +1732,19 @@ sub taxproduct_description {
   $part_pkg_taxproduct ? $part_pkg_taxproduct->description : '';
 }
 
   $part_pkg_taxproduct ? $part_pkg_taxproduct->description : '';
 }
 
+=item units_taxproduct
+
+Returns the L<FS::part_pkg_taxproduct> record used to report the taxable
+service units (usually phone lines) on this package.
+
+=cut
+
+sub units_taxproduct {
+  my $self = shift;
+  $self->units_taxproductnum
+    ? FS::part_pkg_taxproduct->by_key($self->units_taxproductnum)
+    : '';
+}
 
 =item tax_rates DATA_PROVIDER, GEOCODE, [ CLASS ]
 
 
 =item tax_rates DATA_PROVIDER, GEOCODE, [ CLASS ]
 
index e86d028..51bc37f 100644 (file)
@@ -223,7 +223,8 @@ sub batch_import {
   
   my $imported = 0;
   my $csv = Text::CSV_XS->new;
   
   my $imported = 0;
   my $csv = Text::CSV_XS->new;
-  # fields: taxproduct, description
+  my $error;
+  # for importing the "transervdesc.txt" file
   while ( my $row = $csv->getline($fh) ) {
     if (!defined $row) {
       $dbh->rollback if $oldAutoCommit;
   while ( my $row = $csv->getline($fh) ) {
     if (!defined $row) {
       $dbh->rollback if $oldAutoCommit;
@@ -236,15 +237,32 @@ sub batch_import {
       );
     }
 
       );
     }
 
-    my $new = FS::part_pkg_taxproduct->new({
-        'data_vendor' => 'billsoft',
-        'taxproduct'  => $row->[0],
-        'description' => $row->[1],
+    # columns 0-2: irrelevant here
+    my $taxproduct = $row->[3] . ':' . $row->[5];
+    my $description = $row->[4];
+    $description =~ s/\s+$//;
+    $description .= ':' . $row->[6];
+    $description =~ s/\s+$//;
+    my $ppt = qsearchs('part_pkg_taxproduct', {
+      'data_vendor' => 'billsoft',
+      'taxproduct'  => $taxproduct
     });
     });
-    my $error = $new->insert;
+    if ( $ppt ) {
+      $ppt->set('description', $description);
+      $ppt->set('note', $row->[7]);
+      $error = $ppt->replace;
+    } else {
+      $ppt = FS::part_pkg_taxproduct->new({
+          'data_vendor' => 'billsoft',
+          'taxproduct'  => $taxproduct,
+          'description' => $description,
+          'note'        => $row->[7],
+      });
+      $error = $ppt->insert;
+    }
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
-      return "error inserting part_pkg_taxproduct: $error\n";
+      return "error inserting part_pkg_taxproduct $taxproduct: $error\n";
     }
     $imported++;
   }
     }
     $imported++;
   }
index acc3211..8c51b35 100755 (executable)
@@ -601,12 +601,18 @@ if ( $taxclasses ) {
         { 'data'  => &$taxproduct_sub($base_ppt), 'align' => 'right' },
       ];
     }
         { 'data'  => &$taxproduct_sub($base_ppt), 'align' => 'right' },
       ];
     }
+    if ( my $units_ppt = $part_pkg->units_taxproduct ) {
+      push @$out, [
+        { 'data'  => emt('Lines'), 'align' => 'left' },
+        { 'data'  => &$taxproduct_sub($units_ppt), 'align' => 'right' },
+      ];
+    }
     for (my $i = 0; $i < scalar @classnums; $i++) {
       my $num = $part_pkg->option('usage_taxproductnum_' . $classnums[$i]);
       next if !$num;
       my $ppt = FS::part_pkg_taxproduct->by_key($num);
       push @$out, [
     for (my $i = 0; $i < scalar @classnums; $i++) {
       my $num = $part_pkg->option('usage_taxproductnum_' . $classnums[$i]);
       next if !$num;
       my $ppt = FS::part_pkg_taxproduct->by_key($num);
       push @$out, [
-        { 'data'  => $classnames[$i] . ': ', 'align' => 'left', },
+        { 'data'  => $classnames[$i], 'align' => 'left', },
         { 'data'  => &$taxproduct_sub($ppt), 'align' => 'right' },
       ];
     }
         { 'data'  => &$taxproduct_sub($ppt), 'align' => 'right' },
       ];
     }
index 64a7525..84aac5b 100755 (executable)
@@ -40,7 +40,6 @@
                    'setuptax'         => 'Setup fee tax exempt',
                    'recurtax'         => 'Recurring fee tax exempt',
                    'taxclass'         => 'Tax class',
                    'setuptax'         => 'Setup fee tax exempt',
                    'recurtax'         => 'Recurring fee tax exempt',
                    'taxclass'         => 'Tax class',
-                   'taxproduct_select'=> 'Tax products',
                    'plan'             => 'Price plan',
                    'disabled'         => 'Disable new orders',
                    'disable_line_item_date_ranges' => 'Disable line item date ranges',
                    'plan'             => 'Price plan',
                    'disabled'         => 'Disable new orders',
                    'disable_line_item_date_ranges' => 'Disable line item date ranges',
@@ -73,6 +72,7 @@
                    'contract_end_months' => 'Contract ends after ',
                    'expire_months'    => 'Cancel the package after ',
                    'change_to_pkgpart'=> 'and replace it with ',
                    'contract_end_months' => 'Contract ends after ',
                    'expire_months'    => 'Cancel the package after ',
                    'change_to_pkgpart'=> 'and replace it with ',
+                   'units_taxproductnum' => 'Per-line tax product',
                  },
 
      'fields' => [
                  },
 
      'fields' => [
                        type  => 'hidden',
                        value => join(',', @taxproductnums),
                      },
                        type  => 'hidden',
                        value => join(',', @taxproductnums),
                      },
-                     #{ field => 'taxproduct_select',
-                     #  type  => 'selectlayers',
-                     #  options => [ '(default)', @taxproductnums ],
-                     #  curr_value => '(default)',
-                     #  labels  => { ( '(default)' => '(default)' ),
-                     #               map {($_=>$usage_class{$_})}
-                     #               @taxproductnums
-                     #             },
-                     #  layer_fields => \%taxproduct_fields,
-                     #  layer_values_callback => $taxproduct_values,
-                     #  layers_only  =>   !$taxproducts,
-                     #  cell_style   => ( !$taxproducts
-                     #                    ? 'display:none'
-                     #                    : ''
-                     #                  ),
-                     #},
                      { field => 'taxproductnum',
                        type  => 'part_pkg-taxproducts',
                        include_opt_callback =>
                          sub { pkgpart => $_[0]->pkgpart },
                      },
                      { field => 'taxproductnum',
                        type  => 'part_pkg-taxproducts',
                        include_opt_callback =>
                          sub { pkgpart => $_[0]->pkgpart },
                      },
-
+                     { field => 'units_taxproductnum',
+                       type  => ($tax_data_vendor ?
+                                  'select-taxproduct' : 'hidden'),
+                     },
                      { type  => 'tablebreak-tr-title',
                        value => 'Promotions', #better name?
                      },
                      { type  => 'tablebreak-tr-title',
                        value => 'Promotions', #better name?
                      },
@@ -445,7 +432,7 @@ my $agent_clone_extra_sql =
   ' ) ';
 
 my $conf = new FS::Conf;
   ' ) ';
 
 my $conf = new FS::Conf;
-my $taxproducts = $conf->config('tax_data_vendor') ne '';
+my $tax_data_vendor = $conf->config('tax_data_vendor');
 
 my $fcc_opts = $conf->exists('part_pkg-show_fcc_options');
 
 
 my $fcc_opts = $conf->exists('part_pkg-show_fcc_options');
 
@@ -1112,13 +1099,8 @@ my $html_bottom = sub {
   my $return =
     include('/elements/selectlayers.html', %selectlayers, 'layers_only'=>1 ).
     '<SCRIPT TYPE="text/javascript">'.
   my $return =
     include('/elements/selectlayers.html', %selectlayers, 'layers_only'=>1 ).
     '<SCRIPT TYPE="text/javascript">'.
-      include('/elements/selectlayers.html', %selectlayers, 'js_only'=>1 );
-
-#  $return .=
-#    "taxproduct_selectchanged(document.getElementById('taxproduct_select'));\n"
-#      if $taxproducts;
-
-  $return .= '</SCRIPT>';
+      include('/elements/selectlayers.html', %selectlayers, 'js_only'=>1 ) .
+    '</SCRIPT>';
 
   $return;
 
 
   $return;
 
@@ -1199,16 +1181,8 @@ my $field_callback = sub {
   my $field = $fieldref->{field};
   if ($field eq 'taxproductnums') {
     $fieldref->{value} = join(',', @taxproductnums);
   my $field = $fieldref->{field};
   if ($field eq 'taxproductnums') {
     $fieldref->{value} = join(',', @taxproductnums);
-  } elsif ($field eq 'taxproduct_select') {
-    $fieldref->{options} = [ '(default)', @taxproductnums ];
-    $fieldref->{labels}  = { ( '(default)' => '(default)' ),
-                             map {( $_ => ($usage_class{$_} || $_) )}
-                               @taxproductnums
-                           };
-    $fieldref->{layer_fields} = \%taxproduct_fields;
-    $fieldref->{layer_values_callback} = $taxproduct_values;
   } elsif ($field eq 'taxproductnum') { # part_pkg-taxproduct, new style
   } elsif ($field eq 'taxproductnum') { # part_pkg-taxproduct, new style
-    if ( !$taxproducts ) {
+    if ( !$tax_data_vendor ) {
       # then make the widget go away
       $fieldref->{type} = 'hidden';
     }
       # then make the widget go away
       $fieldref->{type} = 'hidden';
     }
index 5dcea09..50dace7 100644 (file)
@@ -54,7 +54,8 @@ my %pkg_options;
 if ($pkgpart) {
   my $part_pkg = FS::part_pkg->by_key($pkgpart);
   %pkg_options = $part_pkg->options;
 if ($pkgpart) {
   my $part_pkg = FS::part_pkg->by_key($pkgpart);
   %pkg_options = $part_pkg->options;
-  $curr_values{''} = $part_pkg->taxproductnum;
+  $curr_values{''} =   $cgi->param('taxproductnum')
+                    || $part_pkg->taxproductnum;
 }
 
 foreach my $usage_class (@classes) {
 }
 
 foreach my $usage_class (@classes) {
@@ -66,4 +67,5 @@ foreach my $usage_class (@classes) {
   $curr_values{$classnum} = $curr_value;
   $separate = 1 if ( length($classnum) and length($curr_value) );
 }
   $curr_values{$classnum} = $curr_value;
   $separate = 1 if ( length($classnum) and length($curr_value) );
 }
+
 </%init>
 </%init>