From 60f71e8b94fcd4403ad59c0508de59f90eb6edc5 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 4 Feb 2015 21:12:56 -0800 Subject: [PATCH] allow fees to be grouped with taxes on the invoice, #32223 --- FS/FS/Template_Mixin.pm | 106 ++++++++++++++++++++++-------------- FS/FS/cust_bill_pkg_tax_location.pm | 2 +- FS/FS/part_fee.pm | 14 ++++- FS/FS/pkg_category.pm | 36 +++++++++++- conf/invoice_html | 7 +-- conf/invoice_latex | 6 +- 6 files changed, 118 insertions(+), 53 deletions(-) diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index d46f61772..61e708ed9 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -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, diff --git a/FS/FS/cust_bill_pkg_tax_location.pm b/FS/FS/cust_bill_pkg_tax_location.pm index 140982e53..f16e930b9 100644 --- a/FS/FS/cust_bill_pkg_tax_location.pm +++ b/FS/FS/cust_bill_pkg_tax_location.pm @@ -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') diff --git a/FS/FS/part_fee.pm b/FS/FS/part_fee.pm index a10b06634..954f70b34 100644 --- a/FS/FS/part_fee.pm +++ b/FS/FS/part_fee.pm @@ -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 that the fee belongs to, for reporting +and placement on multisection invoices. Unlike packages, fees I 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') diff --git a/FS/FS/pkg_category.pm b/FS/FS/pkg_category.pm index adfadd772..c2361cc2b 100644 --- a/FS/FS/pkg_category.pm +++ b/FS/FS/pkg_category.pm @@ -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; + } + ''; } diff --git a/conf/invoice_html b/conf/invoice_html index e9b0bdf95..06ee77588 100644 --- a/conf/invoice_html +++ b/conf/invoice_html @@ -166,15 +166,12 @@ &{$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 .= ' '; - #if ( $line->{'ref'} ne $lastref ) { - # $OUT .= $line->{'ref'}; - #} $OUT .= ' '. $line->{'description'}. ''; if ( $unitprices ) { @@ -185,7 +182,7 @@ $OUT .= ''. $line->{'amount'}. ''; } $OUT .= ''; - $lastref = $line->{'ref'}; + $lastref = $line->{'ref'} || 0; if ( @{$line->{'ext_description'} } ) { unless ( $section->{description_generator} ) { $OUT .= '{'ref'} && $line->{'ref'} ne $lastref); + $OUT .= "\\hline\n" if (($line->{'ref'} || 0) ne $lastref); if ($section->{description_generator}) { $OUT .= &{$section->{description_generator}}($line); } else { $OUT .= '\FSdesc'. - '{}'. #'{' . ( $line->{'ref'} ne $lastref ? $line->{'ref'} : '' ) . '}'. + '{}'. '{' . $line->{'description'} . '}' ; if ( $unitprices and length($line->{'unit_amount'}) ) { # then show the unit amount and quantity @@ -345,7 +345,7 @@ } $OUT .= '{\\dollar' . $line->{'amount'} . "}${rowbreak}\n"; } - $lastref = $line->{'ref'}; + $lastref = $line->{'ref'} || 0; foreach my $ext_desc (@$ext_description) { if ($section->{extended_description_generator}) { -- 2.11.0