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