supplemental packages, #20689
authorMark Wells <mark@freeside.biz>
Sat, 12 Jan 2013 20:03:16 +0000 (12:03 -0800)
committerMark Wells <mark@freeside.biz>
Sat, 12 Jan 2013 20:07:06 +0000 (12:07 -0800)
19 files changed:
FS/FS/Schema.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_main/Packages.pm
FS/FS/cust_pkg.pm
FS/FS/part_pkg.pm
FS/FS/part_pkg_link.pm
httemplate/browse/part_pkg.cgi
httemplate/edit/REAL_cust_pkg.cgi
httemplate/edit/cust_pkg.cgi
httemplate/edit/part_pkg.cgi
httemplate/edit/process/REAL_cust_pkg.cgi
httemplate/edit/process/part_pkg.cgi
httemplate/misc/confirm-cust_pkg-edit_dates.html [new file with mode: 0755]
httemplate/search/elements/search-html.html
httemplate/search/elements/search.html
httemplate/view/cust_main/packages.html
httemplate/view/cust_main/packages/package.html
httemplate/view/cust_main/packages/section.html
httemplate/view/cust_main/packages/status.html

index 69e21da..5ac2b5f 100644 (file)
@@ -1738,6 +1738,8 @@ sub tables_hashref {
         'change_pkgnum',       'int', 'NULL', '', '', '',
         'change_pkgpart',      'int', 'NULL', '', '', '',
         'change_locationnum',  'int', 'NULL', '', '', '',
+        'main_pkgnum',         'int', 'NULL', '', '', '',
+        'pkglinknum',          'int', 'NULL', '', '', '',
         'manual_flag',        'char', 'NULL',  1, '', '', 
         'no_auto',            'char', 'NULL',  1, '', '', 
         'quantity',            'int', 'NULL', '', '', '',
index cd46c73..deb5e84 100644 (file)
@@ -410,6 +410,7 @@ sub bill {
   my @precommit_hooks = ();
 
   $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ];  #param checks?
+
   foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
 
     next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
@@ -1031,9 +1032,31 @@ sub _make_lines {
 
     if ( $increment_next_bill ) {
 
-      my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
+      my $next_bill;
+
+      if ( my $main_pkg = $cust_pkg->main_pkg ) {
+        # supplemental package
+        # to keep in sync with the main package, simulate billing at 
+        # its frequency
+        my $main_pkg_freq = $main_pkg->part_pkg->freq;
+        my $supp_pkg_freq = $part_pkg->freq;
+        my $ratio = $supp_pkg_freq / $main_pkg_freq;
+        if ( $ratio != int($ratio) ) {
+          # the UI should prevent setting up packages like this, but just
+          # in case
+          return "supplemental package period is not an integer multiple of main  package period";
+        }
+        $next_bill = $sdate;
+        for (1..$ratio) {
+          $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq );
+        }
+
+      } else {
+        # the normal case
+      $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
       return "unparsable frequency: ". $part_pkg->freq
         if $next_bill == -1;
+      }  
   
       #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
       # only for figuring next bill date, nothing else, so, reset $sdate again
index 395cce7..ee38f04 100644 (file)
@@ -29,6 +29,9 @@ These methods are available on FS::cust_main objects;
 
 Orders a single package.
 
+Note that if the package definition has supplemental packages, those will
+be ordered as well.
+
 Options may be passed as a list of key/value pairs or as a hash reference.
 Options are:
 
@@ -141,6 +144,34 @@ sub order_pkg {
     }
   }
 
+  # add supplemental packages, if any are needed
+  my $part_pkg = FS::part_pkg->by_key($cust_pkg->pkgpart);
+  foreach my $link ($part_pkg->supp_part_pkg_link) {
+    warn "inserting supplemental package ".$link->dst_pkgpart;
+    my $pkg = FS::cust_pkg->new({
+        'pkgpart'       => $link->dst_pkgpart,
+        'pkglinknum'    => $link->pkglinknum,
+        'custnum'       => $self->custnum,
+        'main_pkgnum'   => $cust_pkg->pkgnum,
+        'locationnum'   => $cust_pkg->locationnum,
+        # try to prevent as many surprises as possible
+        'pkgbatch'      => $cust_pkg->pkgbatch,
+        'start_date'    => $cust_pkg->start_date,
+        'order_date'    => $cust_pkg->order_date,
+        'expire'        => $cust_pkg->expire,
+        'adjourn'       => $cust_pkg->adjourn,
+        'contract_end'  => $cust_pkg->contract_end,
+        'refnum'        => $cust_pkg->refnum,
+        'discountnum'   => $cust_pkg->discountnum,
+        'waive_setup'   => $cust_pkg->waive_setup,
+    });
+    $error = $self->order_pkg('cust_pkg' => $pkg);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "inserting supplemental package: $error";
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   ''; #no error
 
index 22a7b2c..6d85a11 100644 (file)
@@ -197,6 +197,15 @@ Previous locationnum
 
 =item waive_setup
 
+=item main_pkgnum
+
+The pkgnum of the package that this package is supplemental to, if any.
+
+=item pkglinknum
+
+The package link (L<FS::part_pkg_link>) that defines this supplemental
+package, if it is one.
+
 =back
 
 Note: setup, last_bill, bill, adjourn, susp, expire, cancel and change_date
@@ -616,6 +625,8 @@ sub check {
     || $self->ut_numbern('agent_pkgid')
     || $self->ut_enum('recur_show_zero', [ '', 'Y', 'N', ])
     || $self->ut_enum('setup_show_zero', [ '', 'Y', 'N', ])
+    || $self->ut_foreign_keyn('main_pkgnum', 'cust_pkg', 'pkgnum')
+    || $self->ut_foreign_keyn('pkglinknum', 'part_pkg_link', 'pkglinknum')
   ;
   return $error if $error;
 
@@ -730,6 +741,11 @@ sub cancel {
   my( $self, %options ) = @_;
   my $error;
 
+  # pass all suspend/cancel actions to the main package
+  if ( $self->main_pkgnum and !$options{'from_main'} ) {
+    return $self->main_pkg->cancel(%options);
+  }
+
   my $conf = new FS::Conf;
 
   warn "cust_pkg::cancel called with options".
@@ -835,6 +851,14 @@ sub cancel {
     return $error;
   }
 
+  foreach my $supp_pkg ( $self->supplemental_pkgs ) {
+    $error = $supp_pkg->cancel(%options, 'from_main' => 1);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "canceling supplemental pkg#".$supp_pkg->pkgnum.": $error";
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   return '' if $date; #no errors
 
@@ -894,6 +918,9 @@ svc_fatal: service provisioning errors are fatal
 
 svc_errors: pass an array reference, will be filled in with any provisioning errors
 
+main_pkgnum: link the package as a supplemental package of this one.  For 
+internal use only.
+
 =cut
 
 sub uncancel {
@@ -902,6 +929,10 @@ sub uncancel {
   #in case you try do do $uncancel-date = $cust_pkg->uncacel 
   return '' unless $self->get('cancel');
 
+  if ( $self->main_pkgnum and !$options{'main_pkgnum'} ) {
+    return $self->main_pkg->uncancel(%options);
+  }
+
   ##
   # Transaction-alize
   ##
@@ -926,6 +957,7 @@ sub uncancel {
     bill            => ( $options{'bill'}      || $self->get('bill')      ),
     uncancel        => time,
     uncancel_pkgnum => $self->pkgnum,
+    main_pkgnum     => ($options{'main_pkgnum'} || ''),
     map { $_ => $self->get($_) } qw(
       custnum pkgpart locationnum
       setup
@@ -1023,6 +1055,20 @@ sub uncancel {
   }
 
   ##
+  # Uncancel any supplemental packages, and make them supplemental to the 
+  # new one.
+  ##
+
+  foreach my $supp_pkg ( $self->supplemental_pkgs ) {
+    my $new_pkg;
+    $error = $supp_pkg->uncancel(%options, 'main_pkgnum' => $cust_pkg->pkgnum);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "canceling supplemental pkg#".$supp_pkg->pkgnum.": $error";
+    }
+  }
+
+  ##
   # Finish
   ##
 
@@ -1111,6 +1157,9 @@ of final invoices or unused-time credits
 unsuspended.  This may be more convenient than calling C<unsuspend()>
 separately.
 
+=item from_main - allows a supplemental package to be suspended, rather
+than redirecting the method call to its main package.  For internal use.
+
 =back
 
 If there is an error, returns the error, otherwise returns false.
@@ -1121,6 +1170,11 @@ sub suspend {
   my( $self, %options ) = @_;
   my $error;
 
+  # pass all suspend/cancel actions to the main package
+  if ( $self->main_pkgnum and !$options{'from_main'} ) {
+    return $self->main_pkg->suspend(%options);
+  }
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE'; 
@@ -1271,6 +1325,14 @@ sub suspend {
 
   }
 
+  foreach my $supp_pkg ( $self->supplemental_pkgs ) {
+    $error = $supp_pkg->suspend(%options, 'from_main' => 1);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "suspending supplemental pkg#".$supp_pkg->pkgnum.": $error";
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   ''; #no errors
@@ -1353,6 +1415,11 @@ sub unsuspend {
   my( $self, %opt ) = @_;
   my $error;
 
+  # pass all suspend/cancel actions to the main package
+  if ( $self->main_pkgnum and !$opt{'from_main'} ) {
+    return $self->main_pkg->unsuspend(%opt);
+  }
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE'; 
@@ -1511,6 +1578,14 @@ sub unsuspend {
 
   }
 
+  foreach my $supp_pkg ( $self->supplemental_pkgs ) {
+    $error = $supp_pkg->unsuspend(%opt, 'from_main' => 1);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "unsuspending supplemental pkg#".$supp_pkg->pkgnum.": $error";
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   ''; #no errors
@@ -1700,7 +1775,6 @@ sub change {
     locationnum  => ( $opt->{'locationnum'}                        ),
     %hash,
   };
-
   $error = $cust_pkg->insert( 'change' => 1 );
   if ($error) {
     $dbh->rollback if $oldAutoCommit;
@@ -1749,11 +1823,63 @@ sub change {
     }
   }
 
+  # Order any supplemental packages.
+  my $part_pkg = $cust_pkg->part_pkg;
+  my @old_supp_pkgs = $self->supplemental_pkgs;
+  my @new_supp_pkgs;
+  foreach my $link ($part_pkg->supp_part_pkg_link) {
+    my $old;
+    foreach (@old_supp_pkgs) {
+      if ($_->pkgpart == $link->dst_pkgpart) {
+        $old = $_;
+        $_->pkgpart(0); # so that it can't match more than once
+      }
+      last if $old;
+    }
+    # false laziness with FS::cust_main::Packages::order_pkg
+    my $new = FS::cust_pkg->new({
+        pkgpart       => $link->dst_pkgpart,
+        pkglinknum    => $link->pkglinknum,
+        custnum       => $self->custnum,
+        main_pkgnum   => $cust_pkg->pkgnum,
+        locationnum   => $cust_pkg->locationnum,
+        start_date    => $cust_pkg->start_date,
+        order_date    => $cust_pkg->order_date,
+        expire        => $cust_pkg->expire,
+        adjourn       => $cust_pkg->adjourn,
+        contract_end  => $cust_pkg->contract_end,
+        refnum        => $cust_pkg->refnum,
+        discountnum   => $cust_pkg->discountnum,
+        waive_setup   => $cust_pkg->waive_setup
+    });
+    if ( $old and $opt->{'keep_dates'} ) {
+      foreach (qw(setup bill last_bill)) {
+        $new->set($_, $old->get($_));
+      }
+    }
+    $error = $new->insert;
+    # transfer services
+    if ( $old ) {
+      $error ||= $old->transfer($new);
+    }
+    if ( $error and $error > 0 ) {
+      # no reason why this should ever fail, but still...
+      $error = "Unable to transfer all services from supplemental package ".
+        $old->pkgnum;
+    }
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+    push @new_supp_pkgs, $new;
+  }
+
   #Good to go, cancel old package.  Notify 'cancel' of whether to credit 
   #remaining time.
   #Don't allow billing the package (preceding period packages and/or 
   #outstanding usage) if we are keeping dates (i.e. location changing), 
   #because the new package will be billed for the same date range.
+  #Supplemental packages are also canceled here.
   $error = $self->cancel(
     quiet         => 1, 
     unused_credit => $unused_credit,
@@ -1766,7 +1892,9 @@ sub change {
 
   if ( $conf->exists('cust_pkg-change_pkgpart-bill_now') ) {
     #$self->cust_main
-    my $error = $cust_pkg->cust_main->bill( 'pkg_list' => [ $cust_pkg ] );
+    my $error = $cust_pkg->cust_main->bill( 
+      'pkg_list' => [ $cust_pkg, @new_supp_pkgs ]
+    );
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
@@ -3139,6 +3267,31 @@ sub cust_pkg_discount_active {
 
 =back
 
+=item supplemental_pkgs
+
+Returns a list of all packages supplemental to this one.
+
+=cut
+
+sub supplemental_pkgs {
+  my $self = shift;
+  qsearch('cust_pkg', { 'main_pkgnum' => $self->pkgnum });
+}
+
+=item main_pkg
+
+Returns the package that this one is supplemental to, if any.
+
+=cut
+
+sub main_pkg {
+  my $self = shift;
+  if ( $self->main_pkgnum ) {
+    return FS::cust_pkg->by_key($self->main_pkgnum);
+  }
+  return;
+}
+
 =head1 CLASS METHODS
 
 =over 4
@@ -3951,11 +4104,25 @@ sub order {
                                       %hash,
                                     };
     $error = $cust_pkg->insert( 'change' => $change );
+    push @$return_cust_pkg, $cust_pkg;
+
+    foreach my $link ($cust_pkg->part_pkg->supp_part_pkg_link) {
+      my $supp_pkg = FS::cust_pkg->new({
+          custnum => $custnum,
+          pkgpart => $link->dst_pkgpart,
+          refnum  => $refnum,
+          main_pkgnum => $cust_pkg->pkgnum,
+          %hash,
+      });
+      $error ||= $supp_pkg->insert( 'change' => $change );
+      push @$return_cust_pkg, $supp_pkg;
+    }
+
     if ($error) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
     }
-    push @$return_cust_pkg, $cust_pkg;
+
   }
   # $return_cust_pkg now contains refs to all of the newly 
   # created packages.
index 6e7f8f8..d4c420f 100644 (file)
@@ -1175,6 +1175,17 @@ sub svc_part_pkg_link {
   shift->_part_pkg_link('svc', @_);
 }
 
+=item supp_part_pkg_link
+
+Returns the associated part_pkg_link records of type 'supp' (supplemental
+packages).
+
+=cut
+
+sub supp_part_pkg_link {
+  shift->_part_pkg_link('supp', @_);
+}
+
 sub _part_pkg_link {
   my( $self, $type ) = @_;
   qsearch({ table    => 'part_pkg_link',
index fb7a8d3..9ce8e6a 100644 (file)
@@ -49,12 +49,13 @@ Destination package (see L<FS::part_pkg>)
 =item link_type
 
 Link type - currently, "bill" (source package bills a line item from target
-package), or "svc" (source package includes services from target package).
+package), or "svc" (source package includes services from target package), 
+or "supp" (ordering source package creates a target package).
 
 =item hidden
 
 Flag indicating that this subpackage should be felt, but not seen as an invoice
-line item when set to 'Y'
+line item when set to 'Y'.  Not allowed for "supp" links.
 
 =back
 
@@ -119,11 +120,26 @@ sub check {
     $self->ut_numbern('pkglinknum')
     || $self->ut_foreign_key('src_pkgpart', 'part_pkg', 'pkgpart')
     || $self->ut_foreign_key('dst_pkgpart', 'part_pkg', 'pkgpart')
-    || $self->ut_enum('link_type', [ 'bill', 'svc' ] )
+    || $self->ut_enum('link_type', [ 'bill', 'svc', 'supp' ] )
     || $self->ut_enum('hidden', [ '', 'Y' ] )
   ;
   return $error if $error;
 
+  if ( $self->link_type eq 'supp' ) {
+    # some sanity checking
+    my $src_pkg = $self->src_pkg;
+    my $dst_pkg = $self->dst_pkg;
+    if ( $src_pkg->freq eq '0' and $dst_pkg->freq ne '0' ) {
+      return "One-time charges can't have supplemental packages."
+    } elsif ( $dst_pkg->freq ne '0' ) {
+      my $ratio = $dst_pkg->freq / $src_pkg->freq;
+      if ($ratio != int($ratio)) {
+        return "Supplemental package period (pkgpart ".$dst_pkg->pkgpart.
+               ") must be an integer multiple of main package period.";
+      }
+    }
+  }
+
   $self->SUPER::check;
 }
 
index 57a4297..5b19a30 100755 (executable)
@@ -20,6 +20,7 @@
                  'fields'                => \@fields,
                  'links'                 => \@links,
                  'align'                 => $align,
+                 'link_field'            => 'pkgpart',
              )
 %>
 <%init>
@@ -274,6 +275,18 @@ push @fields, sub {
         : ()
       ),
     ],
+    ( map { my $dst_pkg = $_->dst_pkg;
+            [
+              { data => 'Supplemental: &nbsp;'.
+                        '<A HREF="#'. $dst_pkg->pkgpart . '">' .
+                        $dst_pkg->pkg . '</A>',
+                align=> 'center',
+                colspan => 2,
+              }
+            ]
+          }
+      $part_pkg->supp_part_pkg_link
+    ),
     ( map { 
             my $dst_pkg = $_->dst_pkg;
             [ 
index 166a3b7..4bcf55c 100755 (executable)
@@ -9,6 +9,29 @@
 <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
 <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
 
+<SCRIPT TYPE="text/javascript">
+var submit_fields = [];
+function confirm_changes() {
+  var i;
+  var querystring = 'pkgnum=<%$pkgnum%>';
+  var f = document.forms.formname;
+  for(i = 0; i < submit_fields.length; i++) {
+    querystring += ';'
+                + submit_fields[i]
+                + '='
+                + encodeURIComponent(f.elements[submit_fields[i] + '_text'].value);
+  }
+  overlib(
+    OLiframeContent(
+      '<%$p%>/misc/confirm-cust_pkg-edit_dates.html?' + querystring,
+      576, 576, 'confirm_popup'
+    ),
+    CAPTION, 'Package date changes', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', 
+    MIDX, 0, MIDY, 0, DRAGGABLE, BGCOLOR, '#333399', CGCOLOR, '#333399', 
+    TEXTSIZE, 3
+  );
+}
+</SCRIPT>
 <FORM NAME="formname" ACTION="process/REAL_cust_pkg.cgi" METHOD="POST">
 <INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
 
     <TD BGCOLOR="#ffffff"><% $part_pkg->pkg %></TD>
   </TR>
 
+% if ( $cust_pkg->main_pkgnum ) {
+%   my $main_pkg = $cust_pkg->main_pkg;
+  <TR>
+    <TD ALIGN="right">Supplemental to</TD>
+    <TD BGCOLOR="#ffffff">Package #<% $cust_pkg->main_pkgnum%>:&nbsp;\
+    <% $main_pkg->part_pkg->pkg %></TD>
+  </TR>
+
+% }
   <TR>
     <TD ALIGN="right">Custom</TD>
     <TD BGCOLOR="#ffffff"><% $part_pkg->custom %></TD>
 % if ( $cust_pkg->setup && ! $cust_pkg->start_date ) {
   <& .row_display, cust_pkg=>$cust_pkg, column=>'start_date',   label=>'Start' &>
 % } else {
-  <& .row_edit, cust_pkg=>$cust_pkg, column=>'start_date', label=>'Start' &>
+  <& .row_edit, cust_pkg=>$cust_pkg, column=>'start_date', label=>'Start', if_primary=>1 &>
 % }
 
-  <& .row_edit, cust_pkg=>$cust_pkg, column=>'setup',     label=>'Setup' &>
+  <& .row_edit, cust_pkg=>$cust_pkg, column=>'setup',     label=>'Setup', if_primary=>1 &>
   <& .row_edit, cust_pkg=>$cust_pkg, column=>'last_bill', label=>$last_bill_or_renewed &>
   <& .row_edit, cust_pkg=>$cust_pkg, column=>'bill',      label=>$next_bill_or_prepaid_until &>
 %#if ( $cust_pkg->contract_end or $part_pkg->option('contract_end_months',1) ) {
-    <& .row_edit, cust_pkg=>$cust_pkg, column=>'contract_end',label=>'Contract end' &>
+    <& .row_edit, cust_pkg=>$cust_pkg, column=>'contract_end',label=>'Contract end', if_primary=>1 &>
 %#}
   <& .row_display, cust_pkg=>$cust_pkg, column=>'adjourn',  label=>'Adjournment', note=>'(will <b>suspend</b> this package when the date is reached)' &>
   <& .row_display, cust_pkg=>$cust_pkg, column=>'susp',     label=>'Suspension' &>
   $column
   $label
   $note => ''
+  $if_primary => 0
 </%args>
 % my $value = $cust_pkg->get($column);
 % $value = $value ? time2str($format, $value) : "";
-
+%
+% # if_primary for the dates that can't be edited on supplemental packages
+% if ($if_primary and $cust_pkg->main_pkgnum) {
+  <INPUT TYPE="hidden" ID="<%$column%>_text" VALUE="<% $cust_pkg->get($column) %>">
+  <SCRIPT>submit_fields.push('<%$column%>');</SCRIPT>
+  <& .row_display, %ARGS &>
+% } else {
   <TR>
     <TD ALIGN="right"><% $label %> date</TD>
     <TD>
       button:     "<% $column %>_button",
       align:      "BR"
     });
-  </SCRIPT>
 
+    submit_fields.push('<%$column%>');
+
+  </SCRIPT>
+% }
 </%def>
 
 <%def .row_display>
   $column
   $label
   $note => ''
+  $is_primary => 0 #ignored
 </%args>
 % if ( $cust_pkg->get($column) ) { 
     <TR>
 </TABLE>
 
 <BR>
-<INPUT TYPE="submit" VALUE="<% mt('Apply changes') |h %>">
+<INPUT TYPE="button" VALUE="<% mt('Apply changes') |h %>" onclick="confirm_changes()">
 </FORM>
 
 <% include('/elements/footer.html') %>
@@ -160,38 +203,6 @@ if ( $cgi->param('error') ) {
     my @errors = ();
     my %errors = map { $_=>1 } split(',', $cgi->param('error'));
     $cgi->param('error', '');
-
-    if ( $errors{'_bill_areyousure'} ) {
-      if ( $cgi->param('bill') =~ /^([\s\d\/\:\-\(\w\)]*)$/ ) {
-        my $bill = $1;
-        push @errors,
-          "You are attempting to set the next bill date to $bill, which is
-           in the past.  This will charge the customer for the interval
-           from $bill until now.  Are you sure you want to do this? ".
-          '<INPUT TYPE="checkbox" NAME="bill_areyousure" VALUE="1">';
-      }
-    }
-
-    if ( $errors{'_setup_areyousure'} ) {
-      push @errors,
-        "You are attempting to remove the setup date.  This will re-charge the
-         customer for the setup fee.  Are you sure you want to do this? ".
-        '<INPUT TYPE="checkbox" NAME="setup_areyousure" VALUE="1">';
-    }
-
-    if ( $errors{'_setupadd_areyousure'} ) {
-      push @errors,
-        "You are attempting to add a setup date.  This will prevent charging the
-         customer for the setup fee.  Are you sure you want to do this? ".
-        '<INPUT TYPE="checkbox" NAME="setupadd_areyousure" VALUE="1">';
-    }
-
-    if ( $errors{'_start'} ) {
-      push @errors,
-        "You are attempting to add a start date to a package that has already
-         started billing.";
-    }
-
     $error = join('<BR><BR>', @errors );
 
   }
index dd1ed33..88e9254 100755 (executable)
@@ -7,7 +7,6 @@
 <INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
 
 %#current packages
-%my @cust_pkg = qsearch('cust_pkg', { 'custnum' => $custnum, 'cancel' => '' } );
 %if (@cust_pkg) {
 
   Current packages - select to remove (services are moved to a new package below)
     </TR>
   <BR><BR>
 %
-%
-%  foreach ( sort {     $all_pkg{ $a->getfield('pkgpart') }
-%                   cmp $all_pkg{ $b->getfield('pkgpart') }
-%                 }
-%                 @cust_pkg
-%          )
-%  {
+%  foreach ( @main_pkgs ) {
 %    my($pkgnum,$pkgpart)=( $_->getfield('pkgnum'), $_->getfield('pkgpart') );
 %    my $checked = $remove_pkg{$pkgnum} ? ' CHECKED' : '';
 %
       <TD ALIGN="right"><% $pkgnum %>:</TD>
       <TD><% $all_pkg{$pkgpart} %> - <% $all_comment{$pkgpart} %></TD>
     </TR>
+%   foreach my $supp_pkg ( @{ $supp_pkgs_of{$pkgnum} } ) {
+    <TR>
+      <TD></TD>
+      <TD></TD>
+      <TD>+ <% $all_pkg{$supp_pkg->pkgpart} %> - <% $all_comment{$supp_pkg->pkgpart} %></TD>
+    </TR>
+%   }
 % } 
 
 
@@ -147,4 +147,24 @@ if ( $cgi->param('error') ) {
 
 my $p1 = popurl(1);
 
+my @cust_pkg = qsearch('cust_pkg', { 'custnum' => $custnum, 'cancel' => '' } );
+my @main_pkgs;
+my %supp_pkgs_of; # main pkgnum => arrayref of cust_pkgs
+
+
+foreach my $cust_pkg
+  ( sort { $all_pkg{ $a->pkgpart } cmp $all_pkg{ $b->getfield('pkgpart') } }
+    @cust_pkg
+  )
+  # XXX does not properly handle recursive supplemental links
+{
+  if ( my $main_pkgnum = $cust_pkg->main_pkgnum ) {
+    $supp_pkgs_of{$main_pkgnum} ||= [];
+    push @{ $supp_pkgs_of{$main_pkgnum} }, $cust_pkg;
+  } else {
+    push @main_pkgs, $cust_pkg;
+    $supp_pkgs_of{$cust_pkg->pkgnum} ||= [];
+  }
+}
+
 </%init>
index c3f4f88..7baf84d 100755 (executable)
@@ -53,6 +53,7 @@
                             'discountnum'      => 'Offer discounts for longer terms',
                             'bill_dst_pkgpart' => 'Include line item(s) from package',
                             'svc_dst_pkgpart'  => 'Include services of package',
+                            'supp_dst_pkgpart' => 'Include complete package',
                             'report_option'    => 'Report classes',
                             'fcc_ds0s'         => 'Voice-grade equivalents',
                             'fcc_voip_class'   => 'Category',
                             },
 
                             { 'type'    => 'tablebreak-tr-title',
+                              'value'   => 'Supplemental packages',
+                              'colspan' => '4',
+                            },
+                            { 'field'       => 'supp_dst_pkgpart',
+                              'type'        => 'select-part_pkg',
+                              'm2_label'    => 'Include complete package',
+                              'm2m_method'  => 'supp_part_pkg_link',
+                              'm2m_dstcol'  => 'dst_pkgpart',
+                              'm2_error_callback' =>
+                                &{$m2_error_callback_maker}('supp'),
+                            },
+
+                            { 'type'    => 'tablebreak-tr-title',
                               'value'   => 'Pricing add-ons',
                               'colspan' => 4,
                             },
index 3e0ef59..fd28934 100755 (executable)
@@ -19,36 +19,41 @@ die "access denied"
 my $pkgnum = $cgi->param('pkgnum') or die;
 my $old = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
 my %hash = $old->hash;
-$hash{$_}= $cgi->param($_) ? parse_datetime($cgi->param($_)) : ''
-  foreach qw( start_date setup bill last_bill contract_end );
+foreach ( qw( start_date setup bill last_bill contract_end ) ) {
+  if ( $cgi->param($_) =~ /^(\d+)$/ ) {
+    $hash{$_} = $1;
+  } else {
+    $hash{$_} = '';
+  }
   # adjourn, expire, resume not editable this way
-
-my @errors = ();
-
-push @errors, '_bill_areyousure'
-  if $hash{'bill'} != $old->bill             # if the next bill date was changed
-  && $hash{'bill'} < time                    # to a date in the past
-  && ! $cgi->param('bill_areyousure');       # and it wasn't confirmed
-
-push @errors, '_setup_areyousure'
-  if ! $hash{'setup'} && $old->setup         # if the setup date was removed
-  && ! $cgi->param('setup_areyousure');      # and it wasn't confirmed 
-
-push @errors, '_setupadd_areyousure'
-  if $hash{'setup'} && ! $old->setup         # if the setup date was added
-  && ! $cgi->param('setupadd_areyousure');   # and it wasn't confirmed 
-
-push @errors, '_start'
-  if $hash{'start_date'} && !$old->start_date # if a start date was added
-  && $hash{'setup'};                          # but there's a setup date
+}
 
 my $new;
 my $error;
-if ( @errors ) {
-  $error = join(',', @errors);
-} else {
-  $new = new FS::cust_pkg \%hash;
-  $error = $new->replace($old);
+$new = new FS::cust_pkg \%hash;
+$error = $new->replace($old);
+
+if (!$error) {
+  my @supp_pkgs = $old->supplemental_pkgs;
+  foreach $new (@supp_pkgs) {
+    foreach ( qw( start_date setup contract_end ) ) {
+      # propagate these to supplementals
+      $new->set($_, $hash{$_});
+    }
+    if ( $hash{'bill'} ne $old->get('bill') ) {
+      if ( $hash{'bill'} and $old->get('bill') ) {
+        # adjust by the same interval
+        my $diff = $hash{'bill'} - $old->get('bill');
+        $new->set('bill', $new->get('bill') + $diff);
+      } else {
+        # absolute date
+        $new->set('bill', $hash{'bill'});
+      }
+    }
+    $error = $new->replace;
+    $error .= ' (supplemental package '.$new->pkgnum.')' if $error; 
+    last if $error;
+  }
 }
 
 </%init>
index c388676..2ac57f9 100755 (executable)
@@ -185,6 +185,15 @@ my @process_m2m = (
                         grep /^svc_dst_pkgpart/, $cgi->param
                       ],
   },
+  { 'link_table'   => 'part_pkg_link',
+    'target_table' => 'part_pkg',
+    'base_field'   => 'src_pkgpart',
+    'target_field' => 'dst_pkgpart',
+    'hashref'      => { 'link_type' => 'supp', 'hidden' => '' },
+    'params'       => [ map $cgi->param($_),
+                        grep /^supp_dst_pkgpart/, $cgi->param
+                      ],
+  },
   map { 
     my $hidden = $_;
     { 'link_table'   => 'part_pkg_link',
diff --git a/httemplate/misc/confirm-cust_pkg-edit_dates.html b/httemplate/misc/confirm-cust_pkg-edit_dates.html
new file mode 100755 (executable)
index 0000000..27b9a82
--- /dev/null
@@ -0,0 +1,283 @@
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('Edit customer package dates');
+
+my %arg = $cgi->Vars;
+
+my $pkgnum = $arg{'pkgnum'};
+$pkgnum =~ /^\d+$/ or die "bad pkgnum '$pkgnum'";
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+my %hash = $cust_pkg->hash;
+foreach (qw( start_date setup bill last_bill contract_end )) {
+  # adjourn, expire, resume not editable this way
+  if( $arg{$_} =~ /^\d+$/ ) {
+    $hash{$_} = $arg{$_};
+  } elsif ( $arg{$_} ) {
+    $hash{$_} = parse_datetime($arg{$_});
+  } else {
+    $hash{$_} = '';
+  }
+}
+
+my (@changes, @confirm, @errors);
+
+my $part_pkg = $cust_pkg->part_pkg;
+my @supp_pkgs = $cust_pkg->supplemental_pkgs;
+my $main_pkg = $cust_pkg->main_pkg;
+
+my $conf = FS::Conf->new;
+my $date_format = $conf->config('date_format') || '%b %o, %Y';
+# Start date
+if ( $hash{'start_date'} != $cust_pkg->get('start_date') and !$hash{'setup'} ) {
+  my $start = '';
+  $start = time2str($date_format, $hash{'start_date'}) if $hash{'start_date'};
+  my $text = 'Set this package';
+  if ( @supp_pkgs ) {
+    $text .= ' and all its supplemental packages';
+  }
+  $text .= ' to start billing';
+  if ( $start ) {
+    $text .= ' on [_1].';
+    push @changes, mt($text, $start);
+  } else {
+    $text .= ' immediately.';
+    push @changes, mt($text);
+  }
+  push @confirm, '';
+}
+
+# Setup date changes
+if ( $hash{'setup'} != $cust_pkg->get('setup') ) {
+  my $setup = time2str($date_format, $hash{'setup'});
+  my $has_setup_fee = grep { $_->part_pkg->option('setup_fee',1) > 0 }
+                      $cust_pkg, @supp_pkgs;
+  if ( !$hash{'setup'} ) {
+    my $text = 'Remove the setup date';
+    $text .= ' from this and all its supplemental packages' if @supp_pkgs;
+    $text .= '.';
+    push @changes, mt($text);
+    if ( $has_setup_fee ) {
+      push @confirm, mt('This will re-charge the customer for the setup fee.');
+    } else {
+      push @confirm, '';
+    }
+  } elsif ( $cust_pkg->get('setup') ) {
+    my $text = 'Add a setup date of [_1]';
+    $text .= ' to this and all its supplemental packages' if @supp_pkgs;
+    $text .= '.';
+    push @changes, mt($text, $setup);
+    if ( $has_setup_fee ) {
+      push @confirm, mt('This will prevent charging the setup fee.');
+    } else {
+      push @confirm, '';
+    }
+  } else {
+    my $text = 'Set the setup date to [_1]';
+    $text .= ' on this and all its supplemental packages' if @supp_pkgs;
+    $text .= '.';
+    push @changes, mt($text, $setup);
+    push @confirm, '';
+  }
+}
+
+# Check for start date + setup date
+if ( $hash{'start_date'} and $hash{'setup'} ) {
+  if ( $cust_pkg->get('setup') ) {
+    push @errors, mt('Since the package has already started billing, it '.
+                     'cannot have a start date.');
+  } else {
+    push @errors, mt('You cannot set both a start date and a setup date on '.
+                     'the same package.');
+  }
+}
+
+# Last bill date change
+if ( $hash{'last_bill'} != $cust_pkg->get('last_bill') ) {
+  my $last_bill = time2str($date_format, $hash{'last_bill'});
+  my $name = 'last bill date';
+  $name = 'last renewal date' if $part_pkg->is_prepaid;
+  if ( $hash{'last_bill'} ) {
+    push @changes, mt('Set the [_1] to [_2].', $name, $last_bill);
+  } else {
+    push @changes, mt('Remove the [_1].', $name);
+  }
+  push @confirm, '';
+  # I don't think we want to adjust this on supplemental packages.
+}
+
+# Bill date change
+if ( $hash{'bill'} != $cust_pkg->get('bill') ) {
+  my $bill = time2str($date_format, $hash{'bill'});
+  $bill = 'the current day' if !$hash{'bill'}; # or 'the end of today'?...
+  my $name = 'next bill date';
+  $name = 'end of the prepaid period' if $part_pkg->is_prepaid;
+  push @changes, mt('Set the [_1] to [_2].', $name, $bill);
+
+  if ( $hash{'bill'} < time and $hash{'bill'} ) {
+    push @confirm, 
+      mt('The customer will be charged for the interval from [_1] until now.',
+         $bill);
+  } else {
+    push @confirm, '';
+  }
+
+  if ( @supp_pkgs ) {
+    push @changes, '';
+    if ( $cust_pkg->get('bill') and $hash{'bill'} ) {
+      # the package already has a bill date, so adjust the dates 
+      # of supplementals by the same interval
+      my $diff = $hash{'bill'} - $cust_pkg->get('bill');
+      my $sign = $diff < 0 ? -1 : 1;
+      $diff = $diff * $sign / 86400;
+      if ( $diff < 1 ) {
+        $diff = mt('[quant,_1,hour]', int($diff * 24));
+      } else {
+        $diff = mt('[quant,_1,day]', int($diff));
+      }
+      push @confirm,
+        mt('[_1] supplemental package will also be billed [_2] [_3].',
+            (@supp_pkgs > 1 ? 'Each' : 'The'),
+            $diff,
+            ($sign > 0 ? 'later' : 'earlier')
+        );
+    } else {
+      # the package hasn't been billed yet, or you've set bill = null
+      push @confirm,
+        mt('[_1] supplemental package will also be billed on [_2].',
+            (@supp_pkgs > 1 ? 'Each' : 'The'),
+            $bill
+        );
+    }
+  } #if @supp_pkgs
+
+  if ( $main_pkg ) {
+    push @changes, '';
+    push @confirm,
+      mt('This package is a supplemental package.  The bill date of its '.
+         'main package will not be adjusted.');
+  }
+}
+
+# Contract end change
+if ( $hash{'contract_end'} != $cust_pkg->get('contract_end') ) {
+  if ( $hash{'contract_end'} ) {
+    my $contract_end = time2str($date_format, $hash{'contract_end'});
+    push @changes,
+      mt('Set this package\'s contract end date to [_1]', $contract_end);
+  } else {
+    push @changes, mt('Remove this package\'s contract end date.');
+  }
+  if ( @supp_pkgs ) {
+    my $text = 'This change will also apply to ' .
+      (@supp_pkgs > 1 ?
+        'all supplemental packages.':
+        'the supplemental package.');
+    push @confirm, mt($text);
+  } else {
+    push @confirm, '';
+  }
+}
+
+my $title = '';
+if ( @errors ) {
+  $title = 'Error changing package dates';
+} else {
+  $title = 'Confirm date changes';
+}
+</%init>
+<& /elements/header-popup.html, { title => $title, etc => 'BGCOLOR=""' } &>
+<STYLE TYPE="text/css">
+.error { 
+  color: #ff0000;
+  font-weight: bold;
+  text-align: center;
+}
+.confirm { color: #ff0000 }
+.button-container {
+  position: fixed;
+  bottom: 5px;
+  text-align: center;
+  width: 100%
+}
+</STYLE>
+<DIV STYLE="text-align: center; padding:1em">
+<% emt('Package #') %><B><% $pkgnum %></B>: <B><% $cust_pkg->part_pkg->pkg %></B><BR>
+% if ( @changes ) {
+  <% emt('The following changes will be made:') %>
+% } else {
+  <% emt('No changes will be made.') %>
+% }
+</DIV>
+<TABLE WIDTH="100%">
+% if ( @errors ) {
+%   foreach my $error ( @errors ) {
+<TR>
+  <TD><IMG SRC="<%$p%>images/cross.png"></TD>
+  <TD CLASS="error"><% $error %></TD>
+</TR>
+%   }
+% } else {
+%   while (@changes, @confirm) {
+%     my $text = shift @changes;
+%     if (length $text) {
+<TR>
+  <TD><IMG SRC="<%$p%>images/tick.png"></TD>
+  <TD><% $text %></TD>
+</TR>
+%     }
+%     $text = shift @confirm;
+%     if (length $text) {
+<TR>
+  <TD>
+    <INPUT TYPE="checkbox" NAME="areyousure" VALUE=1 onclick="submit_ready()">
+  </TD>
+  <TD CLASS="confirm"><% $text %></TD>
+</TR>
+%     }
+%   }
+% }
+</TABLE>
+%# action buttons
+<DIV CLASS="button-container">
+  <BUTTON TYPE="button" STYLE="width:145px" ID="submit_cancel"\
+    onclick="submit_cancel()">
+    <IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel
+  </BUTTON>
+% if (!@errors ) {
+  <BUTTON TYPE="button" STYLE="width:145px" ID="submit_continue"\
+    onclick="submit_continue()">
+    <IMG SRC="<%$p%>images/tick.png" ALT=""> Continue
+  </BUTTON>
+</DIV>
+% }
+<FORM NAME="DateEditForm" STYLE="display:none" TARGET="_parent" ACTION="<%$p%>edit/process/REAL_cust_pkg.cgi" METHOD="POST">
+% foreach (keys %hash) {
+<INPUT TYPE="hidden" NAME="<%$_%>" VALUE="<% $hash{$_} |h%>">
+% }
+</FORM>
+<SCRIPT>
+function submit_ready() {
+  var ready = true;
+  var checkboxes = document.getElementsByName('areyousure');
+  var i;
+  for (i=0; i < checkboxes.length; i++) {
+    if (! checkboxes[i].checked ) {
+      ready = false;
+    }
+  }
+  document.getElementById('submit_continue').disabled = !ready;
+  return ready;
+}
+function submit_cancel() {
+  parent.nd(1);
+}
+function submit_continue() {
+  if ( submit_ready() ) {
+    document.forms.DateEditForm.submit();
+  }
+}
+submit_ready();
+</SCRIPT>
+<& /elements/footer.html &>
index 7ccf356..e760bc5 100644 (file)
 %                   $bgcolor = $bgcolor1;
 %                 }
 
-                  <TR>
+%                 my $trid = '';
+%                   if ( $opt{'link_field' } ) {
+%                     my $link_field = $opt{'link_field'};
+%                     if ( ref($link_field) eq 'CODE' ) {
+%                       $trid = &{$link_field}($row);
+%                     } else {
+%                       $trid = $row->$link_field();
+%                     }
+%                   }
+                  <TR ID="<%$trid |h%>">
+                      
 
 %                   if ( $opt{'fields'} ) {
 %
index 5a16a22..68c4888 100644 (file)
@@ -167,6 +167,11 @@ Example:
     # miscellany
    'download_label' => 'Download this report',
                         # defaults to 'Download full results' 
+   'link_field'     => 'pkgpart'
+                        # will create internal links for each row,
+                        # with the value of this field as the NAME attribute
+                        # If this is a coderef, will evaluate it, passing the
+                        # row as an argument, and use the result as the NAME.
   &>
 
 </%doc>
index 7d79306..7b5b156 100755 (executable)
@@ -1,3 +1,22 @@
+<STYLE TYPE="text/css">
+td.package {
+  vertical-align: top;
+  border-width: 0;
+  border-style: solid;
+  border-color: #bbbbff;
+}
+table.package {
+  border: none;
+  padding: 0;
+  border-spacing: 0;
+  width: 100%;
+}
+<!-- even/odd rows -->
+  
+.row0 { background-color: #eeeeee; }
+.row1 { background-color: #ffffff; }
+
+</STYLE>
 % my $s = 0;
 
 % if ( $curuser->access_right('Qualify service') ) { 
@@ -116,7 +135,7 @@ my( $packages, $num_old_packages ) = get_packages($cust_main, $conf);
 
 
 my $show_location = $conf->exists('cust_pkg-always_show_location') 
-                        || (grep $_->locationnum, @$packages); # ? '1' : '0';
+  || (grep $_->locationnum ne $cust_main->ship_locationnum, @$packages);
 
 my $countrydefault = scalar($conf->config('countrydefault')) || 'US';
 #subroutines
@@ -178,6 +197,10 @@ sub get_packages {
   }
 
   $num_old_packages -= scalar(@packages);
+  
+  # don't include supplemental packages in this list; they'll be found from
+  # their main packages
+  @packages = grep !$_->main_pkgnum, @packages;
 
   ( \@packages, $num_old_packages );
 }
index 5d93ad4..3a362b6 100644 (file)
@@ -1,5 +1,6 @@
-<TD CLASS="inv" BGCOLOR="<% $bgcolor %>" VALIGN="top">
-  <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
+<TD CLASS="inv package" BGCOLOR="<% $bgcolor %>" VALIGN="top"
+  STYLE="border-left-width: <% $supplemental * 30 %>px">
+  <TABLE CLASS="inv package"> 
     <TR>
       <TD COLSPAN=2>
         <A NAME="cust_pkg<% $cust_pkg->pkgnum %>"
@@ -17,7 +18,7 @@
         <B><% $cust_pkg->quantity %></B>
       </TD>
     </TR>
-%  }
+% }
 
     <TR>
       <TD COLSPAN=2>
 
 %         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 ( $supplemental ) {
+%             # then only show "Edit dates", "Add invoice details", and "Add
+%             # comments".
+%             if ( $curuser->access_right('Edit customer package dates') ) {
+                (&nbsp;<%pkg_dates_link($cust_pkg)%>&nbsp;)
+%             }
+%           } else {
+%             # the usual case
+%             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('Edit customer package dates') ) {
+%               $br=1;
+                (&nbsp;<%pkg_dates_link($cust_pkg)%>&nbsp;)
+%             
 %
-%           if ( $curuser->access_right('Discount customer package')
-%                && $part_pkg->can_discount
-%                && ! scalar($cust_pkg->cust_pkg_discount_active)
-%                && ! scalar($cust_pkg->part_pkg->part_pkg_discount)
-%              )
-%           {
-%             $br=1;
-              (&nbsp;<%pkg_discount_link($cust_pkg)%>&nbsp;)
-%           }
+%             if ( $curuser->access_right('Discount customer package')
+%                  && $part_pkg->can_discount
+%                  && ! scalar($cust_pkg->cust_pkg_discount_active)
+%                  && ! scalar($cust_pkg->part_pkg->part_pkg_discount)
+%                )
+%             {
+%               $br=1;
+                (&nbsp;<%pkg_discount_link($cust_pkg)%>&nbsp;)
+%             }
 %
-%           if ( $curuser->access_right('Customize customer package') ) {
-%             $br=1;
-              (&nbsp;<%pkg_customize_link($cust_pkg,$part_pkg)%>&nbsp;)
-%           } 
+%             if ( $curuser->access_right('Customize customer package') ) {
+%               $br=1;
+                (&nbsp;<%pkg_customize_link($cust_pkg,$part_pkg)%>&nbsp;)
+%             
 %
-            <% $br ? '<BR>' : '' %>
-%         } 
+              <% $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;)
-%         }
+%           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;)
+%           }
+%         } #!$supplemental
 
         </FONT>
       </TD>
       </TR>
 %     if ( $curuser->access_right('Change customer package') and 
 %           !$cust_pkg->get('cancel') and
+%           !$supplemental and
 %           !$opt{'show_location'}) {
       <TR>
         <TD><FONT SIZE="-1">
@@ -196,6 +207,7 @@ my $countrydefault = $opt{'countrydefault'} || 'US';
 my $statedefault   = $opt{'statedefault'}
                      || ($countrydefault eq 'US' ? 'CA' : '');
 
+my $supplemental = $opt{'supplemental'} || 0;
 #subroutines
 
 #false laziness w/status.html
index 85f0c79..53bdfa1 100755 (executable)
@@ -1,8 +1,4 @@
 % if ( @$packages ) { 
-%   my $bgcolor1 = '#eeeeee';
-%   my $bgcolor2 = '#ffffff';
-%   my $bgcolor = '';
-
 <TR>
 % #my $width = $show_location ? 'WIDTH="25%"' : 'WIDTH="33%"';
   <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Package') |h %></TH>
 
 % #$FS::cust_pkg::DEBUG = 2;
 %   foreach my $cust_pkg (@$packages) {
+    <& .packagerow, $cust_pkg,
+        'cust_main' => $opt{'cust_main'},
+        %conf_opt
+    &>
+%   }
+% } else { # there are no packages
+<BR>
+% }
+<%def .packagerow>
 %
-%     if ( $bgcolor eq $bgcolor1 ) {
-%       $bgcolor = $bgcolor2;
-%     } else {
-%       $bgcolor = $bgcolor1;
-%     }
-%
-%     my %iopt = (
-%       'bgcolor'   => $bgcolor,
-%       'cust_pkg'  => $cust_pkg,
-%       'part_pkg'  => $cust_pkg->part_pkg,
-%       'cust_main' => $opt{'cust_main'},
-%       %conf_opt,
-%     );
-%
-
+% my ($cust_pkg, %iopt) = @_;
+% $iopt{'cust_pkg'} = $cust_pkg;
+% $iopt{'part_pkg'} = $cust_pkg->part_pkg;
   <!--pkgnum: <% $cust_pkg->pkgnum %>-->
-  <TR>
+  <TR CLASS="row<%$row % 2%>">
     <& package.html, %iopt &>
     <& status.html, %iopt &>
-%     if ( $show_location ) {
+%     if ( $iopt{'show_location'} ) {
     <& location.html, %iopt &>
 %     }
     <& services.html, %iopt &>
   </TR>
-
-%   } #foreach $cust_pkg
-%# </TABLE>
-% } #if @$packages
-% else {
-<BR>
+% $row++;
+% # include supplemental packages if any
+% $iopt{'supplemental'} = ($iopt{'supplemental'} || 0) + 1;
+% foreach my $supp_pkg ($cust_pkg->supplemental_pkgs) {
+% warn $supp_pkg->pkgnum;
+    <& .packagerow, $supp_pkg, %iopt &>
 % }
-
+</%def>
+<%shared>
+my $row = 0;
+</%shared>
 <%init>
 
 my %opt = @_;
index e901774..6be0296 100644 (file)
@@ -3,7 +3,9 @@
 
 %#this should use cust_pkg->status and cust_pkg->statuscolor eventually
 
-% if ( $cust_pkg->order_date ) {
+% if ( $supplemental ) {
+    <% pkg_status_row_colspan($cust_pkg, emt('Supplemental'), '', 'color' => '7777FF', %opt) %>
+% } elsif ( $cust_pkg->order_date ) {
     <% pkg_status_row($cust_pkg, emt('Ordered'), 'order_date', %opt ) %>
 % }
 
 
     <% pkg_status_row($cust_pkg, emt('Cancelled'), 'cancel', 'color'=>'FF0000', %opt ) %>
 
-    <% pkg_status_row_colspan( $cust_pkg,
-         ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '',
-         'align'=>'right', 'color'=>'ff0000', 'size'=>'-2', 'colspan'=>$colspan,
-         %opt
-       )
-    %>
+    <% pkg_reason_row($cust_pkg, $cpr, color => 'ff0000', %opt) %>
 
 %   unless ( $cust_pkg->get('setup') ) { 
 
-        <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', 'colspan'=>$colspan, %opt, ) %>
+        <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', %opt, ) %>
 
 %   } else { 
 
        <% pkg_status_row( $cust_pkg, emt('Setup'), 'setup', %opt ) %>
-       <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+       <% pkg_status_row_changed( $cust_pkg, %opt ) %>
        <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %>
        <% pkg_status_row_if( $cust_pkg, emt('Suspended'), 'susp', %opt, curuser=>$curuser ) %>
 
 %   } 
 %
-%   if ( $part_pkg->freq ) { #?
+%   if ( $part_pkg->freq and !$supplemental ) { #?
 
       <TR>
-        <TD COLSPAN=<%$colspan%>>
+        <TD COLSPAN=<%$opt{colspan}%>>
           <FONT SIZE=-1>
 %           if ( $curuser->access_right('Un-cancel customer package') ) { 
               (&nbsp;<% pkg_uncancel_link($cust_pkg) %>&nbsp;)
 
     <% pkg_status_row( $cust_pkg, emt('Suspended'), 'susp', 'color'=>'FF9900', %opt ) %>
 
-    <% pkg_status_row_colspan( $cust_pkg,
-         ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '',
-         'align'=>'right', 'color'=>'FF9900', 'size'=>'-2', 'colspan'=>$colspan,
-         %opt,
-       )
-    %>
+    <% pkg_reason_row( $cust_pkg, $cpr, 'color' => 'FF9900', %opt ) %>
 
-    <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+    <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
 
-    <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+    <% pkg_status_row_discount( $cust_pkg, %opt ) %>
 
 %   unless ( $cust_pkg->get('setup') ) { 
-      <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', 'colspan'=>$colspan, %opt ) %>
+      <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', %opt ) %>
 %   } else { 
       <% pkg_status_row($cust_pkg, emt('Setup'), 'setup', %opt ) %>
 %   } 
 
     <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
 
-    <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+    <% pkg_status_row_changed( $cust_pkg, %opt ) %>
     <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %>
 %   if ( $cust_pkg->option('suspend_bill', 1)
 %        || ( $part_pkg->option('suspend_bill', 1)
     <% pkg_status_row_if( $cust_pkg, emt('Expires'), 'expire', %opt, curuser=>$curuser ) %>
     <% pkg_status_row_if( $cust_pkg, emt('Contract ends'), 'contract_end', %opt ) %>
 
-    <TR>
-      <TD COLSPAN=<%$colspan%>>
-        <FONT SIZE=-1>
-%         if ( $curuser->access_right('Unsuspend customer package') ) { 
-            (&nbsp;<% pkg_unsuspend_link($cust_pkg) %>&nbsp;)
-            (&nbsp;<% pkg_resume_link($cust_pkg) %>&nbsp;)
-%         }
-%         if ( $curuser->access_right('Cancel customer package immediately') ) {
-            (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
-%         } 
-        </FONT>
-      </TD>
-    </TR>
-
+% if ( !$supplemental ) {
+      <TR>
+        <TD COLSPAN=<%$opt{colspan}%>>
+          <FONT SIZE=-1>
+%           if ( $curuser->access_right('Unsuspend customer package') ) { 
+              (&nbsp;<% pkg_unsuspend_link($cust_pkg) %>&nbsp;)
+              (&nbsp;<% pkg_resume_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( $cust_pkg, emt('Not yet billed (one-time charge)'), '', 'colspan'=>$colspan, %opt ) %>
+          <% pkg_status_row_colspan( $cust_pkg, emt('Not yet billed (one-time charge)'), '', %opt ) %>
 
-          <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
 
-          <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_discount( $cust_pkg, %opt ) %>
 
           <% pkg_status_row_if(
                $cust_pkg,
 
           <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
 
+%         if (!$supplemental) {
           <TR>
-            <TD COLSPAN=<%$colspan%>>
+            <TD COLSPAN=<%$opt{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($cust_pkg, emt("Not yet billed ($billed_or_prepaid [_1])", myfreq($part_pkg) ), '', 'colspan'=>$colspan, %opt ) %>
+          <% pkg_status_row_colspan($cust_pkg, emt("Not yet billed ($billed_or_prepaid [_1])", myfreq($part_pkg) ), '', %opt ) %>
 
-          <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
 
-          <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_discount( $cust_pkg, %opt ) %>
 
           <% pkg_status_row_if($cust_pkg, emt('Start billing'), 'start_date', %opt) %>
           <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
 %
 %       unless ( $part_pkg->freq ) { 
 
-          <% pkg_status_row_colspan($cust_pkg, emt('One-time charge'), '', 'colspan'=>$colspan, %opt ) %>
+          <% pkg_status_row_colspan($cust_pkg, emt('One-time charge'), '', %opt ) %>
 
           <% pkg_status_row($cust_pkg, emt('Billed'), 'setup', %opt) %>
 
-          <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
 
-          <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_discount( $cust_pkg, %opt ) %>
 
           <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
 
             <% pkg_status_row_colspan( $cust_pkg,
                  emt('Overlimit'),
                  $billed_or_prepaid. '&nbsp;'. myfreq($part_pkg),
-                 'color'=>'FFD000', 'colspan'=>$colspan,
+                 'color'=>'FFD000',
                  %opt
                )
             %>
             <% pkg_status_row_colspan( $cust_pkg,
                  emt('Active'),
                  $billed_or_prepaid. '&nbsp;'. myfreq($part_pkg),
-                 'color'=>'00CC00', 'colspan'=>$colspan,
+                 'color'=>'00CC00',
                  %opt
                )
             %>
 %         } 
 
-          <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
 
-          <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_discount( $cust_pkg, %opt ) %>
 
           <% pkg_status_row($cust_pkg, emt('Setup'), 'setup', %opt) %>
 
 %       $cust_pkg->set('autosuspend', $autosuspend) if $autosuspend;
 %     }
 
-      <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+      <% pkg_status_row_changed( $cust_pkg, %opt ) %>
       <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %>
       <% pkg_status_row_if( $cust_pkg, $next_bill_or_prepaid_until, 'bill', %opt, curuser=>$curuser ) %>
       <% pkg_status_row_if($cust_pkg, emt('Will automatically suspend by'), 'autosuspend', %opt) %>
       <% pkg_status_row_if( $cust_pkg, emt('Expires'), 'expire', %opt, curuser=>$curuser ) %>
       <% pkg_status_row_if( $cust_pkg, emt('Contract ends'), 'contract_end', %opt ) %>
 
-%     if ( $part_pkg->freq ) { 
+%     if ( $part_pkg->freq and !$supplemental ) { 
 
         <TR>
-          <TD COLSPAN=<%$colspan%>>
+          <TD COLSPAN=<%$opt{colspan}%>>
             <FONT SIZE=-1>
 %             if ( $curuser->access_right('Suspend customer package') ) { 
                 (&nbsp;<% pkg_suspend_link($cust_pkg) %>&nbsp;)
@@ -251,8 +247,10 @@ my $bgcolor  = $opt{'bgcolor'};
 my $cust_pkg = $opt{'cust_pkg'};
 my $part_pkg = $opt{'part_pkg'};
 my $curuser  = $FS::CurrentUser::CurrentUser;
-my $colspan  = $opt{'cust_pkg-display_times'} ? 8 : 4;
 my $width    = $opt{'cust_pkg-display_times'} ? '38%' : '56%';
+my $supplemental = $opt{'supplemental'};
+
+$opt{colspan}  = $opt{'cust_pkg-display_times'} ? 8 : 4;
 
 #false laziness w/edit/REAL_cust_pkg.cgi
 my( $billed_or_prepaid, $last_bill_or_renewed, $next_bill_or_prepaid_until );
@@ -285,9 +283,27 @@ sub pkg_link {
 sub pkg_status_row {
   my( $cust_pkg, $title, $field, %opt ) = @_;
 
+  if ( $field and $cust_pkg->main_pkgnum ) {
+    # for supplemental packages, we mostly only show these if they're 
+    # different from the main package
+    my $main_pkg = $cust_pkg-> main_pkg;
+    if (    $main_pkg->get($field) ne $cust_pkg->get($field)
+        # with some exceptions
+        or  $field eq 'bill'
+        or  $field eq 'last_bill'
+        or  $field eq 'setup'
+        or  $field eq 'susp'
+        or  $field eq 'cancel'
+      ) {
+      # handle it normally
+    } else {
+      return '';
+    }
+  }
+
   my $color = $opt{'color'};
 
-  my $html = qq(<TR><TD WIDTH="<%$width%>" ALIGN="right">);
+  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);
@@ -338,7 +354,6 @@ sub pkg_status_row_changed {
                                      '',
                                      'size'    => '-1',
                                      'align'   => 'right',
-                                     'colspan' => $opt{'colspan'},
                                    );
   }
 
@@ -356,9 +371,7 @@ sub pkg_status_row_noauto {
   return '' unless $cust_main->payby =~ /^(CARD|CHEK)$/;
   my $what = lc(FS::payby->shortname($cust_main->payby));
 
-  pkg_status_row_colspan( $cust_pkg, emt("No automatic $what charge"), '',
-                          'colspan' => $opt{'colspan'},
-                        );
+  pkg_status_row_colspan( $cust_pkg, emt("No automatic $what charge"), '');
 }
 
 sub pkg_status_row_discount {
@@ -382,15 +395,24 @@ sub pkg_status_row_discount {
                   $cust_pkg_discount->pkgdiscountnum.
                 '">'.emt('remove discount').'</A>)</FONT>';
 
-    $html .= pkg_status_row_colspan( $cust_pkg, $label, '',
-                                     'colspan' => $opt{'colspan'},
-                                   );
+    $html .= pkg_status_row_colspan( $cust_pkg, $label, '', %opt );
 
   }
 
   $html;
 }
 
+sub pkg_reason_row {
+  my ($cust_pkg, $cpr, %opt) = @_;
+  return '' if $cust_pkg->main_pkgnum;
+
+  my $reasontext = '';
+  $reasontext = $cpr->reasontext . ' by ' . $cpr->otaker if $cpr;
+  pkg_status_row_colspan( $cust_pkg, $reasontext, '',
+    'align'=>'right', 'size'=>'-2', %opt
+  );
+}
+
 sub pkg_status_row_colspan {
   my($cust_pkg, $title, $addl, %opt) = @_;