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