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