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