X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=b2cad50ad6c455bac0a5f19c373fe2a46d570cc2;hb=548bde0209edab634e5b2ea919e4a2096607d2d0;hp=493bc097bdbd5b65e75ee2a86388320b10c33f41;hpb=bdf4497fd8d3778e9cc0f8b90dd8a742f3a84158;p=freeside.git
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index 493bc097b..b2cad50ad 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -1,7 +1,7 @@
package FS::cust_bill;
use strict;
-use vars qw( @ISA $DEBUG $me $conf $money_char );
+use vars qw( @ISA $DEBUG $me $conf $money_char $date_format $rdate_format );
use vars qw( $invoice_lines @buf ); #yuck
use Fcntl qw(:flock); #for spool_csv
use List::Util qw(min max);
@@ -11,6 +11,7 @@ use File::Temp 0.14;
use String::ShellQuote;
use HTML::Entities;
use Locale::Country;
+use Storable qw( freeze thaw );
use FS::UID qw( datasrc );
use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
use FS::Record qw( qsearch qsearchs dbh );
@@ -19,6 +20,7 @@ use FS::cust_main;
use FS::cust_statement;
use FS::cust_bill_pkg;
use FS::cust_bill_pkg_display;
+use FS::cust_bill_pkg_detail;
use FS::cust_credit;
use FS::cust_pay;
use FS::cust_pkg;
@@ -41,7 +43,9 @@ $me = '[FS::cust_bill]';
#ask FS::UID to run this stuff for us later
FS::UID->install_callback( sub {
$conf = new FS::Conf;
- $money_char = $conf->config('money_char') || '$';
+ $money_char = $conf->config('money_char') || '$';
+ $date_format = $conf->config('date_format') || '%x';
+ $rdate_format = $conf->config('date_format') || '%m/%d/%Y';
} );
=head1 NAME
@@ -352,11 +356,24 @@ this invoice.
sub cust_pkg {
my $self = shift;
- my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
+ my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
+ $self->cust_bill_pkg;
my %saw = ();
grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
}
+=item no_auto
+
+Returns true if any of the packages (or their definitions) corresponding to the
+line items for this invoice have the no_auto flag set.
+
+=cut
+
+sub no_auto {
+ my $self = shift;
+ grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
+}
+
=item open_cust_bill_pkg
Returns the open line items for this invoice.
@@ -1895,6 +1912,14 @@ sub realtime_bop {
$cust_main->realtime_bop($method, $amount,
'description' => $description,
'invnum' => $self->invnum,
+#this didn't do what we want, it just calls apply_payments_and_credits
+# 'apply' => 1,
+ 'apply_to_invoice' => 1,
+ #what we want:
+ #this changes application behavior: auto payments
+ #triggered against a specific invoice are now applied
+ #to that invoice instead of oldest open.
+ #seem okay to me...
);
}
@@ -2067,6 +2092,7 @@ notice_name - overrides "Invoice" as the name of the sent document (templates fr
#what's with all the sprintf('%10.2f')'s in here? will it cause any
# (alignment in text invoice?) problems to change them all to '%.2f' ?
+# yes: fixed width (dot matrix) text printing will be borked
sub print_generic {
my( $self, %params ) = @_;
@@ -2289,7 +2315,7 @@ sub print_generic {
'template' => $template, #params{'template'},
'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
'current_charges' => sprintf("%.2f", $self->charged),
- 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
+ 'duedate' => $self->due_date2str($rdate_format), #date_format?
#customer info
'custnum' => $cust_main->display_custnum,
@@ -2456,25 +2482,61 @@ sub print_generic {
sprintf('%.2f', $pr_total),
'summarized' => $summarypage ? 'Y' : '',
};
+ $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
+ join(' / ', map { $cust_main->balance_date_range(@$_) }
+ $self->_prior_month30s
+ )
+ if $conf->exists('invoice_include_aging');
my $taxtotal = 0;
my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
'subtotal' => $taxtotal, # adjusted below
'summarized' => $summarypage ? 'Y' : '',
};
+ my $tax_weight = _pkg_category($tax_section->{description})
+ ? _pkg_category($tax_section->{description})->weight
+ : 0;
+ $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
+ $tax_section->{'sort_weight'} = $tax_weight;
+
my $adjusttotal = 0;
my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
'subtotal' => 0, # adjusted below
'summarized' => $summarypage ? 'Y' : '',
};
+ my $adjust_weight = _pkg_category($adjust_section->{description})
+ ? _pkg_category($adjust_section->{description})->weight
+ : 0;
+ $adjust_section->{'summarized'} = $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('invoice_sections', $cust_main->agentnum);
my $late_sections = [];
+ my $extra_sections = [];
+ my $extra_lines = ();
if ( $multisection ) {
+ ($extra_sections, $extra_lines) =
+ $self->_items_extra_usage_sections($escape_function, $format)
+ if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
+
+ 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, $summarypage, $escape_function );
+ $self->_items_sections( $late_sections, # this could stand a refactor
+ $summarypage,
+ $escape_function,
+ $extra_sections,
+ $format, #bah
+ );
+ if ($conf->exists('svc_phone_sections')) {
+ my ($phone_sections, $phone_lines) =
+ $self->_items_svc_phone_sections($escape_function, $format);
+ push @{$late_sections}, @$phone_sections;
+ push @detail_items, @$phone_lines;
+ }
}else{
push @sections, { 'description' => '', 'subtotal' => '' };
}
@@ -2527,12 +2589,18 @@ sub print_generic {
sprintf('%.2f', $section->{'subtotal'})
if $multisection;
+ # begin some normalization
+ $section->{'amount'} = $section->{'subtotal'}
+ if $multisection;
+
+
if ( $section->{'description'} ) {
push @buf, ( [ &$escape_function($section->{'description'}), '' ],
[ '', '' ],
);
}
+ my $multilocation = scalar($cust_main->cust_location); #too expensive?
my %options = ();
$options{'section'} = $section if $multisection;
$options{'format'} = $format;
@@ -2540,6 +2608,9 @@ sub print_generic {
$options{'format_function'} = sub { () } unless $unsquelched;
$options{'unsquelched'} = $unsquelched;
$options{'summary_page'} = $summarypage;
+ $options{'skip_usage'} =
+ scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
+ $options{'multilocation'} = $multilocation;
foreach my $line_item ( $self->_items_pkg(%options) ) {
my $detail = {
@@ -2581,7 +2652,9 @@ sub print_generic {
$invoice_data{current_less_finance} =
sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
- if ( $multisection && !$conf->exists('disable_previous_balance') ) {
+ if ( $multisection && !$conf->exists('disable_previous_balance')
+ || $conf->exists('previous_balance-summary_only') )
+ {
unshift @sections, $previous_section if $pr_total;
}
@@ -2660,8 +2733,13 @@ sub print_generic {
)
);
if ( $multisection ) {
- $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
- sprintf('%.2f', $self->charged );
+ if ( $adjust_section->{'sort_weight'} ) {
+ $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
+ sprintf("%.2f", ($self->billing_balance || 0) );
+ } else {
+ $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
+ sprintf('%.2f', $self->charged );
+ }
}else{
push @total_items, $total;
}
@@ -2744,7 +2822,8 @@ sub print_generic {
if ( $multisection ) {
$adjust_section->{'subtotal'} = $other_money_char.
sprintf('%.2f', $adjusttotal);
- push @sections, $adjust_section;
+ push @sections, $adjust_section
+ unless $adjust_section->{sort_weight};
}
{
@@ -2758,7 +2837,7 @@ sub print_generic {
: $self->owed + $pr_total
)
);
- if ( $multisection ) {
+ if ( $multisection && !$adjust_section->{sort_weight} ) {
$adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
$total->{'total_amount'};
}else{
@@ -2771,6 +2850,18 @@ sub print_generic {
}
if ( $multisection ) {
+ if ($conf->exists('svc_phone_sections')) {
+ my $total;
+ $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
+ $total->{'total_amount'} =
+ &$embolden_function(
+ $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
+ );
+ my $last_section = pop @sections;
+ $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
+ $total->{'total_amount'};
+ push @sections, $last_section;
+ }
push @sections, @$late_sections
if $unsquelched;
}
@@ -2868,6 +2959,22 @@ sub print_generic {
}
}
+# helper routine for generating date ranges
+sub _prior_month30s {
+ my $self = shift;
+ my @ranges = (
+ [ 1, 2592000 ], # 0-30 days ago
+ [ 2592000, 5184000 ], # 30-60 days ago
+ [ 5184000, 7776000 ], # 60-90 days ago
+ [ 7776000, 0 ], # 90+ days ago
+ );
+
+ map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
+ $_->[1] ? $self->_date - $_->[1] - 1 : '',
+ ] }
+ @ranges;
+}
+
=item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
Returns an postscript invoice, as a scalar.
@@ -3068,7 +3175,7 @@ sub balance_due_msg {
my $msg = 'Balance Due';
return $msg unless $self->terms;
if ( $self->due_date ) {
- $msg .= ' - Please pay by '. $self->due_date2str('%x');
+ $msg .= ' - Please pay by '. $self->due_date2str($date_format);
} elsif ( $self->terms ) {
$msg .= ' - '. $self->terms;
}
@@ -3080,7 +3187,7 @@ sub balance_due_date {
my $duedate = '';
if ( $conf->exists('invoice_default_terms')
&& $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
- $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
+ $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
}
$duedate;
}
@@ -3105,73 +3212,79 @@ Returns a string with the date, for example: "3/20/2008"
sub _date_pretty {
my $self = shift;
- time2str('%x', $self->_date);
+ time2str($date_format, $self->_date);
}
+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 %s = ();
- my %l = ();
+ my %subtotal = ();
+ my %late_subtotal = ();
my %not_tax = ();
foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
{
-
my $usage = $cust_bill_pkg->usage;
foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
next if ( $display->summary && $summarypage );
- my $desc = $display->section;
- my $type = $display->type;
+ my $section = $display->section;
+ my $type = $display->type;
- if ( $cust_bill_pkg->pkgnum > 0 ) {
- $not_tax{$desc} = 1;
- }
+ $not_tax{$section} = 1
+ unless $cust_bill_pkg->pkgnum == 0;
if ( $display->post_total && !$summarypage ) {
if (! $type || $type eq 'S') {
- $l{$desc} += $cust_bill_pkg->setup
- if ( $cust_bill_pkg->setup != 0 );
+ $late_subtotal{$section} += $cust_bill_pkg->setup
+ if $cust_bill_pkg->setup != 0;
}
if (! $type) {
- $l{$desc} += $cust_bill_pkg->recur
- if ( $cust_bill_pkg->recur != 0 );
+ $late_subtotal{$section} += $cust_bill_pkg->recur
+ if $cust_bill_pkg->recur != 0;
}
if ($type && $type eq 'R') {
- $l{$desc} += $cust_bill_pkg->recur - $usage
- if ( $cust_bill_pkg->recur != 0 );
+ $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
+ if $cust_bill_pkg->recur != 0;
}
if ($type && $type eq 'U') {
- $l{$desc} += $usage;
+ $late_subtotal{$section} += $usage
+ unless scalar(@$extra_sections);
}
} else {
+
+ next if $cust_bill_pkg->pkgnum == 0 && ! $section;
+
if (! $type || $type eq 'S') {
- $s{$desc} += $cust_bill_pkg->setup
- if ( $cust_bill_pkg->setup != 0 );
+ $subtotal{$section} += $cust_bill_pkg->setup
+ if $cust_bill_pkg->setup != 0;
}
if (! $type) {
- $s{$desc} += $cust_bill_pkg->recur
- if ( $cust_bill_pkg->recur != 0 );
+ $subtotal{$section} += $cust_bill_pkg->recur
+ if $cust_bill_pkg->recur != 0;
}
if ($type && $type eq 'R') {
- $s{$desc} += $cust_bill_pkg->recur - $usage
- if ( $cust_bill_pkg->recur != 0 );
+ $subtotal{$section} += $cust_bill_pkg->recur - $usage
+ if $cust_bill_pkg->recur != 0;
}
if ($type && $type eq 'U') {
- $s{$desc} += $usage;
+ $subtotal{$section} += $usage
+ unless scalar(@$extra_sections);
}
}
@@ -3180,27 +3293,532 @@ sub _items_sections {
}
- my %cache = map { $_->categoryname => $_ }
- qsearch( 'pkg_category', {disabled => 'Y'} );
- $cache{$_->categoryname} = $_
- foreach qsearch( 'pkg_category', {disabled => ''} );
+ %pkg_category_cache = ();
push @$late, map { { 'description' => &{$escape}($_),
- 'subtotal' => $l{$_},
+ 'subtotal' => $late_subtotal{$_},
'post_total' => 1,
+ 'sort_weight' => ( _pkg_category($_)
+ ? _pkg_category($_)->weight
+ : 0
+ ),
+ ((_pkg_category($_) && _pkg_category($_)->condense)
+ ? $self->_condense_section($format)
+ : ()
+ ),
} }
- sort { $cache{$a}->weight <=> $cache{$b}->weight } keys %l;
-
- map { { 'description' => &{$escape}($_),
- 'subtotal' => $s{$_},
- 'summarized' => $not_tax{$_} ? '' : 'Y',
- 'tax_section' => $not_tax{$_} ? '' : 'Y',
- } }
- sort { $cache{$a}->weight <=> $cache{$b}->weight }
- ( $summarypage
- ? ( grep { exists($s{$_}) || !$cache{$_}->disabled } keys %cache )
- : ( keys %s )
- );
+ sort _sectionsort keys %late_subtotal;
+
+ my @sections;
+ if ( $summarypage ) {
+ @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
+ map { $_->categoryname } qsearch('pkg_category', {});
+ } else {
+ @sections = keys %subtotal;
+ }
+
+ 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;
+
+}
+
+#helper subs for above
+
+sub _sectionsort {
+ _pkg_category($a)->weight <=> _pkg_category($b)->weight;
+}
+
+sub _pkg_category {
+ my $categoryname = shift;
+ $pkg_category_cache{$categoryname} ||=
+ qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
+}
+
+my %condensed_format = (
+ 'label' => [ qw( Description Qty Amount ) ],
+ 'fields' => [
+ sub { shift->{description} },
+ sub { shift->{quantity} },
+ sub { shift->{amount} },
+ ],
+ 'align' => [ qw( l r r ) ],
+ 'span' => [ qw( 5 1 1 ) ], # unitprices?
+ 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
+);
+
+sub _condense_section {
+ my ( $self, $format ) = ( shift, shift );
+ ( 'condensed' => 1,
+ map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
+ qw( description_generator
+ header_generator
+ total_generator
+ total_line_generator
+ )
+ );
+}
+
+sub _condensed_generator_defaults {
+ my ( $self, $format ) = ( shift, shift );
+ return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
+}
+
+my %html_align = (
+ 'c' => 'center',
+ 'l' => 'left',
+ 'r' => 'right',
+);
+
+sub _condensed_header_generator {
+ my ( $self, $format ) = ( shift, shift );
+
+ my ( $f, $prefix, $suffix, $separator, $column ) =
+ _condensed_generator_defaults($format);
+
+ if ($format eq 'latex') {
+ $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
+ $suffix = "\\\\\n\\hline";
+ $separator = "&\n";
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
+ };
+ } elsif ( $format eq 'html' ) {
+ $prefix = '
| ';
+ $suffix = '';
+ $separator = '';
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return qq!$d | !;
+ };
+ }
+
+ sub {
+ my @args = @_;
+ my @result = ();
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ push @result,
+ &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
+ }
+
+ $prefix. join($separator, @result). $suffix;
+ };
+
+}
+
+sub _condensed_description_generator {
+ my ( $self, $format ) = ( shift, shift );
+
+ my ( $f, $prefix, $suffix, $separator, $column ) =
+ _condensed_generator_defaults($format);
+
+ if ($format eq 'latex') {
+ $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
+ $suffix = '\\\\';
+ $separator = " & \n";
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
+ };
+ }elsif ( $format eq 'html' ) {
+ $prefix = '"> | ';
+ $suffix = '';
+ $separator = '';
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return qq!$d | !;
+ };
+ }
+
+ sub {
+ my @args = @_;
+ my @result = ();
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
+ map { $f->{$_}->[$i] } qw(align span width)
+ );
+ }
+
+ $prefix. join( $separator, @result ). $suffix;
+ };
+
+}
+
+sub _condensed_total_generator {
+ my ( $self, $format ) = ( shift, shift );
+
+ my ( $f, $prefix, $suffix, $separator, $column ) =
+ _condensed_generator_defaults($format);
+ my $style = '';
+
+ if ($format eq 'latex') {
+ $prefix = "& ";
+ $suffix = "\\\\\n";
+ $separator = " & \n";
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
+ };
+ }elsif ( $format eq 'html' ) {
+ $prefix = '';
+ $suffix = '';
+ $separator = '';
+ $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return qq!$d | !;
+ };
+ }
+
+
+ sub {
+ my @args = @_;
+ my @result = ();
+
+ # my $r = &{$f->{fields}->[$i]}(@args);
+ # $r .= ' Total' unless $i;
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ push @result,
+ &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
+ map { $f->{$_}->[$i] } qw(align span width)
+ );
+ }
+
+ $prefix. join( $separator, @result ). $suffix;
+ };
+
+}
+
+=item total_line_generator FORMAT
+
+Returns a coderef used for generation of invoice total line items for this
+usage_class. FORMAT is either html or latex
+
+=cut
+
+# should not be used: will have issues with hash element names (description vs
+# total_item and amount vs total_amount -- another array of functions?
+
+sub _condensed_total_line_generator {
+ my ( $self, $format ) = ( shift, shift );
+
+ my ( $f, $prefix, $suffix, $separator, $column ) =
+ _condensed_generator_defaults($format);
+ my $style = '';
+
+ if ($format eq 'latex') {
+ $prefix = "& ";
+ $suffix = "\\\\\n";
+ $separator = " & \n";
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
+ };
+ }elsif ( $format eq 'html' ) {
+ $prefix = '';
+ $suffix = '';
+ $separator = '';
+ $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return qq!$d | !;
+ };
+ }
+
+
+ sub {
+ my @args = @_;
+ my @result = ();
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ push @result,
+ &{$column}( &{$f->{fields}->[$i]}(@args),
+ map { $f->{$_}->[$i] } qw(align span width)
+ );
+ }
+
+ $prefix. join( $separator, @result ). $suffix;
+ };
+
+}
+
+#sub _items_extra_usage_sections {
+# my $self = shift;
+# my $escape = shift;
+#
+# my %sections = ();
+#
+# my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
+# foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
+# {
+# next unless $cust_bill_pkg->pkgnum > 0;
+#
+# foreach my $section ( keys %usage_class ) {
+#
+# my $usage = $cust_bill_pkg->usage($section);
+#
+# next unless $usage && $usage > 0;
+#
+# $sections{$section} ||= 0;
+# $sections{$section} += $usage;
+#
+# }
+#
+# }
+#
+# map { { 'description' => &{$escape}($_),
+# 'subtotal' => $sections{$_},
+# 'summarized' => '',
+# 'tax_section' => '',
+# }
+# }
+# sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
+#
+#}
+
+sub _items_extra_usage_sections {
+ my $self = shift;
+ my $escape = shift;
+ my $format = shift;
+
+ my %sections = ();
+ my %classnums = ();
+ my %lines = ();
+
+ my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ next unless $cust_bill_pkg->pkgnum > 0;
+
+ foreach my $classnum ( keys %usage_class ) {
+ my $section = $usage_class{$classnum}->classname;
+ $classnums{$section} = $classnum;
+
+ foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
+ my $amount = $detail->amount;
+ next unless $amount && $amount > 0;
+
+ $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
+ $sections{$section}{amount} += $amount; #subtotal
+ $sections{$section}{calls}++;
+ $sections{$section}{duration} += $detail->duration;
+
+ my $desc = $detail->regionname;
+ my $description = $desc;
+ $description = substr($desc, 0, 50). '...'
+ if $format eq 'latex' && length($desc) > 50;
+
+ $lines{$section}{$desc} ||= {
+ description => &{$escape}($description),
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => $cust_bill_pkg->pkgnum,
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ #unit_amount => $cust_bill_pkg->unitrecur,
+ quantity => $cust_bill_pkg->quantity,
+ product_code => 'N/A',
+ ext_description => [],
+ };
+
+ $lines{$section}{$desc}{amount} += $amount;
+ $lines{$section}{$desc}{calls}++;
+ $lines{$section}{$desc}{duration} += $detail->duration;
+
+ }
+ }
+ }
+
+ my %sectionmap = ();
+ foreach (keys %sections) {
+ my $usage_class = $usage_class{$classnums{$_}};
+ $sectionmap{$_} = { 'description' => &{$escape}($_),
+ 'amount' => $sections{$_}{amount}, #subtotal
+ 'calls' => $sections{$_}{calls},
+ 'duration' => $sections{$_}{duration},
+ 'summarized' => '',
+ 'tax_section' => '',
+ 'sort_weight' => $usage_class->weight,
+ ( $usage_class->format
+ ? ( map { $_ => $usage_class->$_($format) }
+ qw( description_generator header_generator total_generator total_line_generator )
+ )
+ : ()
+ ),
+ };
+ }
+
+ my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
+ values %sectionmap;
+
+ my @lines = ();
+ foreach my $section ( keys %lines ) {
+ foreach my $line ( keys %{$lines{$section}} ) {
+ my $l = $lines{$section}{$line};
+ $l->{section} = $sectionmap{$section};
+ $l->{amount} = sprintf( "%.2f", $l->{amount} );
+ #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+ push @lines, $l;
+ }
+ }
+
+ return(\@sections, \@lines);
+
+}
+
+sub _items_svc_phone_sections {
+ my $self = shift;
+ my $escape = shift;
+ my $format = shift;
+
+ my %sections = ();
+ my %classnums = ();
+ my %lines = ();
+
+ my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
+
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ next unless $cust_bill_pkg->pkgnum > 0;
+
+ foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
+
+ my $phonenum = $detail->phonenum;
+ next unless $phonenum;
+
+ my $amount = $detail->amount;
+ next unless $amount && $amount > 0;
+
+ $sections{$phonenum} ||= { 'amount' => 0,
+ 'calls' => 0,
+ 'duration' => 0,
+ 'sort_weight' => -1,
+ 'phonenum' => $phonenum,
+ };
+ $sections{$phonenum}{amount} += $amount; #subtotal
+ $sections{$phonenum}{calls}++;
+ $sections{$phonenum}{duration} += $detail->duration;
+
+ my $desc = $detail->regionname;
+ my $description = $desc;
+ $description = substr($desc, 0, 50). '...'
+ if $format eq 'latex' && length($desc) > 50;
+
+ $lines{$phonenum}{$desc} ||= {
+ description => &{$escape}($description),
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => '',
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ #unit_amount => '',
+ quantity => '',
+ product_code => 'N/A',
+ ext_description => [],
+ };
+
+ $lines{$phonenum}{$desc}{amount} += $amount;
+ $lines{$phonenum}{$desc}{calls}++;
+ $lines{$phonenum}{$desc}{duration} += $detail->duration;
+
+ my $line = $usage_class{$detail->classnum}->classname;
+ $sections{"$phonenum $line"} ||=
+ { 'amount' => 0,
+ 'calls' => 0,
+ 'duration' => 0,
+ 'sort_weight' => $usage_class{$detail->classnum}->weight,
+ 'phonenum' => $phonenum,
+ };
+ $sections{"$phonenum $line"}{amount} += $amount; #subtotal
+ $sections{"$phonenum $line"}{calls}++;
+ $sections{"$phonenum $line"}{duration} += $detail->duration;
+
+ $lines{"$phonenum $line"}{$desc} ||= {
+ description => &{$escape}($description),
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => '',
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ #unit_amount => '',
+ quantity => '',
+ product_code => 'N/A',
+ ext_description => [],
+ };
+
+ $lines{"$phonenum $line"}{$desc}{amount} += $amount;
+ $lines{"$phonenum $line"}{$desc}{calls}++;
+ $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
+ push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
+ $detail->formatted('format' => $format);
+
+ }
+ }
+
+ my %sectionmap = ();
+ my $simple = new FS::usage_class { format => 'simple' }; #bleh
+ my $usage_simple = new FS::usage_class { format => 'usage_simple' }; #bleh
+ foreach ( keys %sections ) {
+ my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
+ my $usage_class = $summary ? $simple : $usage_simple;
+ my $ending = $summary ? ' usage charges' : '';
+ $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
+ 'amount' => $sections{$_}{amount}, #subtotal
+ 'calls' => $sections{$_}{calls},
+ 'duration' => $sections{$_}{duration},
+ 'summarized' => '',
+ 'tax_section' => '',
+ 'phonenum' => $sections{$_}{phonenum},
+ 'sort_weight' => $sections{$_}{sort_weight},
+ 'post_total' => $summary, #inspire pagebreak
+ (
+ ( map { $_ => $usage_class->$_($format) }
+ qw( description_generator
+ header_generator
+ total_generator
+ total_line_generator
+ )
+ )
+ ),
+ };
+ }
+
+ my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
+ $a->{sort_weight} <=> $b->{sort_weight}
+ }
+ values %sectionmap;
+
+ my @lines = ();
+ foreach my $section ( keys %lines ) {
+ foreach my $line ( keys %{$lines{$section}} ) {
+ my $l = $lines{$section}{$line};
+ $l->{section} = $sectionmap{$section};
+ $l->{amount} = sprintf( "%.2f", $l->{amount} );
+ #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+ push @lines, $l;
+ }
+ }
+
+ return(\@sections, \@lines);
}
@@ -3227,9 +3845,11 @@ sub _items_previous {
my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
my @b = ();
foreach ( @pr_cust_bill ) {
+ my $date = $conf->exists('invoice_show_prior_due_date')
+ ? 'due '. $_->due_date2str($date_format)
+ : time2str($date_format, $_->_date);
push @b, {
- 'description' => 'Previous Balance, Invoice #'. $_->invnum.
- ' ('. time2str('%x',$_->_date). ')',
+ 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
#'pkgpart' => 'N/A',
'pkgnum' => 'N/A',
'amount' => sprintf("%.2f", $_->owed),
@@ -3253,8 +3873,30 @@ sub _items_previous {
sub _items_pkg {
my $self = shift;
+ my %options = @_;
my @cust_bill_pkg = 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 ($options{section} && $options{section}->{condensed}) {
+ my %itemshash = ();
+ local $Storable::canonical = 1;
+ foreach ( @items ) {
+ my $item = { %$_ };
+ delete $item->{ref};
+ delete $item->{ext_description};
+ my $key = freeze($item);
+ $itemshash{$key} ||= 0;
+ $itemshash{$key} ++; # += $item->{quantity};
+ }
+ @items = sort { $a->{description} cmp $b->{description} }
+ map { my $i = thaw($_);
+ $i->{quantity} = $itemshash{$_};
+ $i->{amount} =
+ sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
+ $i;
+ }
+ keys %itemshash;
+ }
+ @items;
}
sub _taxsort {
@@ -3283,17 +3925,20 @@ sub _items_cust_bill_pkg {
my $unsquelched = $opt{unsquelched} || '';
my $section = $opt{section}->{description} if $opt{section};
my $summary_page = $opt{summary_page} || '';
+ my $multilocation = $opt{multilocation} || '';
my @b = ();
my ($s, $r, $u) = ( undef, undef, undef );
foreach my $cust_bill_pkg ( @$cust_bill_pkg )
{
- foreach ( $s, $r, $u ) {
+ foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
if ( $_ && !$cust_bill_pkg->hidden ) {
$_->{amount} = sprintf( "%.2f", $_->{amount} ),
+ $_->{amount} =~ s/^\-0\.00$/0.00/;
$_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
- push @b, { %$_ };
+ push @b, { %$_ }
+ unless $_->{amount} == 0;
$_ = undef;
}
}
@@ -3302,7 +3947,7 @@ sub _items_cust_bill_pkg {
? $_->section eq $section
: 1
}
- grep { $_->summary || !$summary_page }
+ grep { !$_->summary || !$summary_page }
$cust_bill_pkg->cust_bill_pkg_display
)
{
@@ -3328,10 +3973,18 @@ sub _items_cust_bill_pkg {
$description .= ' Setup' if $cust_bill_pkg->recur != 0;
my @d = ();
- push @d, map &{$escape_function}($_),
- $cust_pkg->h_labels_short($self->_date)
- unless $cust_pkg->part_pkg->hide_svc_detail
- || $cust_bill_pkg->hidden;
+ unless ( $cust_pkg->part_pkg->hide_svc_detail
+ || $cust_bill_pkg->hidden )
+ {
+ push @d, map &{$escape_function}($_),
+ $cust_pkg->h_labels_short($self->_date);
+ if ( $multilocation ) {
+ my $loc = $cust_pkg->location_label;
+ $loc = substr($desc, 0, 50). '...'
+ if $format eq 'latex' && length($loc) > 50;
+ push @d, &{$escape_function}($loc);
+ }
+ }
push @d, $cust_bill_pkg->details(%details_opt)
if $cust_bill_pkg->recur == 0;
@@ -3363,8 +4016,8 @@ sub _items_cust_bill_pkg {
? "Usage charges" : $desc;
unless ( $conf->exists('disable_line_item_date_ranges') ) {
- $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
- " - ". time2str("%x", $cust_bill_pkg->edate). ")";
+ $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
+ " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
}
my @d = ();
@@ -3375,14 +4028,23 @@ sub _items_cust_bill_pkg {
my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
push @dates, $prev->sdate if $prev;
- push @d, map &{$escape_function}($_),
- $cust_pkg->h_labels_short(@dates)
- #$cust_bill_pkg->edate,
- #$cust_bill_pkg->sdate)
- unless $cust_pkg->part_pkg->hide_svc_detail
+ unless ( $cust_pkg->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' )
+ {
+ push @d, map &{$escape_function}($_),
+ $cust_pkg->h_labels_short(@dates)
+ #$cust_bill_pkg->edate,
+ #$cust_bill_pkg->sdate)
+ ;
+ if ( $multilocation ) {
+ my $loc = $cust_pkg->location_label;
+ $loc = substr($desc, 0, 50). '...'
+ if $format eq 'latex' && length($loc) > 50;
+ push @d, &{$escape_function}($loc);
+ }
+ }
push @d, $cust_bill_pkg->details(%details_opt)
unless ($is_summary || $type && $type eq 'R');
@@ -3447,8 +4109,8 @@ sub _items_cust_bill_pkg {
if ( $cust_bill_pkg->recur != 0 ) {
push @b, {
'description' => "$desc (".
- time2str("%x", $cust_bill_pkg->sdate). ' - '.
- time2str("%x", $cust_bill_pkg->edate). ')',
+ time2str($date_format, $cust_bill_pkg->sdate). ' - '.
+ time2str($date_format, $cust_bill_pkg->edate). ')',
'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
};
}
@@ -3459,11 +4121,13 @@ sub _items_cust_bill_pkg {
}
- foreach ( $s, $r, $u ) {
- if ( $_ ) {
+ foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
+ if ( $_ ) {
$_->{amount} = sprintf( "%.2f", $_->{amount} ),
+ $_->{amount} =~ s/^\-0\.00$/0.00/;
$_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
- push @b, { %$_ };
+ push @b, { %$_ }
+ unless $_->{amount} == 0;
}
}
@@ -3490,7 +4154,7 @@ sub _items_credits {
# " (". time2str("%x",$_->cust_credit->_date) .")".
# $reason,
'description' => 'Credit applied '.
- time2str("%x",$_->cust_credit->_date). $reason,
+ time2str($date_format,$_->cust_credit->_date). $reason,
'amount' => sprintf("%.2f",$_->amount),
};
}
@@ -3510,7 +4174,7 @@ sub _items_payments {
push @b, {
'description' => "Payment received ".
- time2str("%x",$_->cust_pay->_date ),
+ time2str($date_format,$_->cust_pay->_date ),
'amount' => sprintf("%.2f", $_->amount )
};
}
@@ -3627,7 +4291,7 @@ sub re_X {
my $distinct = '';
my $orderby = 'ORDER BY cust_bill._date';
- my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
+ my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
@@ -3717,20 +4381,16 @@ sub credited_sql {
WHERE cust_bill.invnum = cust_credit_bill.invnum )";
}
-=item search_sql HASHREF
+=item search_sql_where HASHREF
Class method which returns an SQL WHERE fragment to search for parameters
specified in HASHREF. Valid parameters are
=over 4
-=item begin
-
-Epoch date (UNIX timestamp) setting a lower bound for _date values
-
-=item end
+=item _date
-Epoch date (UNIX timestamp) setting an upper bound for _date values
+List reference of start date, end date, as UNIX timestamps.
=item invnum_min
@@ -3738,10 +4398,22 @@ Epoch date (UNIX timestamp) setting an upper bound for _date values
=item agentnum
+=item charged
+
+List reference of charged limits (exclusive).
+
=item owed
+List reference of charged limits (exclusive).
+
+=item open
+
+flag, return open invoices only
+
=item net
+flag, return net invoices only
+
=item days
=item newest_percust
@@ -3752,40 +4424,68 @@ Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
=cut
-sub search_sql {
+sub search_sql_where {
my($class, $param) = @_;
if ( $DEBUG ) {
- warn "$me search_sql called with params: \n".
+ warn "$me search_sql_where called with params: \n".
join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
}
my @search = ();
- if ( $param->{'begin'} =~ /^(\d+)$/ ) {
- push @search, "cust_bill._date >= $1";
+ #agentnum
+ if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
+ push @search, "cust_main.agentnum = $1";
}
- if ( $param->{'end'} =~ /^(\d+)$/ ) {
- push @search, "cust_bill._date < $1";
+
+ #_date
+ if ( $param->{_date} ) {
+ my($beginning, $ending) = @{$param->{_date}};
+
+ push @search, "cust_bill._date >= $beginning",
+ "cust_bill._date < $ending";
}
+
+ #invnum
if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
push @search, "cust_bill.invnum >= $1";
}
if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
push @search, "cust_bill.invnum <= $1";
}
- if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
- push @search, "cust_main.agentnum = $1";
+
+ #charged
+ if ( $param->{charged} ) {
+ my @charged = ref($param->{charged})
+ ? @{ $param->{charged} }
+ : ($param->{charged});
+
+ push @search, map { s/^charged/cust_bill.charged/; $_; }
+ @charged;
}
- push @search, '0 != '. FS::cust_bill->owed_sql
- if $param->{'open'};
+ my $owed_sql = FS::cust_bill->owed_sql;
+
+ #owed
+ if ( $param->{owed} ) {
+ my @owed = ref($param->{owed})
+ ? @{ $param->{owed} }
+ : ($param->{owed});
+ push @search, map { s/^owed/$owed_sql/; $_; }
+ @owed;
+ }
+ #open/net flags
+ push @search, "0 != $owed_sql"
+ if $param->{'open'};
push @search, '0 != '. FS::cust_bill->net_sql
if $param->{'net'};
+ #days
push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
if $param->{'days'};
+ #newest_percust
if ( $param->{'newest_percust'} ) {
#$distinct = 'DISTINCT ON ( cust_bill.custnum )';
@@ -3809,6 +4509,7 @@ sub search_sql {
}
+ #agent virtualization
my $curuser = $FS::CurrentUser::CurrentUser;
if ( $curuser->username eq 'fs_queue'
&& $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
@@ -3823,7 +4524,6 @@ sub search_sql {
warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
}
}
-
push @search, $curuser->agentnums_sql;
join(' AND ', @search );