start adding package locations, RT#4499
authorivan <ivan>
Thu, 8 Jan 2009 01:45:22 +0000 (01:45 +0000)
committerivan <ivan>
Thu, 8 Jan 2009 01:45:22 +0000 (01:45 +0000)
15 files changed:
FS/FS.pm
FS/FS/Conf.pm
FS/FS/Schema.pm
FS/FS/cust_location.pm [new file with mode: 0644]
FS/FS/cust_main.pm
FS/FS/cust_pkg.pm
FS/MANIFEST
FS/t/cust_location.t [new file with mode: 0644]
eg/table_template-svc.pm
eg/table_template.pm
httemplate/view/cust_main/packages.html
httemplate/view/cust_main/packages/location.html [new file with mode: 0644]
httemplate/view/cust_main/packages/package.html [new file with mode: 0644]
httemplate/view/cust_main/packages/services.html [new file with mode: 0644]
httemplate/view/cust_main/packages/status.html [new file with mode: 0644]

index 7e9b04e..c4be977 100644 (file)
--- a/FS/FS.pm
+++ b/FS/FS.pm
@@ -222,6 +222,8 @@ L<FS::cust_pkg_reason> - Package reason class
 
 L<FS::cust_main> - Customer class
 
 
 L<FS::cust_main> - Customer class
 
+L<FS::cust_main_location> - Customer location class
+
 L<FS::cust_main_Mixin> - Mixin class for records that contain fields from cust_main
 
 L<FS::cust_main_invoice> - Invoice destination class
 L<FS::cust_main_Mixin> - Mixin class for records that contain fields from cust_main
 
 L<FS::cust_main_invoice> - Invoice destination class
index 5d56169..9c9c6aa 100644 (file)
@@ -2001,6 +2001,13 @@ worry that config_items is freeside-specific and icky.
     'section'     => 'billing',
     'description' => 'By default, tax calculations are done based on the billing address.  Enable this switch to calculate tax based on the shipping address instead.  Note: Tax reports can take a long time when enabled.',
     'type'        => 'checkbox',
     'section'     => 'billing',
     'description' => 'By default, tax calculations are done based on the billing address.  Enable this switch to calculate tax based on the shipping address instead.  Note: Tax reports can take a long time when enabled.',
     'type'        => 'checkbox',
+  }
+,
+  {
+    'key'         => 'tax-pkg_address',
+    'section'     => 'billing',
+    'description' => 'By default, tax calculations are done based on the billing address.  Enable this switch to calculate tax based on the package address instead (when present).  Note: Tax reports can take a long time when enabled.',
+    'type'        => 'checkbox',
   },
 
   {
   },
 
   {
index 17665b1..ecf017e 100644 (file)
@@ -671,6 +671,28 @@ sub tables_hashref {
                  ],
     },
 
                  ],
     },
 
