[freeside-commits] branch master updated. 03dfd875f74410d283060a4e54eaedf845bfcd39

Mark Wells mark at 420.am
Wed Jun 19 14:56:29 PDT 2013


The branch, master has been updated
       via  03dfd875f74410d283060a4e54eaedf845bfcd39 (commit)
      from  67e981bedc57f0834bdb43069366e6026c058705 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 03dfd875f74410d283060a4e54eaedf845bfcd39
Author: Mark Wells <mark at freeside.biz>
Date:   Wed Jun 19 14:56:25 2013 -0700

    improve accuracy of historical package counts, #8461

diff --git a/FS/FS/h_cust_pkg.pm b/FS/FS/h_cust_pkg.pm
index e796f41..99037c2 100644
--- a/FS/FS/h_cust_pkg.pm
+++ b/FS/FS/h_cust_pkg.pm
@@ -20,6 +20,79 @@ FS::h_cust_pkg - Historical record of customer package changes
 An FS::h_cust_pkg object represents historical changes to packages.
 FS::h_cust_pkg inherits from FS::h_Common and FS::cust_pkg.
 
+=head1 CLASS METHODS
+
+=over 4
+
+=item search HASHREF
+
+Like L<FS::cust_pkg::search>, but adapted for searching historical records.
+Takes the additional parameter "date", which is the timestamp to perform 
+the search "as of" (i.e. search the most recent insert or replace_new record
+for each pkgnum that is not later than that date).
+
+=cut
+
+sub search {
+  my ($class, $params) = @_;
+  my $date = delete $params->{'date'};
+  $date =~ /^\d*$/ or die "invalid search date '$date'\n";
+
+  my $query = FS::cust_pkg->search($params);
+
+  # allow multiple status criteria
+  # this might be useful in the base cust_pkg search, but I haven't 
+  # tested it there yet
+  my $status = delete $params->{'status'};
+  if( $status ) {
+    my @status_where;
+    foreach ( split(',', $status) ) {
+      if ( /^active$/ ) {
+        push @status_where, $class->active_sql();
+      } elsif ( /^not[ _]yet[ _]billed$/ ) {
+        push @status_where, $class->not_yet_billed_sql();
+      } elsif ( /^(one-time charge|inactive)$/ ) {
+        push @status_where, $class->inactive_sql();
+      } elsif ( /^suspended$/ ) {
+        push @status_where, $class->suspended_sql();
+      } elsif ( /^cancell?ed$/ ) {
+        push @status_where, $class->cancelled_sql();
+      }
+    }
+    if ( @status_where ) {
+      $query->{'extra_sql'}   .= ' AND ('.join(' OR ', @status_where).')';
+      $query->{'count_query'} .= ' AND ('.join(' OR ', @status_where).')';
+    }
+  }
+
+  # make some adjustments
+  $query->{'table'} = 'h_cust_pkg';
+  foreach (qw(select addl_from extra_sql count_query)) {
+    $query->{$_} =~ s/cust_pkg\b/h_cust_pkg/g;
+    $query->{$_} =~ s/cust_main\b/h_cust_main/g;
+  }
+  
+  my $and_where = " AND h_cust_pkg.historynum = 
+  (SELECT historynum FROM h_cust_pkg AS mostrecent
+  WHERE mostrecent.pkgnum = h_cust_pkg.pkgnum 
+  AND mostrecent.history_date <= $date
+  AND mostrecent.history_action IN ('insert', 'replace_new')
+  ORDER BY history_date DESC,historynum DESC LIMIT 1
+  ) AND h_cust_main.historynum =
+  (SELECT historynum FROM h_cust_main AS mostrecent
+  WHERE mostrecent.custnum = h_cust_main.custnum
+  AND mostrecent.history_date <= h_cust_pkg.history_date
+  AND mostrecent.history_action IN ('insert', 'replace_new')
+  ORDER BY history_date DESC,historynum DESC LIMIT 1
+  )";
+
+  $query->{'extra_sql'} .= $and_where;
+  $query->{'count_query'} .= $and_where;
+
+  $query;
+}
+
+
 =head1 BUGS
 
 =head1 SEE ALSO
