time/data/etc. unit pricing add-ons, RT#24392
[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     (
281       map { my $amount = $_->amount
282               / (FS::part_pkg_usageprice->targets->{$_->target}{multiplier}||1);
283             my $label = FS::part_pkg_usageprice->targets->{$_->target}{label};
284             [
285               { data    => "Plus&nbsp;$money_char". $_->price. '&nbsp;'.
286                            ( $_->action eq 'increment' ? 'per' : 'for' ).
287                            "&nbsp;$amount&nbsp;$label",
288                 align   => 'center', #left?
289                 colspan => 2,
290               },
291             ];
292           }
293         $part_pkg->part_pkg_usageprice
294     ),
295     ( map { my $dst_pkg = $_->dst_pkg;
296             [
297               { data => 'Supplemental: &nbsp;'.
298                         '<A HREF="#'. $dst_pkg->pkgpart . '">' .
299                         $dst_pkg->pkg . '</A>',
300                 align=> 'center',
301                 colspan => 2,
302               }
303             ]
304           }
305       $part_pkg->supp_part_pkg_link
306     ),
307     ( map { 
308             my $dst_pkg = $_->dst_pkg;
309             [ 
310               { data => 'Add-on:&nbsp;'.$dst_pkg->pkg_comment,
311                 align=>'center', #?
312                 colspan=>2,
313               }
314             ]
315           }
316       $part_pkg->bill_part_pkg_link
317     ),
318     ( scalar(@discounts)
319         ?  [ 
320               { data => '<b>Discounts</b>',
321                 align=>'center', #?
322                 colspan=>2,
323               }
324             ]
325         : ()  
326     ),
327     ( scalar(@discounts)
328         ? map { 
329             [ 
330               { data  => $_->months. ':',
331                 align => 'right',
332               },
333               { data => $_->amount ? '$'. $_->amount : $_->percent. '%'
334               }
335             ]
336           }
337           @discounts
338         : ()
339     ),
340   ];
341
342 #  $plan_labels{$part_pkg->plan}.'<BR>'.
343 #    $money_char.sprintf('%.2f setup<BR>', $part_pkg->option('setup_fee') ).
344 #    ( $part_pkg->freq ne '0'
345 #      ? $money_char.sprintf('%.2f ', $part_pkg->option('recur_fee') )
346 #      : ''
347 #    ).
348 #    $part_pkg->freq_pretty; #.'<BR>'
349 };
350
351 ###
352 # Agent goes here if displayed
353 ###
354
355 #agent type
356 if ( $acl_edit_global ) {
357   #really we just want a count, but this is fine unless someone has tons
358   my @all_agent_types = map {$_->typenum} qsearch('agent_type',{});
359   if ( scalar(@all_agent_types) > 1 ) {
360     push @header, 'Agent types';
361     my $typelink = $p. 'edit/agent_type.cgi?';
362     push @fields, sub { my $part_pkg = shift;
363                         [
364                           map { my $agent_type = $_->agent_type;
365                                 [ 
366                                   { 'data'  => $agent_type->atype, #escape?
367                                     'align' => 'left',
368                                     'link'  => ( $acl_config
369                                                    ? $typelink.
370                                                      $agent_type->typenum
371                                                    : ''
372                                                ),
373                                   },
374                                 ];
375                               }
376                               $part_pkg->type_pkgs
377                         ];
378                       };
379     $align .= 'l';
380   }
381 }
382
383 #if ( $cgi->param('active') ) {
384   push @header, 'Customer<BR>packages';
385   my %col = (
386     'not yet billed'  => '009999', #teal? cyan?
387     'active'          => '00CC00',
388     'suspended'       => 'FF9900',
389     'cancelled'       => 'FF0000',
390     #'one-time charge' => '000000',
391     'charge'          => '000000',
392   );
393   my $cust_pkg_link = $p. 'search/cust_pkg.cgi?pkgpart=';
394   push @fields, sub { my $part_pkg = shift;
395                         [
396                         map( {
397                               my $magic = $_;
398                               my $label = $_;
399                               if ( $magic eq 'active' && $part_pkg->freq == 0 ) {
400                                 $magic = 'inactive';
401                                 #$label = 'one-time charge',
402                                 $label = 'charge',
403                               }
404                               $label= 'not yet billed' if $magic eq 'not_yet_billed';
405                           
406                               [
407                                 {
408                                  'data'  => '<B><FONT COLOR="#'. $col{$label}. '">'.
409                                             $part_pkg->get("num_$_").
410                                             '</FONT></B>',
411                                  'align' => 'right',
412                                 },
413                                 {
414                                  'data'  => $label.
415                                               ( $part_pkg->get("num_$_") != 1
416                                                 && $label =~ /charge$/
417                                                   ? 's'
418                                                   : ''
419                                               ),
420                                  'align' => 'left',
421                                  'link'  => ( $part_pkg->get("num_$_")
422                                                 ? $cust_pkg_link.
423                                                   $part_pkg->pkgpart.
424                                                   ";magic=$magic"
425                                                 : ''
426                                             ),
427                                 },
428                               ],
429                             } (qw( not_yet_billed active suspended cancelled ))
430                           ),
431                       ($acl_config ? 
432                         [ {}, 
433                           { 'data'  => '<FONT SIZE="-1">[ '.
434                               include('/elements/popup_link.html',
435                                 'label'       => 'change',
436                                 'action'      => "${p}edit/bulk-cust_pkg.html?".
437                                                  'pkgpart='.$part_pkg->pkgpart,
438                                 'actionlabel' => 'Change Packages',
439                                 'width'       => 569,
440                                 'height'      => 210,
441                               ).' ]</FONT>',
442                             'align' => 'left',
443                           } 
444                         ] : () ),
445                       ]; 
446   };
447   $align .= 'r';
448 #}
449
450 if ( $taxclasses ) {
451   push @header, 'Taxclass';
452   push @fields, sub { shift->taxclass() || '&nbsp;'; };
453   $align .= 'l';
454 }
455
456 # make a table of report class optionnames =>  the actual 
457 my %report_optionname_name = map { 'report_option_'.$_->num, $_->name }
458   qsearch('part_pkg_report_option', { disabled => '' });
459
460 push @header, 'Plan options',
461               'Services';
462               #'Service', 'Quan', 'Primary';
463
464 push @fields, 
465               sub {
466                     my $part_pkg = shift;
467                     if ( $part_pkg->plan ) {
468
469                       my %options = $part_pkg->options;
470                       # gather any options that are really report options,
471                       # convert them to their user-friendly names,
472                       # and sort them (I think?)
473                       my @report_options =
474                         sort { $a cmp $b }
475                         map { $report_optionname_name{$_} }
476                         grep { $options{$_}
477                                and exists($report_optionname_name{$_}) }
478                         keys %options;
479
480                       my @rows = (
481                         map { 
482                               [
483                                 { 'data'  => "$_: ",
484                                   'align' => 'right',
485                                 },
486                                 { 'data'  => $part_pkg->format($_,$options{$_}),
487                                   'align' => 'left',
488                                 },
489                               ];
490                             }
491                         grep { $options{$_} =~ /\S/ } 
492                         grep { $_ !~ /^(setup|recur)_fee$/ 
493                                and $_ !~ /^report_option_\d+$/ }
494                         keys %options
495                       );
496                       if ( @report_options ) {
497                         push @rows,
498                           [ { 'data'  => 'Report classes',
499                               'align' => 'center',
500                               'style' => 'font-weight: bold',
501                               'colspan' => 2
502                             } ];
503                         foreach (@report_options) {
504                           push @rows, [
505                             { 'data'  => $_,
506                               'align' => 'center',
507                               'colspan' => 2
508                             }
509                           ];
510                         } # foreach @report_options
511                       } # if @report_options
512
513                       return \@rows;
514
515                     } else { # should never happen...
516
517                       [ map { [
518                                 { 'data'  => uc($_),
519                                   'align' => 'right',
520                                 },
521                                 {
522                                   'data'  => $part_pkg->$_(),
523                                   'align' => 'left',
524                                 },
525                               ];
526                             }
527                         (qw(setup recur))
528                       ];
529
530                     }
531
532                   },
533
534               sub {
535                     my $part_pkg = shift;
536                     my @part_pkg_usage = sort { $a->priority <=> $b->priority }
537                                          $part_pkg->part_pkg_usage;
538
539                     [ 
540                       (map {
541                              my $pkg_svc = $_;
542                              my $part_svc = $pkg_svc->part_svc;
543                              my $svc = $part_svc->svc;
544                              if ( $pkg_svc->primary_svc =~ /^Y/i ) {
545                                $svc = "<B>$svc (PRIMARY)</B>";
546                              }
547                              $svc =~ s/ +/&nbsp;/g;
548
549                              [
550                                {
551                                  'data'  => '<B>'. $pkg_svc->quantity. '</B>',
552                                  'align' => 'right'
553                                },
554                                {
555                                  'data'  => $svc,
556                                  'align' => 'left',
557                                  'link'  => ( $acl_config
558                                                 ? $p. 'edit/part_svc.cgi?'.
559                                                   $part_svc->svcpart
560                                                 : ''
561                                             ),
562                                },
563                              ];
564                            }
565                       sort {     $b->primary_svc =~ /^Y/i
566                              <=> $a->primary_svc =~ /^Y/i
567                            }
568                            $part_pkg->pkg_svc('disable_linked'=>1)
569                       ),
570                       ( map { 
571                               my $dst_pkg = $_->dst_pkg;
572                               [
573                                 { data => 'Add-on:&nbsp;'.$dst_pkg->pkg_comment,
574                                   align=>'center', #?
575                                   colspan=>2,
576                                 }
577                               ]
578                             }
579                         $part_pkg->svc_part_pkg_link
580                       ),
581                       ( scalar(@part_pkg_usage) ? 
582                           [ { data  => 'Usage minutes',
583                               align => 'center',
584                               colspan    => 2,
585                               data_style => 'b',
586                               link  => $p.'browse/part_pkg_usage.html#pkgpart'.
587                                        $part_pkg->pkgpart 
588                             } ]
589                           : ()
590                       ),
591                       ( map {
592                               [ { data  => $_->minutes,
593                                   align => 'right'
594                                 },
595                                 { data  => $_->description,
596                                   align => 'left'
597                                 },
598                               ]
599                             } @part_pkg_usage
600                       ),
601                     ];
602
603                   };
604
605 $align .= 'lrl'; #rr';
606
607 # --------
608
609 my $count_extra_sql = $extra_sql;
610 $count_extra_sql =~ s/^\s*AND /WHERE /i;
611 $extra_count = ( $count_extra_sql ? ' AND ' : ' WHERE ' ). $extra_count
612   if $extra_count;
613 my $count_query = "SELECT COUNT(*) FROM part_pkg $count_extra_sql $extra_count";
614
615 my $html_form = '';
616 my $html_foot = '';
617 if ( $acl_edit_bulk ) {
618   # insert a checkbox column
619   push @header, '';
620   push @fields, sub {
621     '<INPUT TYPE="checkbox" NAME="pkgpart" VALUE=' . $_[0]->pkgpart .'>';
622   };
623   push @links, '';
624   $align .= 'c';
625   $html_form = qq!<FORM ACTION="${p}edit/bulk-part_pkg.html" METHOD="POST">!;
626   $html_foot = include('/search/elements/checkbox-foot.html',
627       submit  => 'edit report classes', # for now it's only report classes
628   ) . '</FORM>';
629 }
630
631 my @menubar;
632 # show this if there are any voip_cdr packages defined
633 if ( FS::part_pkg->count("plan = 'voip_cdr'") ) {
634   push @menubar, 'Per-package usage minutes' => $p.'browse/part_pkg_usage.html';
635 }
636 </%init>