time/data/etc. unit pricing add-ons, RT#24392
[freeside.git] / httemplate / browse / part_pkg.cgi
index e226ce1..574cf7a 100755 (executable)
@@ -1,6 +1,8 @@
 <% include( 'elements/browse.html',
                  'title'                 => 'Package Definitions',
+                 'menubar'               => \@menubar,
                  'html_init'             => $html_init,
+                 'html_form'             => $html_form,
                  'html_posttotal'        => $html_posttotal,
                  'name'                  => 'package definitions',
                  'disableable'           => 1,
@@ -20,6 +22,9 @@
                  'fields'                => \@fields,
                  'links'                 => \@links,
                  'align'                 => $align,
+                 'link_field'            => 'pkgpart',
+                 'html_init'             => $html_init,
+                 'html_foot'             => $html_foot,
              )
 %>
 <%init>
@@ -32,6 +37,8 @@ my $acl_edit        = $curuser->access_right($edit);
 my $acl_edit_global = $curuser->access_right($edit_global);
 my $acl_config      = $curuser->access_right('Configuration'); #to edit services
                                                                #and agent types
+                                                               #and bulk change
+my $acl_edit_bulk   = $curuser->access_right('Bulk edit package definitions');
 
 die "access denied"
   unless $acl_edit || $acl_edit_global;
@@ -44,6 +51,7 @@ my $select = '*';
 my $orderby = 'pkgpart';
 my %hash = ();
 my $extra_count = '';
+my $family_pkgpart;
 
 if ( $cgi->param('active') ) {
   $orderby = 'num_active DESC';
@@ -72,10 +80,20 @@ if ( $cgi->param('missing_recur_fee') ) {
   push @where, "0 = ( SELECT COUNT(*) FROM part_pkg_option
                         WHERE optionname = 'recur_fee'
                           AND part_pkg_option.pkgpart = part_pkg.pkgpart
-                          AND CAST ( optionvalue AS NUMERIC ) > 0
+                          AND CAST( optionvalue AS NUMERIC ) > 0
                     )";
 }
 
+if ( $cgi->param('family') =~ /^(\d+)$/ ) {
+  $family_pkgpart = $1;
+  push @where, "family_pkgpart = $1";
+  # Hiding disabled or one-time charges and limiting by classnum aren't 
+  # very useful in this mode, so all links should still refer back to the 
+  # non-family-limited display.
+  $cgi->param('showdisabled', 1);
+  $cgi->delete('family');
+}
+
 push @where, FS::part_pkg->curuser_pkgs_sql
   unless $acl_edit_global;
 
@@ -84,11 +102,11 @@ my $extra_sql = scalar(@where)
                   join( 'AND ', @where)
                 : '';
 
-my $agentnums = join(',', $curuser->agentnums);
+my $agentnums_sql = $curuser->agentnums_sql( 'table'=>'cust_main' );
 my $count_cust_pkg = "
   SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )
     WHERE cust_pkg.pkgpart = part_pkg.pkgpart
-      AND cust_main.agentnum IN ($agentnums)
+      AND $agentnums_sql
 ";
 
 $select = "
@@ -96,6 +114,13 @@ $select = "
   *,
 
   ( $count_cust_pkg
+      AND ( setup IS NULL OR setup = 0 )
+      AND ( cancel IS NULL OR cancel = 0 )
+      AND ( susp IS NULL OR susp = 0 )
+  ) AS num_not_yet_billed,
+
+  ( $count_cust_pkg
+      AND setup IS NOT NULL AND setup != 0
       AND ( cancel IS NULL OR cancel = 0 )
       AND ( susp IS NULL OR susp = 0 )
   ) AS num_active,
@@ -111,13 +136,11 @@ $select = "
 
 ";
 
-my $html_init;
-#unless ( $cgi->param('active') ) {
-  $html_init = qq!
+my $html_init = qq!
     One or more service definitions are grouped together into a package 
     definition and given pricing information.  Customers purchase packages
     rather than purchase services directly.<BR><BR>
-    <FORM METHOD="POST" ACTION="${p}edit/part_pkg.cgi">
+    <FORM METHOD="GET" ACTION="${p}edit/part_pkg.cgi">
     <A HREF="${p}edit/part_pkg.cgi"><I>Add a new package definition</I></A>
     or
     !.include('/elements/select-part_pkg.html', 'element_name' => 'clone' ). qq!
@@ -125,7 +148,6 @@ my $html_init;
     </FORM>
     <BR><BR>
   !;
-#}
 
 $cgi->param('dummy', 1);
 
@@ -140,6 +162,7 @@ my $filter_change =
 #restore this so pagination works
 $cgi->param('classnum', $classnum) if length($classnum);
 
