allow fees to be grouped with taxes on the invoice, #32223
authorMark Wells <mark@freeside.biz>
Thu, 5 Feb 2015 05:12:56 +0000 (21:12 -0800)
committerMark Wells <mark@freeside.biz>
Thu, 5 Feb 2015 05:12:56 +0000 (21:12 -0800)
FS/FS/Template_Mixin.pm
FS/FS/cust_bill_pkg_tax_location.pm
FS/FS/part_fee.pm
FS/FS/pkg_category.pm
conf/invoice_html
conf/invoice_latex

index d46f617..61e708e 100644 (file)
@@ -7,7 +7,7 @@ use vars qw( $DEBUG $me
            );
              # but NOT $conf
 use vars qw( $invoice_lines @buf ); #yuck
-use List::Util qw(sum);
+use List::Util qw(sum first);
 use Date::Format;
 use Date::Language;
 use Text::Template 1.20;
@@ -908,29 +908,6 @@ sub print_generic {
   warn "$me generating sections\n"
     if $DEBUG > 1;
 
-  my $taxtotal = 0;
-  my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
-                      'subtotal'    => $taxtotal,   # adjusted below
-                      'tax_section' => 1,
-                    };
-  my $tax_weight = _pkg_category($tax_section->{description})
-                        ? _pkg_category($tax_section->{description})->weight
-                        : 0;
-  $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
-  $tax_section->{'sort_weight'} = $tax_weight;
-
-  my $adjusttotal = 0;
-  my $adjust_section = {
-    'description'    => $self->mt('Credits, Payments, and Adjustments'),
-    'adjust_section' => 1,
-    'subtotal'       => 0,   # adjusted below
-  };
-  my $adjust_weight = _pkg_category($adjust_section->{description})
-                        ? _pkg_category($adjust_section->{description})->weight
-                        : 0;
-  $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
-  $adjust_section->{'sort_weight'} = $adjust_weight;
-
   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
   my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
                      $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
@@ -971,6 +948,21 @@ sub print_generic {
     $previous_section = $default_section;
   }
 
