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',
8 'disabled_statuspos' => 4,
10 'agent_null_right' => [ $edit, $edit_global ],
11 'agent_null_right_link' => $edit_global,
13 'query' => { 'select' => $select,
14 'table' => 'part_pkg',
16 'extra_sql' => $extra_sql,
17 'order_by' => "ORDER BY $orderby"
19 'count_query' => $count_query,
24 'link_field' => 'pkgpart',
25 'html_init' => $html_init,
26 'html_foot' => $html_foot,
31 my $curuser = $FS::CurrentUser::CurrentUser;
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
40 my $acl_edit_bulk = $curuser->access_right('Bulk edit package definitions');
43 unless $acl_edit || $acl_edit_global;
45 my $conf = new FS::Conf;
46 my $taxclasses = $conf->exists('enable_taxclasses');
47 my $money_char = $conf->config('money_char') || '$';
50 my $orderby = 'pkgpart';
55 if ( $cgi->param('active') ) {
56 $orderby = 'num_active DESC';
61 #if ( $cgi->param('activeONLY') ) {
62 # push @where, ' WHERE num_active > 0 '; #XXX doesn't affect count...
65 if ( $cgi->param('recurring') ) {
66 $hash{'freq'} = { op=>'!=', value=>'0' };
67 $extra_count = " freq != '0' ";
71 if ( $cgi->param('classnum') =~ /^(\d+)$/ ) {
73 push @where, $classnum ? "classnum = $classnum"
76 $cgi->delete('classnum');
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
86 if ( $cgi->param('family') =~ /^(\d+)$/ ) {
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');
96 push @where, FS::part_pkg->curuser_pkgs_sql
97 unless $acl_edit_global;
99 my $extra_sql = scalar(@where)
100 ? ( scalar(keys %hash) ? ' AND ' : ' WHERE ' ).
101 join( 'AND ', @where)
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
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,
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 )
128 AND ( cancel IS NULL OR cancel = 0 )
129 AND susp IS NOT NULL AND susp != 0
133 AND cancel IS NOT NULL AND cancel != 0
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>
145 !.include('/elements/select-part_pkg.html', 'element_name' => 'clone' ). qq!
146 <INPUT TYPE="submit" VALUE="Clone existing package">
151 $cgi->param('dummy', 1);
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".
161 #restore this so pagination works
162 $cgi->param('classnum', $classnum) if length($classnum);
164 #should hide this if there aren't any classes
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',
173 'disable_empty' => 1,
177 my $recur_toggle = $cgi->param('recurring') ? 'show' : 'hide';
178 $cgi->param('recurring', $cgi->param('recurring') ^ 1 );
181 '( <A HREF="'. $cgi->self_url.'">'. "$recur_toggle one-time charges</A> )";
183 $cgi->param('recurring', $cgi->param('recurring') ^ 1 ); #put it back
187 my $link = [ $p.'edit/part_pkg.cgi?', 'pkgpart' ];
189 my @header = ( '#', 'Package', 'Comment', 'Custom' );
190 my @fields = ( 'pkgpart', 'pkg', 'comment',
191 sub{ '<B><FONT COLOR="#0000CC">'.$_[0]->custom.'</FONT></B>' }
194 my @links = ( $link, $link, '', '' );
196 unless ( 0 ) { #already showing only one class or something?
197 push @header, 'Class';
198 push @fields, sub { shift->classname || '(none)'; };
202 if ( $conf->exists('pkg-addon_classnum') ) {
203 push @header, "Add'l order class";
204 push @fields, sub { shift->addon_classname || '(none)'; };
208 tie my %plans, 'Tie::IxHash', %{ FS::part_pkg::plan_info() };
210 tie my %plan_labels, 'Tie::IxHash',
211 map { $_ => ( $plans{$_}->{'shortname'} || $plans{$_}->{'name'} ) }
214 push @header, 'Pricing';
217 my $part_pkg = shift;
218 (my $plan = $plan_labels{$part_pkg->plan} ) =~ s/ / /g;
219 my $is_recur = ( $part_pkg->freq ne '0' );
220 my @discounts = sort { $a->months <=> $b->months }
222 $part_pkg->part_pkg_discount;
225 ( !$family_pkgpart &&
226 $part_pkg->pkgpart == $part_pkg->family_pkgpart ? () : [
231 'data' => '<b>Show all versions</b>',
232 'link' => $p.'browse/part_pkg.cgi?family='.$part_pkg->family_pkgpart,
242 { data =>$money_char.
243 sprintf('%.2f', $part_pkg->option('setup_fee') ),
246 { data => ( ( $is_recur ? ' setup' : ' one-time' ).
247 ( $part_pkg->option('recur_fee') == 0
248 && $part_pkg->setup_show_zero
249 ? ' (printed on invoices)'
259 ? $money_char. sprintf('%.2f ', $part_pkg->option('recur_fee'))
260 : $part_pkg->freq_pretty
262 align=> ( $is_recur ? 'right' : 'center' ),
263 colspan=> ( $is_recur ? 1 : 2 ),
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)'
279 ( map { my $dst_pkg = $_->dst_pkg;
281 { data => 'Supplemental: '.
282 '<A HREF="#'. $dst_pkg->pkgpart . '">' .
283 $dst_pkg->pkg . '</A>',
289 $part_pkg->supp_part_pkg_link
292 my $dst_pkg = $_->dst_pkg;
294 { data => 'Add-on: '.$dst_pkg->pkg_comment,
300 $part_pkg->bill_part_pkg_link
304 { data => '<b>Discounts</b>',
314 { data => $_->months. ':',
317 { data => $_->amount ? '$'. $_->amount : $_->percent. '%'
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') )
332 # $part_pkg->freq_pretty; #.'<BR>'
336 # Agent goes here if displayed
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;
348 map { my $agent_type = $_->agent_type;
350 { 'data' => $agent_type->atype, #escape?
352 'link' => ( $acl_config
367 #if ( $cgi->param('active') ) {
368 push @header, 'Customer<BR>packages';
370 'not yet billed' => '009999', #teal? cyan?
371 'active' => '00CC00',
372 'suspended' => 'FF9900',
373 'cancelled' => 'FF0000',
374 #'one-time charge' => '000000',
375 'charge' => '000000',
377 my $cust_pkg_link = $p. 'search/cust_pkg.cgi?pkgpart=';
378 push @fields, sub { my $part_pkg = shift;
383 if ( $magic eq 'active' && $part_pkg->freq == 0 ) {
385 #$label = 'one-time charge',
388 $label= 'not yet billed' if $magic eq 'not_yet_billed';
392 'data' => '<B><FONT COLOR="#'. $col{$label}. '">'.
393 $part_pkg->get("num_$_").
399 ( $part_pkg->get("num_$_") != 1
400 && $label =~ /charge$/
405 'link' => ( $part_pkg->get("num_$_")
413 } (qw( not_yet_billed active suspended cancelled ))
417 { 'data' => '<FONT SIZE="-1">[ '.
418 include('/elements/popup_link.html',
420 'action' => "${p}edit/bulk-cust_pkg.html?".
421 'pkgpart='.$part_pkg->pkgpart,
422 'actionlabel' => 'Change Packages',
435 push @header, 'Taxclass';
436 push @fields, sub { shift->taxclass() || ' '; };
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 => '' });
444 push @header, 'Plan options',
446 #'Service', 'Quan', 'Primary';
450 my $part_pkg = shift;
451 if ( $part_pkg->plan ) {
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?)
459 map { $report_optionname_name{$_} }
461 and exists($report_optionname_name{$_}) }
470 { 'data' => $part_pkg->format($_,$options{$_}),
475 grep { $options{$_} =~ /\S/ }
476 grep { $_ !~ /^(setup|recur)_fee$/
477 and $_ !~ /^report_option_\d+$/ }
480 if ( @report_options ) {
482 [ { 'data' => 'Report classes',
484 'style' => 'font-weight: bold',
487 foreach (@report_options) {
494 } # foreach @report_options
495 } # if @report_options
499 } else { # should never happen...
506 'data' => $part_pkg->$_(),
519 my $part_pkg = shift;
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>";
529 $svc =~ s/ +/ /g;
533 'data' => '<B>'. $pkg_svc->quantity. '</B>',
539 'link' => ( $acl_config
540 ? $p. 'edit/part_svc.cgi?'.
547 sort { $b->primary_svc =~ /^Y/i
548 <=> $a->primary_svc =~ /^Y/i
550 $part_pkg->pkg_svc('disable_linked'=>1)
553 my $dst_pkg = $_->dst_pkg;
555 { data => 'Add-on: '.$dst_pkg->pkg_comment,
561 $part_pkg->svc_part_pkg_link
567 $align .= 'lrl'; #rr';
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
575 my $count_query = "SELECT COUNT(*) FROM part_pkg $count_extra_sql $extra_count";
579 if ( $acl_edit_bulk ) {
580 # insert a checkbox column
583 '<INPUT TYPE="checkbox" NAME="pkgpart" VALUE=' . $_[0]->pkgpart .'>';
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