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