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