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