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