diff options
-rw-r--r-- | FS/FS/Conf.pm | 14 | ||||
-rw-r--r-- | FS/FS/Record.pm | 19 | ||||
-rw-r--r-- | FS/FS/cust_bill.pm | 66 | ||||
-rw-r--r-- | FS/FS/cust_location.pm | 33 | ||||
-rw-r--r-- | FS/FS/cust_main.pm | 55 | ||||
-rw-r--r-- | FS/FS/cust_pkg.pm | 12 |
6 files changed, 186 insertions, 13 deletions
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 3e159fa19..6d0f06efd 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -1030,6 +1030,20 @@ worry that config_items is freeside-specific and icky. }, { + 'key' => 'invoice_show_prior_due_date', + 'section' => 'billing', + 'description' => 'Show previous invoice due dates when showing prior balances. Default is to show invoice date.', + 'type' => 'checkbox', + }, + + { + 'key' => 'invoice_include_aging', + 'section' => 'billing', + 'description' => 'Show an aging line after the prior balance section. Only valud when invoice_sections is enabled.', + 'type' => 'checkbox', + }, + + { 'key' => 'invoice_sections', 'section' => 'billing', 'description' => 'Split invoice into sections and label according to package category when enabled.', diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index b05fb3e90..ef8ef0026 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -2764,6 +2764,25 @@ sub h_date { $h ? $h->history_date : ''; } +=item scalar_sql SQL + +A class method with a propensity for becoming an instance method. This +method executes the sql statement represented by SQL and returns a scalar +representing the result. Don't ask for rows -- you get the first column +of the first row. Don't give me bogus SQL or I'll die on you. + +Returns an empty string in the event of no rows. + +=cut + +sub scalar_sql { + my($self, $sql ) = ( shift, shift ); + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute + or die "Unexpected error executing statement $sql: ". $sth->errstr; + $sth->fetchrow_arrayref->[0] || ''; +} + =back =head1 SUBROUTINES diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index c30ee144b..ca81c03dc 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 ); use vars qw( $invoice_lines @buf ); #yuck use Fcntl qw(:flock); #for spool_csv use List::Util qw(min max); @@ -44,6 +44,7 @@ $me = '[FS::cust_bill]'; FS::UID->install_callback( sub { $conf = new FS::Conf; $money_char = $conf->config('money_char') || '$'; + $date_format = $conf->config('date_format') || '%x'; } ); =head1 NAME @@ -2459,6 +2460,11 @@ 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', @@ -2572,6 +2578,7 @@ sub print_generic { ); } + my $multilocation = scalar($cust_main->cust_location); #too expensive? my %options = (); $options{'section'} = $section if $multisection; $options{'format'} = $format; @@ -2581,6 +2588,7 @@ sub print_generic { $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 = { @@ -2927,6 +2935,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. @@ -3797,9 +3821,13 @@ 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('%x', $_->_date); # date_format here, too, + # but fix _items_cust_bill_pkg, + # header, others? 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), @@ -3875,6 +3903,7 @@ 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 ); @@ -3922,10 +3951,15 @@ 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); + push @d, map &{$escape_function}($_), + $cust_pkg->location_label_short + if $multilocation; + } push @d, $cust_bill_pkg->details(%details_opt) if $cust_bill_pkg->recur == 0; @@ -3969,14 +4003,20 @@ 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) + ; + push @d, map &{$escape_function}($_), + $cust_pkg->location_label_short + if $multilocation; + } push @d, $cust_bill_pkg->details(%details_opt) unless ($is_summary || $type && $type eq 'R'); diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm index 50d2a1846..3a9cc7f97 100644 --- a/FS/FS/cust_location.pm +++ b/FS/FS/cust_location.pm @@ -179,6 +179,39 @@ sub line { $line; } +=item line_short + +Returns this location on one line in a shortened form + +=cut + +# configurable? + +sub line_short { + my $self = shift; + my $cydefault = FS::conf->new->config('countrydefault') || 'US'; + + my $line = $self->address1; + #$line .= ', '. $self->address2 if $self->address2; + $line .= ', '. $self->city; + $line .= ', '. $self->state if $self->state; + $line .= ' '. $self->zip if $self->zip; + $line .= ' '. code2country($self->country) if $self->country ne $cydefault; + + $line; +} + +=item location_label_short + +Synonym for line_short + +=cut + +sub location_label_short { + my $self = shift; + $self->line_short; +} + =back =head1 BUGS diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 80db9dd8f..1b663c9ae 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -1955,6 +1955,28 @@ sub cust_location { qsearch('cust_location', { 'custnum' => $self->custnum } ); } +=item location_label_short + +Returns the short label of the service location (see analog in L<FS::cust_location>) for this customer. + +=cut + +# false laziness with FS::cust_location::line_short + +sub location_label_short { + my $self = shift; + my $cydefault = FS::conf->new->config('countrydefault') || 'US'; + + my $line = $self->address1; + #$line .= ', '. $self->address2 if $self->address2; + $line .= ', '. $self->city; + $line .= ', '. $self->state if $self->state; + $line .= ' '. $self->zip if $self->zip; + $line .= ' '. code2country($self->country) if $self->country ne $cydefault; + + $line; +} + =item ncancelled_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ] Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer. @@ -2016,6 +2038,9 @@ sub _cust_pkg { # This should be generalized to use config options to determine order. sub sort_packages { + my $locationsort = $a->locationnum <=> $b->locationnum; + return $locationsort if $locationsort; + if ( $a->get('cancel') xor $b->get('cancel') ) { return -1 if $b->get('cancel'); return 1 if $a->get('cancel'); @@ -6853,6 +6878,36 @@ sub balance_date { ); } +=item balance_date_range START_TIME [ END_TIME [ OPTION => VALUE ... ] ] + +Returns the balance for this customer, only considering invoices with date +earlier than START_TIME, and optionally not later than END_TIME +(total_owed_date minus total_unapplied_credits minus total_unapplied_payments). + +Times are specified as SQL fragments or numeric +UNIX timestamps; see L<perlfunc/"time">). Also see L<Time::Local> and +L<Date::Parse> for conversion functions. The empty string can be passed +to disable that time constraint completely. + +Available options are: + +=over 4 + +=item unapplied_date + +set to true to disregard unapplied credits, payments and refunds outside the specified time period - by default the time period restriction only applies to invoices (useful for reporting, probably a bad idea for event triggering) + +=back + +=cut + +sub balance_date_range { + my $self = shift; + my $sql = 'SELECT SUM('. $self->balance_date_sql(@_). + ') FROM cust_main WHERE custnum='. $self->custnum; + sprintf( "%.2f", $self->scalar_sql($sql) ); +} + =item balance_pkgnum PKGNUM Returns the balance for this customer's specific package when using diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 7632d9ae5..29f699f5c 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -1945,6 +1945,18 @@ sub cust_location_or_main { $self->cust_location || $self->cust_main; } +=item location_label_short + +Returns the short label of the location object (see L<FS::cust_location>). + +=cut + +sub location_label_short { + my $self = shift; + my $object = $self->cust_location_or_main; + $object->location_label_short; +} + =item seconds_since TIMESTAMP Returns the number of seconds all accounts (see L<FS::svc_acct>) in this |