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