[freeside-commits] freeside/FS/FS cdr.pm, 1.83, 1.84 cust_bill_pkg.pm, 1.66, 1.67 detail_format.pm, NONE, 1.1

Mark Wells mark at wavetail.420.am
Tue Jan 3 13:13:34 PST 2012


Update of /home/cvs/cvsroot/freeside/FS/FS
In directory wavetail.420.am:/tmp/cvs-serv18734/FS/FS

Modified Files:
	cdr.pm cust_bill_pkg.pm 
Added Files:
	detail_format.pm 
Log Message:
detail format refactor, #15535

--- NEW FILE: detail_format.pm ---
package FS::detail_format;

use strict;
use vars qw( $DEBUG );
use FS::Conf;
use FS::cdr;
use FS::cust_bill_pkg_detail;
use Date::Format qw(time2str);
use Text::CSV_XS;

my $me = '[FS::detail_format]';

=head1 NAME

FS::detail_format - invoice detail formatter

=head1 DESCRIPTION

An FS::detail_format object is a converter to create invoice details 
(L<FS::cust_bill_pkg_detail>) from call detail records (L<FS::cdr>)
or other usage information.  FS::detail_format inherits from nothing.

Subclasses of FS::detail_format represent specific detail formats.

=head1 CLASS METHODS

=over 4

=item new FORMAT, OPTIONS

Returns a new detail formatter.  The FORMAT argument is the name of 
a subclass.

OPTIONS may contain:

- buffer: an arrayref to store details into.  This may avoid the need for 
  a large copy operation at the end of processing.  However, since 
  summary formats will produce nothing until the end of processing, 
  C<finish> must be called after all CDRs have been appended.

- inbound: a flag telling the formatter to format CDRs for display to 
  the receiving party, rather than the originator.  In this case, the 
  L<FS::cdr_termination> object will be fetched and its values used for
  rated_price, rated_seconds, rated_minutes, and svcnum.  This can be 
  changed with the C<inbound> method.

=cut

sub new {
  my $class = shift;
  if ( $class eq 'FS::detail_format' ) {
    my $format = shift
      or die "$me format name required";
    $class = "FS::detail_format::$format"
      unless $format =~ /^FS::detail_format::/;
  }
  eval "use $class";
  die "$me error loading $class: $@" if $@;
  my %opt = @_;

  my $self = { conf => FS::Conf->new,
               csv  => Text::CSV_XS->new,
               inbound  => ($opt{'inbound'} ? 1 : 0),
               buffer   => ($opt{'buffer'} || []),
             }; 
  bless $self, $class;
}

=back

=head1 METHODS

=item inbound VALUE

Set/get the 'inbound' flag.

=cut

sub inbound {
  my $self = shift;
  $self->{inbound} = ($_[0] > 0) if (@_);
  $self->{inbound};
}

=item append CDRS

Takes any number of call detail records (as L<FS::cdr> objects),
formats them, and appends them to the internal buffer.

By default, this simply calls C<single_detail> on each CDR in the 
set.  Subclasses should override C<append> and maybe C<finish> if 
they do not produce detail lines from CDRs in a 1:1 fashion.

The 'billpkgnum', 'invnum', 'pkgnum', and 'phonenum' fields will 
be set later.

=cut

sub append {
  my $self = shift;
  foreach (@_) {
    push @{ $self->{buffer} }, $self->single_detail($_);
  }
}

=item details

Returns all invoice detail records in the buffer.  This will perform 
a C<finish> first.  Subclasses generally shouldn't override this.

=cut

sub details {
  my $self = shift;
  $self->finish;
  @{ $self->{buffer} }
}

=item finish

Ensures that all invoice details are generated given the CDRs that 
have been appended.  By default, this does nothing.

=cut

sub finish {}

=item header

Returns a header row for the format, as an L<FS::cust_bill_pkg_detail>
object.  By default this has 'format' = 'C', 'detail' = the value 
returned by C<header_detail>, and all other fields empty.

This is called after C<finish>, so it can use information from the CDRs.

=cut

sub header {
  my $self = shift;

  FS::cust_bill_pkg_detail->new(
    { 'format' => 'C', 'detail' => $self->header_detail }
  )
}

=item single_detail CDR

Takes a single CDR and returns an invoice detail to describe it.

By default, this maps the following fields from the CDR:

rated_price       => amount
rated_classnum    => classnum
rated_seconds     => duration
rated_regionname  => regionname
accountcode       => accountcode
startdate         => startdate

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 
'detail' field.

=cut

sub single_detail {
  my $self = shift;
  my $cdr = shift;

  my @columns = $self->columns($cdr);
  my $status = $self->csv->combine(@columns);
  die "$me error combining ".$self->csv->error_input."\n"
    if !$status;

  FS::cust_bill_pkg_detail->new( {
      'amount'      => $cdr->rated_price,
      'classnum'    => $cdr->rated_classnum,
      'duration'    => $cdr->rated_seconds,
      'regionname'  => $cdr->rated_regionname,
      'accountcode' => $cdr->accountcode,
      'startdate'   => $cdr->startdate,
      'format'      => 'C',
      'detail'      => $self->csv->string,
  });
}

