historical (immutable) invoice details about services and other history infrastructure
authorivan <ivan>
Wed, 29 Dec 2004 12:00:08 +0000 (12:00 +0000)
committerivan <ivan>
Wed, 29 Dec 2004 12:00:08 +0000 (12:00 +0000)
26 files changed:
ANNOUNCE.1.5.0
FS/FS/Record.pm
FS/FS/cust_bill.pm
FS/FS/cust_main.pm
FS/FS/cust_pkg.pm
FS/FS/cust_svc.pm
FS/FS/h_Common.pm [new file with mode: 0644]
FS/FS/h_cust_svc.pm [new file with mode: 0644]
FS/FS/h_svc_acct.pm [new file with mode: 0644]
FS/FS/h_svc_broadband.pm [new file with mode: 0644]
FS/FS/h_svc_domain.pm [new file with mode: 0644]
FS/FS/h_svc_external.pm [new file with mode: 0644]
FS/FS/h_svc_forward.pm [new file with mode: 0644]
FS/FS/h_svc_www.pm [new file with mode: 0644]
FS/FS/svc_acct.pm
FS/MANIFEST
FS/t/h_Common.t [new file with mode: 0644]
FS/t/h_cust_svc.t [new file with mode: 0644]
FS/t/h_svc_acct.t [new file with mode: 0644]
FS/t/h_svc_broadband.t [new file with mode: 0644]
FS/t/h_svc_domain.t [new file with mode: 0644]
FS/t/h_svc_external.t [new file with mode: 0644]
FS/t/h_svc_forward.t [new file with mode: 0644]
FS/t/h_svc_www.t [new file with mode: 0644]
httemplate/view/cust_main.cgi
httemplate/view/cust_main/packages.html

index 9d2b97e..fad6c92 100644 (file)
@@ -21,7 +21,7 @@
 1.5.0pre7:
 - fix bug that could cause mis-billing on upgrades! (new installs ok)
 - update install documentation for 1.5 HTML::Mason or Apache::ASP install
-- historical late notice viewing in web interface
+- historical late notice viewing in web interface
 - VoIP billing for CDRs from RADIUS
 - promotional codes for signup
 - lots of RT integration, integrated RT upgraded to 3.2.2
index 5a6bb57..cf0ac3b 100644 (file)
@@ -180,7 +180,7 @@ sub create {
   }
 }
 
