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