=item columns CDR

Returns a list of CSV columns (to be shown on the invoice) for
the CDR.  This is the method most subclasses should override.

=cut

sub columns {
  my $self = shift;
  die "$me no columns method in ".ref($self);
}

=item header_detail

Returns the 'detail' field for the header row.  This should 
probably be a CSV string of column headers for the values returned
by C<columns>.

=cut

sub header_detail {
  my $self = shift;
  die "$me no header_detail method in ".ref($self);
}

# convenience methods for subclasses

sub conf { $_[0]->{conf} }

sub csv { $_[0]->{csv} }

sub date_format {
  my $self = shift;
  $self->{date_format} ||= ($self->conf->config('date_format') || '%m/%d/%Y');
}

sub money_char {
  my $self = shift;
  $self->{money_char} ||= ($self->conf->config('money_char') || '$');
}

#imitate previous behavior for now

sub duration {
  my $self = shift;
  my $cdr = shift;
  my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
  my $sec = $object->rated_seconds if $object;
  # XXX termination objects don't have rated_granularity so this may 
  # result in inbound CDRs being displayed as min/sec when they shouldn't.
  # Should probably fix this.
  if ( $cdr->rated_granularity eq '0' ) {
    '1 call';
  }
  elsif ( $cdr->rated_granularity eq '60' ) {
    sprintf('%dm', ($sec + 59)/60);
  }
  else {
    sprintf('%dm %ds', $sec / 60, $sec % 60);
  }
}

sub price {
  my $self = shift;
  my $cdr = shift;
  my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
  my $price = $object->rated_price if $object;
  length($price) ? $self->money_char . $price : '';
}

1;

Index: cust_bill_pkg.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/cust_bill_pkg.pm,v
retrieving revision 1.66
retrieving revision 1.67
diff -u -w -d -r1.66 -r1.67
--- cust_bill_pkg.pm	13 Dec 2011 20:40:28 -0000	1.66
+++ cust_bill_pkg.pm	3 Jan 2012 21:13:32 -0000	1.67
@@ -19,6 +19,8 @@
 use FS::cust_bill_pkg_tax_rate_location;
 use FS::cust_tax_adjustment;
 
+use List::Util qw(sum);
+
 @ISA = qw( FS::cust_main_Mixin FS::Record );
 
 $DEBUG = 0;
@@ -147,30 +149,8 @@
 
   if ( $self->get('details') ) {
     foreach my $detail ( @{$self->get('details')} ) {
-      my %hash = ();
-      if ( ref($detail) ) {
-        if ( ref($detail) eq 'ARRAY' ) {
-          #carp "this way sucks, use a hash"; #but more useful/friendly
-          $hash{'format'}      = $detail->[0];
-          $hash{'detail'}      = $detail->[1];
-          $hash{'amount'}      = $detail->[2];
-          $hash{'classnum'}    = $detail->[3];
-          $hash{'phonenum'}    = $detail->[4];
-          $hash{'accountcode'} = $detail->[5];
-          $hash{'startdate'}   = $detail->[6];
-          $hash{'duration'}    = $detail->[7];
-          $hash{'regionname'}  = $detail->[8];
-        } elsif ( ref($detail) eq 'HASH' ) {
-          %hash = %$detail;
-        } else {
-          die "unknow detail type ". ref($detail);
-        }
-      } else {
-        $hash{'detail'} = $detail;
-      }
-      $hash{'billpkgnum'} = $self->billpkgnum;
-      my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail \%hash;
-      $error = $cust_bill_pkg_detail->insert;
+      $detail->billpkgnum($self->billpkgnum);
+      $error = $detail->insert;
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
         return "error inserting cust_bill_pkg_detail: $error";
@@ -351,6 +331,8 @@
   ;
   return $error if $error;
 
+  $self->regularize_details;
+
   #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
   if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
     return "Unknown pkgnum ". $self->pkgnum
@@ -363,6 +345,50 @@
   $self->SUPER::check;
 }
 
+=item regularize_details
+
+Converts the contents of the 'details' pseudo-field to 
+L<FS::cust_bill_pkg_detail> objects, if they aren't already.
+
+=cut
+
+sub regularize_details {
+  my $self = shift;
+  if ( $self->get('details') ) {
+    foreach my $detail ( @{$self->get('details')} ) {
+      if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
+        # then turn it into one
+        my %hash = ();
+        if ( ! ref($detail) ) {
+          $hash{'detail'} = 'detail';
+        }
+        elsif ( ref($detail) eq 'HASH' ) {
+          %hash = %$detail;
+        }
+        elsif ( ref($detail) eq 'ARRAY' ) {
+          carp "passing invoice details as arrays is deprecated";
+          #carp "this way sucks, use a hash"; #but more useful/friendly
+          $hash{'format'}      = $detail->[0];
+          $hash{'detail'}      = $detail->[1];
+          $hash{'amount'}      = $detail->[2];
+          $hash{'classnum'}    = $detail->[3];
+          $hash{'phonenum'}    = $detail->[4];
+          $hash{'accountcode'} = $detail->[5];
+          $hash{'startdate'}   = $detail->[6];
+          $hash{'duration'}    = $detail->[7];
+          $hash{'regionname'}  = $detail->[8];
+        }
+        else {
+          die "unknown detail type ". ref($detail);
+        }
+        $detail = new FS::cust_bill_pkg_detail \%hash;
+      }
+      $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
+    }
+  }
+  return;
+}
+
 =item cust_pkg
 
 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
