RT# 82949 - added the ability to bulk increase package fees (setup and/or recurring...
[freeside.git] / httemplate / browse / part_pkg.cgi
1 <% include( 'elements/browse.html',
2                  'title'                 => 'Package Definitions',
3                  'menubar'               => \@menubar,
4                  'html_init'             => $html_init,
5                  'html_form'             => $html_form,
6                  'html_posttotal'        => $html_posttotal,
7                  'name'                  => 'package definitions',
8                  'disableable'           => 1,
9                  'disabled_statuspos'    => 4,
10                  'agent_virt'            => 1,
11                  'agent_null_right'      => [ $edit, $edit_global ],
12                  'agent_null_right_link' => $edit_global,
13                  'agent_pos'             => 7, #5?
14                  'query'                 => { 'select'    => $select,
15                                               'table'     => 'part_pkg',
16                                               'hashref'   => \%hash,
17                                               'extra_sql' => $extra_sql,
18                                               'order_by'  => "ORDER BY $orderby"
19                                             },
20                  'count_query'           => $count_query,
21                  'header'                => \@header,
22                  'fields'                => \@fields,
23                  'links'                 => \@links,
24                  'align'                 => $align,
25                  'link_field'            => 'pkgpart',
26                  'html_init'             => $html_init,
27                  'html_foot'             => $html_foot,
28              )
29 %>
30
31 <%def .style>
32   <SCRIPT TYPE="text/javascript">
33     $(document).ready(function(){
34       $(this).scrollTop(0);
35     });
36   </SCRIPT>
37 </%def>
38
39 <%init>
40
41 my $curuser = $FS::CurrentUser::CurrentUser;
42
43 my $edit        = 'Edit package definitions';
44 my $edit_global = 'Edit global package definitions';
45 my $acl_edit        = $curuser->access_right($edit);
46 my $acl_edit_global = $curuser->access_right($edit_global);
47 my $acl_config      = $curuser->access_right('Configuration'); #to edit services
48                                                                #and agent types
49                                                                #and bulk change
50 my $acl_edit_bulk   = $curuser->access_right('Bulk edit package definitions');
51
52 die "access denied"
53   unless $acl_edit || $acl_edit_global;
54
55 my $conf = new FS::Conf;
56 my $taxclasses = $conf->exists('enable_taxclasses');
57 my $money_char = $conf->config('money_char') || '$';
58
59 my $select = '*';
60 my $orderby = 'pkgpart';
61 my %hash = ();
62 my $extra_count = '';
63 my $family_pkgpart;
64
65 if ( $cgi->param('active') ) {
66   $orderby = 'num_active DESC';
67 }
68
69 my @where = ();
70
71 #if ( $cgi->param('activeONLY') ) {
72 #  push @where, ' WHERE num_active > 0 '; #XXX doesn't affect count...
73 #}
74
75 if ( $cgi->param('recurring') ) {
76   $hash{'freq'} = { op=>'!=', value=>'0' };
77   $extra_count = " freq != '0' ";
78 }
79
80 my $classnum = '';
81 if ( $cgi->param('classnum') =~ /^(\d+)$/ ) {
82   $classnum = $1;
83   push @where, $classnum ? "classnum =  $classnum"
84                          : "classnum IS NULL";
85 }
86 $cgi->delete('classnum');
87
88 if ( $cgi->param('pkgpartbatch') =~ /^([\w\/\-\:\. ]+)$/ ) {
89   push @where, "pkgpartbatch = '$1' ";
90 }
91
92 if ( $cgi->param('missing_recur_fee') ) {
93   push @where, "NOT EXISTS ( SELECT 1 FROM part_pkg_option
94                                WHERE optionname = 'recur_fee'
95                                  AND part_pkg_option.pkgpart = part_pkg.pkgpart
96                                  AND CAST( optionvalue AS NUMERIC ) > 0
97                            )";
98 }
99
100 if ( $cgi->param('ratenum') =~ /^(\d+)$/ ) {
101   push @where, "EXISTS( SELECT 1 FROM part_pkg_option
102                           WHERE optionname LIKE '%ratenum'
103                             AND optionvalue = '$1'
104                             AND part_pkg_option.pkgpart = part_pkg.pkgpart
105                       )";
106 }
107
108 if ( $cgi->param('family') =~ /^(\d+)$/ ) {
109   $family_pkgpart = $1;
110   push @where, "family_pkgpart = $1";
111   # Hiding disabled or one-time charges and limiting by classnum aren't 
112   # very useful in this mode, so all links should still refer back to the 
113   # non-family-limited display.
114   $cgi->param('showdisabled', 1);
115   $cgi->delete('family');
116 }
117
118 push @where, FS::part_pkg->curuser_pkgs_sql
119   unless $acl_edit_global;
120
121 my $extra_sql = scalar(@where)
122                 ? ( scalar(keys %hash) ? ' AND ' : ' WHERE ' ).
123                   join( 'AND ', @where)
124                 : '';
125
126 my $agentnums_sql = $curuser->agentnums_sql( 'table'=>'cust_main' );
127 my $count_cust_pkg = "
128   SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )
129     WHERE cust_pkg.pkgpart = part_pkg.pkgpart
130       AND $agentnums_sql
131 ";
132 my $count_cust_pkg_cancel = "
133   SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )
134     LEFT JOIN cust_pkg AS cust_pkg_next
135       ON (cust_pkg.pkgnum = cust_pkg_next.change_pkgnum)
136     WHERE cust_pkg.pkgpart = part_pkg.pkgpart
137       AND $agentnums_sql
138       AND cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0
139 ";
140
141 $select = "
142
143   *,
144
145   ( $count_cust_pkg
146       AND ( setup IS NULL OR setup = 0 )
147       AND ( cancel IS NULL OR cancel = 0 )
148       AND ( susp IS NULL OR susp = 0 )
149   ) AS num_not_yet_billed,
150
151   ( $count_cust_pkg
152       AND setup IS NOT NULL AND setup != 0
153       AND ( cancel IS NULL OR cancel = 0 )
154       AND ( susp IS NULL OR susp = 0 )
155   ) AS num_active,
156
157   ( $count_cust_pkg
158       AND ( cancel IS NULL OR cancel = 0 )
159       AND susp IS NOT NULL AND susp != 0
160       AND setup IS NOT NULL AND setup != 0
161   ) AS num_suspended,
162
163   ( $count_cust_pkg
164       AND ( cancel IS NULL OR cancel = 0 )
165       AND susp IS NOT NULL AND susp != 0
166       AND ( setup IS NULL OR setup = 0 )
167   ) AS num_on_hold,
168
169   ( $count_cust_pkg_cancel
170       AND (cust_pkg_next.pkgnum IS NULL
171            OR cust_pkg_next.pkgpart != cust_pkg.pkgpart)
172   ) AS num_cancelled
173
174 ";
175 # About the num_cancelled expression: packages that were changed, but 
176 # kept the same pkgpart, are considered "moved", not "canceled" (because
177 # this is the part_pkg UI).  We could show the count of those but it's 
178 # probably not interesting.
179
180 my $html_init = qq!
181     One or more service definitions are grouped together into a package 
182     definition and given pricing information.  Customers purchase packages
183     rather than purchase services directly.<BR><BR>
184     <FORM METHOD="GET" ACTION="${p}edit/part_pkg.cgi">
185     <A HREF="${p}edit/part_pkg.cgi"><I>Add a new package definition</I></A>
186     or
187     !.include('/elements/select-part_pkg.html', 'element_name' => 'clone' ). qq!
188     <INPUT TYPE="submit" VALUE="Clone existing package">
189     </FORM>
190     <BR><BR>
191   !;
192
193 $cgi->param('dummy', 1);
194
195 my $filter_change =
196   qq(\n<SCRIPT TYPE="text/javascript">\n).
197   "function filter_change() {".
198   "  window.location = '". $cgi->self_url.
199        ";classnum=' + document.getElementById('classnum').options[document.getElementById('classnum').selectedIndex].value".
200   "}".
201   "\n</SCRIPT>\n";
202
203 #restore this so pagination works
204 $cgi->param('classnum', $classnum) if length($classnum);
205
206 #should hide this if there aren't any classes
207 my $html_posttotal =
208   "$filter_change\n<BR>( show class: ".
209   include('/elements/select-pkg_class.html',
210             #'curr_value'    => $classnum,
211             'value'         => $classnum, #insist on 0 :/
212             'onchange'      => 'filter_change()',
213             'pre_options'   => [ '-1' => 'all',
214                                  '0'  => '(none)', ],
215             'disable_empty' => 1,
216          ).
217   ' )';
218
219 my $recur_toggle = $cgi->param('recurring') ? 'show' : 'hide';
220 $cgi->param('recurring', $cgi->param('recurring') ^ 1 );
221
222 $html_posttotal .=
223   '( <A HREF="'. $cgi->self_url.'">'. "$recur_toggle one-time charges</A> )";
224
225 $cgi->param('recurring', $cgi->param('recurring') ^ 1 ); #put it back
226
227 # ------
228
229 my $link = [ $p.'edit/part_pkg.cgi?', 'pkgpart' ];
230
231 my @header = ( '#', 'Package', 'Comment', 'Custom' );
232 my @fields = ( 'pkgpart', 'pkg', 'comment',
233                sub{ '<B><FONT COLOR="#0000CC">'.$_[0]->custom.'</FONT></B>' }
234              );
235 my $align = 'rllc';
236 my @links = ( $link, $link, '', '' );
237
238 unless ( 0 ) { #already showing only one class or something?
239   push @header, 'Class';
240   push @fields, sub { shift->classname || '(none)'; };
241   $align .= 'l';
242 }
243
244 if ( $conf->exists('pkg-addon_classnum') ) {
245   push @header, "Add'l order class";
246   push @fields, sub { shift->addon_classname || '(none)'; };
247   $align .= 'l';
248 }
249
250 tie my %plans, 'Tie::IxHash', %{ FS::part_pkg::plan_info() };
251
252 tie my %plan_labels, 'Tie::IxHash',
253   map {  $_ => ( $plans{$_}->{'shortname'} || $plans{$_}->{'name'} ) }
254       keys %plans;
255
256 push @header, 'Pricing';
257 $align .= 'r'; #?
258 push @fields, sub {
259   my $part_pkg = shift;
260   (my $plan = $plan_labels{$part_pkg->plan} ) =~ s/ /&nbsp;/g;
261   my $is_recur = ( $part_pkg->freq ne '0' );
262   my @discounts = sort { $a->months <=> $b->months }
263                   map { $_->discount  }
264                   $part_pkg->part_pkg_discount;
265
266   [
267     ( !$family_pkgpart &&
268       $part_pkg->pkgpart == $part_pkg->family_pkgpart ? () : [
269       {
270         'align'=> 'center',
271         'colspan' => 2,
272         'size' => '-1',
273         'data' => '<b>Show all versions</b>',
274         'link' => $p.'browse/part_pkg.cgi?family='.$part_pkg->family_pkgpart,
275       }
276     ] ),
277     [
278       { data =>$plan,
279         align=>'center',
280         colspan=>2,
281       },
282     ],
283     [
284       { data =>$money_char.
285                sprintf('%.2f ', $part_pkg->option('setup_fee') ),
286         align=>'right'
287       },
288       { data => ( ( $is_recur ? ' &nbsp; setup' : ' &nbsp; one-time' ).
289                   ( $part_pkg->option('recur_fee') == 0
290                       && $part_pkg->setup_show_zero
291                     ? ' (printed on invoices)'
292                     : ''
293                   )
294                 ),
295         align=>'left',
296       },
297     ],
298     [
299       { data=>(
300           $is_recur
301             ? $money_char. sprintf('%.2f', $part_pkg->option('recur_fee'))
302             : $part_pkg->freq_pretty
303         ),
304         align=> ( $is_recur ? 'right' : 'center' ),
305         colspan=> ( $is_recur ? 1 : 2 ),
306       },
307       ( $is_recur
308         ?  { data => ( $is_recur
309                ? ' &nbsp; '. $part_pkg->freq_pretty.
310                  ( $part_pkg->option('recur_fee') == 0
311                      && $part_pkg->recur_show_zero
312                    ? ' (printed on invoices)'
313                    : ''
314                  )
315                : '' ),
316              align=>'left',
317            }
318         : ()
319       ),
320     ],
321     ( map { my $dst_pkg = $_->dst_pkg;
322             [
323               { data => 'Supplemental: &nbsp;'.
324                         '<A HREF="#'. $dst_pkg->pkgpart . '">' .
325                         $dst_pkg->pkg . '</A>',
326                 align=> 'center',
327                 colspan => 2,
328               }
329             ]
330           }
331       $part_pkg->supp_part_pkg_link
332     ),
333     ( map { 
334             my $dst_pkg = $_->dst_pkg;
335             [ 
336               { data => 'Add-on:&nbsp;'.$dst_pkg->pkg_comment,
337                 align=>'center', #?
338                 colspan=>2,
339               }
340             ]
341           }
342       $part_pkg->bill_part_pkg_link
343     ),
344     ( scalar(@discounts)
345         ?  [ 
346               { data => '<b>Discounts</b>',
347                 align=>'center', #?
348                 colspan=>2,
349               }
350             ]
351         : ()  
352     ),
353     ( scalar(@discounts)
354         ? map { 
355             [ 
356               { data  => $_->months. ':',
357                 align => 'right',
358               },
359               { data => $_->amount ? '$'. $_->amount : $_->percent. '%'
360               }
361             ]
362           }
363           @discounts
364         : ()
365     ),
366   ];
367
368 #  $plan_labels{$part_pkg->plan}.'<BR>'.
369 #    $money_char.sprintf('%.2f setup<BR>', $part_pkg->option('setup_fee') ).
370 #    ( $part_pkg->freq ne '0'
371 #      ? $money_char.sprintf('%.2f ', $part_pkg->option('recur_fee') )
372 #      : ''
373 #    ).
374 #    $part_pkg->freq_pretty; #.'<BR>'
375 };
376
377 push @header, 'Cost&nbsp;tracking';
378 $align .= 'r'; #?
379 push @fields, sub {
380   my $part_pkg = shift;
381   #(my $plan = $plan_labels{$part_pkg->plan} ) =~ s/ /&nbsp;/g;
382   my $is_recur = ( $part_pkg->freq ne '0' );
383
384   [
385     [
386       { data => '&nbsp;', # $plan,
387         align=>'center',
388         colspan=>2,
389       },
390     ],
391     [
392       { data =>$money_char.
393                sprintf('%.2f ', $part_pkg->setup_cost ),
394         align=>'right'
395       },
396       { data => ( $is_recur ? '&nbsp;setup' : '&nbsp;one-time' ),
397         align=>'left',
398       },
399     ],
400     [
401       { data=>(
402           $is_recur
403             ? $money_char. sprintf('%.2f', $part_pkg->recur_cost)
404             : '(no&nbsp;recurring)' #$part_pkg->freq_pretty
405         ),
406         align=> ( $is_recur ? 'right' : 'center' ),
407         colspan=> ( $is_recur ? 1 : 2 ),
408       },
409       ( $is_recur
410         ?  { data => ( $is_recur
411                          ? '&nbsp;'. $part_pkg->freq_pretty
412                          : ''
413                      ),
414              align=>'left',
415            }
416         : ()
417       ),
418     ],
419   ];
420 };
421
422 ###
423 # Agent goes here if displayed
424 ###
425
426 #agent type
427 if ( $acl_edit_global ) {
428   #really we just want a count, but this is fine unless someone has tons
429   my @all_agent_types = map {$_->typenum}
430                           qsearch('agent_type', { 'disabled'=>'' });
431   if ( scalar(@all_agent_types) > 1 ) {
432     push @header, 'Agent types';
433     my $typelink = $p. 'edit/agent_type.cgi?';
434     push @fields, sub { my $part_pkg = shift;
435                         [
436                           map { my $agent_type = $_->agent_type;
437                                 [ 
438                                   { 'data'  => $agent_type->atype, #escape?
439                                     'align' => 'left',
440                                     'link'  => ( $acl_config
441                                                    ? $typelink.
442                                                      $agent_type->typenum
443                                                    : ''
444                                                ),
445                                   },
446                                 ];
447                               }
448                               $part_pkg->type_pkgs
449                         ];
450                       };
451     $align .= 'l';
452   }
453 }
454
455 #if ( $cgi->param('active') ) {
456   push @header, 'Customer<BR>packages';
457   my %col = %{ FS::cust_pkg->statuscolors };
458   my $cust_pkg_link = $p. 'search/cust_pkg.cgi?pkgpart=';
459   push @fields, sub { my $part_pkg = shift;
460                         [
461                         map( {
462                               my $magic = $_;
463                               my $label = $_;
464                               if ( $magic eq 'active' && $part_pkg->freq == 0 ) {
465                                 $magic = 'inactive';
466                                 #$label = 'one-time charge';
467                                 $label = 'charge';
468                               }
469                               $label= 'not yet billed' if $magic eq 'not_yet_billed';
470                               $label= 'on hold' if $magic eq 'on_hold';
471                           
472                               [
473                                 {
474                                  'data'  => '<B><FONT COLOR="#'. $col{$label}. '">'.
475                                             $part_pkg->get("num_$_").
476                                             '</FONT></B>',
477                                  'align' => 'right',
478                                 },
479                                 {
480                                  'data'  => $label.
481                                               ( $part_pkg->get("num_$_") != 1
482                                                 && $label =~ /charge$/
483                                                   ? 's'
484                                                   : ''
485                                               ),
486                                  'align' => 'left',
487                                  'link'  => ( $part_pkg->get("num_$_")
488                                                 ? $cust_pkg_link.
489                                                   $part_pkg->pkgpart.
490                                                   ";magic=$magic"
491                                                 : ''
492                                             ),
493                                 },
494                               ],
495                             } (qw( on_hold not_yet_billed active suspended cancelled ))
496                           ),
497                       ($acl_config ? 
498                         [ {}, 
499                           { 'data'  => '<FONT SIZE="-1">[ '.
500                               include('/elements/popup_link.html',
501                                 'label'       => 'change',
502                                 'action'      => "${p}edit/bulk-cust_pkg.html?".
503                                                  'pkgpart='.$part_pkg->pkgpart,
504                                 'actionlabel' => 'Change Packages',
505                                 'width'       => 960,
506                                 'height'      => 210,
507                               ).' ]</FONT>',
508                             'align' => 'left',
509                           } 
510                         ] : () ),
511                       ]; 
512   };
513   $align .= 'r';
514 #}
515
516 if ( $taxclasses ) {
517   push @header, 'Taxclass';
518   push @fields, sub { shift->taxclass() || '&nbsp;'; };
519   $align .= 'l';
520 }
521
522 # make a table of report class optionnames =>  the actual 
523 my %report_optionname_name = map { 'report_option_'.$_->num, $_->name }
524   qsearch('part_pkg_report_option', { disabled => '' });
525
526 push @header, 'Plan options',
527               'Services';
528               #'Service', 'Quan', 'Primary';
529
530 push @fields, 
531               sub {
532                     my $part_pkg = shift;
533                     if ( $part_pkg->plan ) {
534
535                       my %options = $part_pkg->options;
536                       # gather any options that are really report options,
537                       # convert them to their user-friendly names,
538                       # and sort them (I think?)
539                       my @report_options =
540                         sort { $a cmp $b }
541                         map { $report_optionname_name{$_} }
542                         grep { $options{$_}
543                                and exists($report_optionname_name{$_}) }
544                         keys %options;
545
546                       my @rows = (
547                         map { 
548                               [
549                                 { 'data'  => "$_: ",
550                                   'align' => 'right',
551                                 },
552                                 { 'data'  => $part_pkg->format($_,$options{$_}),
553                                   'align' => 'left',
554                                 },
555                               ];
556                             }
557                         sort
558                         grep { $options{$_} =~ /\S/ } 
559                         grep { $_ !~ /^(setup|recur)_fee$/ 
560                                and $_ !~ /^report_option_\d+$/ }
561                         keys %options
562                       );
563                       if ( @report_options ) {
564                         push @rows,
565                           [ { 'data'  => 'Report classes',
566                               'align' => 'center',
567                               'style' => 'font-weight: bold',
568                               'colspan' => 2
569                             } ];
570                         foreach (@report_options) {
571                           push @rows, [
572                             { 'data'  => $_,
573                               'align' => 'center',
574                               'colspan' => 2
575                             }
576                           ];
577                         } # foreach @report_options
578                       } # if @report_options
579
580                       return \@rows;
581
582                     } else { # should never happen...
583
584                       [ map { [
585                                 { 'data'  => uc($_),
586                                   'align' => 'right',
587                                 },
588                                 {
589                                   'data'  => $part_pkg->$_(),
590                                   'align' => 'left',
591                                 },
592                               ];
593                             }
594                         (qw(setup recur))
595                       ];
596
597                     }
598
599                   },
600
601               sub {
602                     my $part_pkg = shift;
603                     my @part_pkg_usage = sort { $a->priority <=> $b->priority }
604                                          $part_pkg->part_pkg_usage;
605
606                     [ 
607                       (map {
608                              my $pkg_svc = $_;
609                              my $part_svc = $pkg_svc->part_svc;
610                              my $svc = $part_svc->svc;
611                              if ( $pkg_svc->primary_svc =~ /^Y/i ) {
612                                $svc = "<B>$svc (PRIMARY)</B>";
613                              }
614                              $svc =~ s/ +/&nbsp;/g;
615
616                              [
617                                {
618                                  'data'  => '<B>'. $pkg_svc->quantity. '</B>',
619                                  'align' => 'right'
620                                },
621                                {
622                                  'data'  => $svc,
623                                  'align' => 'left',
624                                  'link'  => ( $acl_config
625                                                 ? $p. 'edit/part_svc.cgi?'.
626                                                   $part_svc->svcpart
627                                                 : ''
628                                             ),
629                                },
630                              ];
631                            }
632                       sort {     $b->primary_svc =~ /^Y/i
633                              <=> $a->primary_svc =~ /^Y/i
634                            }
635                            $part_pkg->pkg_svc('disable_linked'=>1)
636                       ),
637                       ( map { 
638                               my $dst_pkg = $_->dst_pkg;
639                               [
640                                 { data => 'Add-on:&nbsp;'.$dst_pkg->pkg_comment,
641                                   align=>'center', #?
642                                   colspan=>2,
643                                 }
644                               ]
645                             }
646                         $part_pkg->svc_part_pkg_link
647                       ),
648                       ( scalar(@part_pkg_usage) ? 
649                           [ { data  => 'Usage minutes',
650                               align => 'center',
651                               colspan    => 2,
652                               data_style => 'b',
653                               link  => $p.'browse/part_pkg_usage.html#pkgpart'.
654                                        $part_pkg->pkgpart 
655                             } ]
656                           : ()
657                       ),
658                       ( map {
659                               [ { data  => $_->minutes,
660                                   align => 'right'
661                                 },
662                                 { data  => $_->description,
663                                   align => 'left'
664                                 },
665                               ]
666                             } @part_pkg_usage
667                       ),
668                     ];
669
670                   };
671
672 $align .= 'lrl'; #rr';
673
674 # --------
675
676 my $count_extra_sql = $extra_sql;
677 $count_extra_sql =~ s/^\s*AND /WHERE /i;
678 $extra_count = ( $count_extra_sql ? ' AND ' : ' WHERE ' ). $extra_count
679   if $extra_count;
680 my $count_query = "SELECT COUNT(*) FROM part_pkg $count_extra_sql $extra_count";
681
682 my $html_form = '';
683 my $html_foot = '';
684 if ( $acl_edit_bulk ) {
685   # insert a checkbox column
686   push @header, '';
687   push @fields, sub {
688     '<INPUT TYPE="checkbox" NAME="pkgpart" VALUE=' . $_[0]->pkgpart .'>';
689   };
690   push @links, '';
691   $align .= 'c';
692   $html_form = qq!<FORM ACTION="${p}edit/bulk-part_pkg.html" METHOD="POST">!;
693   $html_foot = include('/search/elements/checkbox-foot.html',
694                  actions => [
695                    { label  => 'edit packages',
696                      onclick=> include('/elements/popup_link_onclick.html',
697                                  'label'       => 'edit',
698                                  'js_action'   => qq{
699                                    '${p}edit/bulk-part_pkg.html?' + \$('input[name=pkgpart]').serialize()
700                                  },
701                                  'actionlabel' => 'Bulk edit packages',
702                                  'width'       => 960,
703                                  'height'      => 420,
704                                )
705                    },
706                    { label  => 'change customers packages',
707                      onclick=> include('/elements/popup_link_onclick.html',
708                                  'label'       => 'change',
709                                  'js_action'   => qq{
710                                    '${p}edit/bulk-cust_pkg.html?' + \$('input[name=pkgpart]').serialize()
711                                  },
712                                  'actionlabel' => 'Change customer packages',
713                                  'width'       => 960,
714                                  'height'      => 420,
715                                )
716                    },
717                  ],
718                ).
719                '</FORM>';
720 }
721
722 my @menubar;
723 # show this if there are any voip_cdr packages defined
724 if ( FS::part_pkg->count("plan = 'voip_cdr'") ) {
725   push @menubar, 'Per-package usage minutes' => $p.'browse/part_pkg_usage.html';
726 }
727 </%init>