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