summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorivan <ivan>2004-12-29 12:00:08 +0000
committerivan <ivan>2004-12-29 12:00:08 +0000
commitf7afca1829f8496509d10806439c37fcc1349135 (patch)
treeac127f51396332ba007d85f04bc414e7e8b1bfc0
parent72312a46911b3e71a4ea9739ee1571d74ae433fd (diff)
historical (immutable) invoice details about services and other history infrastructure
-rw-r--r--ANNOUNCE.1.5.02
-rw-r--r--FS/FS/Record.pm5
-rw-r--r--FS/FS/cust_bill.pm54
-rw-r--r--FS/FS/cust_main.pm21
-rw-r--r--FS/FS/cust_pkg.pm98
-rw-r--r--FS/FS/cust_svc.pm37
-rw-r--r--FS/FS/h_Common.pm83
-rw-r--r--FS/FS/h_cust_svc.pm84
-rw-r--r--FS/FS/h_svc_acct.pm53
-rw-r--r--FS/FS/h_svc_broadband.pm33
-rw-r--r--FS/FS/h_svc_domain.pm33
-rw-r--r--FS/FS/h_svc_external.pm33
-rw-r--r--FS/FS/h_svc_forward.pm65
-rw-r--r--FS/FS/h_svc_www.pm53
-rw-r--r--FS/FS/svc_acct.pm4
-rw-r--r--FS/MANIFEST17
-rw-r--r--FS/t/h_Common.t5
-rw-r--r--FS/t/h_cust_svc.t5
-rw-r--r--FS/t/h_svc_acct.t5
-rw-r--r--FS/t/h_svc_broadband.t5
-rw-r--r--FS/t/h_svc_domain.t5
-rw-r--r--FS/t/h_svc_external.t5
-rw-r--r--FS/t/h_svc_forward.t5
-rw-r--r--FS/t/h_svc_www.t5
-rwxr-xr-xhttemplate/view/cust_main.cgi7
-rwxr-xr-xhttemplate/view/cust_main/packages.html58
26 files changed, 701 insertions, 79 deletions
diff --git a/ANNOUNCE.1.5.0 b/ANNOUNCE.1.5.0
index 9d2b97e..fad6c92 100644
--- a/ANNOUNCE.1.5.0
+++ b/ANNOUNCE.1.5.0
@@ -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
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index 5a6bb57..cf0ac3b 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -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 {
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index 0306c01..93c179a 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -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,
+ ],
};
}
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index c42d222..44fb8d6 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -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>
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 630e88e..ced1423 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -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>).
diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index 8990d54..9cb7a81 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -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
index 0000000..58ead88
--- /dev/null
+++ b/FS/FS/h_Common.pm
@@ -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
index 0000000..9ef60fd
--- /dev/null
+++ b/FS/FS/h_cust_svc.pm
@@ -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
index 0000000..cd98dd4
--- /dev/null
+++ b/FS/FS/h_svc_acct.pm
@@ -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
index 0000000..d6038fb
--- /dev/null
+++ b/FS/FS/h_svc_broadband.pm
@@ -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
index 0000000..60d54f7
--- /dev/null
+++ b/FS/FS/h_svc_domain.pm
@@ -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
index 0000000..5eb7064
--- /dev/null
+++ b/FS/FS/h_svc_external.pm
@@ -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
index 0000000..231f9df
--- /dev/null
+++ b/FS/FS/h_svc_forward.pm
@@ -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
index 0000000..f2f8af8
--- /dev/null
+++ b/FS/FS/h_svc_www.pm
@@ -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;
+
diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm
index ec0e1d5..8e47abf 100644
--- a/FS/FS/svc_acct.pm
+++ b/FS/FS/svc_acct.pm
@@ -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
diff --git a/FS/MANIFEST b/FS/MANIFEST
index dabc08c..32785c2 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -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
index 0000000..174bb99
--- /dev/null
+++ b/FS/t/h_Common.t
@@ -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
index 0000000..a7dabbe
--- /dev/null
+++ b/FS/t/h_cust_svc.t
@@ -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
index 0000000..9c94d08
--- /dev/null
+++ b/FS/t/h_svc_acct.t
@@ -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
index 0000000..b8e5c7c
--- /dev/null
+++ b/FS/t/h_svc_broadband.t
@@ -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
index 0000000..87d2a09
--- /dev/null
+++ b/FS/t/h_svc_domain.t
@@ -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
index 0000000..5248f87
--- /dev/null
+++ b/FS/t/h_svc_external.t
@@ -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
index 0000000..64731d5
--- /dev/null
+++ b/FS/t/h_svc_forward.t
@@ -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
index 0000000..07558ce
--- /dev/null
+++ b/FS/t/h_svc_www.t
@@ -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";
diff --git a/httemplate/view/cust_main.cgi b/httemplate/view/cust_main.cgi
index d5d77f2..8794f30 100755
--- a/httemplate/view/cust_main.cgi
+++ b/httemplate/view/cust_main.cgi
@@ -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' ) { %>
diff --git a/httemplate/view/cust_main/packages.html b/httemplate/view/cust_main/packages.html
index c5a0706..068a827 100755
--- a/httemplate/view/cust_main/packages.html
+++ b/httemplate/view/cust_main/packages.html
@@ -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;