changes to support new invoice template features, #28080
authorMark Wells <mark@freeside.biz>
Tue, 3 Jun 2014 23:59:41 +0000 (16:59 -0700)
committerMark Wells <mark@freeside.biz>
Tue, 3 Jun 2014 23:59:41 +0000 (16:59 -0700)
FS/FS/Conf.pm
FS/FS/Misc.pm
FS/FS/Schema.pm
FS/FS/TemplateItem_Mixin.pm
FS/FS/Template_Mixin.pm
FS/FS/cdr.pm
FS/FS/cust_bill.pm
FS/FS/cust_bill_pkg_detail.pm
FS/FS/detail_format.pm
FS/FS/detail_format/sum_count_class.pm [new file with mode: 0644]
FS/FS/part_pkg/voip_cdr.pm

index b198598..9404c06 100644 (file)
@@ -1060,6 +1060,7 @@ sub reason_type_options {
                        '%m/%d/%Y' => 'MM/DD/YYYY',
                        '%d/%m/%Y' => 'DD/MM/YYYY',
                       '%Y/%m/%d' => 'YYYY/MM/DD',
+                       '%e %b %Y' => 'DD Mon YYYY',
                      ],
     'per_locale'  => 1,
   },
@@ -1576,6 +1577,13 @@ and customer address. Include units.',
   #  'per_agent'   => 1,
   #},
 