+    #eventually use for billing & ship from cust_main too
+    #for now, just cust_pkg locations
+    'cust_location' => {
+      'columns' => [
+        'locationnum',  'serial',     '',      '', '', '',
+        'custnum',         'int',     '',      '', '', '',
+        'address1',    'varchar',     '', $char_d, '', '', 
+        'address2',    'varchar', 'NULL', $char_d, '', '', 
+        'city',        'varchar',     '', $char_d, '', '', 
+        'county',      'varchar', 'NULL', $char_d, '', '', 
+        'state',       'varchar', 'NULL', $char_d, '', '', 
+        'zip',         'varchar', 'NULL',      10, '', '', 
+        'country',        'char',     '',       2, '', '', 
+        'geocode',     'varchar', 'NULL',      20, '', '',
+      ],
+      'primary_key' => 'locationnum',
+      'unique'      => [],
+      'index'       => [ [ 'custnum' ],
+                         [ 'county' ], [ 'state' ], [ 'country' ], [ 'zip' ],
+                       ],
+    },
+
     'cust_main_invoice' => {
       'columns' => [
         'destnum',  'serial',  '',     '', '', '', 
     'cust_main_invoice' => {
       'columns' => [
         'destnum',  'serial',  '',     '', '', '', 
@@ -938,26 +960,27 @@ sub tables_hashref {
 
     'cust_pkg' => {
       'columns' => [
 
     'cust_pkg' => {
       'columns' => [
-        'pkgnum',         'serial',    '',   '', '', '', 
-        'custnum',        'int',    '',   '', '', '', 
-        'pkgpart',        'int',    '',   '', '', '', 
-        'otaker',         'varchar', '', 32, '', '', 
-        'setup',          @date_type, '', '', 
-        'bill',           @date_type, '', '', 
-        'last_bill',      @date_type, '', '', 
-        'susp',           @date_type, '', '', 
-        'adjourn',        @date_type, '', '', 
-        'cancel',         @date_type, '', '', 
-        'expire',         @date_type, '', '', 
-        'change_date',    @date_type, '', '',
-        'change_pkgnum',  'int', 'NULL', '', '', '',
-        'change_pkgpart', 'int', 'NULL', '', '', '',
-        'manual_flag',    'char', 'NULL', 1, '', '', 
-        'quantity',       'int', 'NULL', '', '', '',
+        'pkgnum',           'serial',     '', '', '', '', 
+        'custnum',             'int',     '', '', '', '', 
+        'pkgpart',             'int',     '', '', '', '', 
+        'locationnum',         'int', 'NULL', '', '', '',
+        'otaker',          'varchar',     '', 32, '', '', 
+        'setup',          @date_type,             '', '', 
+        'bill',           @date_type,             '', '', 
+        'last_bill',      @date_type,             '', '', 
+        'susp',           @date_type,             '', '', 
+        'adjourn',        @date_type,             '', '', 
+        'cancel',         @date_type,             '', '', 
+        'expire',         @date_type,             '', '', 
+        'change_date',    @date_type,             '', '',
+        'change_pkgnum',       'int', 'NULL', '', '', '',
+        'change_pkgpart',      'int', 'NULL', '', '', '',
+        'manual_flag',        'char', 'NULL',  1, '', '', 
+        'quantity',            'int', 'NULL', '', '', '',
       ],
       'primary_key' => 'pkgnum',
       'unique' => [],
       ],
       'primary_key' => 'pkgnum',
       'unique' => [],
-      'index' => [ ['custnum'], ['pkgpart'],
+      'index' => [ ['custnum'], ['pkgpart'], [ 'locationnum' ],
                    ['setup'], ['last_bill'], ['bill'], ['susp'], ['adjourn'],
                    ['expire'], ['cancel'],
                    ['change_date'],
                    ['setup'], ['last_bill'], ['bill'], ['susp'], ['adjourn'],
                    ['expire'], ['cancel'],
                    ['change_date'],
diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm
new file mode 100644 (file)
index 0000000..0544dcf
--- /dev/null
@@ -0,0 +1,175 @@
+package FS::cust_location;
+
+use strict;
+use base qw( FS::Record );
+use Locale::Country;
+use FS::Record qw( qsearch ); #qsearchs );
+use FS::cust_main;
+use FS::cust_main_county;
+
+=head1 NAME
+
+FS::cust_location - Object methods for cust_location records
+
+=head1 SYNOPSIS
+
+  use FS::cust_location;
+
+  $record = new FS::cust_location \%hash;
+  $record = new FS::cust_location { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_location object represents a customer location.  FS::cust_location
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item locationnum
+
+primary key
+
+=item custnum
+
+custnum
+
+=item address1
+
+Address line one (required)
+
+=item address2
+
+Address line two (optional)
+
+=item city
+
+City
+
+=item county
+
+County (optional, see L<FS::cust_main_county>)
+
+=item state
+
+State (see L<FS::cust_main_county>)
+
+=item zip
+
+Zip
+
+=item country
+
+Country (see L<FS::cust_main_county>)
+
+=item geocode
+
+Geocode
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new location.  To add the location to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_location'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=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.
+
+=item check
+
+Checks all fields to make sure this is a valid location.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+#some false laziness w/cust_main, but since it should eventually lose these
+#fields anyway...
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('locationnum')
+    || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
+    || $self->ut_text('address1')
+    || $self->ut_textn('address2')
+    || $self->ut_text('city')
+    || $self->ut_textn('county')
+    || $self->ut_textn('state')
+    || $self->ut_country('country')
+    || $self->ut_zip('zip', $self->country)
+    || $self->ut_alphan('geocode')
+  ;
+  return $error if $error;
+
+  unless ( qsearch('cust_main_county', {
+    'country' => $self->country,
+    'state'   => '',
+   } ) ) {
+    return "Unknown state/county/country: ".
+      $self->state. "/". $self->county. "/". $self->country
+      unless qsearch('cust_main_county',{
+        'state'   => $self->state,
+        'county'  => $self->county,
+        'country' => $self->country,
+      } );
+  }
+
+  $self->SUPER::check;
+}
+
+=item country_full
+
+Returns this locations's full country name
+
+=cut
+
+sub country_full {
+  my $self = shift;
+  code2country($self->country);
+}
+
+=back
+
+=head1 BUGS
+
+Not yet used for cust_main billing and shipping addresses.
+
+=head1 SEE ALSO
+
+L<FS::cust_main_county>, L<FS::cust_pkg>, L<FS::Record>,
+schema.html from the base documentation.
+
+=cut
+
+1;
+
index da1d3e1..2b94dca 100644 (file)
@@ -135,101 +135,181 @@ FS::Record.  The following fields are currently supported:
 
 =over 4
 
 
 =over 4
 
-=item custnum - primary key (assigned automatically for new customers)
+=item custnum
 
 
-=item agentnum - agent (see L<FS::agent>)
+Primary key (assigned automatically for new customers)
 
 
-=item refnum - Advertising source (see L<FS::part_referral>)
+=item agentnum
+
+Agent (see L<FS::agent>)
+
+=item refnum
+
+Advertising source (see L<FS::part_referral>)
+
+=item first
+
+First name
 
 
-=item first - name
+=item last
 
 
-=item last - name
+Last name
 
 
-=item ss - social security number (optional)
+=item ss
 
 
-=item company - (optional)
+Cocial security number (optional)
+
+=item company
+
+(optional)
 
 =item address1
 
 
 =item address1
 
-=item address2 - (optional)
+=item address2
+
+(optional)
 
 =item city
 
 
 =item city
 
-=item county - (optional, see L<FS::cust_main_county>)
+=item county
 
 
-=item state - (see L<FS::cust_main_county>)
+(optional, see L<FS::cust_main_county>)
+
+=item state
+
+(see L<FS::cust_main_county>)
 
 =item zip
 
 
 =item zip
 
-=item country - (see L<FS::cust_main_county>)
+=item country
+
+(see L<FS::cust_main_county>)
 
 
-=item daytime - phone (optional)
+=item daytime
 
 
-=item night - phone (optional)
+phone (optional)
 
 
-=item fax - phone (optional)
+=item night
 
 
-=item ship_first - name
+phone (optional)
 
 
-=item ship_last - name
+=item fax
 
 
-=item ship_company - (optional)
+phone (optional)
+
+=item ship_first
+
+Shipping first name
+
+=item ship_last
+
+Shipping last name
+
+=item ship_company
+
+(optional)
 
 =item ship_address1
 
 
 =item ship_address1
 
-=item ship_address2 - (optional)
+=item ship_address2
+
+(optional)
 
 =item ship_city
 
 
 =item ship_city
 
-=item ship_county - (optional, see L<FS::cust_main_county>)
+=item ship_county
+
+(optional, see L<FS::cust_main_county>)
+
+=item ship_state
 
 
-=item ship_state - (see L<FS::cust_main_county>)
+(see L<FS::cust_main_county>)
 
 =item ship_zip
 
 
 =item ship_zip
 
-=item ship_country - (see L<FS::cust_main_county>)
+=item ship_country
 
 
-=item ship_daytime - phone (optional)
+(see L<FS::cust_main_county>)
 
 
-=item ship_night - phone (optional)
+=item ship_daytime
 
 
-=item ship_fax - phone (optional)
+phone (optional)
 
 
-=item payby - Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
+=item ship_night
 
 
-=item payinfo - Payment Information (See L<FS::payinfo_Mixin> for data format)
+phone (optional)
 
 
-=item paymask - Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
+=item ship_fax
+
+phone (optional)
+
+=item payby
+
+Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
+
+=item payinfo
+
+Payment Information (See L<FS::payinfo_Mixin> for data format)
+
+=item paymask
+
+Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
 
 =item paycvv
 
 Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
 
 
 =item paycvv
 
 Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
 
-=item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
+=item paydate
+
+Expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
+
+=item paystart_month
+
+Start date month (maestro/solo cards only)
+
+=item paystart_year
+
+Start date year (maestro/solo cards only)
 
 
-=item paystart_month - start date month (maestro/solo cards only)
+=item payissue
 
 
-=item paystart_year - start date year (maestro/solo cards only)
+Issue number (maestro/solo cards only)
 
 
-=item payissue - issue number (maestro/solo cards only)
+=item payname
 
 
-=item payname - name on card or billing name
+Name on card or billing name
 
 
-=item payip - IP address from which payment information was received
+=item payip
 
 
-=item tax - tax exempt, empty or `Y'
+IP address from which payment information was received
 
 
-=item otaker - order taker (assigned automatically, see L<FS::UID>)
+=item tax
 
 
-=item comments - comments (optional)
+Tax exempt, empty or `Y'
 
 
-=item referral_custnum - referring customer number
+=item otaker
 
 
-=item spool_cdr - Enable individual CDR spooling, empty or `Y'
+Order taker (assigned automatically, see L<FS::UID>)
 
 
-=item dundate - a suggestion to events (see L<FS::part_bill_event">) to delay until this unix timestamp
+=item comments
 
 
-=item squelch_cdr - Discourage individual CDR printing, empty or `Y'
+Comments (optional)
+
+=item referral_custnum
+
+Referring customer number
+
+=item spool_cdr
+
+Enable individual CDR spooling, empty or `Y'
+
+=item dundate
+
+A suggestion to events (see L<FS::part_bill_event">) to delay until this unix timestamp
+
+=item squelch_cdr
+
+Discourage individual CDR printing, empty or `Y'
 
 =back
 
 
 =back
 
@@ -2905,6 +2985,11 @@ Only return events for the specified eventtable (by default, events of all event
 
 Explicitly pass the objects to be tested (typically used with eventtable).
 
 
 Explicitly pass the objects to be tested (typically used with eventtable).
 
+=item testonly
+
+Set to true to return the objects, but not actually insert them into the
+database.
+
 =back
 
 =cut
 =back
 
 =cut
@@ -2935,7 +3020,8 @@ sub due_cust_event {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  $self->select_for_update; #mutex
+  $self->select_for_update #mutex
+    unless $opt{testonly};
 
   ###
   # 1: find possible events (initial search)
 
   ###
   # 1: find possible events (initial search)
index 70f23df..03cec75 100644 (file)
@@ -13,6 +13,7 @@ use FS::cust_main_Mixin;
 use FS::cust_svc;
 use FS::part_pkg;
 use FS::cust_main;
 use FS::cust_svc;
 use FS::part_pkg;
 use FS::cust_main;
+use FS::cust_location;
 use FS::type_pkgs;
 use FS::pkg_svc;
 use FS::cust_bill_pkg;
 use FS::type_pkgs;
 use FS::pkg_svc;
 use FS::cust_bill_pkg;
@@ -106,7 +107,7 @@ inherits from FS::Record.  The following fields are currently supported:
 
 =item pkgnum
 
 
 =item pkgnum
 
-primary key (assigned automatically for new billing items)
+Primary key (assigned automatically for new billing items)
 
 =item custnum
 
 
 =item custnum
 
@@ -116,6 +117,10 @@ Customer (see L<FS::cust_main>)
 
 Billing item definition (see L<FS::part_pkg>)
 
 
 Billing item definition (see L<FS::part_pkg>)
 
+=item locationnum
+
+Optional link to package location (see L<FS::location>)
+
 =item setup
 
 date
 =item setup
 
 date
@@ -435,6 +440,7 @@ sub check {
     $self->ut_numbern('pkgnum')
     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
     || $self->ut_numbern('pkgpart')
     $self->ut_numbern('pkgnum')
     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
     || $self->ut_numbern('pkgpart')
+    || $self->ut_foreign_keyn('locationnum', 'location', 'locationnum')
     || $self->ut_numbern('setup')
     || $self->ut_numbern('bill')
     || $self->ut_numbern('susp')
     || $self->ut_numbern('setup')
     || $self->ut_numbern('bill')
     || $self->ut_numbern('susp')
@@ -1569,6 +1575,30 @@ sub cust_main {
   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
 }
 
   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
 }
 
+=item cust_location
+
+Returns the location object, if any (see L<FS::cust_location>).
+
+=cut
+
+sub cust_location {
+  my $self = shift;
+  return '' unless $self->locationnum;
+  qsearchs( 'cust_main', { 'locationnum' => $self->locationnum } );
+}
+
+=item cust_location_or_main
+
+If this package is associated with a location, returns the locaiton (see
+L<FS::cust_location>), otherwise returns the customer (see L<FS::cust_main>).
+
+=cut
+
+sub cust_location_or_main {
+  my $self = shift;
+  $self->cust_location || $self->cust_main;
+}
+
 =item seconds_since TIMESTAMP
 
 Returns the number of seconds all accounts (see L<FS::svc_acct>) in this
 =item seconds_since TIMESTAMP
 
 Returns the number of seconds all accounts (see L<FS::svc_acct>) in this
index c325242..21d721d 100644 (file)
@@ -427,3 +427,5 @@ FS/cust_bill_pkg_display.pm
 t/cust_bill_pkg_display.t
 FS/cust_pkg_detail.pm
 t/cust_pkg_detail.t
 t/cust_bill_pkg_display.t
 FS/cust_pkg_detail.pm
 t/cust_pkg_detail.t
+FS/cust_location.pm
+t/cust_location.t
diff --git a/FS/t/cust_location.t b/FS/t/cust_location.t
new file mode 100644 (file)
index 0000000..e98372d
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_location;
+$loaded=1;
+print "ok 1\n";
index 47dcbe6..7e10275 100644 (file)
@@ -1,13 +1,10 @@
 package FS::svc_table;
 
 use strict;
 package FS::svc_table;
 
 use strict;
-use vars qw(@ISA);
+use base qw( FS::svc_Common );
 #use FS::Record qw( qsearch qsearchs );
 #use FS::Record qw( qsearch qsearchs );
-use FS::svc_Common;
 use FS::cust_svc;
 
 use FS::cust_svc;
 
-@ISA = qw(FS::svc_Common);
-
 =head1 NAME
 
 FS::table_name - Object methods for table_name records
 =head1 NAME
 
 FS::table_name - Object methods for table_name records
index 5da6f3b..9c71b3a 100644 (file)
@@ -1,11 +1,9 @@
 package FS::table_name;
 
 use strict;
 package FS::table_name;
 
 use strict;
-use vars qw( @ISA );
+use base qw( FS::Record );
 use FS::Record qw( qsearch qsearchs );
 
 use FS::Record qw( qsearch qsearchs );
 
-@ISA = qw(FS::Record);
-
 =head1 NAME
 
 FS::table_name - Object methods for table_name records
 =head1 NAME
 
 FS::table_name - Object methods for table_name records
index 5f1db4a..5fde2f3 100755 (executable)
@@ -123,7 +123,6 @@ Current packages
 % } 
 % if ( @$packages ) { 
 
 % } 
 % if ( @$packages ) { 
 
-
 <% include('/elements/table-grid.html') %>
 % my $bgcolor1 = '#eeeeee';
 %   my $bgcolor2 = '#ffffff';
 <% include('/elements/table-grid.html') %>
 % my $bgcolor1 = '#eeeeee';
 %   my $bgcolor2 = '#ffffff';
@@ -132,444 +131,40 @@ Current packages
 <TR>
   <TH CLASS="grid" BGCOLOR="#cccccc">Package</TH>
   <TH CLASS="grid" BGCOLOR="#cccccc">Status</TH>
 <TR>
   <TH CLASS="grid" BGCOLOR="#cccccc">Package</TH>
   <TH CLASS="grid" BGCOLOR="#cccccc">Status</TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc">Location</TH>
   <TH CLASS="grid" BGCOLOR="#cccccc">Services</TH>
 </TR>
 
   <TH CLASS="grid" BGCOLOR="#cccccc">Services</TH>
 </TR>
 
-%foreach my $cust_pkg (@$packages) {
-%
-%  my $part_pkg = $cust_pkg->part_pkg;
+% foreach my $cust_pkg (@$packages) {
 %
 %
-%  if ( $bgcolor eq $bgcolor1 ) {
-%    $bgcolor = $bgcolor2;
-%  } else {
-%    $bgcolor = $bgcolor1;
-%  }
-
-
-<!--pkgnum: <% $cust_pkg->pkgnum %>-->
-<TR>
-
-  <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
-    <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
-      <TR>
-        <TD COLSPAN=2>
-          <A NAME="cust_pkg<% $cust_pkg->pkgnum %>"
-             ID  ="cust_pkg<% $cust_pkg->pkgnum %>"
-          ><% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><% $part_pkg->pkg %></A>
-          - 
-          <% $part_pkg->comment %>
-        </TD>
-      </TR>
-
-%   if ( $cust_pkg->quantity > 1 ) {
-      <TR>
-        <TD COLSPAN=2>
-          &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Quantity: 
-          <B><% $cust_pkg->quantity %></B>
-        </TD>
-      </TR>
+%   if ( $bgcolor eq $bgcolor1 ) {
+%     $bgcolor = $bgcolor2;
+%   } else {
+%     $bgcolor = $bgcolor1;
 %   }
 %   }
-
-      <TR>
-        <TD COLSPAN=2>
-
-          <FONT SIZE=-1>
-
-% unless ( $cust_pkg->get('cancel') ) { 
-%   my $br = 0;
-%   if ( $curuser->access_right('Change customer package') ) { $br=1;
-
-            (&nbsp;<%pkg_change_link($cust_pkg)%>&nbsp;)
-%   } 
-%   if ( $curuser->access_right('Edit customer package dates') ) { $br=1;
-
-            (&nbsp;<%pkg_dates_link($cust_pkg)%>&nbsp;)
-%   } 
-%   if ( $curuser->access_right('Customize customer package') ) { $br=1;
-
-            (&nbsp;<%pkg_customize_link($cust_pkg,$cust_main->custnum)%>&nbsp;)
-%   } 
-    <% $br ? '<BR>' : '' %>
-% } 
-
-% if ( $cust_pkg->num_cust_event
-%      && (    $curuser->access_right('Billing event reports')
-%           || $curuser->access_right('View customer billing events')
-%         )
-%    ) {
-    (&nbsp;<%pkg_event_link($cust_pkg)%>&nbsp;)
-% }
-
-          </FONT>
-
-        </TD>
-      </TR>
-
-%   my $editi = $curuser->access_right('Edit customer package invoice details');
-%   my $editc = $curuser->access_right('Edit customer package comments');
 %
 %
-%   if ( $cust_pkg->cust_pkg_detail('I') || $cust_pkg->cust_pkg_detail('C')
-%        || $editi || $editc                                                ) {
-%
-%     my $editlink = $p. 'edit/cust_pkg_detail?pkgnum='. $cust_pkg->pkgnum.
-%                    ';detailtype=';
-
-      <TR>
-
-%       if ( $cust_pkg->cust_pkg_detail('I') ) { 
-          <TD VALIGN="top">
-            <% include('/elements/table-grid.html') %>
-              <TR>
-                <TH BGCOLOR="#dddddd" STYLE="border-bottom: dashed 1px black; padding-bottom: 1px">
-                  <FONT SIZE="-1">
-                    Invoice details
-%                   if ( $editi && ! $cust_pkg->get('cancel') ) {
-                      (<% include('/elements/popup_link.html', { 
-                                    'action'      => $editlink. 'I',
-                                    'label'       => 'edit',
-                                    'actionlabel' => 'Edit invoice details',
-                                    'color'       => '#333399',
-                                    'width'       => 763,
-                                 })
-                       %>)
-%                   }
-                  </FONT>
-                </TH>
-              </TR>
-%             foreach my $cust_pkg_detail ( $cust_pkg->cust_pkg_detail('I') ) {
-                <TR>
-                  <TD><FONT SIZE="-1">&nbsp;-&nbsp;<% $cust_pkg_detail->detail |h %></FONT></TD>
-                </TR>
-%             }
-            </TABLE>
-          </TD>
-%       } else {
-          <TD>
-%           if ( $editi && ! $cust_pkg->get('cancel') ) {
-              <FONT SIZE="-1">
-                (&nbsp;<% include('/elements/popup_link.html', { 
-                               'action'      => $editlink. 'I',
-                               'label'       => 'Add&nbsp;invoice&nbsp;details',
-                               'actionlabel' => 'Add invoice details',
-                               'color'       => '#333399',
-                               'width'       => 763,
-                            })
-                  %>&nbsp;)
-              </FONT>
-%           }
-          </TD>
-%       }
-
-%       if ( $cust_pkg->cust_pkg_detail('C') ) { 
-          <TD VALIGN="top">
-            <% include('/elements/table-grid.html') %>
-              <TR>
-                <TH BGCOLOR="#dddddd" STYLE="border-bottom: dashed 1px black; padding-bottom: 1px">
-                  <FONT SIZE="-1">
-                    Comments
-%                   if ( $editc ) {
-                      (<% include('/elements/popup_link.html', { 
-                                    'action'      => $editlink. 'C',
-                                    'label'       => 'edit',
-                                    'actionlabel' => 'Edit comments',
-                                    'color'       => '#333399',
-                                    'width'       => 763,
-                                 })
-                       %>)
-%                   }
-                  </FONT>
-                </TH>
-              </TR>
-%             foreach my $cust_pkg_detail ( $cust_pkg->cust_pkg_detail('C') ) {
-                <TR>
-                  <TD><FONT SIZE="-1">&nbsp;-&nbsp;<% $cust_pkg_detail->detail |h %></FONT></TD>
-                </TR>
-%             }
-            </TABLE>
-          </TD>
-%       } else {
-          <TD>
-%           if ( $editc ) {
-              <FONT SIZE="-1">
-                (&nbsp;<% include('/elements/popup_link.html', { 
-                               'action'      => $editlink. 'C',
-                               'label'       => 'Add&nbsp;comments',
-                               'actionlabel' => 'Add comments',
-                               'color'       => '#333399',
-                               'width'       => 763,
-                            })
-                  %>&nbsp;)
-              </FONT>
-%           }
-          </TD>
-%       }
-
-      </TR>
-%   }
-
-    </TABLE>
-
-  </TD>
-
-  <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
-    <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
-%
-%  sub myfreq {
-%    my $part_pkg = shift;
-%    my $freq = $part_pkg->freq_pretty;
-%    $freq =~ s/ /&nbsp;/g;
-%    $freq;
-%  }
-%
-%  #this should use cust_pkg->status and cust_pkg->statuscolor eventually
-%  #my $colspan = $conf->exists('cust_pkg-display_times') ? 8 : 4;
-%  #my $width = $conf->exists('cust_pkg-display_times') ? '38%' : '56%';
-%
-%  #false laziness w/edit/REAL_cust_pkg.cgi
-%  my( $billed_or_prepaid, $last_bill_or_renewed, $next_bill_or_prepaid_until );
-%  unless ( $part_pkg->is_prepaid ) {
-%    $billed_or_prepaid = 'billed';
-%    $last_bill_or_renewed = 'Last&nbsp;bill';
-%    $next_bill_or_prepaid_until = 'Next&nbsp;bill';
-%  } else {
-%    $billed_or_prepaid = 'prepaid';
-%    $last_bill_or_renewed = 'Renewed';
-%    $next_bill_or_prepaid_until = 'Prepaid&nbsp;until';
-%  }
-%
-%
-% if ( $cust_pkg->get('cancel') ) { #status: cancelled
-%   my $cpr = $cust_pkg->last_cust_pkg_reason('cancel');
-
-    <% pkg_status_row($cust_pkg, 'Cancelled', 'cancel', 'color'=>'FF0000', conf=>$conf ) %>
-
-    <% pkg_status_row_colspan(
-         ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '',
-         'align' => 'right', 'color' => 'ff0000', 'size' => '-2',
-       )
-    %>
-
-%   unless ( $cust_pkg->get('setup') ) { 
-
-        <% pkg_status_row_colspan('Never billed') %>
-
-%   } else { 
-
-       <% pkg_status_row( $cust_pkg, 'Setup', 'setup', conf=>$conf ) %>
-       <% pkg_status_row_changed( $cust_pkg, conf=>$conf ) %>
-       <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', conf=>$conf, curuser=>$curuser ) %>
-       <% pkg_status_row_if( $cust_pkg, 'Suspended', 'susp', conf=>$conf, curuser=>$curuser ) %>
-
-%   } 
-%
-% } else { 
-%
-%   if ( $cust_pkg->get('susp') ) { #status: suspended
-%     my $cpr = $cust_pkg->last_cust_pkg_reason('susp');
-
-    <% pkg_status_row( $cust_pkg, 'Suspended', 'susp', 'color'=>'FF9900', conf=>$conf ) %>
-
-    <% pkg_status_row_colspan(
-         ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '',
-         'align' => 'right', 'color' => 'FF9900', 'size' => '-2',
-       )
-    %>
-
-%   unless ( $cust_pkg->get('setup') ) { 
-      <% pkg_status_row_colspan('Never billed') %>
-%   } else { 
-      <% pkg_status_row($cust_pkg, 'Setup', 'setup', conf=>$conf ) %>
-%   } 
-
-    <% pkg_status_row_changed( $cust_pkg, conf=>$conf ) %>
-    <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', conf=>$conf, curuser=>$curuser ) %>
-%   # pkg_status_row($cust_pkg, 'Next bill', 'bill', conf=>$conf)
-    <% pkg_status_row_if( $cust_pkg, 'Expires', 'expire', conf=>$conf, curuser=>$curuser ) %>
+%   my %iopt = (
+%     'bgcolor'  => $bgcolor,
+%     'cust_pkg' => $cust_pkg,
+%     'part_pkg' => $cust_pkg->part_pkg,
+%   );
 
 
+    <!--pkgnum: <% $cust_pkg->pkgnum %>-->
     <TR>
     <TR>
-      <TD COLSPAN=<%$colspan%>>
-        <FONT SIZE=-1>
-%         if ( $curuser->access_right('Unsuspend customer package') ) { 
-            (&nbsp;<% pkg_unsuspend_link($cust_pkg) %>&nbsp;)
-%         } 
-%         if ( $curuser->access_right('Cancel customer package immediately') ) {
-            (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
-%         } 
-        </FONT>
-      </TD>
+      <% include('packages/package.html',  %iopt) %>
+      <% include('packages/status.html',   %iopt) %>
+      <% include('packages/location.html', %iopt) %>
+      <% include('packages/services.html', %iopt) %>
     </TR>
 
     </TR>
 
-%   } else { #status: active
-%
-%     unless ( $cust_pkg->get('setup') ) { #not setup
-%
-%       unless ( $part_pkg->freq ) { 
-
-          <% pkg_status_row_colspan('Not&nbsp;yet&nbsp;billed&nbsp;(one-time&nbsp;charge)') %>
-
-          <TR>
-            <TD COLSPAN=<%$colspan%>>
-              <FONT SIZE=-1>
-%               if ( $curuser->access_right('Cancel customer package immediately') ) { 
-                  (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
-%               } 
-              </FONT>
-            </TD>
-          </TR>
-
-%       } else { 
-
-         <% pkg_status_row_colspan("Not&nbsp;yet&nbsp;billed&nbsp;($billed_or_prepaid&nbsp;". myfreq($part_pkg). ')' ) %>
-
-%       } 
-%
-%     } else { #setup
-%
-%       unless ( $part_pkg->freq ) { 
-
-          <% pkg_status_row_colspan('One-time&nbsp;charge') %>
-
-          <% pkg_status_row($cust_pkg, 'Billed', 'setup', conf=>$conf) %>
-
-%       } else { 
-%
-%         if (scalar($cust_pkg->overlimit)) {
-
-            <% pkg_status_row_colspan(
-                 'Overlimit',
-                 $billed_or_prepaid. '&nbsp;'. myfreq($part_pkg),
-                 'color' => 'FFD000',
-               )
-            %>
-
-%         } else {
-            <% pkg_status_row_colspan(
-                 'Active',
-                 $billed_or_prepaid. '&nbsp;'. myfreq($part_pkg),
-                 'color' => '00CC00',
-               )
-            %>
-%         } 
-
-          <% pkg_status_row($cust_pkg, 'Setup', 'setup', conf=>$conf) %>
-
-%       } 
-%
-%     } 
-%   my $autosuspend = pkg_autosuspend_time( $cust_pkg );
-%   $cust_pkg->set('autosuspend', $autosuspend) if $autosuspend;
-
-      <% pkg_status_row_changed( $cust_pkg, conf=>$conf ) %>
-      <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', conf=>$conf, curuser=>$curuser ) %>
-      <% pkg_status_row_if( $cust_pkg, $next_bill_or_prepaid_until, 'bill', conf=>$conf, curuser=>$curuser ) %>
-      <% pkg_status_row_if($cust_pkg, 'Will automatically suspend by', 'autosuspend', conf=>$conf) %>
-      <% pkg_status_row_if( $cust_pkg, 'Will suspend on', 'adjourn', conf=>$conf, curuser=>$curuser ) %>
-      <% pkg_status_row_if( $cust_pkg, 'Expires', 'expire', conf=>$conf, curuser=>$curuser ) %>
-
-%     if ( $part_pkg->freq ) { 
-
-        <TR>
-          <TD COLSPAN=<%$colspan%>>
-            <FONT SIZE=-1>
-%             if ( $curuser->access_right('Suspend customer package') ) { 
-                (&nbsp;<% pkg_suspend_link($cust_pkg) %>&nbsp;)
-%             } 
-%             if ( $curuser->access_right('Suspend customer package later') ) { 
-                (&nbsp;<% pkg_adjourn_link($cust_pkg) %>&nbsp;)
-%             } 
-%             if ( $curuser->access_right('Delay suspension events') ) { 
-                (&nbsp;<% pkg_delay_link($cust_pkg) %>&nbsp;)
-%             } 
-%             if ( $curuser->access_right('Cancel customer package immediately') ) { 
-                (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
-%             } 
-%             if ( $curuser->access_right('Cancel customer package later') ) { 
-                (&nbsp;<% pkg_expire_link($cust_pkg) %>&nbsp;)
-%             } 
-
-            <FONT>
-          </TD>
-        </TR>
-%     }
-%
-%   } 
-% } 
-
-</TABLE>
-</TD>
-
-<TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
-  <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
-
-%  #foreach my $svcpart (sort {$a->{svcpart} <=> $b->{svcpart}} @{$pkg->{svcparts}}) {
-%  foreach my $part_svc ( $cust_pkg->part_svc ) {
-
-%    #foreach my $service (@{$svcpart->{services}}) {
-%    foreach my $cust_svc ( @{ $part_svc->cust_pkg_svc } ) {
-
-      <TR>
-        <TD ALIGN="right" VALIGN="top"><% FS::UI::Web::svc_link($m, $part_svc, $cust_svc) %></TD>
-        <TD STYLE="padding-bottom:0px"><B><% FS::UI::Web::svc_label_link($m, $part_svc, $cust_svc) %></B></TD>
-        <TD><% FS::UI::Web::svc_export_links($m, $part_svc, $cust_svc) %></TD>
-      </TR>
-
-      <TR>
-        <TD ALIGN="right" COLSPAN="3" VALIGN="top" STYLE="padding-bottom:1px;padding-top:0px"><FONT SIZE="-2" COLOR="#FFD000">
-
-            <% $cust_svc->overlimit ? "Overlimit: ". time2str('%b %o %Y' . ($conf->exists('cust_pkg-display_times') ? ' %l:%M %P' : ''), $cust_svc->overlimit) : '' %>
-          </FONT></TD>
-      </TR>
-
-      <TR>
-        <TD ALIGN="right" VALIGN="top" STYLE="padding-bottom:5px;padding-top:0px"><FONT SIZE="-2">
-
-%         if ( $curuser->access_right('Recharge customer service')
-%              && $part_svc->svcdb eq 'svc_acct'
-%              && (    $cust_svc->svc_x->seconds    ne ''
-%                   || $cust_svc->svc_x->upbytes    ne ''
-%                   || $cust_svc->svc_x->downbytes  ne ''
-%                   || $cust_svc->svc_x->totalbytes ne ''
-%                 )
-%         ) { 
-            (&nbsp;<%svc_recharge_link($cust_svc)%>&nbsp;)
-%         } 
-          </FONT></TD>
-
-          <TD ALIGN="right" VALIGN="top" STYLE="padding-bottom:5px;padding-top:0px"><FONT SIZE="-2">
-
-%         if ( $curuser->access_right('Unprovision customer service') ) { 
-            (&nbsp;<%svc_unprovision_link($cust_svc)%>&nbsp;)
-%         } 
-          </FONT></TD>
-        </TR>
-%   } 
-
-%   if (    ! $cust_pkg->get('cancel')
-%        && $curuser->access_right('Provision customer service') 
-%        && $part_svc->num_avail
-%      ) {
-
-      <TR>
-        <TD COLSPAN=3 ALIGN="center" STYLE="padding-bottom:4px;padding-top:0px">
-          <B><% svc_provision_link($cust_pkg, $part_svc, $conf, $curuser) %></B>
-        </TD>
-      </TR>
-
-%   } 
-
-% } 
-
-</TABLE>
-</TD>
-% } #end display packages
-%
-
+% }
 
 </TABLE>
 
 </TABLE>
-% } else { 
 
 
+% } else {
 <BR>
 % } 
 <BR>
 % } 
+
 % if ( $cgi->param('fragment') =~ /^cust_pkg(\d+)$/ ) {
   <SCRIPT>
     // IE-specific hack.  other browsers listen to #fragments
 % if ( $cgi->param('fragment') =~ /^cust_pkg(\d+)$/ ) {
   <SCRIPT>
     // IE-specific hack.  other browsers listen to #fragments
@@ -588,74 +183,6 @@ my $curuser = $FS::CurrentUser::CurrentUser;
 
 my $packages = get_packages($cust_main, $conf);
 
 
 my $packages = get_packages($cust_main, $conf);
 
-my $colspan = $conf->exists('cust_pkg-display_times') ? 8 : 4;
-my $width = $conf->exists('cust_pkg-display_times') ? '38%' : '56%';
-
-sub pkg_status_row {
-  my( $cust_pkg, $title, $field, %opt ) = @_;
-
-  my $color = $opt{'color'};
-
-  my $html = qq(<TR><TD WIDTH="<%$width%>" ALIGN="right">);
-  $html   .= qq(<FONT COLOR="#$color"><B>) if length($color);
-  $html   .= qq($title&nbsp;);
-  $html   .= qq(</B></FONT>) if length($color);
-  $html   .= qq(</TD>);
-  $html   .= pkg_datestr($cust_pkg, $field, $opt{conf}).'</TR>';
-
-  $html;
-}
-
-sub pkg_status_row_if {
-  my( $cust_pkg, $title, $field, %opt ) = @_;
-  
-  $title = '<FONT SIZE=-1>(&nbsp;'. pkg_unadjourn_link($cust_pkg). '&nbsp;)&nbsp;</FONT>'. $title
-    if ( $field eq 'adjourn' &&
-         $opt{curuser}->access_right('Suspend customer package later')
-       );
-
-  $title = '<FONT SIZE=-1>(&nbsp;'. pkg_unexpire_link($cust_pkg). '&nbsp;)&nbsp;</FONT>'. $title
-    if ( $field eq 'expire' &&
-         $opt{curuser}->access_right('Cancel customer package later')
-       );
-
-  $cust_pkg->get($field) ? pkg_status_row($cust_pkg, $title, $field, %opt) : '';
-}
-
-sub pkg_status_row_changed {
-  my( $cust_pkg, %opt ) = @_;
-  return '' unless $cust_pkg->change_date;
-  my $html = pkg_status_row( $cust_pkg, 'Package&nbsp;changed', 'change_date', conf=>$opt{'conf'} );
-  my $old = $cust_pkg->old_cust_pkg;
-  if ( $old ) {
-    my $part_pkg = $old->part_pkg;
-    my $label = 'Changed from '. $cust_pkg->change_pkgnum. ': '.
-                $part_pkg->pkg. ' - '. $part_pkg->comment;
-    $html .= pkg_status_row_colspan( $label, '', size=>'-1', align=>'right' );
-  }
-  $html;
-}
-
-sub pkg_status_row_colspan {
-  my($title, $addl, %opt) = @_;
-
-  my $align = $opt{'align'} ? 'ALIGN="'. $opt{'align'}.'"' : '';
-  my $color = $opt{'color'} ? 'COLOR="#'.$opt{'color'}.'"' : '';
-  my $size  = $opt{'size'}  ? 'SIZE="'.  $opt{'size'}. '"' : '';
-
-  my $html = qq(<TR><TD COLSPAN=$colspan $align>);
-  $html   .= qq(<FONT $color $size>) if length($color) || $size;
-  $html   .= qq(<B>) if $color && !$size;
-  $html   .= $title;
-  $html   .= qq(</B>) if $color && !$size;
-  $html   .= qq(</FONT>) if length($color) || $size;
-  $html   .= ",&nbsp;$addl" if length($addl);
-  $html   .= qq(</TD></TR>);
-
-  $html;
-
-}
-
 #subroutines
 
 sub get_packages {
 #subroutines
 
 sub get_packages {
@@ -677,190 +204,15 @@ sub get_packages {
   [ $cust_main->$method() ];
 }
 
   [ $cust_main->$method() ];
 }
 
-sub svc_provision_link {
-  my ($cust_pkg, $part_svc, $conf, $curuser) = @_;
-  ( my $svc_nbsp = $part_svc->svc ) =~ s/\s+/&nbsp;/g;
-  my $num_avail = $part_svc->num_avail;
-  my $pkgnum_svcpart = "pkgnum=". $cust_pkg->pkgnum. ';'.
-                       "svcpart=". $part_svc->svcpart;
-  my $url;
-  if ( $part_svc->svcdb eq 'svc_external' #could be generalized
-       && $conf->exists('svc_external-skip_manual')
-  ) {
-    $url = "${p}edit/process/". $part_svc->svcdb. ".cgi?$pkgnum_svcpart";
-  } else {
-    $url = svc_url(
-                    'm'        => $m,
-                    'action'   => 'edit',
-                    'part_svc' => $part_svc, 
-                    'query'    => $pkgnum_svcpart,
-                  );
-    #$url = "${p}edit/$svcpart->{svcdb}.cgi?$pkgnum_svcpart";
-  }
-
-  my $link = qq!<A CLASS="provision" HREF="$url">!.
-             "Provision&nbsp;$svc_nbsp&nbsp;($num_avail)</A>";
-  if ( $conf->exists('legacy_link')
-       && $curuser->access_right('View/link unlinked services')
-     )
-  {
-    $link .= '<BR>'.
-             qq!<A CLASS="provision" HREF="${p}misc/link.cgi?!.
-             qq!$pkgnum_svcpart">!.
-            "Link&nbsp;to&nbsp;legacy&nbsp;$svc_nbsp&nbsp;($num_avail)</A>";
-  }
-  $link;
-}
-
-sub svc_unprovision_link {
-  my $cust_svc = shift or return '';
-  qq!<A HREF="javascript:areyousure('${p}misc/unprovision.cgi?!. $cust_svc->svcnum.
-  qq!', 'Permanently unprovision and delete this service?')">Unprovision</A>!;
-}
-
-sub pkg_datestr {
-  my($cust_pkg, $field, $conf) = @_ or return '';
-  return '&nbsp;' unless $cust_pkg->get($field);
-  my $format = '<TD align="left"><B>%b</B></TD>'.
-               '<TD align="right"><B>&nbsp;%o,</B></TD>'.
-               '<TD align="right"><B>&nbsp;%Y</B></TD>';
-  #$format .= '&nbsp;<FONT SIZE=-3>%l:%M:%S%P&nbsp;%z</FONT>'
-  $format .= '<TD ALIGN="right"><B>&nbsp;%l</TD>'.
-             '<TD ALIGN="center"><B>:</B></TD>'.
-             '<TD ALIGN="left"><B>%M</B></TD>'.
-             '<TD ALIGN="left"><B>&nbsp;%P</B></TD>'
-    if $conf->exists('cust_pkg-display_times');
-  my $strip = time2str($format, $cust_pkg->get($field) );
-  $strip =~ s/ (\d)/$1/g;
-  $strip;
-}
-
-sub pkg_change_link { include( '/elements/popup_link-cust_pkg.html',
-                               { 'action'      => $p. 'misc/change_pkg.cgi?dummy=value',
-                                 'label'       => 'Change&nbsp;package',
-                                 'actionlabel' => 'Change',
-                                 'cust_pkg'    => shift,
-                               }
-                             )
-                     }
-
-sub pkg_suspend_link { include( '/elements/popup_link-cust_pkg.html',
-                                { 'action'      => $p. 'misc/cancel_pkg.html?method=suspend',
-                                  'label'       => 'Suspend&nbsp;now',
-                                  'actionlabel' => 'Suspend',
-                                  'color'       => '#FF9900',
-                                  'cust_pkg'    => shift,
-                                }
-                              )
-                     }
-
-
-sub pkg_adjourn_link { include( '/elements/popup_link-cust_pkg.html',
-                                { 'action'      => $p. 'misc/cancel_pkg.html?method=adjourn',
-                                  'label'       => 'Suspend&nbsp;later',
-                                  'actionlabel' => 'Adjourn',
-                                  'color'       => '#CC6600',
-                                  'cust_pkg'    => shift,
-                                }
-                              )
-                     }
-
-sub pkg_delay_link   { include( '/elements/popup_link-cust_pkg.html',
-                                { 'action'      => $p. 'misc/delay_susp_pkg.html',
-                                  'label'       => 'Delay&nbsp;suspend',
-                                  'actionlabel' => 'Delay suspend for',
-                                  'cust_pkg'    => shift,
-                                }
-                              )
-                     }
-
-sub pkg_unsuspend_link { pkg_link('misc/unsusp_pkg',    'Unsuspend',           @_ ); }
-sub pkg_dates_link     { pkg_link('edit/REAL_cust_pkg', 'Edit&nbsp;dates',     @_ ); }
-sub pkg_unadjourn_link { pkg_link('misc/unadjourn_pkg', 'Abort',               @_ ); }
-sub pkg_unexpire_link  { pkg_link('misc/unexpire_pkg',  'Abort',               @_ ); }
-
-sub pkg_cancel_link { include( '/elements/popup_link-cust_pkg.html',
-                               { 'action'      => $p. 'misc/cancel_pkg.html?method=cancel',
-                                 'label'       => 'Cancel&nbsp;now',
-                                 'actionlabel' => 'Cancel',
-                                 'color'       => '#ff0000',
-                                 'cust_pkg'    => shift,
-                               }
-                             )
-                    }
-
-sub pkg_expire_link { include( '/elements/popup_link-cust_pkg.html',
-                               { 'action'      => $p. 'misc/cancel_pkg.html?method=expire',
-                                 'label'       => 'Cancel&nbsp;later',
-                                 'actionlabel' => 'Expire', #"Cancel package $num later"
-                                 'color'       => '#CC0000',
-                                 'cust_pkg'    => shift,
-                               }
-                             )
-                    }
-
-sub svc_recharge_link { include( '/elements/popup_link-cust_svc.html',
-                                 { 'action'      => $p. 'misc/recharge_svc.html',
-                                   'label'       => 'Recharge',
-                                   'actionlabel' => 'Recharge',
-                                   'color'       => '#333399',
-                                   'cust_svc'    => shift,
-                                 }
-                               )
-                      }
-
-sub order_pkg_link { include( '/elements/popup_link-cust_main.html',
-                              { 'action'      => $p. 'misc/order_pkg.html',
-                                'label'       => 'Order&nbsp;new&nbsp;package',
-                                'actionlabel' => 'Order new package',
-                                'color'       => '#333399',
-                                'cust_main'   => shift,
-                                'closetext'   => 'Close',
-                              }
-                            )
-                   }
-
-sub pkg_event_link {
-  my($cust_pkg) = @_;
-  qq!<a href="${p}search/cust_event.html?pkgnum=!. $cust_pkg->pkgnum. qq!">!.
-  'View package events'.
-  '</a>';
-}
-
-sub pkg_link {
-  my($action, $label, $cust_pkg) = @_;
-  return '' unless $cust_pkg;
-  qq!<a href="$p$action.cgi?!. $cust_pkg->pkgnum. qq!">$label</a>!;
+sub order_pkg_link {
+  include( '/elements/popup_link-cust_main.html',
+             'action'      => $p. 'misc/order_pkg.html',
+             'label'       => 'Order&nbsp;new&nbsp;package',
+             'actionlabel' => 'Order new package',
+             'color'       => '#333399',
+             'cust_main'   => shift,
+             'closetext'   => 'Close',
+         )
 }
 
 }
 
-sub pkg_customize_link {
-  my $cust_pkg = shift or return '';
-  my $custnum = $cust_pkg->custnum;
-  qq!<A HREF="${p}edit/part_pkg.cgi?!.
-    "clone=". $cust_pkg->part_pkg->pkgpart. ';'.
-    "pkgnum=". $cust_pkg->pkgnum.
-    qq!">Customize</A>!;
-}
-
-sub pkg_autosuspend_time {
-  my $cust_pkg = shift or return '';
-  my $days = 7;
-  my $time = time;
-  my $pending_suspend = 0;
-  while ( $days > 0 &&
-          scalar(
-            grep { $_->part_event->action eq 'suspend' }
-            @{$cust_pkg->cust_main->due_cust_event( time => $time + 86400*$days,
-                                                    testonly => 1,
-                                                  ) }
-          )
-        )
-  {
-    $pending_suspend = 1;
-    $days--;
-  }
-
-  $pending_suspend ? time + ($days + 1) * 86400 : '';
-
-}
 </%init>
 </%init>
diff --git a/httemplate/view/cust_main/packages/location.html b/httemplate/view/cust_main/packages/location.html
new file mode 100644 (file)
index 0000000..3f84148
--- /dev/null
@@ -0,0 +1,35 @@
+<TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+
+% unless ( $cust_pkg->locationnum ) {
+  <I><FONT SIZE=-1>(default service address)</FONT><BR>
+% }
+
+  <% $loc->address1 |h %><BR>
+
+% if ( $loc->address2 !~ /^\s*$/ ) {
+    <% $loc->address2 |h %><BR>
+% }
+
+  <% $loc->city |h %><% %>,
+  <% $loc->state |h %> &nbsp; <% $loc->zip |h %><BR>
+
+% if ( $loc->country ne $countrydefault ) {
+  <% code2country( $loc->country ) %>
+% }
+
+  </I>
+
+</TD>
+<%init>
+
+my %opt = @_;
+
+my $bgcolor        = $opt{'bgcolor'};
+my $cust_pkg       = $opt{'cust_pkg'};
+my $part_pkg       = $opt{'part_pkg'};
+my $conf           = new FS::Conf;
+my $countrydefault = $conf->config('countrydefault') || 'US';
+
+my $loc = $cust_pkg->cust_location_or_main;
+
+</%init>
diff --git a/httemplate/view/cust_main/packages/package.html b/httemplate/view/cust_main/packages/package.html
new file mode 100644 (file)
index 0000000..4311d8c
--- /dev/null
@@ -0,0 +1,213 @@
+<TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+  <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
+    <TR>
+      <TD COLSPAN=2>
+        <A NAME="cust_pkg<% $cust_pkg->pkgnum %>"
+           ID  ="cust_pkg<% $cust_pkg->pkgnum %>"
+        ><% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B></A>
+        - 
+        <% $part_pkg->comment |h %>
+      </TD>
+    </TR>
+
+% if ( $cust_pkg->quantity > 1 ) {
+    <TR>
+      <TD COLSPAN=2>
+        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Quantity: 
+        <B><% $cust_pkg->quantity %></B>
+      </TD>
+    </TR>
+%  }
+
+    <TR>
+      <TD COLSPAN=2>
+        <FONT SIZE=-1>
+
+%         unless ( $cust_pkg->get('cancel') ) { 
+%
+%           my $br = 0;
+%           if ( $curuser->access_right('Change customer package') ) {
+%             $br=1;
+              (&nbsp;<%pkg_change_link($cust_pkg)%>&nbsp;)
+%           } 
+%
+%           if ( $curuser->access_right('Edit customer package dates') ) {
+%             $br=1;
+              (&nbsp;<%pkg_dates_link($cust_pkg)%>&nbsp;)
+%           } 
+%
+%           if ( $curuser->access_right('Customize customer package') ) {
+%             $br=1;
+              (&nbsp;<%pkg_customize_link($cust_pkg,$cust_pkg->custnum)%>&nbsp;)
+%           } 
+%
+            <% $br ? '<BR>' : '' %>
+%         } 
+
+%         if ( $cust_pkg->num_cust_event
+%              && (    $curuser->access_right('Billing event reports')
+%                   || $curuser->access_right('View customer billing events')
+%                 )
+%            ) {
+            (&nbsp;<%pkg_event_link($cust_pkg)%>&nbsp;)
+%         }
+
+        </FONT>
+      </TD>
+    </TR>
+
+%   my $editi = $curuser->access_right('Edit customer package invoice details');
+%   my $editc = $curuser->access_right('Edit customer package comments');
+%
+%   if (    $cust_pkg->cust_pkg_detail('I')
+%        || $cust_pkg->cust_pkg_detail('C')
+%        || $editi
+%        || $editc                          ) {
+%
+%     my $editlink = $p. 'edit/cust_pkg_detail?pkgnum='. $cust_pkg->pkgnum.
+%                    ';detailtype=';
+
+      <TR>
+
+%       if ( $cust_pkg->cust_pkg_detail('I') ) { 
+          <TD VALIGN="top">
+            <% include('/elements/table-grid.html') %>
+              <TR>
+                <TH BGCOLOR="#dddddd" STYLE="border-bottom: dashed 1px black; padding-bottom: 1px">
+                  <FONT SIZE="-1">
+                    Invoice details
+%                   if ( $editi && ! $cust_pkg->get('cancel') ) {
+                      (<% include('/elements/popup_link.html', { 
+                                    'action'      => $editlink. 'I',
+                                    'label'       => 'edit',
+                                    'actionlabel' => 'Edit invoice details',
+                                    'color'       => '#333399',
+                                    'width'       => 763,
+                                 })
+                       %>)
+%                   }
+                  </FONT>
+                </TH>
+              </TR>
+%             foreach my $cust_pkg_detail ( $cust_pkg->cust_pkg_detail('I') ) {
+                <TR>
+                  <TD><FONT SIZE="-1">&nbsp;-&nbsp;<% $cust_pkg_detail->detail |h %></FONT></TD>
+                </TR>
+%             }
+            </TABLE>
+          </TD>
+%       } else {
+          <TD>
+%           if ( $editi && ! $cust_pkg->get('cancel') ) {
+              <FONT SIZE="-1">
+                (&nbsp;<% include('/elements/popup_link.html', { 
+                               'action'      => $editlink. 'I',
+                               'label'       => 'Add&nbsp;invoice&nbsp;details',
+                               'actionlabel' => 'Add invoice details',
+                               'color'       => '#333399',
+                               'width'       => 763,
+                            })
+                  %>&nbsp;)
+              </FONT>
+%           }
+          </TD>
+%       }
+
+%       if ( $cust_pkg->cust_pkg_detail('C') ) { 
+          <TD VALIGN="top">
+            <% include('/elements/table-grid.html') %>
+              <TR>
+                <TH BGCOLOR="#dddddd" STYLE="border-bottom: dashed 1px black; padding-bottom: 1px">
+                  <FONT SIZE="-1">
+                    Comments
+%                   if ( $editc ) {
+                      (<% include('/elements/popup_link.html', { 
+                                    'action'      => $editlink. 'C',
+                                    'label'       => 'edit',
+                                    'actionlabel' => 'Edit comments',
+                                    'color'       => '#333399',
+                                    'width'       => 763,
+                                 })
+                       %>)
+%                   }
+                  </FONT>
+                </TH>
+              </TR>
+%             foreach my $cust_pkg_detail ( $cust_pkg->cust_pkg_detail('C') ) {
+                <TR>
+                  <TD><FONT SIZE="-1">&nbsp;-&nbsp;<% $cust_pkg_detail->detail |h %></FONT></TD>
+                </TR>
+%             }
+            </TABLE>
+          </TD>
+%       } else {
+          <TD>
+%           if ( $editc ) {
+              <FONT SIZE="-1">
+                (&nbsp;<% include('/elements/popup_link.html', { 
+                               'action'      => $editlink. 'C',
+                               'label'       => 'Add&nbsp;comments',
+                               'actionlabel' => 'Add comments',
+                               'color'       => '#333399',
+                               'width'       => 763,
+                            })
+                  %>&nbsp;)
+              </FONT>
+%           }
+          </TD>
+%       }
+
+      </TR>
+%   }
+
+  </TABLE>
+
+</TD>
+
+<%init>
+
+my %opt = @_;
+
+my $bgcolor  = $opt{'bgcolor'};
+my $cust_pkg = $opt{'cust_pkg'};
+my $part_pkg = $opt{'part_pkg'};
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+#subroutines
+
+#false laziness w/status.html
+sub pkg_link {
+  my($action, $label, $cust_pkg) = @_;
+  return '' unless $cust_pkg;
+  qq!<a href="$p$action.cgi?!. $cust_pkg->pkgnum. qq!">$label</a>!;
+}
+
+sub pkg_change_link {
+  include( '/elements/popup_link-cust_pkg.html',
+             'action'      => $p. 'misc/change_pkg.cgi?dummy=value',
+             'label'       => 'Change&nbsp;package',
+             'actionlabel' => 'Change',
+             'cust_pkg'    => shift,
+         )
+}
+
+sub pkg_dates_link { pkg_link('edit/REAL_cust_pkg', 'Edit&nbsp;dates', @_ ); }
+
+sub pkg_customize_link {
+  my $cust_pkg = shift or return '';
+  my $custnum = $cust_pkg->custnum;
+  qq!<A HREF="${p}edit/part_pkg.cgi?!.
+    "clone=". $cust_pkg->part_pkg->pkgpart. ';'.
+    "pkgnum=". $cust_pkg->pkgnum.
+    qq!">Customize</A>!;
+}
+
+sub pkg_event_link {
+  my($cust_pkg) = @_;
+  qq!<a href="${p}search/cust_event.html?pkgnum=!. $cust_pkg->pkgnum. qq!">!.
+  'View package events'.
+  '</a>';
+}
+
+</%init>
diff --git a/httemplate/view/cust_main/packages/services.html b/httemplate/view/cust_main/packages/services.html
new file mode 100644 (file)
index 0000000..f46afb9
--- /dev/null
@@ -0,0 +1,120 @@
+% ###
+% # Services
+% ###
+
+  <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+    <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
+
+%  #foreach my $svcpart (sort {$a->{svcpart} <=> $b->{svcpart}} @{$pkg->{svcparts}}) {
+%  foreach my $part_svc ( $cust_pkg->part_svc ) {
+
+%    #foreach my $service (@{$svcpart->{services}}) {
+%    foreach my $cust_svc ( @{ $part_svc->cust_pkg_svc } ) {
+
+      <TR>
+        <TD ALIGN="right" VALIGN="top"><% FS::UI::Web::svc_link($m, $part_svc, $cust_svc) %></TD>
+        <TD STYLE="padding-bottom:0px"><B><% FS::UI::Web::svc_label_link($m, $part_svc, $cust_svc) %></B></TD>
+        <TD><% FS::UI::Web::svc_export_links($m, $part_svc, $cust_svc) %></TD>
+      </TR>
+
+      <TR>
+        <TD ALIGN="right" COLSPAN="3" VALIGN="top" STYLE="padding-bottom:1px;padding-top:0px"><FONT SIZE="-2" COLOR="#FFD000">
+
+            <% $cust_svc->overlimit ? "Overlimit: ". time2str('%b %o %Y' . ($conf->exists('cust_pkg-display_times') ? ' %l:%M %P' : ''), $cust_svc->overlimit) : '' %>
+          </FONT></TD>
+      </TR>
+
+      <TR>
+        <TD ALIGN="right" VALIGN="top" STYLE="padding-bottom:5px;padding-top:0px"><FONT SIZE="-2">
+
+%         if ( $curuser->access_right('Recharge customer service')
+%              && $part_svc->svcdb eq 'svc_acct'
+%              && (    $cust_svc->svc_x->seconds    ne ''
+%                   || $cust_svc->svc_x->upbytes    ne ''
+%                   || $cust_svc->svc_x->downbytes  ne ''
+%                   || $cust_svc->svc_x->totalbytes ne ''
+%                 )
+%         ) { 
+            (&nbsp;<%svc_recharge_link($cust_svc)%>&nbsp;)
+%         } 
+          </FONT></TD>
+
+          <TD ALIGN="right" VALIGN="top" STYLE="padding-bottom:5px;padding-top:0px"><FONT SIZE="-2">
+
+%         if ( $curuser->access_right('Unprovision customer service') ) { 
+            (&nbsp;<%svc_unprovision_link($cust_svc)%>&nbsp;)
+%         } 
+          </FONT></TD>
+        </TR>
+%   } 
+
+%   if (    ! $cust_pkg->get('cancel')
+%        && $curuser->access_right('Provision customer service') 
+%        && $part_svc->num_avail
+%      ) {
+
+      <TR>
+        <TD COLSPAN=3 ALIGN="center" STYLE="padding-bottom:4px;padding-top:0px">
+          <B><% svc_provision_link($cust_pkg, $part_svc, $conf, $curuser) %></B>
+        </TD>
+      </TR>
+
+%   } 
+
+% } 
+
+    </TABLE>
+  </TD>
+
+<%init>
+
+my %opt = @_;
+
+my $bgcolor  = $opt{'bgcolor'};
+my $cust_pkg = $opt{'cust_pkg'};
+my $part_pkg = $opt{'part_pkg'};
+my $curuser  = $FS::CurrentUser::CurrentUser;
+my $conf     = new FS::Conf;
+
+sub svc_provision_link {
+  my ($cust_pkg, $part_svc, $conf, $curuser) = @_;
+  ( my $svc_nbsp = $part_svc->svc ) =~ s/\s+/&nbsp;/g;
+  my $num_avail = $part_svc->num_avail;
+  my $pkgnum_svcpart = "pkgnum=". $cust_pkg->pkgnum. ';'.
+                       "svcpart=". $part_svc->svcpart;
+  my $url;
+  if ( $part_svc->svcdb eq 'svc_external' #could be generalized
+       && $conf->exists('svc_external-skip_manual')
+  ) {
+    $url = "${p}edit/process/". $part_svc->svcdb. ".cgi?$pkgnum_svcpart";
+  } else {
+    $url = svc_url(
+                    'm'        => $m,
+                    'action'   => 'edit',
+                    'part_svc' => $part_svc, 
+                    'query'    => $pkgnum_svcpart,
+                  );
+    #$url = "${p}edit/$svcpart->{svcdb}.cgi?$pkgnum_svcpart";
+  }
+
+  my $link = qq!<A CLASS="provision" HREF="$url">!.
+             "Provision&nbsp;$svc_nbsp&nbsp;($num_avail)</A>";
+  if ( $conf->exists('legacy_link')
+       && $curuser->access_right('View/link unlinked services')
+     )
+  {
+    $link .= '<BR>'.
+             qq!<A CLASS="provision" HREF="${p}misc/link.cgi?!.
+             qq!$pkgnum_svcpart">!.
+            "Link&nbsp;to&nbsp;legacy&nbsp;$svc_nbsp&nbsp;($num_avail)</A>";
+  }
+  $link;
+}
+
+sub svc_unprovision_link {
+  my $cust_svc = shift or return '';
+  qq!<A HREF="javascript:areyousure('${p}misc/unprovision.cgi?!. $cust_svc->svcnum.
+  qq!', 'Permanently unprovision and delete this service?')">Unprovision</A>!;
+}
+
+</%init>
diff --git a/httemplate/view/cust_main/packages/status.html b/httemplate/view/cust_main/packages/status.html
new file mode 100644 (file)
index 0000000..4e7462b
--- /dev/null
@@ -0,0 +1,374 @@
+<TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+  <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
+
+%#this should use cust_pkg->status and cust_pkg->statuscolor eventually
+
+% if ( $cust_pkg->get('cancel') ) { #status: cancelled
+%   my $cpr = $cust_pkg->last_cust_pkg_reason('cancel');
+
+    <% pkg_status_row($cust_pkg, 'Cancelled', 'cancel', 'color'=>'FF0000', conf=>$conf ) %>
+
+    <% pkg_status_row_colspan(
+         ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '',
+         'align' => 'right', 'color' => 'ff0000', 'size' => '-2',
+       )
+    %>
+
+%   unless ( $cust_pkg->get('setup') ) { 
+
+        <% pkg_status_row_colspan('Never billed') %>
+
+%   } else { 
+
+       <% pkg_status_row( $cust_pkg, 'Setup', 'setup', conf=>$conf ) %>
+       <% pkg_status_row_changed( $cust_pkg, conf=>$conf ) %>
+       <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', conf=>$conf, curuser=>$curuser ) %>
+       <% pkg_status_row_if( $cust_pkg, 'Suspended', 'susp', conf=>$conf, curuser=>$curuser ) %>
+
+%   } 
+%
+% } else { 
+%
+%   if ( $cust_pkg->get('susp') ) { #status: suspended
+%     my $cpr = $cust_pkg->last_cust_pkg_reason('susp');
+
+    <% pkg_status_row( $cust_pkg, 'Suspended', 'susp', 'color'=>'FF9900', conf=>$conf ) %>
+
+    <% pkg_status_row_colspan(
+         ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '',
+         'align' => 'right', 'color' => 'FF9900', 'size' => '-2',
+       )
+    %>
+
+%   unless ( $cust_pkg->get('setup') ) { 
+      <% pkg_status_row_colspan('Never billed') %>
+%   } else { 
+      <% pkg_status_row($cust_pkg, 'Setup', 'setup', conf=>$conf ) %>
+%   } 
+
+    <% pkg_status_row_changed( $cust_pkg, conf=>$conf ) %>
+    <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', conf=>$conf, curuser=>$curuser ) %>
+%   # pkg_status_row($cust_pkg, 'Next bill', 'bill', conf=>$conf)
+    <% pkg_status_row_if( $cust_pkg, 'Expires', 'expire', conf=>$conf, curuser=>$curuser ) %>
+
+    <TR>
+      <TD COLSPAN=<%$colspan%>>
+        <FONT SIZE=-1>
+%         if ( $curuser->access_right('Unsuspend customer package') ) { 
+            (&nbsp;<% pkg_unsuspend_link($cust_pkg) %>&nbsp;)
+%         } 
+%         if ( $curuser->access_right('Cancel customer package immediately') ) {
+            (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
+%         } 
+        </FONT>
+      </TD>
+    </TR>
+
+%   } else { #status: active
+%
+%     unless ( $cust_pkg->get('setup') ) { #not setup
+%
+%       unless ( $part_pkg->freq ) { 
+
+          <% pkg_status_row_colspan('Not&nbsp;yet&nbsp;billed&nbsp;(one-time&nbsp;charge)') %>
+
+          <TR>
+            <TD COLSPAN=<%$colspan%>>
+              <FONT SIZE=-1>
+%               if ( $curuser->access_right('Cancel customer package immediately') ) { 
+                  (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
+%               } 
+              </FONT>
+            </TD>
+          </TR>
+
+%       } else { 
+
+         <% pkg_status_row_colspan("Not&nbsp;yet&nbsp;billed&nbsp;($billed_or_prepaid&nbsp;". myfreq($part_pkg). ')' ) %>
+
+%       } 
+%
+%     } else { #setup
+%
+%       unless ( $part_pkg->freq ) { 
+
+          <% pkg_status_row_colspan('One-time&nbsp;charge') %>
+
+          <% pkg_status_row($cust_pkg, 'Billed', 'setup', conf=>$conf) %>
+
+%       } else { 
+%
+%         if (scalar($cust_pkg->overlimit)) {
+
+            <% pkg_status_row_colspan(
+                 'Overlimit',
+                 $billed_or_prepaid. '&nbsp;'. myfreq($part_pkg),
+                 'color' => 'FFD000',
+               )
+            %>
+
+%         } else {
+            <% pkg_status_row_colspan(
+                 'Active',
+                 $billed_or_prepaid. '&nbsp;'. myfreq($part_pkg),
+                 'color' => '00CC00',
+               )
+            %>
+%         } 
+
+          <% pkg_status_row($cust_pkg, 'Setup', 'setup', conf=>$conf) %>
+
+%       } 
+%
+%     } 
+%   my $autosuspend = pkg_autosuspend_time( $cust_pkg );
+%   $cust_pkg->set('autosuspend', $autosuspend) if $autosuspend;
+
+      <% pkg_status_row_changed( $cust_pkg, conf=>$conf ) %>
+      <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', conf=>$conf, curuser=>$curuser ) %>
+      <% pkg_status_row_if( $cust_pkg, $next_bill_or_prepaid_until, 'bill', conf=>$conf, curuser=>$curuser ) %>
+      <% pkg_status_row_if($cust_pkg, 'Will automatically suspend by', 'autosuspend', conf=>$conf) %>
+      <% pkg_status_row_if( $cust_pkg, 'Will suspend on', 'adjourn', conf=>$conf, curuser=>$curuser ) %>
+      <% pkg_status_row_if( $cust_pkg, 'Expires', 'expire', conf=>$conf, curuser=>$curuser ) %>
+
+%     if ( $part_pkg->freq ) { 
+
+        <TR>
+          <TD COLSPAN=<%$colspan%>>
+            <FONT SIZE=-1>
+%             if ( $curuser->access_right('Suspend customer package') ) { 
+                (&nbsp;<% pkg_suspend_link($cust_pkg) %>&nbsp;)
+%             } 
+%             if ( $curuser->access_right('Suspend customer package later') ) { 
+                (&nbsp;<% pkg_adjourn_link($cust_pkg) %>&nbsp;)
+%             } 
+%             if ( $curuser->access_right('Delay suspension events') ) { 
+                (&nbsp;<% pkg_delay_link($cust_pkg) %>&nbsp;)
+%             } 
+%             if ( $curuser->access_right('Cancel customer package immediately') ) { 
+                (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
+%             } 
+%             if ( $curuser->access_right('Cancel customer package later') ) { 
+                (&nbsp;<% pkg_expire_link($cust_pkg) %>&nbsp;)
+%             } 
+
+            <FONT>
+          </TD>
+        </TR>
+%     }
+%
+%   } 
+% } 
+
+  </TABLE>
+</TD>
+
+<%init>
+
+my %opt = @_;
+
+my $bgcolor  = $opt{'bgcolor'};
+my $cust_pkg = $opt{'cust_pkg'};
+my $part_pkg = $opt{'part_pkg'};
+my $curuser  = $FS::CurrentUser::CurrentUser;
+my $conf     = new FS::Conf;
+my $colspan  = $conf->exists('cust_pkg-display_times') ? 8 : 4;
+my $width    = $conf->exists('cust_pkg-display_times') ? '38%' : '56%';
+
+#false laziness w/edit/REAL_cust_pkg.cgi
+my( $billed_or_prepaid, $last_bill_or_renewed, $next_bill_or_prepaid_until );
+unless ( $part_pkg->is_prepaid ) {
+  $billed_or_prepaid = 'billed';
+  $last_bill_or_renewed = 'Last&nbsp;bill';
+  $next_bill_or_prepaid_until = 'Next&nbsp;bill';
+} else {
+  $billed_or_prepaid = 'prepaid';
+  $last_bill_or_renewed = 'Renewed';
+  $next_bill_or_prepaid_until = 'Prepaid&nbsp;until';
+}
+
+#subroutines
+
+sub myfreq {
+  my $part_pkg = shift;
+  my $freq = $part_pkg->freq_pretty;
+  $freq =~ s/ /&nbsp;/g;
+  $freq;
+}
+
+#false laziness w/package.html
+sub pkg_link {
+  my($action, $label, $cust_pkg) = @_;
+  return '' unless $cust_pkg;
+  qq!<a href="$p$action.cgi?!. $cust_pkg->pkgnum. qq!">$label</a>!;
+}
+
+sub pkg_status_row {
+  my( $cust_pkg, $title, $field, %opt ) = @_;
+
+  my $color = $opt{'color'};
+
+  my $html = qq(<TR><TD WIDTH="<%$width%>" ALIGN="right">);
+  $html   .= qq(<FONT COLOR="#$color"><B>) if length($color);
+  $html   .= qq($title&nbsp;);
+  $html   .= qq(</B></FONT>) if length($color);
+  $html   .= qq(</TD>);
+  $html   .= pkg_datestr($cust_pkg, $field, $opt{conf}).'</TR>';
+
+  $html;
+}
+
+sub pkg_status_row_if {
+  my( $cust_pkg, $title, $field, %opt ) = @_;
+  
+  $title = '<FONT SIZE=-1>(&nbsp;'. pkg_unadjourn_link($cust_pkg). '&nbsp;)&nbsp;</FONT>'. $title
+    if ( $field eq 'adjourn' &&
+         $opt{curuser}->access_right('Suspend customer package later')
+       );
+
+  $title = '<FONT SIZE=-1>(&nbsp;'. pkg_unexpire_link($cust_pkg). '&nbsp;)&nbsp;</FONT>'. $title
+    if ( $field eq 'expire' &&
+         $opt{curuser}->access_right('Cancel customer package later')
+       );
+
+  $cust_pkg->get($field) ? pkg_status_row($cust_pkg, $title, $field, %opt) : '';
+}
+
+sub pkg_status_row_changed {
+  my( $cust_pkg, %opt ) = @_;
+  return '' unless $cust_pkg->change_date;
+  my $html = pkg_status_row( $cust_pkg, 'Package&nbsp;changed', 'change_date', conf=>$opt{'conf'} );
+  my $old = $cust_pkg->old_cust_pkg;
+  if ( $old ) {
+    my $part_pkg = $old->part_pkg;
+    my $label = 'Changed from '. $cust_pkg->change_pkgnum. ': '.
+                $part_pkg->pkg. ' - '. $part_pkg->comment;
+    $html .= pkg_status_row_colspan( $label, '', size=>'-1', align=>'right' );
+  }
+  $html;
+}
+
+sub pkg_status_row_colspan {
+  my($title, $addl, %opt) = @_;
+
+  my $align = $opt{'align'} ? 'ALIGN="'. $opt{'align'}.'"' : '';
+  my $color = $opt{'color'} ? 'COLOR="#'.$opt{'color'}.'"' : '';
+  my $size  = $opt{'size'}  ? 'SIZE="'.  $opt{'size'}. '"' : '';
+
+  my $html = qq(<TR><TD COLSPAN=$colspan $align>);
+  $html   .= qq(<FONT $color $size>) if length($color) || $size;
+  $html   .= qq(<B>) if $color && !$size;
+  $html   .= $title;
+  $html   .= qq(</B>) if $color && !$size;
+  $html   .= qq(</FONT>) if length($color) || $size;
+  $html   .= ",&nbsp;$addl" if length($addl);
+  $html   .= qq(</TD></TR>);
+
+  $html;
+
+}
+
+sub pkg_datestr {
+  my($cust_pkg, $field, $conf) = @_ or return '';
+  return '&nbsp;' unless $cust_pkg->get($field);
+  my $format = '<TD align="left"><B>%b</B></TD>'.
+               '<TD align="right"><B>&nbsp;%o,</B></TD>'.
+               '<TD align="right"><B>&nbsp;%Y</B></TD>';
+  #$format .= '&nbsp;<FONT SIZE=-3>%l:%M:%S%P&nbsp;%z</FONT>'
+  $format .= '<TD ALIGN="right"><B>&nbsp;%l</TD>'.
+             '<TD ALIGN="center"><B>:</B></TD>'.
+             '<TD ALIGN="left"><B>%M</B></TD>'.
+             '<TD ALIGN="left"><B>&nbsp;%P</B></TD>'
+    if $conf->exists('cust_pkg-display_times');
+  my $strip = time2str($format, $cust_pkg->get($field) );
+  $strip =~ s/ (\d)/$1/g;
+  $strip;
+}
+
+sub pkg_suspend_link {
+  include( '/elements/popup_link-cust_pkg.html',
+             'action'      => $p. 'misc/cancel_pkg.html?method=suspend',
+             'label'       => 'Suspend&nbsp;now',
+             'actionlabel' => 'Suspend',
+             'color'       => '#FF9900',
+             'cust_pkg'    => shift,
+         )
+}
+
+sub pkg_adjourn_link {
+  include( '/elements/popup_link-cust_pkg.html',
+             'action'      => $p. 'misc/cancel_pkg.html?method=adjourn',
+             'label'       => 'Suspend&nbsp;later',
+             'actionlabel' => 'Adjourn',
+             'color'       => '#CC6600',
+             'cust_pkg'    => shift,
+         )
+}
+
+sub pkg_delay_link  {
+  include( '/elements/popup_link-cust_pkg.html',
+             'action'      => $p. 'misc/delay_susp_pkg.html',
+             'label'       => 'Delay&nbsp;suspend',
+             'actionlabel' => 'Delay suspend for',
+             'cust_pkg'    => shift,
+         )
+}
+
+sub pkg_unsuspend_link { pkg_link('misc/unsusp_pkg',    'Unsuspend', @_ ); }
+sub pkg_unadjourn_link { pkg_link('misc/unadjourn_pkg', 'Abort',     @_ ); }
+sub pkg_unexpire_link  { pkg_link('misc/unexpire_pkg',  'Abort',     @_ ); }
+
+sub pkg_cancel_link {
+  include( '/elements/popup_link-cust_pkg.html',
+             'action'      => $p. 'misc/cancel_pkg.html?method=cancel',
+             'label'       => 'Cancel&nbsp;now',
+             'actionlabel' => 'Cancel',
+             'color'       => '#ff0000',
+             'cust_pkg'    => shift,
+         )
+}
+
+sub pkg_expire_link {
+  include( '/elements/popup_link-cust_pkg.html',
+             'action'      => $p. 'misc/cancel_pkg.html?method=expire',
+             'label'       => 'Cancel&nbsp;later',
+             'actionlabel' => 'Expire', #"Cancel package $num later"
+             'color'       => '#CC0000',
+             'cust_pkg'    => shift,
+         )
+}
+
+sub svc_recharge_link {
+  include( '/elements/popup_link-cust_svc.html',
+             'action'      => $p. 'misc/recharge_svc.html',
+             'label'       => 'Recharge',
+             'actionlabel' => 'Recharge',
+             'color'       => '#333399',
+             'cust_svc'    => shift,
+         )
+}
+
+sub pkg_autosuspend_time {
+  my $cust_pkg = shift or return '';
+  my $days = 7;
+  my $time = time;
+  my $pending_suspend = 0;
+  #this seems to be extremely inefficient...
+  while ( $days > 0 &&
+          scalar(
+            grep { $_->part_event->action eq 'suspend' }
+            @{$cust_pkg->cust_main->due_cust_event( time => $time + 86400*$days,
+                                                    testonly => 1,
+                                                  ) }
+          )
+        )
+  {
+    $pending_suspend = 1;
+    $days--;
+  }
+
+  $pending_suspend ? time + ($days + 1) * 86400 : '';
+
+}
+
+</%init>