X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2FTemplate_Mixin.pm;h=b5dbd343acf6a43eb16fd65c11e09d9c66fc7d69;hp=64b9db5606af178931c162022e0db283c9c9ad40;hb=55753aaf5b1189c06a99fe5e0791fc33316df06f;hpb=36f114fa4084bfcfb52ed4673d54d1a0a7e33d15 diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index 64b9db560..b5dbd343a 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -12,6 +12,7 @@ use Text::Template 1.20; use File::Temp 0.14; use HTML::Entities; use Locale::Country; +use Cwd; use FS::UID; use FS::Record qw( qsearch qsearchs ); use FS::Misc qw( generate_ps generate_pdf ); @@ -105,7 +106,7 @@ sub print_latex { $params{'time'} = $today if $today; $params{'template'} = $template if $template; $params{$_} = $opt{$_} - foreach grep $opt{$_}, qw( unsquelch_cdr notice_name ); + foreach grep $opt{$_}, qw( unsquelch_cdr notice_name no_date no_number ); $template ||= $self->_agent_template if $self->can('_agent_template'); @@ -121,7 +122,7 @@ sub print_latex { UNLINK => 0, ) or die "can't open temp file: $!\n"; - my $agentnum = $self->cust_main->agentnum; + my $agentnum = $self->agentnum; if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) { print $lh $conf->config_binary("logo_${template}.eps", $agentnum) @@ -133,7 +134,9 @@ sub print_latex { close $lh; $params{'logo_file'} = $lh->filename; - if( $conf->exists('invoice-barcode') && $self->can('invoice_barcode') ) { + if( $conf->exists('invoice-barcode') + && $self->can('invoice_barcode') + && $self->invnum ) { # don't try to barcode statements my $png_file = $self->invoice_barcode($dir); my $eps_file = $png_file; $eps_file =~ s/\.png$/.eps/g; @@ -171,6 +174,12 @@ sub print_latex { } +sub agentnum { + my $self = shift; + my $cust_main = $self->cust_main; + $cust_main ? $cust_main->agentnum : $self->prospect_main->agentnum; +} + =item print_generic OPTION => VALUE ... Internal method - returns a filled-in template for this invoice as a scalar. @@ -360,14 +369,6 @@ sub print_generic { my $date_format = $date_formats{$format}; - my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}' - }, - 'html' => sub { return ''. shift(). '' - }, - 'template' => sub { shift }, - ); - my $embolden_function = $embolden_functions{$format}; - my %newline_tokens = ( 'latex' => '\\\\', 'html' => '
', 'template' => "\n", @@ -444,9 +445,15 @@ sub print_generic { 'agent' => &$escape_function($cust_main->agent->agent), #invoice/quotation info - 'invnum' => $self->invnum, + 'no_number' => $params{'no_number'}, + 'invnum' => ( $params{'no_number'} ? '' : $self->invnum ), 'quotationnum' => $self->quotationnum, - 'date' => time2str($date_format, $self->_date), + 'no_date' => $params{'no_date'}, + '_date' => ( $params{'no_date'} ? '' : $self->_date ), + 'date' => ( $params{'no_date'} + ? '' + : time2str($date_format, $self->_date) + ), 'today' => time2str($date_format_long, $today), 'terms' => $self->terms, 'template' => $template, #params{'template'}, @@ -581,27 +588,79 @@ sub print_generic { #my $balance_due = $self->owed + $pr_total - $cr_total; my $balance_due = $self->owed + $pr_total; - # the customer's current balance as shown on the invoice before this one - $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) ); + #these are used on the summary page only + + # the customer's current balance as shown on the invoice before this one + $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) ); - # the change in balance from that invoice to this one - $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) ); + # the change in balance from that invoice to this one + $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) ); - # the sum of amount owed on all previous invoices - $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total); + # the sum of amount owed on all previous invoices + # ($pr_total is used elsewhere but not as $previous_balance) + $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total); # the sum of amount owed on all invoices + # (this is used in the summary & on the payment coupon) $invoice_data{'balance'} = sprintf("%.2f", $balance_due); # info from customer's last invoice before this one, for some # summary formats $invoice_data{'last_bill'} = {}; - my $last_bill = $pr_cust_bill[-1]; - if ( $last_bill ) { - $invoice_data{'last_bill'} = { - '_date' => $last_bill->_date, #unformatted - # all we need for now - }; + + # returns the last unpaid bill, not the last bill + #my $last_bill = $pr_cust_bill[-1]; + + if ( $self->custnum && $self->invnum ) { + + # THIS returns the customer's last bill before this one + my $last_bill = qsearchs({ + 'table' => 'cust_bill', + 'hashref' => { 'custnum' => $self->custnum, + 'invnum' => { op => '<', value => $self->invnum }, + }, + 'order_by' => ' ORDER BY invnum DESC LIMIT 1' + }); + if ( $last_bill ) { + $invoice_data{'last_bill'} = { + '_date' => $last_bill->_date, #unformatted + # all we need for now + }; + my (@payments, @credits); + # for formats that itemize previous payments + foreach my $cust_pay ( qsearch('cust_pay', { + 'custnum' => $self->custnum, + '_date' => { op => '>=', + value => $last_bill->_date } + } ) ) + { + next if $cust_pay->_date > $self->_date; + push @payments, { + '_date' => $cust_pay->_date, + 'date' => time2str($date_format, $cust_pay->_date), + 'payinfo' => $cust_pay->payby_payinfo_pretty, + 'amount' => sprintf('%.2f', $cust_pay->paid), + }; + # not concerned about applications + } + foreach my $cust_credit ( qsearch('cust_credit', { + 'custnum' => $self->custnum, + '_date' => { op => '>=', + value => $last_bill->_date } + } ) ) + { + next if $cust_credit->_date > $self->_date; + push @credits, { + '_date' => $cust_credit->_date, + 'date' => time2str($date_format, $cust_credit->_date), + 'creditreason'=> $cust_credit->reason, + 'amount' => sprintf('%.2f', $cust_credit->amount), + }; + } + $invoice_data{'previous_payments'} = \@payments; + $invoice_data{'previous_credits'} = \@credits; + } + } my $summarypage = ''; @@ -686,6 +745,11 @@ sub print_generic { my $other_money_char = $other_money_chars{$format}; $invoice_data{'dollar'} = $other_money_char; + my %minus_signs = ( 'latex' => '$-$', + 'html' => '−', + 'template' => '- ' ); + my $minus = $minus_signs{$format}; + my @detail_items = (); my @total_items = (); my @buf = (); @@ -699,6 +763,8 @@ sub print_generic { warn "$me generating sections\n" if $DEBUG > 1; + # Previous Charges section + # subtotal is the first return value from $self->previous my $previous_section = { 'description' => $self->mt('Previous Charges'), 'subtotal' => $other_money_char. sprintf('%.2f', $pr_total), @@ -722,10 +788,11 @@ sub print_generic { my $adjusttotal = 0; - my $adjust_section = { 'description' => - $self->mt('Credits, Payments, and Adjustments'), - 'subtotal' => 0, # adjusted below - }; + 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; @@ -733,9 +800,10 @@ sub print_generic { $adjust_section->{'sort_weight'} = $adjust_weight; my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y'; - my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum); + my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) || + $conf->exists($tc.'sections_by_location', $cust_main->agentnum); $invoice_data{'multisection'} = $multisection; - my $late_sections = []; + my $late_sections; my $extra_sections = []; my $extra_lines = (); @@ -753,13 +821,24 @@ sub print_generic { push @$extra_sections, $adjust_section if $adjust_section->{sort_weight}; push @detail_items, @$extra_lines if $extra_lines; - push @sections, - $self->_items_sections( $late_sections, # this could stand a refactor - $summarypage, - $escape_function_nonbsp, - $extra_sections, - $format, #bah + + # the code is written so that both methods can be used together, but + # we haven't yet changed the template to take advantage of that, so for + # now, treat them as mutually exclusive. + my %section_method = ( by_category => 1 ); + if ( $conf->exists($tc.'sections_by_location') ) { + %section_method = ( by_location => 1 ); + } + my ($early, $late) = + $self->_items_sections( 'summary' => $summarypage, + 'escape' => $escape_function_nonbsp, + 'extra_sections' => $extra_sections, + 'format' => $format, + %section_method ); + push @sections, @$early; + $late_sections = $late; + if ( $conf->exists('svc_phone_sections') && $self->can('_items_svc_phone_sections') ) @@ -801,11 +880,11 @@ sub print_generic { } } - unless ( $conf->exists('disable_previous_balance', $agentnum) - || $conf->exists('previous_balance-summary_only') - || ! $self->can('_items_previous') - ) - { + # previous invoice balances in the Previous Charges section if there + # is one, otherwise in the main detail section + if ( $self->can('_items_previous') && + $self->enable_previous && + ! $conf->exists('previous_balance-summary_only') ) { warn "$me adding previous balances\n" if $DEBUG > 1; @@ -816,6 +895,7 @@ sub print_generic { ext_description => [], }; $detail->{'ref'} = $line_item->{'pkgnum'}; + $detail->{'pkgpart'} = $line_item->{'pkgpart'}; $detail->{'quantity'} = 1; $detail->{'section'} = $multisection ? $previous_section : $default_section; @@ -836,9 +916,8 @@ sub print_generic { } } - - if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) ) - { + + if ( @pr_cust_bill && $self->enable_previous ) { push @buf, ['','-----------']; push @buf, [ $self->mt('Total Previous Balance'), $money_char. sprintf("%10.2f", $pr_total) ]; @@ -890,7 +969,6 @@ sub print_generic { warn "$me setting options\n" if $DEBUG > 1; - my $multilocation = scalar($cust_main->cust_location); #too expensive? my %options = (); $options{'section'} = $section if $multisection; $options{'format'} = $format; @@ -900,8 +978,6 @@ sub print_generic { $options{'summary_page'} = $summarypage; $options{'skip_usage'} = scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections; - $options{'multilocation'} = $multilocation; - $options{'multisection'} = $multisection; warn "$me searching for line items\n" if $DEBUG > 1; @@ -915,6 +991,7 @@ sub print_generic { ext_description => [], }; $detail->{'ref'} = $line_item->{'pkgnum'}; + $detail->{'pkgpart'} = $line_item->{'pkgpart'}; $detail->{'quantity'} = $line_item->{'quantity'}; $detail->{'section'} = $section; $detail->{'description'} = &$escape_function($line_item->{'description'}); @@ -923,13 +1000,16 @@ sub print_generic { } $detail->{'amount'} = ( $old_latex ? '' : $money_char ). $line_item->{'amount'}; - $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ). - $line_item->{'unit_amount'}; + if ( exists $line_item->{'unit_amount'} ) { + $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ). + $line_item->{'unit_amount'}; + } $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; $detail->{'sdate'} = $line_item->{'sdate'}; $detail->{'edate'} = $line_item->{'edate'}; $detail->{'seconds'} = $line_item->{'seconds'}; + $detail->{'svc_label'} = $line_item->{'svc_label'}; push @detail_items, $detail; push @buf, ( [ $detail->{'description'}, @@ -954,7 +1034,9 @@ sub print_generic { $invoice_data{current_less_finance} = sprintf('%.2f', $self->charged - $invoice_data{finance_amount} ); - if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum) + # create a major section for previous balance if we have major sections, + # or if previous_section is in summary form + if ( ( $multisection && $self->enable_previous ) || $conf->exists('previous_balance-summary_only') ) { unshift @sections, $previous_section if $pr_total; @@ -963,7 +1045,8 @@ sub print_generic { warn "$me adding taxes\n" if $DEBUG > 1; - foreach my $tax ( $self->_items_tax ) { + my @items_tax = $self->_items_tax; + foreach my $tax ( @items_tax ) { $taxtotal += $tax->{'amount'}; @@ -998,7 +1081,7 @@ sub print_generic { } - if ( $taxtotal ) { + if ( @items_tax ) { my $total = {}; $total->{'total_item'} = $self->mt('Sub-total'); $total->{'total_amount'} = @@ -1018,25 +1101,50 @@ sub print_generic { push @buf,['','-----------']; push @buf,[$self->mt( - $conf->exists('disable_previous_balance', $agentnum) + (!$self->enable_previous) ? 'Total Charges' : 'Total New Charges' ), $money_char. sprintf("%10.2f",$self->charged) ]; push @buf,['','']; - { + + ### + # Totals + ### + + my %embolden_functions = ( + 'latex' => sub { return '\textbf{'. shift(). '}' }, + 'html' => sub { return ''. shift(). '' }, + 'template' => sub { shift }, + ); + my $embolden_function = $embolden_functions{$format}; + + if ( $self->can('_items_total') ) { # quotations + + $self->_items_total(\@total_items); + + foreach ( @total_items ) { + $_->{'total_item'} = &$embolden_function( $_->{'total_item'} ); + $_->{'total_amount'} = &$embolden_function( $other_money_char. + $_->{'total_amount'} + ); + } + + } else { #normal invoice case + + # calculate total, possibly including total owed on previous + # invoices my $total = {}; my $item = 'Total'; $item = $conf->config('previous_balance-exclude_from_total') || 'Total New Charges' if $conf->exists('previous_balance-exclude_from_total'); - my $amount = $self->charged + - ( $conf->exists('disable_previous_balance', $agentnum) || - $conf->exists('previous_balance-exclude_from_total') - ? 0 - : $pr_total - ); + my $amount = $self->charged; + if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) { + $amount += $pr_total; + } + $total->{'total_item'} = &$embolden_function($self->mt($item)); $total->{'total_amount'} = &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) ); @@ -1057,125 +1165,128 @@ sub print_generic { sprintf( '%10.2f', $amount ) ]; push @buf,['','']; - } - - unless ( $conf->exists('disable_previous_balance', $agentnum) - || ! $self->can('_items_credits') - || ! $self->can('_items_payments') - ) - { - #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments - - # credits - my $credittotal = 0; - foreach my $credit ( $self->_items_credits('trim_len'=>60) ) { - my $total; - $total->{'total_item'} = &$escape_function($credit->{'description'}); - $credittotal += $credit->{'amount'}; - $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'}; - $adjusttotal += $credit->{'amount'}; - if ( $multisection ) { - my $money = $old_latex ? '' : $money_char; - push @detail_items, { - ext_description => [], - ref => '', - quantity => '', - description => &$escape_function($credit->{'description'}), - amount => $money. $credit->{'amount'}, - product_code => '', - section => $adjust_section, - }; - } else { - push @total_items, $total; - } + # if we're showing previous invoices, also show previous + # credits and payments + if ( $self->enable_previous + and $self->can('_items_credits') + and $self->can('_items_payments') ) + { + #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments + + # credits + my $credittotal = 0; + foreach my $credit ( $self->_items_credits('trim_len'=>60) ) { + + my $total; + $total->{'total_item'} = &$escape_function($credit->{'description'}); + $credittotal += $credit->{'amount'}; + $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'}; + $adjusttotal += $credit->{'amount'}; + if ( $multisection ) { + my $money = $old_latex ? '' : $money_char; + push @detail_items, { + ext_description => [], + ref => '', + quantity => '', + description => &$escape_function($credit->{'description'}), + amount => $money. $credit->{'amount'}, + product_code => '', + section => $adjust_section, + }; + } else { + push @total_items, $total; + } - } - $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal); + } + $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal); - #credits (again) - foreach my $credit ( $self->_items_credits('trim_len'=>32) ) { - push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ]; - } + #credits (again) + foreach my $credit ( $self->_items_credits('trim_len'=>32) ) { + push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ]; + } - # payments - my $paymenttotal = 0; - foreach my $payment ( $self->_items_payments ) { - my $total = {}; - $total->{'total_item'} = &$escape_function($payment->{'description'}); - $paymenttotal += $payment->{'amount'}; - $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'}; - $adjusttotal += $payment->{'amount'}; + # payments + my $paymenttotal = 0; + foreach my $payment ( $self->_items_payments ) { + my $total = {}; + $total->{'total_item'} = &$escape_function($payment->{'description'}); + $paymenttotal += $payment->{'amount'}; + $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'}; + $adjusttotal += $payment->{'amount'}; + if ( $multisection ) { + my $money = $old_latex ? '' : $money_char; + push @detail_items, { + ext_description => [], + ref => '', + quantity => '', + description => &$escape_function($payment->{'description'}), + amount => $money. $payment->{'amount'}, + product_code => '', + section => $adjust_section, + }; + }else{ + push @total_items, $total; + } + push @buf, [ $payment->{'description'}, + $money_char. sprintf("%10.2f", $payment->{'amount'}), + ]; + } + $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal); + if ( $multisection ) { - my $money = $old_latex ? '' : $money_char; - push @detail_items, { - ext_description => [], - ref => '', - quantity => '', - description => &$escape_function($payment->{'description'}), - amount => $money. $payment->{'amount'}, - product_code => '', - section => $adjust_section, - }; - }else{ - push @total_items, $total; + $adjust_section->{'subtotal'} = $other_money_char. + sprintf('%.2f', $adjusttotal); + push @sections, $adjust_section + unless $adjust_section->{sort_weight}; } - push @buf, [ $payment->{'description'}, - $money_char. sprintf("%10.2f", $payment->{'amount'}), - ]; - } - $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal); - - if ( $multisection ) { - $adjust_section->{'subtotal'} = $other_money_char. - sprintf('%.2f', $adjusttotal); - push @sections, $adjust_section - unless $adjust_section->{sort_weight}; - } - # create Balance Due message - { - my $total; - $total->{'total_item'} = &$embolden_function($self->balance_due_msg); - $total->{'total_amount'} = - &$embolden_function( - $other_money_char. sprintf('%.2f', $summarypage - ? $self->charged + - $self->billing_balance - : $self->owed + $pr_total - ) - ); - if ( $multisection && !$adjust_section->{sort_weight} ) { - $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '. - $total->{'total_amount'}; - }else{ - push @total_items, $total; + # create Balance Due message + { + my $total; + $total->{'total_item'} = &$embolden_function($self->balance_due_msg); + $total->{'total_amount'} = + &$embolden_function( + $other_money_char. sprintf('%.2f', #why? $summarypage + # ? $self->charged + + # $self->billing_balance + # : + $self->owed + $pr_total + ) + ); + if ( $multisection && !$adjust_section->{sort_weight} ) { + $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '. + $total->{'total_amount'}; + }else{ + push @total_items, $total; + } + push @buf,['','-----------']; + push @buf,[$self->balance_due_msg, $money_char. + sprintf("%10.2f", $balance_due ) ]; } - push @buf,['','-----------']; - push @buf,[$self->balance_due_msg, $money_char. - sprintf("%10.2f", $balance_due ) ]; - } - if ( $conf->exists('previous_balance-show_credit') - and $cust_main->balance < 0 ) { - my $credit_total = { - 'total_item' => &$embolden_function($self->credit_balance_msg), - 'total_amount' => &$embolden_function( - $other_money_char. sprintf('%.2f', -$cust_main->balance) - ), - }; - if ( $multisection ) { - $adjust_section->{'posttotal'} .= $newline_token . - $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'}; - } - else { - push @total_items, $credit_total; + if ( $conf->exists('previous_balance-show_credit') + and $cust_main->balance < 0 ) { + my $credit_total = { + 'total_item' => &$embolden_function($self->credit_balance_msg), + 'total_amount' => &$embolden_function( + $other_money_char. sprintf('%.2f', -$cust_main->balance) + ), + }; + if ( $multisection ) { + $adjust_section->{'posttotal'} .= $newline_token . + $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'}; + } + else { + push @total_items, $credit_total; + } + push @buf,['','-----------']; + push @buf,[$self->credit_balance_msg, $money_char. + sprintf("%10.2f", -$cust_main->balance ) ]; } - push @buf,['','-----------']; - push @buf,[$self->credit_balance_msg, $money_char. - sprintf("%10.2f", -$cust_main->balance ) ]; } - } + + } #end of default total adding ! can('_items_total') if ( $multisection ) { if ( $conf->exists('svc_phone_sections') @@ -1216,6 +1327,10 @@ sub print_generic { } } @discounts_avail; } + # debugging hook: call this with 'diag' => 1 to just get a hash of + # the invoice variables + return \%invoice_data if ( $params{'diag'} ); + # All sections and items are built; now fill in templates. my @includelist = (); push @includelist, 'summary' if $summarypage; @@ -1580,7 +1695,7 @@ sub _date_pretty { time2str($date_format, $self->_date); } -=item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT +=item _items_sections OPTIONS Generate section information for all items appearing on this invoice. This will only be called for multi-section invoices. @@ -1604,25 +1719,37 @@ If 'condense' is set on the display record, it also contains everything returned from C<_condense_section()>, i.e. C<_condensed_foo_generator> coderefs to generate parts of the invoice. This is not advised. -Arguments: +The method returns two arrayrefs, one of "early" sections and one of "late" +sections. -LATE: an arrayref to push the "late" section hashes onto. The "early" -group is simply returned from the method. +OPTIONS may include: -SUMMARYPAGE: a flag indicating whether this is a summary-format invoice. +by_location: a flag to divide the invoice into sections by location. +Each section hash will have a 'location' element containing a hashref of +the location fields (see L). The section description +will be the location label, but the template can use any of the location +fields to create a suitable label. + +by_category: a flag to divide the invoice into sections using display +records (see L). This is the "traditional" +behavior. Each section hash will have a 'category' element containing +the section name from the display record (which probably equals the +category name of the package, but may not in some cases). + +summary: a flag indicating that this is a summary-format invoice. Turning this on has the following effects: - Ignores display items with the 'summary' flag. -- Combines all items into the "early" group. +- Places all sections in the "early" group even if they have post_total. - Creates sections for all non-disabled package categories, even if they have no charges on this invoice, as well as a section with no name. -ESCAPE: an escape function to use for section titles. +escape: an escape function to use for section titles. -EXTRA_SECTIONS: an arrayref of additional sections to return after the +extra_sections: an arrayref of additional sections to return after the sorted list. If there are any of these, section subtotals exclude usage charges. -FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but +format: 'latex', 'html', or 'template' (i.e. text). Not used, but passed through to C<_condense_section()>. =cut @@ -1630,79 +1757,117 @@ passed through to C<_condense_section()>. use vars qw(%pkg_category_cache); sub _items_sections { my $self = shift; - my $late = shift; - my $summarypage = shift; - my $escape = shift; - my $extra_sections = shift; - my $format = shift; + my %opt = @_; + + my $escape = $opt{escape}; + my @extra_sections = @{ $opt{extra_sections} || [] }; + # $subtotal{$locationnum}{$categoryname} = amount. + # if we're not using by_location, $locationnum is undef. + # if we're not using by_category, you guessed it, $categoryname is undef. + # if we're not using either one, we shouldn't be here in the first place... my %subtotal = (); my %late_subtotal = (); my %not_tax = (); + # About tax items + multisection invoices: + # If either invoice_*summary option is enabled, AND there is a + # package category with the name of the tax, then there will be + # a display record assigning the tax item to that category. + # + # However, the taxes are always placed in the "Taxes, Surcharges, + # and Fees" section regardless of that. The only effect of the + # display record is to create a subtotal for the summary page. + + # cache these + my $pkg_hash = $self->cust_pkg_hash; + foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { my $usage = $cust_bill_pkg->usage; + my $locationnum; + if ( $opt{by_location} ) { + if ( $cust_bill_pkg->pkgnum ) { + $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum; + } else { + $locationnum = ''; + } + } else { + $locationnum = undef; + } + + # as in _items_cust_pkg, if a line item has no display records, + # cust_bill_pkg_display() returns a default record for it + foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) { - next if ( $display->summary && $summarypage ); + next if ( $display->summary && $opt{summary} ); my $section = $display->section; my $type = $display->type; + $section = undef unless $opt{by_category}; - $not_tax{$section} = 1 + $not_tax{$locationnum}{$section} = 1 unless $cust_bill_pkg->pkgnum == 0; - if ( $display->post_total && !$summarypage ) { + # there's actually a very important piece of logic buried in here: + # incrementing $late_subtotal{$section} CREATES + # $late_subtotal{$section}. keys(%late_subtotal) is later used + # to define the list of late sections, and likewise keys(%subtotal). + # When _items_cust_bill_pkg is called to generate line items for + # real, it will be called with 'section' => $section for each + # of these. + if ( $display->post_total && !$opt{summary} ) { if (! $type || $type eq 'S') { - $late_subtotal{$section} += $cust_bill_pkg->setup + $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup if $cust_bill_pkg->setup != 0 || $cust_bill_pkg->setup_show_zero; } if (! $type) { - $late_subtotal{$section} += $cust_bill_pkg->recur + $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur if $cust_bill_pkg->recur != 0 || $cust_bill_pkg->recur_show_zero; } if ($type && $type eq 'R') { - $late_subtotal{$section} += $cust_bill_pkg->recur - $usage + $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage if $cust_bill_pkg->recur != 0 || $cust_bill_pkg->recur_show_zero; } if ($type && $type eq 'U') { - $late_subtotal{$section} += $usage - unless scalar(@$extra_sections); + $late_subtotal{$locationnum}{$section} += $usage + unless scalar(@extra_sections); } - } else { + } else { # it's a pre-total (normal) section + # skip tax items unless they're explicitly included in a section next if $cust_bill_pkg->pkgnum == 0 && ! $section; if (! $type || $type eq 'S') { - $subtotal{$section} += $cust_bill_pkg->setup + $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup if $cust_bill_pkg->setup != 0 || $cust_bill_pkg->setup_show_zero; } if (! $type) { - $subtotal{$section} += $cust_bill_pkg->recur + $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur if $cust_bill_pkg->recur != 0 || $cust_bill_pkg->recur_show_zero; } if ($type && $type eq 'R') { - $subtotal{$section} += $cust_bill_pkg->recur - $usage + $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage if $cust_bill_pkg->recur != 0 || $cust_bill_pkg->recur_show_zero; } if ($type && $type eq 'U') { - $subtotal{$section} += $usage - unless scalar(@$extra_sections); + $subtotal{$locationnum}{$section} += $usage + unless scalar(@extra_sections); } } @@ -1713,53 +1878,80 @@ sub _items_sections { %pkg_category_cache = (); - push @$late, map { { 'description' => &{$escape}($_), - 'subtotal' => $late_subtotal{$_}, - 'post_total' => 1, - 'sort_weight' => ( _pkg_category($_) - ? _pkg_category($_)->weight - : 0 - ), - ((_pkg_category($_) && _pkg_category($_)->condense) - ? $self->_condense_section($format) - : () - ), - } } - sort _sectionsort keys %late_subtotal; - - my @sections; - if ( $summarypage ) { - @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled } - map { $_->categoryname } qsearch('pkg_category', {}); - push @sections, '' if exists($subtotal{''}); - } else { - @sections = keys %subtotal; + # summary invoices need subtotals for all non-disabled package categories, + # even if they're zero + # but currently assume that there are no location sections, or at least + # that the summary page doesn't care about them + if ( $opt{summary} ) { + foreach my $category (qsearch('pkg_category', {disabled => ''})) { + $subtotal{''}{$category->categoryname} ||= 0; + } + $subtotal{''}{''} ||= 0; } - my @early = map { { 'description' => &{$escape}($_), - 'subtotal' => $subtotal{$_}, - 'summarized' => $not_tax{$_} ? '' : 'Y', - 'tax_section' => $not_tax{$_} ? '' : 'Y', - 'sort_weight' => ( _pkg_category($_) - ? _pkg_category($_)->weight - : 0 - ), - ((_pkg_category($_) && _pkg_category($_)->condense) - ? $self->_condense_section($format) - : () - ), - } - } @sections; - push @early, @$extra_sections if $extra_sections; - - sort { $a->{sort_weight} <=> $b->{sort_weight} } @early; - + my @sections; + foreach my $post_total (0,1) { + my @these; + my $s = $post_total ? \%late_subtotal : \%subtotal; + foreach my $locationnum (keys %$s) { + foreach my $sectionname (keys %{ $s->{$locationnum} }) { + my $section = { + 'subtotal' => $s->{$locationnum}{$sectionname}, + 'post_total' => $post_total, + 'sort_weight' => 0, + }; + if ( $locationnum ) { + $section->{'locationnum'} = $locationnum; + my $location = FS::cust_location->by_key($locationnum); + $section->{'description'} = &{ $escape }($location->location_label); + # Better ideas? This will roughly group them by proximity, + # which alpha sorting on any of the address fields won't. + # Sorting by locationnum is meaningless. + # We have to sort on _something_ or the order may change + # randomly from one invoice to the next, which will confuse + # people. + $section->{'sort_weight'} = sprintf('%012s',$location->zip) . + $locationnum; + $section->{'location'} = { + map { $_ => &{ $escape }($location->get($_)) } + $location->fields + }; + } else { + $section->{'category'} = $sectionname; + $section->{'description'} = &{ $escape }($sectionname); + if ( _pkg_category($_) ) { + $section->{'sort_weight'} = _pkg_category($_)->weight; + if ( _pkg_category($_)->condense ) { + $section = { %$section, $self->_condense_section($opt{format}) }; + } + } + } + if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) { + # then it's a tax-only section + $section->{'summarized'} = 'Y'; + $section->{'tax_section'} = 'Y'; + } + push @these, $section; + } # foreach $sectionname + } #foreach $locationnum + push @these, @extra_sections if $post_total == 0; + # need an alpha sort for location sections, because postal codes can + # be non-numeric + $sections[ $post_total ] = [ sort { + $opt{'by_location'} ? + ($a->{sort_weight} cmp $b->{sort_weight}) : + ($a->{sort_weight} <=> $b->{sort_weight}) + } @these ]; + } #foreach $post_total + + return @sections; # early, late } #helper subs for above -sub _sectionsort { - _pkg_category($a)->weight <=> _pkg_category($b)->weight; +sub cust_pkg_hash { + my $self = shift; + $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg }; } sub _pkg_category { @@ -1989,23 +2181,6 @@ sub _condensed_total_line_generator { } -# sub _items { # seems to be unused -# my $self = shift; -# -# #my @display = scalar(@_) -# # ? @_ -# # : qw( _items_previous _items_pkg ); -# # #: qw( _items_pkg ); -# # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments ); -# my @display = qw( _items_previous _items_pkg ); -# -# my @b = (); -# foreach my $display ( @display ) { -# push @b, $self->$display(@_); -# } -# @b; -# } - =item _items_pkg [ OPTIONS ] Return line item hashes for each package item on this invoice. Nearly @@ -2021,6 +2196,11 @@ separate quantities, for some reason). =cut +sub _items_nontax { + my $self = shift; + grep { $_->pkgnum } $self->cust_bill_pkg; +} + sub _items_pkg { my $self = shift; my %options = @_; @@ -2028,7 +2208,7 @@ sub _items_pkg { warn "$me _items_pkg searching for all package line items\n" if $DEBUG > 1; - my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg; + my @cust_bill_pkg = $self->_items_nontax; warn "$me _items_pkg filtering line items\n" if $DEBUG > 1; @@ -2077,7 +2257,17 @@ sub _taxsort { sub _items_tax { my $self = shift; my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg; - $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_); + my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_); + + if ( $self->conf->exists('always_show_tax') ) { + my $itemdesc = $self->conf->config('always_show_tax') || 'Tax'; + if (0 == grep { $_->{description} eq $itemdesc } @items) { + push @items, + { 'description' => $itemdesc, + 'amount' => 0.00 }; + } + } + @items; } =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS @@ -2094,18 +2284,16 @@ escape_function: the function used to escape strings. DEPRECATED? (expensive, mostly unused?) format_function: the function used to format CDRs. -section: a hashref containing 'description'; if this is present, -cust_bill_pkg_display records not belonging to this section are -ignored. +section: a hashref containing 'category' and/or 'locationnum'; if this +is present, only returns line items that belong to that category and/or +location (whichever is defined). multisection: a flag indicating that this is a multisection invoice, which does something complicated. -multilocation: a flag to display the location label for the package. - Returns a list of hashrefs, each of which may contain: -pkgnum, description, amount, unit_amount, quantity, _is_setup, and +pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and ext_description, which is an arrayref of detail lines to show below the package line. @@ -2122,15 +2310,19 @@ sub _items_cust_bill_pkg { my $format_function = $opt{format_function} || ''; my $no_usage = $opt{no_usage} || ''; my $unsquelched = $opt{unsquelched} || ''; #unused - my $section = $opt{section}->{description} if $opt{section}; + my ($section, $locationnum, $category); + if ( $opt{section} ) { + $category = $opt{section}->{category}; + $locationnum = $opt{section}->{locationnum}; + } my $summary_page = $opt{summary_page} || ''; #unused - my $multilocation = $opt{multilocation} || ''; - my $multisection = $opt{multisection} || ''; + my $multisection = defined($category) || defined($locationnum); my $discount_show_always = 0; my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50; my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style + # and location labels my @b = (); my ($s, $r, $u) = ( undef, undef, undef ); @@ -2152,6 +2344,18 @@ sub _items_cust_bill_pkg { } } + if ( $locationnum ) { + # this is a location section; skip packages that aren't at this + # service location. + next if $cust_bill_pkg->pkgnum == 0; + next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum + != $locationnum; + } + + # Consider display records for this item to determine if it belongs + # in this section. Note that if there are no display records, there + # will be a default pseudo-record that includes all charge types + # and has no section name. my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display') ? $cust_bill_pkg->cust_bill_pkg_display : ( $cust_bill_pkg ); @@ -2160,15 +2364,19 @@ sub _items_cust_bill_pkg { $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n" if $DEBUG > 1; - foreach my $display ( grep { defined($section) - ? $_->section eq $section - : 1 - } - #grep { !$_->summary || !$summary_page } # bunk! - grep { !$_->summary || $multisection } - @cust_bill_pkg_display - ) - { + if ( defined($category) ) { + # then this is a package category section; process all display records + # that belong to this section. + @cust_bill_pkg_display = grep { $_->section eq $category } + @cust_bill_pkg_display; + } else { + # otherwise, process all display records that aren't usage summaries + # (I don't think there should be usage summaries if you aren't using + # category sections, but this is the historical behavior) + @cust_bill_pkg_display = grep { !$_->summary } + @cust_bill_pkg_display; + } + foreach my $display (@cust_bill_pkg_display) { warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ". $display->billpkgdisplaynum. "\n" @@ -2176,7 +2384,7 @@ sub _items_cust_bill_pkg { my $type = $display->type; - my $desc = $cust_bill_pkg->desc; + my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' ); $desc = substr($desc, 0, $maxlength). '...' if $format eq 'latex' && length($desc) > $maxlength; @@ -2186,16 +2394,44 @@ sub _items_cust_bill_pkg { 'no_usage' => $opt{'no_usage'}, ); - if ( $cust_bill_pkg->pkgnum > 0 ) { + if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) { + + warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n" + if $DEBUG > 1; + + if ( $cust_bill_pkg->setup != 0 ) { + my $description = $desc; + $description .= ' Setup' + if $cust_bill_pkg->recur != 0 + || $discount_show_always + || $cust_bill_pkg->recur_show_zero; + push @b, { + 'description' => $description, + 'amount' => sprintf("%.2f", $cust_bill_pkg->setup), + }; + } + if ( $cust_bill_pkg->recur != 0 ) { + push @b, { + 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")", + 'amount' => sprintf("%.2f", $cust_bill_pkg->recur), + }; + } + + } elsif ( $cust_bill_pkg->pkgnum > 0 ) { warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n" if $DEBUG > 1; my $cust_pkg = $cust_bill_pkg->cust_pkg; + # which pkgpart to show for display purposes? + my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart; + # start/end dates for invoice formats that do nonstandard # things with them - my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate'); + my %item_dates = (); + %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate') + unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1); if ( (!$type || $type eq 'S') && ( $cust_bill_pkg->setup != 0 @@ -2214,15 +2450,22 @@ sub _items_cust_bill_pkg { || $cust_bill_pkg->recur_show_zero; my @d = (); + my $svc_label; unless ( $cust_pkg->part_pkg->hide_svc_detail || $cust_bill_pkg->hidden ) { - push @d, map &{$escape_function}($_), - $cust_pkg->h_labels_short($self->_date, undef, 'I') + my @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date, undef, 'I'); + push @d, @svc_labels unless $cust_bill_pkg->pkgpart_override; #don't redisplay services + $svc_label = $svc_labels[0]; - if ( $multilocation ) { + my $lnum = $cust_main ? $cust_main->ship_locationnum + : $self->prospect_main->locationnum; + # show the location label if it's not the customer's default + # location, and we're not grouping items by location already + if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) { my $loc = $cust_pkg->location_label; $loc = substr($loc, 0, $maxlength). '...' if $format eq 'latex' && length($loc) > $maxlength; @@ -2242,13 +2485,14 @@ sub _items_cust_bill_pkg { $s = { _is_setup => 1, description => $description, - #pkgpart => $part_pkg->pkgpart, + pkgpart => $pkgpart, pkgnum => $cust_bill_pkg->pkgnum, amount => $cust_bill_pkg->setup, setup_show_zero => $cust_bill_pkg->setup_show_zero, unit_amount => $cust_bill_pkg->unitsetup, quantity => $cust_bill_pkg->quantity, ext_description => \@d, + svc_label => ($svc_label || ''), }; }; @@ -2271,21 +2515,30 @@ sub _items_cust_bill_pkg { my $description = ($is_summary && $type && $type eq 'U') ? "Usage charges" : $desc; + my $part_pkg = $cust_pkg->part_pkg; + #pry be a bit more efficient to look some of this conf stuff up # outside the loop unless ( $conf->exists('disable_line_item_date_ranges') - || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1) + || $part_pkg->option('disable_line_item_date_ranges',1) + || ! $cust_bill_pkg->sdate + || ! $cust_bill_pkg->edate ) { my $time_period; - my $date_style = $conf->config( 'cust_bill-line_item-date_style', - $cust_main->agentnum - ); + my $date_style = ''; + $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly', + $self->agentnum + ) + if $part_pkg && $part_pkg->freq !~ /^1m?$/; + $date_style ||= $conf->config( 'cust_bill-line_item-date_style', + $self->agentnum + ); if ( defined($date_style) && $date_style eq 'month_of' ) { $time_period = time2str('The month of %B', $cust_bill_pkg->sdate); } elsif ( defined($date_style) && $date_style eq 'X_month' ) { my $desc = $conf->config( 'cust_bill-line_item-date_description', - $cust_main->agentnum + $self->agentnum ); $desc .= ' ' unless $desc =~ /\s$/; $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate); @@ -2298,6 +2551,7 @@ sub _items_cust_bill_pkg { my @d = (); my @seconds = (); # for display of usage info + my $svc_label = ''; #at least until cust_bill_pkg has "past" ranges in addition to #the "future" sdate/edate ones... see #3032 @@ -2306,25 +2560,30 @@ sub _items_cust_bill_pkg { push @dates, $prev->sdate if $prev; push @dates, undef if !$prev; - unless ( $cust_pkg->part_pkg->hide_svc_detail + unless ( $part_pkg->hide_svc_detail || $cust_bill_pkg->itemdesc || $cust_bill_pkg->hidden - || $is_summary && $type && $type eq 'U' ) + || $is_summary && $type && $type eq 'U' + ) { warn "$me _items_cust_bill_pkg adding service details\n" if $DEBUG > 1; - push @d, map &{$escape_function}($_), - $cust_pkg->h_labels_short(@dates, 'I') - #$cust_bill_pkg->edate, - #$cust_bill_pkg->sdate) + my @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date, undef, 'I'); + push @d, @svc_labels unless $cust_bill_pkg->pkgpart_override; #don't redisplay services + $svc_label = $svc_labels[0]; warn "$me _items_cust_bill_pkg done adding service details\n" if $DEBUG > 1; - if ( $multilocation ) { + my $lnum = $cust_main ? $cust_main->ship_locationnum + : $self->prospect_main->locationnum; + # show the location label if it's not the customer's default + # location, and we're not grouping items by location already + if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) { my $loc = $cust_pkg->location_label; $loc = substr($loc, 0, $maxlength). '...' if $format eq 'latex' && length($loc) > $maxlength; @@ -2378,6 +2637,10 @@ sub _items_cust_bill_pkg { $amount = $cust_bill_pkg->usage; } + my $unit_amount = + ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur + : $amount; + if ( !$type || $type eq 'R' ) { warn "$me _items_cust_bill_pkg adding recur\n" @@ -2385,19 +2648,20 @@ sub _items_cust_bill_pkg { if ( $cust_bill_pkg->hidden ) { $r->{amount} += $amount; - $r->{unit_amount} += $cust_bill_pkg->unitrecur; + $r->{unit_amount} += $unit_amount; push @{ $r->{ext_description} }, @d; } else { $r = { description => $description, - #pkgpart => $part_pkg->pkgpart, + pkgpart => $pkgpart, pkgnum => $cust_bill_pkg->pkgnum, amount => $amount, recur_show_zero => $cust_bill_pkg->recur_show_zero, - unit_amount => $cust_bill_pkg->unitrecur, + unit_amount => $unit_amount, quantity => $cust_bill_pkg->quantity, %item_dates, ext_description => \@d, + svc_label => ($svc_label || ''), }; $r->{'seconds'} = \@seconds if grep {defined $_} @seconds; } @@ -2409,16 +2673,16 @@ sub _items_cust_bill_pkg { if ( $cust_bill_pkg->hidden ) { $u->{amount} += $amount; - $u->{unit_amount} += $cust_bill_pkg->unitrecur; + $u->{unit_amount} += $unit_amount, push @{ $u->{ext_description} }, @d; } else { $u = { description => $description, - #pkgpart => $part_pkg->pkgpart, + pkgpart => $pkgpart, pkgnum => $cust_bill_pkg->pkgnum, amount => $amount, recur_show_zero => $cust_bill_pkg->recur_show_zero, - unit_amount => $cust_bill_pkg->unitrecur, + unit_amount => $unit_amount, quantity => $cust_bill_pkg->quantity, %item_dates, ext_description => \@d,