+  my $adjust_section = {
+    'description'    => $self->mt('Credits, Payments, and Adjustments'),
+    'adjust_section' => 1,
+    'subtotal'       => 0,   # adjusted below
+  };
+  my $adjust_weight = _pkg_category($adjust_section->{description})
+                        ? _pkg_category($adjust_section->{description})->weight
+                        : 0;
+  $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
+  # Note: 'sort_weight' here is actually a flag telling whether there is an
+  # explicit package category for the adjust section. If so, certain behavior
+  # happens.
+  $adjust_section->{'sort_weight'} = $adjust_weight;
+
+
   if ( $multisection ) {
     ($extra_sections, $extra_lines) =
       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
@@ -1220,6 +1212,26 @@ sub print_generic {
   warn "$me adding taxes\n"
     if $DEBUG > 1;
 
+  # create a tax section if we don't yet have one
+  my $tax_description = 'Taxes, Surcharges, and Fees';
+  my $tax_section = first { $_->{description} eq $tax_description } @sections;
+  if (!$tax_section) {
+    $tax_section = { 'description' => $tax_description };
+    push @sections, $tax_section if $multisection;
+  }
+  $tax_section->{tax_section} = 1; # mark this section as containing taxes
+  # if this is an existing tax section, we're merging the tax items into it.
+  # grab the taxtotal that's already there, strip the money symbol if any
+  my $taxtotal = $tax_section->{'subtotal'} || 0;
+  $taxtotal =~ s/^\Q$other_money_char\E//;
+
+  # this does nothing
+  #my $tax_weight = _pkg_category($tax_section->{description})
+  #                      ? _pkg_category($tax_section->{description})->weight
+  #                      : 0;
+  #$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
+  #$tax_section->{'sort_weight'} = $tax_weight;
+
   my @items_tax = $self->_items_tax;
   foreach my $tax ( @items_tax ) {
 
@@ -1262,14 +1274,20 @@ sub print_generic {
       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
 
     if ( $multisection ) {
-      $tax_section->{'subtotal'} = $other_money_char.
-                                   sprintf('%.2f', $taxtotal);
-      $tax_section->{'pretotal'} = 'New charges sub-total '.
-                                   $total->{'total_amount'};
-      if ( $taxtotal ) {
-        push @sections, $tax_section;
-        push @summary_subtotals, $tax_section;
+      if ( $taxtotal > 0 ) {
+        $tax_section->{'subtotal'} = $other_money_char.
+                                     sprintf('%.2f', $taxtotal);
+        $tax_section->{'pretotal'} = 'New charges sub-total '.
+                                     $total->{'total_amount'};
+        $tax_section->{'description'} = $self->mt($tax_description);
+
+        # append it if it's not already there
+        if ( !grep $tax_section, @sections ) {
+          push @sections, $tax_section;
+          push @summary_subtotals, $tax_section;
+        }
       }
+
     } else {
       unshift @total_items, $total;
     }
@@ -1285,7 +1303,6 @@ sub print_generic {
              $money_char. sprintf("%10.2f",$self->charged) ];
   push @buf,['',''];
 
-
   ###
   # Totals
   ###
@@ -1361,7 +1378,6 @@ sub print_generic {
         $total->{'total_item'} = &$escape_function($credit->{'description'});
         $credittotal += $credit->{'amount'};
         $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
-        $adjusttotal += $credit->{'amount'};
         if ( $multisection ) {
           push @detail_items, {
             ext_description => [],
@@ -1395,7 +1411,6 @@ sub print_generic {
         $total->{'total_item'} = &$escape_function($payment->{'description'});
         $paymenttotal += $payment->{'amount'};
         $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
-        $adjusttotal += $payment->{'amount'};
         if ( $multisection ) {
           push @detail_items, {
             ext_description => [],
@@ -1417,7 +1432,10 @@ sub print_generic {
     
       if ( $multisection ) {
         $adjust_section->{'subtotal'} = $other_money_char.
-                                        sprintf('%.2f', $adjusttotal);
+                                        sprintf('%.2f', $credittotal + $paymenttotal);
+
+        #why this? because {sort_weight} forces the adjust_section to appear
+        #in @extra_sections instead of @sections. obviously.
         push @sections, $adjust_section
           unless $adjust_section->{sort_weight};
         # do not summarize; adjustments there are shown according to 
@@ -2794,11 +2812,16 @@ equivalent to
 
 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
 
-The only OPTIONS accepted is 'section', which may point to a hashref 
-with a key named 'condensed', which may have a true value.  If it 
-does, this method tries to merge identical items into items with 
-'quantity' equal to the number of items (not the sum of their 
-separate quantities, for some reason).
+OPTIONS are passed through to _items_cust_bill_pkg, and should include
+'format' and 'escape_function' at minimum.
+
+To produce items for a specific invoice section, OPTIONS should include
+'section', a hashref containing 'category' and/or 'locationnum' keys.
+
+'section' may also contain a key named 'condensed'. If this is present
+and has a true value, _items_pkg will try to merge identical items into items
+with 'quantity' equal to the number of items (not the sum of their separate
+quantities, for some reason).
 
 =cut
 
@@ -2866,13 +2889,14 @@ sub _items_fee {
     }
     foreach (sort keys(%base_invnums)) {
       next if $_ == $self->invnum;
+      # per convention, we must escape ext_description lines
       push @ext_desc,
         &{$escape_function}(
           $self->mt('from invoice #[_1] on [_2]', $_, $base_invnums{$_})
         );
     }
     my $desc = $part_fee->itemdesc_locale($self->cust_main->locale);
-    $desc = &{$escape_function}($desc);
+    # but not escape the base description line
 
     push @items,
       { feepart     => $cust_bill_pkg->feepart,
index 140982e..f16e930 100644 (file)
@@ -124,7 +124,7 @@ sub check {
     || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg', 'billpkgnum' )
     || $self->ut_number('taxnum') #cust_bill_pkg/tax_rate key, based on taxtype
     || $self->ut_enum('taxtype', [ qw( FS::cust_main_county FS::tax_rate ) ] )
-    || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum' )
+    || $self->ut_number('pkgnum', 'cust_pkg', 'pkgnum' )
     || $self->ut_foreign_key('locationnum', 'cust_location', 'locationnum' )
     || $self->ut_money('amount')
     || $self->ut_foreign_key('taxable_billpkgnum', 'cust_bill_pkg', 'billpkgnum')
index a10b066..954f70b 100644 (file)
@@ -2,7 +2,6 @@ package FS::part_fee;
 
 use strict;
 use base qw( FS::o2m_Common FS::Record );
-use vars qw( $DEBUG );
 use FS::Record qw( qsearch qsearchs );
 use FS::pkg_class;
 use FS::cust_bill_pkg_display;
@@ -10,7 +9,8 @@ use FS::part_pkg_taxproduct;
 use FS::agent;
 use FS::part_fee_usage;
 
-$DEBUG = 0;
+our $DEBUG = 0;
+our $default_class;
 
 =head1 NAME
 
@@ -54,6 +54,9 @@ the invoice
 =item disabled - 'Y' if the fee is disabled
 
 =item classnum - the L<FS::pkg_class> that the fee belongs to, for reporting
+and placement on multisection invoices. Unlike packages, fees I<must> be 
+assigned to a class; they will default to class named "Fees", which belongs 
+to the same invoice section that normally contains taxes.
 
 =item taxable - 'Y' if this fee should be considered a taxable sale.  
 Currently, taxable fees will be treated like they exist at the customer's
@@ -134,6 +137,13 @@ sub check {
   $self->set('amount', 0) unless $self->amount;
   $self->set('percent', 0) unless $self->percent;
 
+  $default_class ||= qsearchs('pkg_class', { classname => 'Fees' })
+    or die "default package fee class not found; run freeside-upgrade to continue.\n";
+
+  if (!$self->get('classnum')) {
+    $self->set('classnum', $default_class->classnum);
+  }
+
   my $error = 
     $self->ut_numbern('feepart')
     || $self->ut_textn('comment')
index adfadd7..c2361cc 100644 (file)
@@ -3,7 +3,7 @@ use base qw( FS::category_Common );
 
 use strict;
 use vars qw( @ISA $me $DEBUG );
-use FS::Record qw( qsearch );
+use FS::Record qw( qsearch qsearchs );
 use FS::pkg_class;
 use FS::part_pkg;
 
@@ -145,6 +145,40 @@ sub _upgrade_data {
       $weight += 10;
     }
   }
+
+  # create default category for package fees
+  my $tax_category_name = 'Taxes, Surcharges, and Fees';
+  my $tax_category = qsearchs('pkg_category', 
+    { categoryname => $tax_category_name }
+  );
+  if (!$tax_category) {
+    $tax_category = FS::pkg_category->new({
+        categoryname => $tax_category_name,
+        weight       => 1000, # doesn't really matter
+    });
+    my $error = $tax_category->insert;
+    die "error creating tax category: $error\n" if $error;
+  }
+
+  my $fee_class_name = 'Fees'; # does not appear on invoice
+  my $fee_class = qsearchs('pkg_class', { classname => $fee_class_name });
+  if (!$fee_class) {
+    $fee_class = FS::pkg_class->new({
+        classname   => $fee_class_name,
+        categorynum => $tax_category->categorynum,
+    });
+    my $error = $fee_class->insert;
+    die "error creating fee class: $error\n" if $error;
+  }
+
+  # assign it to all fee defs that don't otherwise have a class
+  foreach my $part_fee (qsearch('part_fee', { classnum => '' })) {
+    $part_fee->set('classnum', $fee_class->classnum);
+    my $error = $part_fee->replace;
+    die "error assigning default class to fee def#".$part_fee->feepart .
+      ":$error\n" if $error;
+  }
+
   '';
 }
 
index e9b0bdf..06ee775 100644 (file)
                       &{$section->{description_generator}}($line);
             } else {
               my $class = 'invoice_desc_more';
-              if ( $line->{'ref'} and $line->{'ref'} ne $lastref ) {
+              if ( ($line->{'ref'} || 0) ne $lastref ) {
                 # then it's a new package (not a continuation)
                 $class = 'invoice_desc';
               }
               $OUT .= '<tr class="'.$class.'">
                        <td align="center">';
-              #if ( $line->{'ref'} ne $lastref ) {
-              #  $OUT .= $line->{'ref'};
-              #}
               $OUT .= '</td>
                        <td align="left">'. $line->{'description'}. '</td>';
               if ( $unitprices ) {
               $OUT .= '<td align="right">'. $line->{'amount'}. '</td>';
             }
             $OUT .= '</tr>';
-            $lastref = $line->{'ref'};
+            $lastref = $line->{'ref'} || 0;
             if ( @{$line->{'ext_description'} } ) {
               unless ( $section->{description_generator} ) {
                 $OUT .= '<tr class="invoice_extdesc"><td></td><td';
index 822afcb..40ec703 100644 (file)
         # Don't break-up small packages.\r
         my $rowbreak = @$ext_description < 5 ? '*' : '';\r
   \r
-        $OUT .= "\\hline\n" if ($line->{'ref'} && $line->{'ref'} ne $lastref);\r
+        $OUT .= "\\hline\n" if (($line->{'ref'} || 0) ne $lastref);\r
         if ($section->{description_generator}) {\r
           $OUT .= &{$section->{description_generator}}($line);\r
         } else {\r
           $OUT .= '\FSdesc'.\r
-                  '{}'. #'{' . ( $line->{'ref'} ne $lastref ? $line->{'ref'} : '' ) . '}'.\r
+                  '{}'.\r
                   '{' . $line->{'description'} . '}' ;\r
           if ( $unitprices and length($line->{'unit_amount'}) ) {\r
             # then show the unit amount and quantity\r
           }\r
           $OUT .= '{\\dollar' . $line->{'amount'} . "}${rowbreak}\n";\r
         }\r
-        $lastref = $line->{'ref'};\r
+        $lastref = $line->{'ref'} || 0;\r
 \r
         foreach my $ext_desc (@$ext_description) {\r
           if ($section->{extended_description_generator}) {\r