-=item qsearch TABLE, HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ
+=item qsearch TABLE, HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ, AS
 
 Searches the database for all records matching (at least) the key/value pairs
 in HASHREF.  Returns all the records found as `FS::TABLE' objects if that
@@ -199,7 +199,7 @@ objects.
 =cut
 
 sub qsearch {
-  my($stable, $record, $select, $extra_sql, $cache ) = @_;
+  my($stable, $record, $select, $extra_sql, $cache, $as ) = @_;
   #$stable =~ /^([\w\_]+)$/ or die "Illegal table: $table";
   #for jsearch
   $stable =~ /^([\w\s\(\)\.\,\=]+)$/ or die "Illegal table: $stable";
@@ -223,6 +223,7 @@ sub qsearch {
   }
 
   my $statement = "SELECT $select FROM $stable";
+  $statement .= " AS $as" if $as;
   if ( @real_fields or @virtual_fields ) {
     $statement .= ' WHERE '. join(' AND ',
       ( map {
index 0306c01..93c179a 100644 (file)
@@ -356,14 +356,13 @@ sub send {
       'body'    => \@print_text,
     );
     die "can't email invoice: $error\n" if $error;
+    #die "$error\n" if $error;
 
   }
 
-  if ( $conf->config('invoice_latex') ) {
-    @print_text = $self->print_ps('', $template);
-  }
-
   if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
+    @print_text = $self->print_ps('', $template)
+      if $conf->config('invoice_latex');
     my $lpr = $conf->config('lpr');
     open(LPR, "|$lpr")
       or die "Can't open pipe to $lpr: $!\n";
@@ -796,7 +795,8 @@ sub print_text {
         push @buf, [ $description,
                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
         push @buf,
-          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
+          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
+              $cust_pkg->h_labels($self->_date);
       }
 
       if ( $cust_bill_pkg->recur != 0 ) {
@@ -806,7 +806,8 @@ sub print_text {
           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
         ];
         push @buf,
-          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
+          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
+              $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
       }
 
       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
@@ -1366,45 +1367,32 @@ sub _items_cust_bill_pkg {
       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
       my $pkg = $part_pkg->pkg;
 
-      my %labels;
-      #tie %labels, 'Tie::IxHash';
-      push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
-      my @ext_description;
-      foreach my $label ( keys %labels ) {
-        my @values = @{ $labels{$label} };
-        my $num = scalar(@values);
-        if ( $num > 5 ) {
-          push @ext_description, "$label ($num)";
-        } else {
-          push @ext_description, map { "$label: $_" } @values;
-        }
-      }
-
       if ( $cust_bill_pkg->setup != 0 ) {
         my $description = $pkg;
         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
-        my @d = @ext_description;
+        my @d = $cust_pkg->h_labels_short($self->_date);
         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
         push @b, {
-          'description'     => $description,
-          #'pkgpart'         => $part_pkg->pkgpart,
-          'pkgnum'          => $cust_pkg->pkgnum,
-          'amount'          => sprintf("%10.2f", $cust_bill_pkg->setup),
-          'ext_description' => \@d,
+          description     => $description,
+          #pkgpart         => $part_pkg->pkgpart,
+          pkgnum          => $cust_pkg->pkgnum,
+          amount          => sprintf("%10.2f", $cust_bill_pkg->setup),
+          ext_description => \@d,
         };
       }
 
       if ( $cust_bill_pkg->recur != 0 ) {
         push @b, {
-          'description'     => "$pkg (" .
+          description     => "$pkg (" .
                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
                                time2str('%x', $cust_bill_pkg->edate). ')',
-          #'pkgpart'         => $part_pkg->pkgpart,
-          'pkgnum'          => $cust_pkg->pkgnum,
-          'amount'          => sprintf("%10.2f", $cust_bill_pkg->recur),
-          'ext_description' => [ @ext_description,
-                                 $cust_bill_pkg->details,
-                               ],
+          #pkgpart         => $part_pkg->pkgpart,
+          pkgnum          => $cust_pkg->pkgnum,
+          amount          => sprintf("%10.2f", $cust_bill_pkg->recur),
+          ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
+                                                         $cust_bill_pkg->sdate),
+                               $cust_bill_pkg->details,
+                             ],
         };
       }
 
index c42d222..44fb8d6 100644 (file)
@@ -1009,6 +1009,27 @@ sub unsuspended_pkgs {
   grep { ! $_->susp } $self->ncancelled_pkgs;
 }
 
+=item num_cancelled_pkgs
+
+Returns the number of cancelled packages (see L<FS::cust_pkg>) for this
+customer.
+
+=cut
+
+sub num_cancelled_pkgs {
+  my $self = shift;
+  $self->num_pkgs("cancel IS NOT NULL AND cust_pkg.cancel != 0");
+}
+
+sub num_pkgs {
+  my( $self, $sql ) = @_;
+  my $sth = dbh->prepare(
+    "SELECT COUNT(*) FROM cust_pkg WHERE custnum = ? AND $sql"
+  ) or die dbh->errstr;
+  $sth->execute($self->custnum) or die $sth->errstr;
+  $sth->fetchrow_arrayref->[0];
+}
+
 =item unsuspend
 
 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
index 630e88e..ced1423 100644 (file)
@@ -11,6 +11,7 @@ use FS::cust_main;
 use FS::type_pkgs;
 use FS::pkg_svc;
 use FS::cust_bill_pkg;
+use FS::h_cust_svc;
 
 # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
 # setup }
@@ -543,21 +544,50 @@ sub cust_svc {
   #if ( $self->{'_svcnum'} ) {
   #  values %{ $self->{'_svcnum'}->cache };
   #} else {
-    map  { $_->[0] }
-    sort { $b->[1] cmp $a->[1]  or  $a->[2] <=> $b->[2] } 
-    map {
-          my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $self->pkgpart,
-                                               'svcpart' => $_->svcpart     } );
-          [ $_,
-            $pkg_svc ? $pkg_svc->primary_svc : '',
-            $pkg_svc ? $pkg_svc->quantity : 0,
-          ];
-        }
-    qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } );
+    $self->_sort_cust_svc(
+      [ qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } ) ]
+    );
   #}
 
 }
 
+=item h_cust_svc END_TIMESTAMP [ START_TIMESTAMP ] 
+
+Returns historical services for this package created before END TIMESTAMP and
+(optionally) not cancelled before START_TIMESTAMP, as FS::h_cust_svc objects
+(see L<FS::h_cust_svc>).
+
+=cut
+
+sub h_cust_svc {
+  my $self = shift;
+
+  $self->_sort_cust_svc(
+    [ qsearch( 'h_cust_svc',
+               { 'pkgnum' => $self->pkgnum, },
+               FS::h_cust_svc->sql_h_search(@_),
+             )
+    ]
+  );
+}
+
+sub _sort_cust_svc {
+  my( $self, $arrayref ) = @_;
+
+  map  { $_->[0] }
+  sort { $b->[1] cmp $a->[1]  or  $a->[2] <=> $b->[2] } 
+  map {
+        my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $self->pkgpart,
+                                             'svcpart' => $_->svcpart     } );
+        [ $_,
+          $pkg_svc ? $pkg_svc->primary_svc : '',
+          $pkg_svc ? $pkg_svc->quantity : 0,
+        ];
+      }
+  @$arrayref;
+
+}
+
 =item num_cust_svc [ SVCPART ]
 
 Returns the number of provisioned services for this package.  If a svcpart is
@@ -606,6 +636,52 @@ sub labels {
   map { [ $_->label ] } $self->cust_svc;
 }
 
+=item h_labels END_TIMESTAMP [ START_TIMESTAMP ] 
+
+Like the labels method, but returns historical information on services that
+were active as of END_TIMESTAMP and (optionally) not cancelled before
+START_TIMESTAMP.
+
+Returns a list of lists, calling the label method for all (historical) services
+(see L<FS::h_cust_svc>) of this billing item.
+
+=cut
+
+sub h_labels {
+  my $self = shift;
+  map { [ $_->label(@_) ] } $self->h_cust_svc(@_);
+}
+
+=item h_labels_short END_TIMESTAMP [ START_TIMESTAMP ]
+
+Like h_labels, except returns a simple flat list, and shortens long 
+(currently >5) lists of identical services to one line that lists the service
+label and the number of individual services rather than individual items.
+
+=cut
+
+sub h_labels_short {
+  my $self = shift;
+
+  my %labels;
+  #tie %labels, 'Tie::IxHash';
+  push @{ $labels{$_->[0]} }, $_->[1]
+    foreach $self->h_labels(@_);
+  my @labels;
+  foreach my $label ( keys %labels ) {
+    my @values = @{ $labels{$label} };
+    my $num = scalar(@values);
+    if ( $num > 5 ) {
+      push @labels, "$label ($num)";
+    } else {
+      push @labels, map { "$label: $_" } @values;
+    }
+  }
+
+ @labels;
+
+}
+
 =item cust_main
 
 Returns the parent customer object (see L<FS::cust_main>).
index 8990d54..9cb7a81 100644 (file)
@@ -1,8 +1,8 @@
 package FS::cust_svc;
 
 use strict;
-use vars qw( @ISA $ignore_quantity );
-use Carp qw( cluck );
+use vars qw( @ISA $DEBUG $ignore_quantity );
+use Carp qw( carp cluck );
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::cust_pkg;
@@ -19,6 +19,8 @@ use FS::part_export;
 
 @ISA = qw( FS::Record );
 
+$DEBUG = 1;
+
 $ignore_quantity = 0;
 
 sub _cache {
@@ -276,37 +278,44 @@ Returns a list consisting of:
 
 sub label {
   my $self = shift;
-  my $svcdb = $self->part_svc->svcdb;
+  carp "FS::cust_svc::label called on $self" if $DEBUG;
   my $svc_x = $self->svc_x
-    or die "can't find $svcdb.svcnum ". $self->svcnum;
+    or die "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
+  $self->_svc_label($svc_x);
+}
+
+sub _svc_label {
+  my( $self, $svc_x ) = ( shift, shift );
+  my $svcdb = $self->part_svc->svcdb;
+
   my $tag;
   if ( $svcdb eq 'svc_acct' ) {
-    $tag = $svc_x->email;
+    $tag = $svc_x->email(@_);
   } elsif ( $svcdb eq 'svc_forward' ) {
     if ( $svc_x->srcsvc ) {
-      my $svc_acct = $svc_x->srcsvc_acct;
-      $tag = $svc_acct->email;
+      my $svc_acct = $svc_x->srcsvc_acct(@_);
+      $tag = $svc_acct->email(@_);
     } else {
       $tag = $svc_x->src;
     }
     $tag .= '->';
     if ( $svc_x->dstsvc ) {
-      my $svc_acct = $svc_x->dstsvc_acct;
-      $tag .= $svc_acct->email;
+      my $svc_acct = $svc_x->dstsvc_acct(@_);
+      $tag .= $svc_acct->email(@_);
     } else {
       $tag .= $svc_x->dst;
     }
   } elsif ( $svcdb eq 'svc_domain' ) {
     $tag = $svc_x->getfield('domain');
   } elsif ( $svcdb eq 'svc_www' ) {
-    my $domain = qsearchs( 'domain_record', { 'recnum' => $svc_x->recnum } );
-    $tag = $domain->zone;
+    my $domain_record = $svc_x->domain_record;
+    $tag = $domain_record->zone;
   } elsif ( $svcdb eq 'svc_broadband' ) {
     $tag = $svc_x->ip_addr;
   } elsif ( $svcdb eq 'svc_external' ) {
     my $conf = new FS::Conf;
     if ( $conf->config('svc_external-display_type') eq 'artera_turbo' ) {
-      $tag = sprintf('%010d', $svc_x->id). '-'. $svc_x->title;
+      $tag = sprintf('%010d', $svc_x->id). '-'. sprintf('%010d', $svc_x->title);
     } else {
       $tag = $svc_x->id. ': '. $svc_x->title;
     }
@@ -314,7 +323,9 @@ sub label {
     cluck "warning: asked for label of unsupported svcdb; using svcnum";
     $tag = $svc_x->getfield('svcnum');
   }
+
   $self->part_svc->svc, $tag, $svcdb;
+
 }
 
 =item svc_x
@@ -566,7 +577,7 @@ sub get_session_history {
   my @sessions = ();
 
   foreach my $part_export ( @part_export ) {
-    push @sessions, $part_export->usage_sessions( $self->svc_x, $start, $end );
+    push @sessions, $part_export->usage_sessions( $start, $end, $self->svc_x );
   }
 
   \@sessions;
diff --git a/FS/FS/h_Common.pm b/FS/FS/h_Common.pm
new file mode 100644 (file)
index 0000000..58ead88
--- /dev/null
@@ -0,0 +1,83 @@
+package FS::h_Common;
+
+use strict;
+use FS::Record qw(dbdef);
+
+=head1 NAME
+
+FS::h_Common - History table "mixin" common base class
+
+=head1 SYNOPSIS
+
+package FS::h_tablename;
+@ISA = qw( FS::h_Common FS::tablename ); 
+
+sub table { 'h_table_name'; }
+
+sub insert { return "can't insert history records manually"; }
+sub delete { return "can't delete history records"; }
+sub replace { return "can't modify history records"; }
+
+=head1 DESCRIPTION
+
+FS::h_Common is intended as a "mixin" base class for history table classes to
+inherit from.
+
+=head1 METHODS
+
+=over 4
+
+=item sql_h_search END_TIMESTAMP [ START_TIMESTAMP ] 
+
+Returns an a list consisting of the "SELECT" and "EXTRA_SQL" SQL fragments to
+search for the appropriate history records created before END_TIMESTAMP
+and (optionally) not cancelled before START_TIMESTAMP.
+
+=cut
+
+sub sql_h_search {
+  my( $self, $end ) = ( shift, shift );
+
+  my $table = $self->table;
+  my $pkey = dbdef->table($table)->primary_key
+    or die "can't (yet) search history table $table without a primary key";
+
+  my $notcancelled = '';
+  if ( scalar(@_) && $_[0] ) {
+    $notcancelled = "AND 0 = ( SELECT COUNT(*) FROM $table as notdel
+                                WHERE notdel.$pkey = maintable.$pkey
+                                AND notdel.history_action = 'delete'
+                                AND notdel.history_date > maintable.history_date
+                                AND notdel.history_date <= $_[0]
+                             )";
+  }
+
+  (
+    "DISTINCT ON ( $pkey ) *",
+
+    "AND history_date <= $end
+     AND (    history_action = 'insert'
+           OR history_action = 'replace_new'
+         )
+     $notcancelled
+     ORDER BY $pkey ASC, history_date DESC",
+
+     '',
+
+     'maintable',
+  );
+
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation
+
+=cut
+
+1;
+
diff --git a/FS/FS/h_cust_svc.pm b/FS/FS/h_cust_svc.pm
new file mode 100644 (file)
index 0000000..9ef60fd
--- /dev/null
@@ -0,0 +1,84 @@
+package FS::h_cust_svc;
+
+use strict;
+use vars qw( @ISA $DEBUG );
+use Carp;
+use FS::Record qw(qsearchs);
+use FS::h_Common;
+use FS::cust_svc;
+
+@ISA = qw( FS::h_Common FS::cust_svc );
+
+$DEBUG = 0;
+
+sub table { 'h_cust_svc'; }
+
+=head1 NAME
+
+FS::h_cust_svc - Object method for h_cust_svc objects
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+An FS::h_cust_svc object  represents a historical service.  FS::h_cust_svc
+inherits from FS::h_Common and FS::cust_svc.
+
+=head1 METHODS
+
+=over 4
+
+=item label END_TIMESTAMP [ START_TIMESTAMP ] 
+
+Returns a list consisting of:
+- The name of this historical service (from part_svc)
+- A meaningful identifier (username, domain, or mail alias)
+- The table name (i.e. svc_domain) for this historical service
+
+=cut
+
+sub label {
+  my $self = shift;
+  carp "FS::h_cust_svc::label called on $self" if $DEBUG;
+  my $svc_x = $self->h_svc_x(@_)
+    or die "can't find h_". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
+  $self->_svc_label($svc_x, @_);
+}
+
+=item h_svc_x END_TIMESTAMP [ START_TIMESTAMP ] 
+
+Returns the FS::h_svc_XXX object for this service as of END_TIMESTAMP (i.e. an
+FS::h_svc_acct object or FS::h_svc_domain object, etc.) and (optionally) not
+cancelled before START_TIMESTAMP.
+
+=cut
+
+#false laziness w/cust_pkg::h_cust_svc
+sub h_svc_x {
+  my $self = shift;
+  my $svcdb = $self->part_svc->svcdb;
+  #if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
+  #  $self->{'_svc_acct'};
+  #} else {
+    warn "requiring FS/h_$svcdb.pm" if $DEBUG;
+    require "FS/h_$svcdb.pm";
+    qsearchs( "h_$svcdb",
+              { 'svcnum'       => $self->svcnum, },
+              "FS::h_$svcdb"->sql_h_search(@_),
+            );
+  #}
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::h_Common>, L<FS::cust_svc>, L<FS::Record>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/h_svc_acct.pm b/FS/FS/h_svc_acct.pm
new file mode 100644 (file)
index 0000000..cd98dd4
--- /dev/null
@@ -0,0 +1,53 @@
+package FS::h_svc_acct;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw(qsearchs);
+use FS::h_Common;
+use FS::svc_acct;
+use FS::h_svc_domain;
+
+@ISA = qw( FS::h_Common FS::svc_acct );
+
+sub table { 'h_svc_acct' };
+
+=head1 NAME
+
+FS::h_svc_acct - Historical account objects
+
+=head1 SYNOPSIS
+
+=head1 METHODS
+
+=over 4
+
+=item svc_domain
+
+=cut
+
+sub svc_domain {
+  my $self = shift;
+  qsearchs( 'h_svc_domain',
+            { 'svcnum' => $self->domsvc },
+            FS::h_svc_domain->sql_h_search(@_),
+          );
+}
+
+=back
+
+=head1 DESCRIPTION
+
+An FS::h_svc_acct object represents a historical account.  FS::h_svc_acct
+inherits from FS::h_Common and FS::svc_acct.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::h_Common>, L<FS::svc_acct>, L<FS::Record>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/h_svc_broadband.pm b/FS/FS/h_svc_broadband.pm
new file mode 100644 (file)
index 0000000..d6038fb
--- /dev/null
@@ -0,0 +1,33 @@
+package FS::h_svc_broadband;
+
+use strict;
+use vars qw( @ISA );
+use FS::h_Common;
+use FS::svc_broadband;
+
+@ISA = qw( FS::h_Common FS::svc_broadband );
+
+sub table { 'h_svc_broadband' };
+
+=head1 NAME
+
+FS::h_svc_broadband - Historical broadband connection objects
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+An FS::h_svc_broadband object represents a historical broadband connection.
+FS::h_svc_broadband inherits from FS::h_Common and FS::svc_broadband.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::h_Common>, L<FS::svc_broadband>, L<FS::Record>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/h_svc_domain.pm b/FS/FS/h_svc_domain.pm
new file mode 100644 (file)
index 0000000..60d54f7
--- /dev/null
@@ -0,0 +1,33 @@
+package FS::h_svc_domain;
+
+use strict;
+use vars qw( @ISA );
+use FS::h_Common;
+use FS::svc_domain;
+
+@ISA = qw( FS::h_Common FS::svc_domain );
+
+sub table { 'h_svc_domain' };
+
+=head1 NAME
+
+FS::h_svc_domain - Historical domain objects
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+An FS::h_svc_domain object represents a historical domain.  FS::h_svc_domain
+inherits from FS::h_Common and FS::svc_domain.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::h_Common>, L<FS::svc_domain>, L<FS::Record>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/h_svc_external.pm b/FS/FS/h_svc_external.pm
new file mode 100644 (file)
index 0000000..5eb7064
--- /dev/null
@@ -0,0 +1,33 @@
+package FS::h_svc_external;
+
+use strict;
+use vars qw( @ISA );
+use FS::h_Common;
+use FS::svc_external;
+
+@ISA = qw( FS::h_Common FS::svc_external );
+
+sub table { 'h_svc_external' };
+
+=head1 NAME
+
+FS::h_svc_external - Historical externally tracked service objects
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+An FS::h_svc_external object represents a historical externally tracked service.
+FS::h_svc_external inherits from FS::h_Common and FS::svc_external.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::h_Common>, L<FS::svc_external>, L<FS::Record>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/h_svc_forward.pm b/FS/FS/h_svc_forward.pm
new file mode 100644 (file)
index 0000000..231f9df
--- /dev/null
@@ -0,0 +1,65 @@
+package FS::h_svc_forward;
+
+use strict;
+use vars qw( @ISA );
+se FS::Record qw(qsearchs);
+use FS::h_Common;
+use FS::svc_forward;
+use FS::h_svc_acct;
+
+@ISA = qw( FS::h_Common FS::svc_forward );
+
+sub table { 'h_svc_forward' };
+
+=head1 NAME
+
+FS::h_svc_forward - Historical mail forwarding alias objects
+
+=head1 SYNOPSIS
+
+=head1 METHODS
+
+=over 4
+
+=item srcsvc_acct 
+
+=cut
+
+sub srcsvc_acct {
+  my $self = shift;
+  qsearchs( 'h_svc_acct',
+            { 'svcnum' => $self->srcsvc },
+            FS::h_svc_acct->sql_h_search(@_),
+          );
+}
+
+=item dstsvc_acct
+
+=cut
+
+sub dstsvc_acct {
+  my $self = shift;
+  qsearchs( 'h_svc_acct',
+            { 'svcnum' => $self->dstsvc },
+            FS::h_svc_acct->sql_h_search(@_),
+          );
+}
+
+=back
+
+=head1 DESCRIPTION
+
+An FS::h_svc_forward object represents a historical mail forwarding alias.
+FS::h_svc_forward inherits from FS::h_Common and FS::svc_forward.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::h_Common>, L<FS::svc_forward>, L<FS::Record>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/h_svc_www.pm b/FS/FS/h_svc_www.pm
new file mode 100644 (file)
index 0000000..f2f8af8
--- /dev/null
@@ -0,0 +1,53 @@
+package FS::h_svc_www;
+
+use strict;
+use vars qw( @ISA );
+se FS::Record qw(qsearchs);
+use FS::h_Common;
+use FS::svc_www;
+use FS::h_domain_record;
+
+@ISA = qw( FS::h_Common FS::svc_www );
+
+sub table { 'h_svc_www' };
+
+=head1 NAME
+
+FS::h_svc_www - Historical web virtual host objects
+
+=head1 SYNOPSIS
+
+=head1 METHODS
+
+=over 4
+
+=item domain_record
+
+=cut
+
+sub domain_record {
+  my $self = shift;
+  qsearchs( 'h_domain_record',
+            { 'recnum' => $self->recnum },
+            FS::h_domain_record->sql_h_search(@_),
+          );
+}
+
+=back
+
+=head1 DESCRIPTION
+
+An FS::h_svc_www object represents a historical web virtual host.
+FS::h_svc_www inherits from FS::h_Common and FS::svc_www.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::h_Common>, L<FS::svc_www>, L<FS::Record>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
index ec0e1d5..8e47abf 100644 (file)
@@ -1028,7 +1028,7 @@ Returns the domain associated with this account.
 sub domain {
   my $self = shift;
   die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
-  my $svc_domain = $self->svc_domain
+  my $svc_domain = $self->svc_domain(@_)
     or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
   $svc_domain->domain;
 }
@@ -1066,7 +1066,7 @@ Returns an email address associated with the account.
 
 sub email {
   my $self = shift;
-  $self->username. '@'. $self->domain;
+  $self->username. '@'. $self->domain(@_);
 }
 
 =item acct_snarf
index dabc08c..32785c2 100644 (file)
@@ -64,6 +64,14 @@ FS/cust_pkg.pm
 FS/cust_refund.pm
 FS/cust_credit_refund.pm
 FS/cust_svc.pm
+FS/h_Common.pm
+FS/h_cust_svc.pm
+FS/h_svc_acct.pm
+FS/h_svc_broadband.pm
+FS/h_svc_domain.pm
+FS/h_svc_external.pm
+FS/h_svc_forward.pm
+FS/h_svc_www.pm
 FS/part_bill_event.pm
 FS/export_svc.pm
 FS/part_export.pm
@@ -171,6 +179,15 @@ t/cust_pay_refund.t
 t/cust_pkg.t
 t/cust_refund.t
 t/cust_svc.t
+t/h_cust_svc.t
+t/h_Common.t
+t/h_cust_svc.t
+t/h_svc_acct.t
+t/h_svc_broadband.t
+t/h_svc_domain.t
+t/h_svc_external.t
+t/h_svc_forward.t
+t/h_svc_www.t
 t/cust_tax_exempt.t
 t/domain_record.t
 t/nas.t
diff --git a/FS/t/h_Common.t b/FS/t/h_Common.t
new file mode 100644 (file)
index 0000000..174bb99
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::h_Common;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/h_cust_svc.t b/FS/t/h_cust_svc.t
new file mode 100644 (file)
index 0000000..a7dabbe
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::h_cust_svc;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/h_svc_acct.t b/FS/t/h_svc_acct.t
new file mode 100644 (file)
index 0000000..9c94d08
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::h_svc_acct;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/h_svc_broadband.t b/FS/t/h_svc_broadband.t
new file mode 100644 (file)
index 0000000..b8e5c7c
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::h_svc_broadband;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/h_svc_domain.t b/FS/t/h_svc_domain.t
new file mode 100644 (file)
index 0000000..87d2a09
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::h_svc_domain;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/h_svc_external.t b/FS/t/h_svc_external.t
new file mode 100644 (file)
index 0000000..5248f87
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::h_svc_external;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/h_svc_forward.t b/FS/t/h_svc_forward.t
new file mode 100644 (file)
index 0000000..64731d5
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::h_svc_forward;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/h_svc_www.t b/FS/t/h_svc_www.t
new file mode 100644 (file)
index 0000000..07558ce
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::h_svc_www;
+$loaded=1;
+print "ok 1\n";
index d5d77f2..8794f30 100755 (executable)
@@ -109,13 +109,6 @@ Comments
 <% } %>
 
 <BR><BR>
-<%= include('cust_main/order_pkg.html', $cust_main ) %>
-
-<% if ( $conf->config('payby-default') ne 'HIDE' ) { %>
-  <%= include('cust_main/quick-charge.html', $cust_main ) %>
-  <BR>
-<% } %>
-
 <%= include('cust_main/packages.html', $cust_main ) %>
 
 <% if ( $conf->config('payby-default') ne 'HIDE' ) { %>
index c5a0706..068a827 100755 (executable)
@@ -5,8 +5,38 @@
   my $packages = get_packages($cust_main, $conf);
 %>
 
-<A NAME="cust_pkg">Packages</A>
-( <A HREF="<%= $p %>edit/cust_pkg.cgi?<%= $cust_main->custnum %>">Bulk order and cancel packages</A> (preserves services) )
+<A NAME="cust_pkg"><FONT SIZE="+2">Packages</FONT></A>
+
+<%= include('order_pkg.html', $cust_main ) %>
+
+<% if ( $conf->config('payby-default') ne 'HIDE' ) { %>
+  <%= include('quick-charge.html', $cust_main ) %>
+<% } %>
+
+<A HREF="<%= $p %>edit/cust_pkg.cgi?<%= $cust_main->custnum %>">Bulk order and cancel packages</A> (preserves services)
+<BR><BR>
+
+<% if ( @$packages ) { %>
+Current packages
+<% } %>
+
+<% if ( $cust_main->num_cancelled_pkgs ) {
+     if ( $cgi->param('showcancelledpackages') eq '0' #see if it was set by me
+          || ( $conf->exists('hidecancelledpackages')
+               && ! $cgi->param('showcancelledpackages')
+             )
+        )
+     {
+       $cgi->param('showcancelledpackages', 1);
+%>
+  ( <a href="<%= $cgi->self_url %>">show
+<%   } else {
+       $cgi->param('showcancelledpackages', 0);
+%>
+  ( <a href="<%= $cgi->self_url %>">hide
+<%   } %>
+ cancelled packages</a> )
+<% } %>
 
 <% if ( @$packages ) { %>
 
@@ -159,12 +189,14 @@ foreach my $pkg (sort pkgsort_pkgnum_cancel @$packages) {
     }
   }
 }
-print '</TABLE>';
-}
-
 #end display packages
 %>
 
+</TABLE>
+<% } else { %>
+<BR>
+<% } %>
+
 <%
 #subroutines
 
@@ -173,12 +205,18 @@ sub get_packages {
   my $conf = shift;
   
   my @packages = ();
+  my $method;
+  if (  $cgi->param('showcancelledpackages') eq '0' #see if it was set by me
+     || ( $conf->exists('hidecancelledpackages')
+           && ! $cgi->param('showcancelledpackages') )
+     )
+  {
+    $method = 'ncancelled_pkgs';
+  } else {
+    $method = 'all_pkgs';
+  }
   
-  foreach my $cust_pkg (
-    $conf->exists('hidecancelledpackages')
-      ? $cust_main->ncancelled_pkgs
-      : $cust_main->all_pkgs
-  ) { 
+  foreach my $cust_pkg ( $cust_main->$method() ) {
   
     my $part_pkg = $cust_pkg->part_pkg;