summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/Conf.pm14
-rw-r--r--FS/FS/Record.pm19
-rw-r--r--FS/FS/cust_bill.pm66
-rw-r--r--FS/FS/cust_location.pm33
-rw-r--r--FS/FS/cust_main.pm55
-rw-r--r--FS/FS/cust_pkg.pm12
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