X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2FTemplate_Mixin.pm;h=07d203036b3ec23a29de58ed99db556726420cb8;hp=426386b861abba676c9b80db8777bbbcf9d287f4;hb=0af8be13cbde45885b5b3ed2c2e09a2065ba6e2b;hpb=16ad05fd89afe8edb0e1e66098595796cb646200 diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index 426386b86..07d203036 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -2,7 +2,9 @@ package FS::Template_Mixin; use strict; use vars qw( $DEBUG $me - $money_char ); + $money_char + $date_format + ); # but NOT $conf use vars qw( $invoice_lines @buf ); #yuck use List::Util qw(sum); @@ -26,7 +28,8 @@ $DEBUG = 0; $me = '[FS::Template_Mixin]'; FS::UID->install_callback( sub { my $conf = new FS::Conf; #global - $money_char = $conf->config('money_char') || '$'; + $money_char = $conf->config('money_char') || '$'; + $date_format = $conf->config('date_format') || '%x'; #/YY } ); =item conf [ MODE ] @@ -627,18 +630,6 @@ sub print_generic { #my $balance_due = $self->owed + $pr_total - $cr_total; my $balance_due = $self->owed + $pr_total; - #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 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); @@ -649,8 +640,67 @@ sub print_generic { if ( $self->custnum && $self->invnum ) { - if ( $self->previous_bill ) { - my $last_bill = $self->previous_bill; + my $last_bill = $self->previous_bill; + if ( $last_bill ) { + + # "balance_date_range" unfortunately is unsuitable for this, since it + # cares about application dates. We want to know the sum of all + # _top-level transactions_ dated before the last invoice. + my @sql = ( + 'SELECT SUM(charged) FROM cust_bill WHERE _date <= ? AND custnum = ?', + 'SELECT -1*SUM(amount) FROM cust_credit WHERE _date <= ? AND custnum = ?', + 'SELECT -1*SUM(paid) FROM cust_pay WHERE _date <= ? AND custnum = ?', + 'SELECT SUM(refund) FROM cust_refund WHERE _date <= ? AND custnum = ?', + ); + + # the customer's current balance immediately after generating the last + # bill + + my $last_bill_balance = $last_bill->charged; + foreach (@sql) { + #warn "$_\n"; + my $delta = FS::Record->scalar_sql( + $_, + $last_bill->_date - 1, + $self->custnum, + ); + #warn "$delta\n"; + $last_bill_balance += $delta; + } + + warn sprintf("LAST BILL: INVNUM %d, DATE %s, BALANCE %.2f\n\n", + $last_bill->invnum, + $self->time2str_local('%D', $last_bill->_date), + $last_bill_balance + ) if $DEBUG > 0; + # ("true_previous_balance" is a terrible name, but at least it's no + # longer stored in the database) + $invoice_data{'true_previous_balance'} = $last_bill_balance; + + # the change in balance from immediately after that invoice + # to immediately before this one + my $before_this_bill_balance = 0; + foreach (@sql) { + #warn "$_\n"; + my $delta = FS::Record->scalar_sql( + $_, + $self->_date - 1, + $self->custnum, + ); + #warn "$delta\n"; + $before_this_bill_balance += $delta; + } + $invoice_data{'balance_adjustments'} = + sprintf("%.2f", $last_bill_balance - $before_this_bill_balance); + + warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n", + $invoice_data{'balance_adjustments'} + ) if $DEBUG > 0; + + # 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); + $invoice_data{'last_bill'} = { '_date' => $last_bill->_date, #unformatted }; @@ -687,9 +737,15 @@ sub print_generic { } $invoice_data{'previous_payments'} = \@payments; $invoice_data{'previous_credits'} = \@credits; + } else { + # there is no $last_bill + $invoice_data{'true_previous_balance'} = + $invoice_data{'balance_adjustments'} = + $invoice_data{'previous_balance'} = '0.00'; + $invoice_data{'previous_payments'} = []; + $invoice_data{'previous_credits'} = []; } - - } + } # if this is an invoice my $summarypage = ''; if ( $conf->exists('invoice_usesummary', $agentnum) ) { @@ -1022,40 +1078,52 @@ sub print_generic { warn "$me searching for line items\n" if $DEBUG > 1; - foreach my $line_item ( $self->_items_pkg(%options) ) { + foreach my $line_item ( $self->_items_pkg(%options), + $self->_items_fee(%options) ) { warn "$me adding line item $line_item\n" if $DEBUG > 1; - my $detail = { - 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'}); - if ( exists $line_item->{'ext_description'} ) { - @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}}; - } - $detail->{'amount'} = ( $old_latex ? '' : $money_char ). - $line_item->{'amount'}; - if ( exists $line_item->{'unit_amount'} ) { - $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ). - $line_item->{'unit_amount'}; + # this is silly + #my $detail = { + # 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'}); + #if ( exists $line_item->{'ext_description'} ) { + # @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}}; + #} + #$detail->{'amount'} = ( $old_latex ? '' : $money_char ). + # $line_item->{'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'}; + #$detail->{'usage_item'} = $line_item->{'usage_item'}; + $line_item->{'ref'} = $line_item->{'pkgnum'}; + $line_item->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; # mt()? + $line_item->{'section'} = $section; + $line_item->{'description'} = &$escape_function($line_item->{'description'}); + if (!$old_latex) { # dubious; templates should provide this + $line_item->{'amount'} = $money_char.$line_item->{'amount'}; + $line_item->{'unit_amount'} = $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'}, + $line_item->{'ext_description'} ||= []; + + push @detail_items, $line_item; + push @buf, ( [ $line_item->{'description'}, $money_char. sprintf("%10.2f", $line_item->{'amount'}), ], - map { [ " ". $_, '' ] } @{$detail->{'ext_description'}}, + map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}}, ); } @@ -1389,6 +1457,48 @@ sub print_generic { } $invoice_data{summary_subtotals} = \@summary_subtotals; + # usage subtotals + if ( $conf->exists('usage_class_summary') + and $self->can('_items_usage_class_summary') ) { + my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function); + if ( @usage_subtotals ) { + unshift @sections, $usage_subtotals[0]->{section}; + unshift @detail_items, @usage_subtotals; + } + } + + # invoice history "section" (not really a section) + # not to be included in any subtotals, completely independent of + # everything... + if ( $conf->exists('previous_invoice_history') ) { + my %history; + my %monthorder; + foreach my $cust_bill ( $cust_main->cust_bill ) { + # XXX hardcoded format, and currently only 'charged'; add other fields + # if they become necessary + my $date = $self->time2str_local('%b %Y', $cust_bill->_date); + $history{$date} ||= 0; + $history{$date} += $cust_bill->charged; + # just so we have a numeric sort key + $monthorder{$date} ||= $cust_bill->_date; + } + my @sorted_months = sort { $monthorder{$a} <=> $monthorder{$b} } + keys %history; + my @sorted_amounts = map { sprintf('%.2f', $history{$_}) } @sorted_months; + $invoice_data{monthly_history} = [ \@sorted_months, \@sorted_amounts ]; + } + + # service locations: another option for template customization + my %location_info; + foreach my $item (@detail_items) { + if ( $item->{locationnum} ) { + $location_info{ $item->{locationnum} } ||= { + FS::cust_location->by_key( $item->{locationnum} )->location_hash + }; + } + } + $invoice_data{location_info} = \%location_info; + # debugging hook: call this with 'diag' => 1 to just get a hash of # the invoice variables return \%invoice_data if ( $params{'diag'} ); @@ -1745,7 +1855,8 @@ sub credit_balance_msg { =item _date_pretty -Returns a string with the date, for example: "3/20/2008" +Returns a string with the date, for example: "3/20/2008", localized for the +customer. Use _date_pretty_unlocalized for non-end-customer display use. =cut @@ -1754,6 +1865,18 @@ sub _date_pretty { $self->time2str_local('short', $self->_date); } +=item _date_pretty_unlocalized + +Returns a string with the date, for example: "3/20/2008", in the format +configured for the back-office. Use _date_pretty for end-customer display use. + +=cut + +sub _date_pretty_unlocalized { + my $self = shift; + time2str($date_format, $self->_date); +} + =item _items_sections OPTIONS Generate section information for all items appearing on this invoice. @@ -1865,10 +1988,13 @@ sub _items_sections { my $section = $display->section; my $type = $display->type; - $section = undef unless $opt{by_category}; + # Set $section = undef if we're sectioning by location and this + # line item _has_ a location (i.e. isn't a fee). + $section = undef if $locationnum; + # set this flag if the section is not tax-only $not_tax{$locationnum}{$section} = 1 - unless $cust_bill_pkg->pkgnum == 0; + if $cust_bill_pkg->pkgnum or $cust_bill_pkg->feepart; # there's actually a very important piece of logic buried in here: # incrementing $late_subtotal{$section} CREATES @@ -1904,7 +2030,9 @@ sub _items_sections { } 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; + next if $cust_bill_pkg->pkgnum == 0 and + ! $cust_bill_pkg->feepart and + ! $section; if (! $type || $type eq 'S') { $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup @@ -2257,7 +2385,78 @@ separate quantities, for some reason). sub _items_nontax { my $self = shift; - grep { $_->pkgnum } $self->cust_bill_pkg; + # The order of these is important. Bundled line items will be merged into + # the most recent non-hidden item, so it needs to be the one with: + # - the same pkgnum + # - the same start date + # - no pkgpart_override + # + # So: sort by pkgnum, + # then by sdate + # then sort the base line item before any overrides + # then sort hidden before non-hidden add-ons + # then sort by override pkgpart (for consistency) + sort { $a->pkgnum <=> $b->pkgnum or + $a->sdate <=> $b->sdate or + ($a->pkgpart_override ? 0 : -1) or + ($b->pkgpart_override ? 0 : 1) or + $b->hidden cmp $a->hidden or + $a->pkgpart_override <=> $b->pkgpart_override + } + # and of course exclude taxes and fees + grep { $_->pkgnum > 0 } $self->cust_bill_pkg; +} + +sub _items_fee { + my $self = shift; + my %options = @_; + my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg; + my @items; + foreach my $cust_bill_pkg (@cust_bill_pkg) { + # cache this, so we don't look it up again in every section + my $part_fee = $cust_bill_pkg->get('part_fee') + || $cust_bill_pkg->part_fee; + $cust_bill_pkg->set('part_fee', $part_fee); + if (!$part_fee) { + #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense + warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; + next; + } + if ( exists($options{section}) and exists($options{section}{category}) ) + { + my $categoryname = $options{section}{category}; + # then filter for items that have that section + if ( $part_fee->categoryname ne $categoryname ) { + warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG; + next; + } + } # otherwise include them all in the main section + # XXX what to do when sectioning by location? + + my @ext_desc; + my %base_invnums; # invnum => invoice date + foreach ($cust_bill_pkg->cust_bill_pkg_fee) { + if ($_->base_invnum) { + my $base_bill = FS::cust_bill->by_key($_->base_invnum); + my $base_date = $self->time2str_local('short', $base_bill->_date) + if $base_bill; + $base_invnums{$_->base_invnum} = $base_date || ''; + } + } + foreach (sort keys(%base_invnums)) { + next if $_ == $self->invnum; + push @ext_desc, + $self->mt('from invoice \\#[_1] on [_2]', $_, $base_invnums{$_}); + } + push @items, + { feepart => $cust_bill_pkg->feepart, + amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur), + description => $part_fee->itemdesc_locale($self->cust_main->locale), + ext_description => \@ext_desc + # sdate/edate? + }; + } + @items; } sub _items_pkg { @@ -2315,7 +2514,8 @@ sub _taxsort { sub _items_tax { my $self = shift; - my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg; + my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart } + $self->cust_bill_pkg; my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_); if ( $self->conf->exists('always_show_tax') ) { @@ -2406,7 +2606,7 @@ 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 $cust_bill_pkg->pkgnum == 0; # skips fees... next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum != $locationnum; } @@ -2435,6 +2635,9 @@ sub _items_cust_bill_pkg { @cust_bill_pkg_display = grep { !$_->summary } @cust_bill_pkg_display; } + + my $classname = ''; # package class name, will fill in later + foreach my $display (@cust_bill_pkg_display) { warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ". @@ -2495,6 +2698,9 @@ sub _items_cust_bill_pkg { %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate') unless $part_pkg->option('disable_line_item_date_ranges',1); + # not normally used, but pass this to the template anyway + $classname = $part_pkg->classname; + if ( (!$type || $type eq 'S') && ( $cust_bill_pkg->setup != 0 || $cust_bill_pkg->setup_show_zero @@ -2521,16 +2727,20 @@ sub _items_cust_bill_pkg { my @d = (); my $svc_label; + + # always pass the svc_label through to the template, even if + # not displaying it as an ext_description + my @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date, undef, 'I'); + + $svc_label = $svc_labels[0]; + unless ( $cust_pkg->part_pkg->hide_svc_detail || $cust_bill_pkg->hidden ) { - 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]; - my $lnum = $cust_main ? $cust_main->ship_locationnum : $self->prospect_main->locationnum; # show the location label if it's not the customer's default @@ -2563,6 +2773,7 @@ sub _items_cust_bill_pkg { quantity => $cust_bill_pkg->quantity, ext_description => \@d, svc_label => ($svc_label || ''), + locationnum => $cust_pkg->locationnum, # sure, why not? }; }; @@ -2604,6 +2815,10 @@ sub _items_cust_bill_pkg { push @dates, $prev->sdate if $prev; push @dates, undef if !$prev; + my @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short(@dates, 'I'); + $svc_label = $svc_labels[0]; + # show service labels, unless... # the package is set not to display them unless ( $part_pkg->hide_svc_detail @@ -2623,12 +2838,8 @@ sub _items_cust_bill_pkg { warn "$me _items_cust_bill_pkg adding service details\n" if $DEBUG > 1; - my @svc_labels = map &{$escape_function}($_), - $cust_pkg->h_labels_short(@dates, '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; @@ -2715,6 +2926,7 @@ sub _items_cust_bill_pkg { %item_dates, ext_description => \@d, svc_label => ($svc_label || ''), + locationnum => $cust_pkg->locationnum, }; $r->{'seconds'} = \@seconds if grep {defined $_} @seconds; } @@ -2737,9 +2949,11 @@ sub _items_cust_bill_pkg { pkgpart => $pkgpart, pkgnum => $cust_bill_pkg->pkgnum, amount => $amount, + usage_item => 1, recur_show_zero => $cust_bill_pkg->recur_show_zero, %item_dates, ext_description => \@d, + locationnum => $cust_pkg->locationnum, }; } # else this has no usage, so don't create a usage section }