group invoice line items by location, show location address on invoice, option for...
authorjeff <jeff>
Wed, 16 Dec 2009 15:07:38 +0000 (15:07 +0000)
committerjeff <jeff>
Wed, 16 Dec 2009 15:07:38 +0000 (15:07 +0000)
FS/FS/Conf.pm
FS/FS/Record.pm
FS/FS/cust_bill.pm
FS/FS/cust_location.pm
FS/FS/cust_main.pm
FS/FS/cust_pkg.pm

index e801b34..3499625 100644 (file)
@@ -1029,6 +1029,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.',
index b05fb3e..ef8ef00 100644 (file)
@@ -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
index c30ee14..ca81c03 100644 (file)
@@ -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');
index 50d2a18..3a9cc7f 100644 (file)
@@ -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
index a644697..250c40f 100644 (file)
@@ -1953,6 +1953,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.
@@ -2014,6 +2036,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');
@@ -6742,6 +6767,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
index c6d98fa..ae06425 100644 (file)
@@ -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