diff --git a/httemplate/search/cust_pkg_summary.cgi b/httemplate/search/cust_pkg_summary.cgi
index d327489..c0eb699 100644
--- a/httemplate/search/cust_pkg_summary.cgi
+++ b/httemplate/search/cust_pkg_summary.cgi
@@ -1,25 +1,14 @@
-<% include('/elements/header.html', $title) %>
-<% include('/elements/table-grid.html') %>
-  <TR>
-% foreach (@head) {
-    <TH CLASS="grid" BGCOLOR="#cccccc"><% $_ %></TH>
-% }
-  </TR>
-% my $r=0;
-% foreach my $row (@rows) {
-  <TR>
-%   foreach (@$row) {
-    <TD CLASS="grid" ALIGN="right" BGCOLOR="<% $r % 2 ? '#ffffff' : '#eeeeee' %>"><% $_ %></TD>
-%   }
-  </TR>
-%   $r++;
-% }
-  <TR>
-% foreach (@totals) {
-    <TD CLASS="grid" ALIGN="right" BGCOLOR="<% $r % 2 ? '#ffffff' : '#eeeeee' %>"><B><% $_ %></B></TD>
-% }
-  </TR>
-</TABLE>
+<& elements/search.html,
+  'title'       => $title,
+  'name'        => 'package types',
+  'query'       => $query,
+  'count_query' => $count_query,
+  'header'      => \@head,
+  'fields'      => \@fields,
+  'links'       => \@links,
+  'align'       => 'clrrrrr',
+  'footer_data' => $totals,
+&>
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
@@ -34,83 +23,92 @@ if($begin > 0) {
     $cgi->param('beginning').' - '.$cgi->param('ending').')';
 }
 
-my @h_sql = FS::h_cust_pkg->sql_h_search($end);
-
-my ($end_sql, $addl_from) = @h_sql[1,3];
-$end_sql =~ s/ORDER BY.*//; # breaks aggregate queries
-
-my $begin_sql = $end_sql;
-$begin_sql =~ s/$end/$begin/g;
-
-my $active_sql = FS::cust_pkg->active_sql;
-my $suspended_sql = FS::cust_pkg->suspended_sql;
-my @conds = (
-  # SQL WHERE clauses for each column of the table.
-  " $begin_sql AND ($active_sql OR $suspended_sql)",
-  '',
-  " $end_sql AND ($active_sql OR $suspended_sql)",
-  " $end_sql AND $active_sql",
-  " $end_sql AND $suspended_sql",
-  );
-
-$_ =~ s/\bcust_pkg/maintable/g foreach @conds;
-
-my @head = ('Package', 'Before Period', 'Sales', 'Total', 'Active', 'Suspended');
-my @rows = ();
-my @totals = ('Total', 0, 0, 0, 0, 0);
-
-if( !$begin ) {
-  splice @conds, 1, 1;
-  splice @head, 1, 1;
-}
-
 my $agentnums_sql = $curuser->agentnums_sql(
                       'null'       => 1,
-                      'table'      => 'part_pkg',
+                      'table'      => 'main',
                     );
 
-my $extra_sql = " WHERE $agentnums_sql";
+my $extra_sql = " freq != '0' AND $agentnums_sql";
 
 #tiny bit of false laziness w/cust_pkg.pm::search
 if ( grep { $_ eq 'classnum' } $cgi->param ) {
   if ( $cgi->param('classnum') eq '' ) {
-    $extra_sql .= ' AND part_pkg.classnum IS NULL';
+    $extra_sql .= ' AND main.classnum IS NULL';
   } elsif ( $cgi->param('classnum') =~ /^(\d+)$/ && $1 ne '0' ) {
-    $extra_sql .= " AND part_pkg.classnum = $1 ";
+    $extra_sql .= " AND main.classnum = $1 ";
   }
 }
 
