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