+#should hide this if there aren't any classes
 my $html_posttotal =
   "$filter_change\n<BR>( show class: ".
   include('/elements/select-pkg_class.html',
@@ -195,8 +218,21 @@ push @fields, sub {
   my $part_pkg = shift;
   (my $plan = $plan_labels{$part_pkg->plan} ) =~ s/ /&nbsp;/g;
   my $is_recur = ( $part_pkg->freq ne '0' );
+  my @discounts = sort { $a->months <=> $b->months }
+                  map { $_->discount  }
+                  $part_pkg->part_pkg_discount;
 
   [
+    ( !$family_pkgpart &&
+      $part_pkg->pkgpart == $part_pkg->family_pkgpart ? () : [
+      {
+        'align'=> 'center',
+        'colspan' => 2,
+        'size' => '-1',
+        'data' => '<b>Show all versions</b>',
+        'link' => $p.'browse/part_pkg.cgi?family='.$part_pkg->family_pkgpart,
+      }
+    ] ),
     [
       { data =>$plan,
         align=>'center',
@@ -205,28 +241,68 @@ push @fields, sub {
     ],
     [
       { data =>$money_char.
-               sprintf('%.2f', $part_pkg->option('setup_fee') ),
+               sprintf('%.2f ', $part_pkg->option('setup_fee') ),
         align=>'right'
       },
-      { data => ( $is_recur ? ' setup' : ' one-time' ),
+      { data => ( ( $is_recur ? ' &nbsp; setup' : ' &nbsp; one-time' ).
+                  ( $part_pkg->option('recur_fee') == 0
+                      && $part_pkg->setup_show_zero
+                    ? ' (printed on invoices)'
+                    : ''
+                  )
+                ),
         align=>'left',
       },
     ],
     [
-      { data=>( $is_recur
-                  ? $money_char.sprintf('%.2f ', $part_pkg->option('recur_fee') )
-                  : $part_pkg->freq_pretty
-              ),
+      { data=>(
+          $is_recur
+            ? $money_char. sprintf('%.2f', $part_pkg->option('recur_fee'))
+            : $part_pkg->freq_pretty
+        ),
         align=> ( $is_recur ? 'right' : 'center' ),
         colspan=> ( $is_recur ? 1 : 2 ),
       },
       ( $is_recur
-        ?  { data => ( $is_recur ? $part_pkg->freq_pretty : '' ),
+        ?  { data => ( $is_recur
+               ? ' &nbsp; '. $part_pkg->freq_pretty.
+                 ( $part_pkg->option('recur_fee') == 0
+                     && $part_pkg->recur_show_zero
+                   ? ' (printed on invoices)'
+                   : ''
+                 )
+               : '' ),
              align=>'left',
            }
         : ()
       ),
     ],
+    (
+      map { my $amount = $_->amount / ($_->target_info->{multiplier} || 1);
+            my $label = $_->target_info->{label};
+            [
+              { data    => "Plus&nbsp;$money_char". $_->price. '&nbsp;'.
+                           ( $_->action eq 'increment' ? 'per' : 'for' ).
+                           "&nbsp;$amount&nbsp;$label",
+                align   => 'center', #left?
+                colspan => 2,
+              },
+            ];
+          }
+        $part_pkg->part_pkg_usageprice
+    ),
+    ( 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;
             [ 
@@ -238,6 +314,28 @@ push @fields, sub {
           }
       $part_pkg->bill_part_pkg_link
     ),
+    ( scalar(@discounts)
+        ?  [ 
+              { data => '<b>Discounts</b>',
+                align=>'center', #?
+                colspan=>2,
+              }
+            ]
+        : ()  
+    ),
+    ( scalar(@discounts)
+        ? map { 
+            [ 
+              { data  => $_->months. ':',
+                align => 'right',
+              },
+              { data => $_->amount ? '$'. $_->amount : $_->percent. '%'
+              }
+            ]
+          }
+          @discounts
+        : ()
+    ),
   ];
 
 #  $plan_labels{$part_pkg->plan}.'<BR>'.
@@ -284,6 +382,7 @@ if ( $acl_edit_global ) {
 #if ( $cgi->param('active') ) {
   push @header, 'Customer<BR>packages';
   my %col = (
+    'not yet billed'  => '009999', #teal? cyan?
     'active'          => '00CC00',
     'suspended'       => 'FF9900',
     'cancelled'       => 'FF0000',
@@ -292,8 +391,8 @@ if ( $acl_edit_global ) {
   );
   my $cust_pkg_link = $p. 'search/cust_pkg.cgi?pkgpart=';
   push @fields, sub { my $part_pkg = shift;
-                      [
-                        map {
+                        [
+                        map( {
                               my $magic = $_;
                               my $label = $_;
                               if ( $magic eq 'active' && $part_pkg->freq == 0 ) {
@@ -301,6 +400,7 @@ if ( $acl_edit_global ) {
                                 #$label = 'one-time charge',
                                 $label = 'charge',
                               }
+                              $label= 'not yet billed' if $magic eq 'not_yet_billed';
                           
                               [
                                 {
@@ -325,8 +425,24 @@ if ( $acl_edit_global ) {
                                             ),
                                 },
                               ],
-                            } (qw( active suspended cancelled ))
-                      ]; };
+                            } (qw( not_yet_billed active suspended cancelled ))
+                          ),
+                      ($acl_config ? 
+                        [ {}, 
+                          { 'data'  => '<FONT SIZE="-1">[ '.
+                              include('/elements/popup_link.html',
+                                'label'       => 'change',
+                                'action'      => "${p}edit/bulk-cust_pkg.html?".
+                                                 'pkgpart='.$part_pkg->pkgpart,
+                                'actionlabel' => 'Change Packages',
+                                'width'       => 569,
+                                'height'      => 210,
+                              ).' ]</FONT>',
+                            'align' => 'left',
+                          } 
+                        ] : () ),
+                      ]; 
+  };
   $align .= 'r';
 #}
 
@@ -336,6 +452,10 @@ if ( $taxclasses ) {
   $align .= 'l';
 }
 
+# make a table of report class optionnames =>  the actual 
+my %report_optionname_name = map { 'report_option_'.$_->num, $_->name }
+  qsearch('part_pkg_report_option', { disabled => '' });
+
 push @header, 'Plan options',
               'Services';
               #'Service', 'Quan', 'Primary';
@@ -346,10 +466,20 @@ push @fields,
                     if ( $part_pkg->plan ) {
 
                       my %options = $part_pkg->options;
-
-                      [ map { 
+                      # gather any options that are really report options,
+                      # convert them to their user-friendly names,
+                      # and sort them (I think?)
+                      my @report_options =
+                        sort { $a cmp $b }
+                        map { $report_optionname_name{$_} }
+                        grep { $options{$_}
+                               and exists($report_optionname_name{$_}) }
+                        keys %options;
+
+                      my @rows = (
+                        map { 
                               [
-                                { 'data'  => $_,
+                                { 'data'  => "$_: ",
                                   'align' => 'right',
                                 },
                                 { 'data'  => $part_pkg->format($_,$options{$_}),
@@ -358,11 +488,30 @@ push @fields,
                               ];
                             }
                         grep { $options{$_} =~ /\S/ } 
-                        grep { $_ !~ /^(setup|recur)_fee$/ }
+                        grep { $_ !~ /^(setup|recur)_fee$/ 
+                               and $_ !~ /^report_option_\d+$/ }
                         keys %options
-                      ];
+                      );
+                      if ( @report_options ) {
+                        push @rows,
+                          [ { 'data'  => 'Report classes',
+                              'align' => 'center',
+                              'style' => 'font-weight: bold',
+                              'colspan' => 2
+                            } ];
+                        foreach (@report_options) {
+                          push @rows, [
+                            { 'data'  => $_,
+                              'align' => 'center',
+                              'colspan' => 2
+                            }
+                          ];
+                        } # foreach @report_options
+                      } # if @report_options
 
-                    } else {
+                      return \@rows;
+
+                    } else { # should never happen...
 
                       [ map { [
                                 { 'data'  => uc($_),
@@ -383,6 +532,8 @@ push @fields,
 
               sub {
                     my $part_pkg = shift;
+                    my @part_pkg_usage = sort { $a->priority <=> $b->priority }
+                                         $part_pkg->part_pkg_usage;
 
                     [ 
                       (map {
@@ -425,7 +576,27 @@ push @fields,
                               ]
                             }
                         $part_pkg->svc_part_pkg_link
-                      )
+                      ),
+                      ( scalar(@part_pkg_usage) ? 
+                          [ { data  => 'Usage minutes',
+                              align => 'center',
+                              colspan    => 2,
+                              data_style => 'b',
+                              link  => $p.'browse/part_pkg_usage.html#pkgpart'.
+                                       $part_pkg->pkgpart 
+                            } ]
+                          : ()
+                      ),
+                      ( map {
+                              [ { data  => $_->minutes,
+                                  align => 'right'
+                                },
+                                { data  => $_->description,
+                                  align => 'left'
+                                },
+                              ]
+                            } @part_pkg_usage
+                      ),
                     ];
 
                   };
@@ -440,4 +611,25 @@ $extra_count = ( $count_extra_sql ? ' AND ' : ' WHERE ' ). $extra_count
   if $extra_count;
 my $count_query = "SELECT COUNT(*) FROM part_pkg $count_extra_sql $extra_count";
 
+my $html_form = '';
+my $html_foot = '';
+if ( $acl_edit_bulk ) {
+  # insert a checkbox column
+  push @header, '';
+  push @fields, sub {
+    '<INPUT TYPE="checkbox" NAME="pkgpart" VALUE=' . $_[0]->pkgpart .'>';
+  };
+  push @links, '';
+  $align .= 'c';
+  $html_form = qq!<FORM ACTION="${p}edit/bulk-part_pkg.html" METHOD="POST">!;
+  $html_foot = include('/search/elements/checkbox-foot.html',
+      submit  => 'edit report classes', # for now it's only report classes
+  ) . '</FORM>';
+}
+
+my @menubar;
+# show this if there are any voip_cdr packages defined
+if ( FS::part_pkg->count("plan = 'voip_cdr'") ) {
+  push @menubar, 'Per-package usage minutes' => $p.'browse/part_pkg_usage.html';
+}
 </%init>