@@ -863,29 +889,15 @@
 
 sub usage {
   my( $self, $classnum ) = @_;
+  $self->regularize_details;
 
   if ( $self->get('details') ) {
 
-    my $sum = 0;
-    foreach my $value (
-      map { ref($_) eq 'HASH'
-              ? $_->{'amount'}
-              : $_->[2] 
-          }
-      grep { ref($_) && ( defined($classnum)
-                            ? $classnum eq ( ref($_) eq 'HASH'
-                                               ? $_->{'classnum'}
-                                               : $_->[3]
-                                           )
-                            : 1
-                        )
-           }
+    return sum( 
+      map { $_->amount || 0 }
+      grep { !defined($classnum) or $classnum eq $_->classnum }
       @{ $self->get('details') }
-    ) {
-      $sum += $value if $value;
-    }
-
-    return $sum;
+    );
 
   } else {
 
@@ -911,16 +923,11 @@
 
 sub usage_classes {
   my( $self ) = @_;
+  $self->regularize_details;
 
   if ( $self->get('details') ) {
 
-    my %seen = ();
-    foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
-      $seen{ (ref($detail) eq 'HASH'
-               ? $detail->{'classnum'}
-               : $detail->[3]) || ''
-           } = 1;
-    }
+    my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
     keys %seen;
 
   } else {

Index: cdr.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/cdr.pm,v
retrieving revision 1.83
retrieving revision 1.84
diff -u -w -d -r1.83 -r1.84
--- cdr.pm	3 Jan 2012 07:47:27 -0000	1.83
+++ cdr.pm	3 Jan 2012 21:13:32 -0000	1.84
@@ -10,6 +10,7 @@
 use Date::Parse;
 use Date::Format;
 use Time::Local;
+use List::Util qw( first min );
 use FS::UID qw( dbh );
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs );
@@ -426,7 +427,9 @@
 
 Sets the status and rated price.
 
-Available options are: inbound, rated_seconds, rated_minutes, rated_classnum, rated_ratename
+Available options are: inbound, rated_pretty_dst, rated_regionname,
+rated_seconds, rated_minutes, rated_granularity, rated_ratedetailnum,
+rated_classnum, rated_ratename.
 
 If there is an error, returns the error, otherwise returns false.
 
@@ -833,9 +836,11 @@
 sub rate_upstream_simple {
   my( $self, %opt ) = @_;
 
-  $self->set_status_and_rated_price( 'rated',
+  $self->set_status_and_rated_price(
+    'rated',
                                      sprintf('%.3f', $self->upstream_price),
                                      $opt{'svcnum'},
+    'rated_classnum' => $self->calltypenum,
                                    );
 }
 
@@ -874,9 +879,12 @@
     sprintf('%.4f', ( $part_pkg->option_cacheable('min_charge') * $charge_min )
                     + 0.0000000001 ); #so 1.00005 rounds to 1.0001
 
-  $self->set_status_and_rated_price( 'rated',
+  $self->set_status_and_rated_price(
+    'rated',
                                      $charge,
                                      $opt{'svcnum'},
+    'rated_granularity' => $granularity,
+    'rated_seconds'     => $seconds,
                                    );
 
 }
@@ -1021,13 +1029,17 @@
     'invoice_header' => 'Caller,Date,Time,Number,Destination,Duration,Price',
   },
   'sum_duration' => {
-    'name'           => 'Summary (one line per service, with duration)',
+    'name'           => 'Summary, one line per service',
     'invoice_header' => 'Caller,Rate,Calls,Minutes,Price',
   },
   'sum_count' => {
-    'name'           => 'Summary (one line per service, with count)',
+    'name'           => 'Number of calls, one line per service',
     'invoice_header' => 'Caller,Rate,Messages,Price',
   },
+  'sum_duration_prefix' => {
+    'name'           => 'Summary, one line per destination prefix',
+    'invoice_header' => 'Caller,Rate,Calls,Minutes,Price',
+  },
 );
 
 my %export_formats = ();
@@ -1216,6 +1228,8 @@
 
 =cut
 
+# in the future, load this dynamically from detail_format classes
+
 sub invoice_formats {
   map { ($_ => $export_names{$_}->{'name'}) }
     grep { $export_names{$_}->{'invoice_header'} }



More information about the freeside-commits mailing list