-foreach my $part_pkg (qsearch({ 'table'     => 'part_pkg',
-                                'hashref'   => {},
-                                'extra_sql' => $extra_sql,
-                             })
-                     )
-{
-  my @row = ();
-  next if !$part_pkg->freq; # exclude one-time packages
-  push @row, $part_pkg->pkg;
-  my $i=1;
-  foreach my $cond (@conds) {
-    if($cond) {
-      my $result = qsearchs({ 
-                            'table'     => 'h_cust_pkg',
-                            'addl_from' => $addl_from.
-                                           ' LEFT JOIN cust_main USING ( custnum )',
-
-                            'hashref'   => {},
-                            'select'    => 'count(*)',
-                            'extra_sql' => 'WHERE pkgpart = '.$part_pkg->pkgpart.$cond.
-                                           ' AND '. $curuser->agentnums_sql(
-                                                      'table' => 'cust_main',
-                                                    ),
-                            });
-      $row[$i] = $result->getfield('count');
-      $totals[$i] += $row[$i];
-    }
-    $i++;
-  }
-  $row[2] = $row[3]-$row[1];
-  $totals[2] += $row[2];
-  push @rows, \@row;
+my $active_sql = 'setup IS NOT NULL AND susp IS NULL AND cancel IS NULL';
+my $suspended_sql = 'setup IS NOT NULL AND susp IS NOT NULL AND cancel IS NULL';
+my $active_or_suspended_sql = 'setup IS NOT NULL AND cancel IS NULL';
+my %conds;
+
+$conds{'before'} = { 'date' => $begin, 'status' => 'active,suspended' };
+$conds{'after'}  = { 'date' => $end,   'status' => 'active,suspended' };
+$conds{'active'} = { 'date' => $end,   'status' => 'active' };
+$conds{'suspended'} = { 'date' => $end, 'status' => 'suspended' };
+
+my @select;
+my $totals = FS::part_pkg->new({pkg => 'Total'});
+foreach my $column (keys %conds) {
+  my $h_search = FS::h_cust_pkg->search($conds{$column});
+  my $count_query = $h_search->{count_query};
+
+  # push a select expression for the total packages with pkgpart=main.pkgpart
+  push @select, "($count_query AND h_cust_pkg.pkgpart = main.pkgpart) AS $column";
+
+  # and query the total packages with pkgpart=any of the main.pkgparts
+  my $total = FS::Record->scalar_sql($count_query . 
+    " AND h_cust_pkg.pkgpart IN(SELECT pkgpart FROM part_pkg AS main WHERE $extra_sql)"
+  );
+  $totals->set($column => $total);
+}
+
+my $query = {
+  'table'       => 'part_pkg',
+  'addl_from'   => 'AS main',
+  'select'      => join(', ', 'main.*', @select),
+  'extra_sql'   => "WHERE $extra_sql",
+};
+
+my $count_query = "SELECT COUNT(*) FROM part_pkg AS main WHERE $extra_sql";
+
+my $baselink = "h_cust_pkg.html?";
+if ( $cgi->param('classnum') =~ /^\d*$/ ) {
+  $baselink .= "classnum=".$cgi->param('classnum').';';
+}
+my @links = ( #arguments to h_cust_pkg.html, except for pkgpart
+  '',
+  '',
+  [ $baselink . "status=active,suspended;date=$begin;pkgpart=", 'pkgpart' ],
+  '',
+  [ $baselink . "status=active,suspended;date=$end;pkgpart=", 'pkgpart' ],
+  [ $baselink . "status=active;date=$end;pkgpart=", 'pkgpart' ],
+  [ $baselink . "status=suspended;date=$end;pkgpart=", 'pkgpart' ],
+);
+
+my @head = ('#',
+            'Package',
+            'Before Period',
+            'Sales',
+            'Total',
+            'Active',
+            'Suspended');
+
+my @fields = (
+  'pkgpart',
+  'pkg',
+  'before',
+  sub { $_[0]->after - $_[0]->before },
+  'after',
+  'active',
+  'suspended',
+  );
+
+if ( !$begin ) {
+  # remove the irrelevant 'before' column
+  splice(@$_,2,1) foreach \@head, \@fields, \@links;
 }
+
 </%init>
diff --git a/httemplate/search/elements/search-html.html b/httemplate/search/elements/search-html.html
index e760bc5..bee33cf 100644
--- a/httemplate/search/elements/search-html.html
+++ b/httemplate/search/elements/search-html.html
@@ -253,6 +253,12 @@
 %                   $bgcolor = $bgcolor1;
 %                 }
 
+%                 my $rowstyle = '';
+%                 if ( $row eq $opt{'footer_data'} ) {
+%                   $rowstyle = ' STYLE="border-top: dashed 1px black; font-style: italic"';
+%                   $bgcolor = '#dddddd';
+%                 }
+
 %                 my $trid = '';
 %                   if ( $opt{'link_field' } ) {
 %                     my $link_field = $opt{'link_field'};
@@ -262,7 +268,7 @@
 %                       $trid = $row->$link_field();
 %                     }
 %                   }
-                  <TR ID="<%$trid |h%>">
+                  <TR ID="<%$trid |h%>"<%$rowstyle%>>
                       
 
 %                   if ( $opt{'fields'} ) {
diff --git a/httemplate/search/elements/search-xls.html b/httemplate/search/elements/search-xls.html
index bc844a5..8334497 100644
--- a/httemplate/search/elements/search-xls.html
+++ b/httemplate/search/elements/search-xls.html
@@ -32,7 +32,10 @@ my $XLS = new IO::Scalar \$data;
 my $workbook = $format->{class}->new($XLS)
   or die "Error opening Excel file: $!";
 
-my $worksheet = $workbook->add_worksheet(substr($opt{'title'},0,31));
+my $title = $opt{'title'};
+$title =~ s/[\[\]\:\*\?\/\/]//g;
+$title = substr($title, 0, 31);
+my $worksheet = $workbook->add_worksheet($title);
 
 $worksheet->protect();
 
diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html
index d44b454..8f62720 100644
--- a/httemplate/search/elements/search.html
+++ b/httemplate/search/elements/search.html
@@ -453,4 +453,6 @@ if ( ref($opt{query}) ) {
   $header ||= $sth->{NAME};
 }
 
+push @$rows, $opt{'footer_data'} if $opt{'footer_data'};
+
 </%init>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/h_cust_pkg.pm                         |   73 +++++++++++
 httemplate/search/cust_pkg_summary.cgi      |  174 +++++++++++++--------------
 httemplate/search/elements/search-html.html |    8 +-
 httemplate/search/elements/search-xls.html  |    5 +-
 httemplate/search/elements/search.html      |    2 +
 5 files changed, 172 insertions(+), 90 deletions(-)




More information about the freeside-commits mailing list