+  {
+    'key'         => 'usage_class_summary',
+    'section'     => 'invoicing',
+    'description' => 'Summarize total usage by usage class in a separate section.',
+    'type'        => 'checkbox',
+  },
+
   { 
     'key'         => 'usage_class_as_a_section',
     'section'     => 'invoicing',
@@ -1691,6 +1699,14 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'papersize',
+    'section'     => 'billing',
+    'description' => 'Invoice paper size.  Default is "letter" (U.S. standard).  The LaTeX template must be configured to match this size.',
+    'type'        => 'select',
+    'select_enum' => [ qw(letter a4) ],
+  },
+
+  {
     'key'         => 'money_char',
     'section'     => '',
     'description' => 'Currency symbol - defaults to `$\'',
@@ -4246,6 +4262,16 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'previous_invoice_history',
+    'section'     => 'invoicing',
+    'description' => 'Show a month-by-month history of the customer\'s '.
+                     'billing amounts.  This requires template '.
+                     'modification and is currently not supported on the '.
+                     'stock template.',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'balance_due_below_line',
     'section'     => 'invoicing',
     'description' => 'Place the balance due message below a line.  Only meaningful when when invoice_sections is false.',
index 93445ab..380f895 100644 (file)
@@ -718,7 +718,9 @@ sub generate_ps {
 
   _pslatex($file);
 
-  system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
+  my $papersize = $conf->config('papersize') || 'letter';
+
+  system('dvips', '-q', '-t', $papersize, "$file.dvi", '-o', "$file.ps" ) == 0
     or die "dvips failed";
 
   open(POSTSCRIPT, "<$file.ps")
@@ -773,8 +775,10 @@ sub generate_pdf {
   my $sfile = shell_quote $file;
 
   #system('dvipdf', "$file.dvi", "$file.pdf" );
+  my $papersize = $conf->config('papersize') || 'letter';
+
   system(
-    "dvips -q -t letter -f $sfile.dvi ".
+    "dvips -q -f $sfile.dvi -t $papersize ".
     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
     "     -c save pop -"
   ) == 0
index 4e67cf7..fb02e6b 100644 (file)
@@ -1008,7 +1008,7 @@ sub tables_hashref {
 
     'cust_bill_pkg_detail' => {
       'columns' => [
-        'detailnum', 'serial', '', '', '', '', 
+        'detailnum', 'serial', '', '', '', '',
         'billpkgnum', 'int', 'NULL', '', '', '',        # should not be nullable
         'pkgnum',  'int', 'NULL', '', '', '',           # deprecated
         'invnum',  'int', 'NULL', '', '', '',           # deprecated
index fa20c24..6ae3364 100644 (file)
@@ -128,20 +128,21 @@ sub time_period_pretty {
 
 Returns an array of detail information for the invoice line item.
 
-Currently available options are: I<format>, I<escape_function> and
-I<format_function>.
+Options may include:
 
-If I<format> is set to html or latex then the array members are improved
-for tabular appearance in those environments if possible.
+I<format>: set to 'html' or 'latex' to have the detail lines formatted for 
+inclusion in an HTML table (wrapped in <tr> and <td> elements) or LaTeX table
+(delimited with & and \\ operators).
 
-If I<escape_function> is set then the array members are processed by this
+I<escape_function>: if present, then the array elements are processed by this
 function before being returned.
 
-I<format_function> overrides the normal HTML or LaTeX function for returning
-formatted CDRs.  It can be set to a subroutine which returns an empty list
-to skip usage detail:
+I<format_function>: overrides the normal HTML or LaTeX function for returning
+formatted CDRs.
 
-  'format_function' => sub { () },
+I<no_usage>: excludes call detail records.  The method will still return
+some special-case records like prorate details, and manually created package 
+details.
 
 =cut
 
index e0ea6ab..bfa03bc 100644 (file)
@@ -1051,6 +1051,7 @@ sub print_generic {
       $detail->{'edate'} = $line_item->{'edate'};
       $detail->{'seconds'} = $line_item->{'seconds'};
       $detail->{'svc_label'} = $line_item->{'svc_label'};
+      $detail->{'usage_item'} = $line_item->{'usage_item'};
   
       push @detail_items, $detail;
       push @buf, ( [ $detail->{'description'},
@@ -1390,6 +1391,37 @@ 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 ];
+  }
+
   # debugging hook: call this with 'diag' => 1 to just get a hash of 
   # the invoice variables
   return \%invoice_data if ( $params{'diag'} );
@@ -2494,6 +2526,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 ".
@@ -2554,6 +2589,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
@@ -2580,16 +2618,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
@@ -2663,6 +2705,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
@@ -2682,12 +2728,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;
 
@@ -2796,6 +2838,7 @@ 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,
index c2be4f2..4126d5f 100644 (file)
@@ -338,7 +338,7 @@ sub check {
   #check the foreign keys even?
   #do we want to outright *reject* the CDR?
   my $error =
-       $self->ut_numbern('acctid')
+       $self->ut_numbern('acctid');
 
   #add a config option to turn these back on if someone needs 'em
   #
@@ -350,7 +350,7 @@ sub check {
   #
   #  # Telstra =1, Optus = 2, RSL COM = 3
   #  || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
-  ;
+
   return $error if $error;
 
   $self->SUPER::check;
@@ -1210,6 +1210,10 @@ my %export_names = (
     'name'           => 'Summary, one line per destination prefix',
     'invoice_header' => 'Caller,Rate,Calls,Minutes,Price',
   },
+  'sum_count_class' => {
+    'name'           => 'Summary, one line per usage class',
+    'invoice_header' => 'Caller,Class,Calls,Price',
+  },
 );
 
 my %export_formats = ();
index 1b765fa..85c4bac 100644 (file)
@@ -2919,6 +2919,49 @@ sub _items_svc_phone_sections {
 
 }
 
+=sub _items_usage_class_summary OPTIONS
+
+Returns a list of detail items summarizing the usage charges on this 
+invoice.  Each one will have 'amount', 'description' (the usage charge name),
+and 'usage_classnum'.
+
+OPTIONS can include 'escape' (a function to escape the descriptions).
+
+=cut
+
+sub _items_usage_class_summary {
+  my $self = shift;
+  my %opt = @_;
+
+  my $escape = $opt{escape} || sub { $_[0] };
+  my $invnum = $self->invnum;
+  my @classes = qsearch({
+      'table'     => 'usage_class',
+      'select'    => 'classnum, classname, SUM(amount) AS amount',
+      'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
+                     ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
+      'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
+                     ' GROUP BY classnum, classname, weight'.
+                     ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
+                     ' ORDER BY weight ASC',
+  });
+  my @l;
+  my $section = {
+    description   => &{$escape}($self->mt('Usage Summary')),
+    no_subtotal   => 1,
+    usage_section => 1,
+  };
+  foreach my $class (@classes) {
+    push @l, {
+      'description'     => &{$escape}($class->classname),
+      'amount'          => sprintf('%.2f', $class->amount),
+      'usage_classnum'  => $class->classnum,
+      'section'         => $section,
+    };
+  }
+  return @l;
+}
+
 sub _items_previous {
   my $self = shift;
   my $conf = $self->conf;
index 46f6e17..d0cbdbe 100644 (file)
@@ -86,27 +86,15 @@ sub table { 'cust_bill_pkg_detail'; }
 Adds this record to the database.  If there is an error, returns the error,
 otherwise returns false.
 
-=cut
-
-# the insert method can be inherited from FS::Record
-
 =item delete
 
 Delete this record from the database.
 
-=cut
-
-# the delete method can be inherited from FS::Record
-
 =item replace OLD_RECORD
 
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
 returns the error, otherwise returns false.
 
-=cut
-
-# the replace method can be inherited from FS::Record
-
 =item check
 
 Checks all fields to make sure this is a valid line item detail.  If there is
@@ -145,6 +133,7 @@ sub check {
     || $self->ut_text('detail')
     || $self->ut_foreign_keyn('classnum', 'usage_class', 'classnum')
     || $self->$phonenum_check_method('phonenum')
+    || $self->ut_numbern('startdate')
     || $self->SUPER::check
     ;
 
@@ -237,6 +226,18 @@ sub formatted {
   ;
 }
 
+=item cust_bill_pkg
+
+Returns the L<FS::cust_bill_pkg> object (the invoice line item) that
+this detail belongs to.
+
+=cut
+
+sub cust_bill_pkg {
+  my $self = shift;
+  my $billpkgnum = $self->billpkgnum or return '';
+  FS::cust_bill_pkg->by_key($billpkgnum);
+}
 
 # Used by FS::Upgrade to migrate to a new database schema
 sub _upgrade_schema { # class method
index c90d313..b072ff5 100644 (file)
@@ -98,6 +98,19 @@ sub inbound {
   $self->{inbound};
 }
 
+=item phonenum VALUE
+
+Set/get the locally meaningful phone number.  This is used to tag call details
+for presentation on certain kinds of invoices.
+
+=cut
+
+sub phonenum {
+  my $self = shift;
+  $self->{phonenum} = shift if @_;
+  $self->{phonenum};
+}
+
 =item append CDRS
 
 Takes any number of call detail records (as L<FS::cdr> objects),
@@ -165,21 +178,15 @@ Takes a single CDR and returns an invoice detail to describe it.
 
 By default, this maps the following fields from the CDR:
 
-=over 4
+rated_price       => amount
+rated_classnum    => classnum
+rated_seconds     => duration
+rated_regionname  => regionname
+accountcode       => accountcode
+startdate         => startdate
 
-=item rated_price       => amount
-
-=item rated_classnum    => classnum
-
-=item rated_seconds     => duration
-
-=item rated_regionname  => regionname
-
-=item accountcode       => accountcode
-
-=item startdate         => startdate
-
-=back
+'phonenum' is set to the internal C<phonenum> value set on the formatter
+object.
 
 It then calls C<columns> on the CDR to obtain a list of detail
 columns, formats them as a CSV string, and stores that in the 
@@ -209,6 +216,7 @@ sub single_detail {
       'startdate'   => $cdr->startdate,
       'format'      => 'C',
       'detail'      => $self->csv->string,
+      'phonenum'    => $self->phonenum,
   });
 }
 
diff --git a/FS/FS/detail_format/sum_count_class.pm b/FS/FS/detail_format/sum_count_class.pm
new file mode 100644 (file)
index 0000000..749d452
--- /dev/null
@@ -0,0 +1,93 @@
+package FS::detail_format::sum_count_class;
+
+use strict;
+use vars qw( $DEBUG );
+use base qw(FS::detail_format);
+use FS::Record qw(qsearchs);
+use FS::cust_svc;
+use FS::svc_Common; # for label
+
+$DEBUG = 0;
+
+sub name { 'Summary, one line per service and usage class' };
+
+sub header_detail {
+  my $self = shift;
+  if ( $self->{inbound} ) {
+    'Destination,Charge Class,Quantity,Price'
+  }
+  else {
+    'Source,Charge Class,Quantity,Price'
+  }
+}
+
+sub append {
+  my $self = shift;
+  my $svcnums = ($self->{svcnums} ||= {});
+  my $acctids = $self->{acctids} ||= {};
+  foreach my $cdr (@_) {
+    my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
+    my $svcnum = $object->svcnum; # yes, $object->svcnum.
+
+    my $subtotal = ($svcnums->{$svcnum}->{$cdr->rated_classnum} ||=
+      { count => 0, duration => 0, amount => 0 });
+    $subtotal->{count}++;
+    $subtotal->{duration} += $object->rated_seconds;
+    $subtotal->{amount} += $object->rated_price
+      if $object->freesidestatus ne 'no-charge';
+
+    my $these_acctids = $acctids->{$cdr->rated_classnum} ||= [];
+    push @$these_acctids, $cdr->acctid;
+  }
+}
+
+sub finish {
+  my $self = shift;
+  my $svcnums = $self->{svcnums};
+  my $buffer = $self->{buffer};
+  foreach my $svcnum (keys %$svcnums) {
+
+    my $classnums = $svcnums->{$svcnum};
+
+    my $cust_svc = qsearchs('cust_svc', { svcnum => $svcnum })
+      or die "svcnum #$svcnum not found";
+    my $phonenum = $cust_svc->svc_x->label;
+    warn "processing $phonenum\n" if $DEBUG;
+
+    foreach my $classnum (keys %$classnums) {
+      my $subtotal = $classnums->{$classnum};
+      next if $subtotal->{amount} < 0.01;
+      my $classname = ($classnum ?
+                        FS::usage_class->by_key($classnum)->classname :
+                        '');
+      $self->csv->combine(
+        $phonenum,
+        $classname,
+        $subtotal->{count},
+        $self->money_char . sprintf('%.02f',$subtotal->{amount}),
+      );
+
+      warn "adding detail: ".$self->csv->string."\n" if $DEBUG;
+
+      push @$buffer, FS::cust_bill_pkg_detail->new({
+          amount      => $subtotal->{amount},
+          format      => 'C',
+          classnum    => $classnum,
+          duration    => $subtotal->{duration},
+          phonenum    => $phonenum,
+          accountcode => '', #ignored in this format
+          startdate   => '', #could use the earliest startdate in the bunch?
+          regionname  => '',
+          detail      => $self->csv->string,
+          acctid      => $self->{acctids}->{$classnum},
+      });
+    } #foreach $classnum
+  } #foreach $svcnum
+
+  # supposedly the compiler is smart enough to do this in place
+  @$buffer = sort { $a->{Hash}->{phonenum} cmp $b->{Hash}->{phonenum} or
+                    $a->{Hash}->{classnum} <=> $b->{Hash}->{classnum} } 
+              @$buffer;
+}
+
+1;
index b8f1eee..d3eff35 100644 (file)
@@ -465,6 +465,15 @@ sub calc_usage {
 
     #my @invoice_details_sort;
 
+    # for tagging invoice details
+    my $phonenum;
+    if ( $svc_table eq 'svc_phone' ) {
+      $phonenum = $svc_x->phonenum;
+    } elsif ( $svc_table eq 'svc_pbx' ) {
+      $phonenum = $svc_x->title;
+    }
+    $formatter->phonenum($phonenum);
+
     #first rate any outstanding CDRs not yet rated
     # XXX eventually use an FS::Cursor for this
     my $cdr_search = $svc_x->psearch_cdrs(%options);