X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;ds=sidebyside;f=httemplate%2Fbrowse%2Fpart_pkg.cgi;h=1e8b510306dd8f6e034803a731da8b8dc7e819f9;hb=cd254e04ba204b5d0b4a69b65c392fb175dd1e97;hp=886c6c8491ac82eb852a2a9d099c14304dfff7b0;hpb=9832f4d4086970d7612ff2a6facd797fa85d7814;p=freeside.git
diff --git a/httemplate/browse/part_pkg.cgi b/httemplate/browse/part_pkg.cgi
index 886c6c849..1e8b51030 100755
--- a/httemplate/browse/part_pkg.cgi
+++ b/httemplate/browse/part_pkg.cgi
@@ -1,14 +1,16 @@
<% include( 'elements/browse.html',
'title' => 'Package Definitions',
+ 'menubar' => \@menubar,
'html_init' => $html_init,
+ 'html_form' => $html_form,
'html_posttotal' => $html_posttotal,
'name' => 'package definitions',
'disableable' => 1,
- 'disabled_statuspos' => 3,
+ 'disabled_statuspos' => 4,
'agent_virt' => 1,
'agent_null_right' => [ $edit, $edit_global ],
'agent_null_right_link' => $edit_global,
- 'agent_pos' => 5,
+ 'agent_pos' => 7, #5?
'query' => { 'select' => $select,
'table' => 'part_pkg',
'hashref' => \%hash,
@@ -20,6 +22,9 @@
'fields' => \@fields,
'links' => \@links,
'align' => $align,
+ 'link_field' => 'pkgpart',
+ 'html_init' => $html_init,
+ 'html_foot' => $html_foot,
)
%>
<%init>
@@ -32,6 +37,8 @@ my $acl_edit = $curuser->access_right($edit);
my $acl_edit_global = $curuser->access_right($edit_global);
my $acl_config = $curuser->access_right('Configuration'); #to edit services
#and agent types
+ #and bulk change
+my $acl_edit_bulk = $curuser->access_right('Bulk edit package definitions');
die "access denied"
unless $acl_edit || $acl_edit_global;
@@ -44,6 +51,7 @@ my $select = '*';
my $orderby = 'pkgpart';
my %hash = ();
my $extra_count = '';
+my $family_pkgpart;
if ( $cgi->param('active') ) {
$orderby = 'num_active DESC';
@@ -57,7 +65,7 @@ my @where = ();
if ( $cgi->param('recurring') ) {
$hash{'freq'} = { op=>'!=', value=>'0' };
- $extra_count = ' freq != 0 ';
+ $extra_count = " freq != '0' ";
}
my $classnum = '';
@@ -68,12 +76,34 @@ if ( $cgi->param('classnum') =~ /^(\d+)$/ ) {
}
$cgi->delete('classnum');
+if ( $cgi->param('pkgpartbatch') =~ /^([\w\/\-\:\. ]+)$/ ) {
+ push @where, "pkgpartbatch = '$1' ";
+}
+
if ( $cgi->param('missing_recur_fee') ) {
- push @where, "0 = ( SELECT COUNT(*) FROM part_pkg_option
- WHERE optionname = 'recur_fee'
- AND part_pkg_option.pkgpart = part_pkg.pkgpart
- AND CAST ( optionvalue AS NUMERIC ) > 0
- )";
+ push @where, "NOT EXISTS ( SELECT 1 FROM part_pkg_option
+ WHERE optionname = 'recur_fee'
+ AND part_pkg_option.pkgpart = part_pkg.pkgpart
+ AND CAST( optionvalue AS NUMERIC ) > 0
+ )";
+}
+
+if ( $cgi->param('ratenum') =~ /^(\d+)$/ ) {
+ push @where, "EXISTS( SELECT 1 FROM part_pkg_option
+ WHERE optionname LIKE '%ratenum'
+ AND optionvalue = '$1'
+ AND part_pkg_option.pkgpart = part_pkg.pkgpart
+ )";
+}
+
+if ( $cgi->param('family') =~ /^(\d+)$/ ) {
+ $family_pkgpart = $1;
+ push @where, "family_pkgpart = $1";
+ # Hiding disabled or one-time charges and limiting by classnum aren't
+ # very useful in this mode, so all links should still refer back to the
+ # non-family-limited display.
+ $cgi->param('showdisabled', 1);
+ $cgi->delete('family');
}
push @where, FS::part_pkg->curuser_pkgs_sql
@@ -84,11 +114,19 @@ my $extra_sql = scalar(@where)
join( 'AND ', @where)
: '';
-my $agentnums = join(',', $curuser->agentnums);
+my $agentnums_sql = $curuser->agentnums_sql( 'table'=>'cust_main' );
my $count_cust_pkg = "
SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )
WHERE cust_pkg.pkgpart = part_pkg.pkgpart
- AND cust_main.agentnum IN ($agentnums)
+ AND $agentnums_sql
+";
+my $count_cust_pkg_cancel = "
+ SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )
+ LEFT JOIN cust_pkg AS cust_pkg_next
+ ON (cust_pkg.pkgnum = cust_pkg_next.change_pkgnum)
+ WHERE cust_pkg.pkgpart = part_pkg.pkgpart
+ AND $agentnums_sql
+ AND cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0
";
$select = "
@@ -96,6 +134,13 @@ $select = "
*,
( $count_cust_pkg
+ AND ( setup IS NULL OR setup = 0 )
+ AND ( cancel IS NULL OR cancel = 0 )
+ AND ( susp IS NULL OR susp = 0 )
+ ) AS num_not_yet_billed,
+
+ ( $count_cust_pkg
+ AND setup IS NOT NULL AND setup != 0
AND ( cancel IS NULL OR cancel = 0 )
AND ( susp IS NULL OR susp = 0 )
) AS num_active,
@@ -103,21 +148,31 @@ $select = "
( $count_cust_pkg
AND ( cancel IS NULL OR cancel = 0 )
AND susp IS NOT NULL AND susp != 0
+ AND setup IS NOT NULL AND setup != 0
) AS num_suspended,
( $count_cust_pkg
- AND cancel IS NOT NULL AND cancel != 0
+ AND ( cancel IS NULL OR cancel = 0 )
+ AND susp IS NOT NULL AND susp != 0
+ AND ( setup IS NULL OR setup = 0 )
+ ) AS num_on_hold,
+
+ ( $count_cust_pkg_cancel
+ AND (cust_pkg_next.pkgnum IS NULL
+ OR cust_pkg_next.pkgpart != cust_pkg.pkgpart)
) AS num_cancelled
";
+# About the num_cancelled expression: packages that were changed, but
+# kept the same pkgpart, are considered "moved", not "canceled" (because
+# this is the part_pkg UI). We could show the count of those but it's
+# probably not interesting.
-my $html_init;
-#unless ( $cgi->param('active') ) {
- $html_init = qq!
+my $html_init = qq!
One or more service definitions are grouped together into a package
definition and given pricing information. Customers purchase packages
rather than purchase services directly.
-
!;
-#}
$cgi->param('dummy', 1);
@@ -140,6 +194,7 @@ my $filter_change =
#restore this so pagination works
$cgi->param('classnum', $classnum) if length($classnum);
+#should hide this if there aren't any classes
my $html_posttotal =
"$filter_change\n
( show class: ".
include('/elements/select-pkg_class.html',
@@ -152,14 +207,24 @@ my $html_posttotal =
).
' )';
+my $recur_toggle = $cgi->param('recurring') ? 'show' : 'hide';
+$cgi->param('recurring', $cgi->param('recurring') ^ 1 );
+
+$html_posttotal .=
+ '( '. "$recur_toggle one-time charges )";
+
+$cgi->param('recurring', $cgi->param('recurring') ^ 1 ); #put it back
+
# ------
my $link = [ $p.'edit/part_pkg.cgi?', 'pkgpart' ];
-my @header = ( '#', 'Package', 'Comment' );
-my @fields = ( 'pkgpart', 'pkg', 'comment' );
-my $align = 'rll';
-my @links = ( $link, $link, '' );
+my @header = ( '#', 'Package', 'Comment', 'Custom' );
+my @fields = ( 'pkgpart', 'pkg', 'comment',
+ sub{ ''.$_[0]->custom.'' }
+ );
+my $align = 'rllc';
+my @links = ( $link, $link, '', '' );
unless ( 0 ) { #already showing only one class or something?
push @header, 'Class';
@@ -167,6 +232,12 @@ unless ( 0 ) { #already showing only one class or something?
$align .= 'l';
}
+if ( $conf->exists('pkg-addon_classnum') ) {
+ push @header, "Add'l order class";
+ push @fields, sub { shift->addon_classname || '(none)'; };
+ $align .= 'l';
+}
+
tie my %plans, 'Tie::IxHash', %{ FS::part_pkg::plan_info() };
tie my %plan_labels, 'Tie::IxHash',
@@ -179,39 +250,131 @@ push @fields, sub {
my $part_pkg = shift;
(my $plan = $plan_labels{$part_pkg->plan} ) =~ s/ / /g;
my $is_recur = ( $part_pkg->freq ne '0' );
+ my @discounts = sort { $a->months <=> $b->months }
+ map { $_->discount }
+ $part_pkg->part_pkg_discount;
[
- [
+ # Line 0: Family package link (if applicable)
+ ( !$family_pkgpart &&
+ $part_pkg->pkgpart == $part_pkg->family_pkgpart ? () : [
+ {
+ 'align'=> 'center',
+ 'colspan' => 2,
+ 'size' => '-1',
+ 'data' => 'Show all versions',
+ 'link' => $p.'browse/part_pkg.cgi?family='.$part_pkg->family_pkgpart,
+ }
+ ] ),
+ [ # Line 1: Plan type (Anniversary, Prorate, Call Rating, etc.)
{ data =>$plan,
align=>'center',
colspan=>2,
},
],
- [
+ [ # Line 2: Setup fee
{ data =>$money_char.
- sprintf('%.2f', $part_pkg->option('setup_fee') ),
+ sprintf('%.2f ', $part_pkg->option('setup_fee') ),
align=>'right'
},
- { data => ( $is_recur ? ' setup' : ' one-time' ),
+ { data => ( ( $is_recur ? ' setup' : ' one-time' ).
+ ( $part_pkg->option('recur_fee') == 0
+ && $part_pkg->setup_show_zero
+ ? ' (printed on invoices)'
+ : ''
+ )
+ ),
align=>'left',
},
],
- [
- { data=>( $is_recur
- ? $money_char.sprintf('%.2f ', $part_pkg->option('recur_fee') )
- : $part_pkg->freq_pretty
- ),
+ [ # Line 3: Recurring fee
+ { data=>(
+ $is_recur
+ ? $money_char. sprintf('%.2f', $part_pkg->option('recur_fee'))
+ : $part_pkg->freq_pretty
+ ),
align=> ( $is_recur ? 'right' : 'center' ),
colspan=> ( $is_recur ? 1 : 2 ),
},
( $is_recur
- ? { data => ( $is_recur ? $part_pkg->freq_pretty : '' ),
+ ? { data => ' '. $part_pkg->freq_pretty.
+ ( $part_pkg->option('recur_fee') == 0
+ && $part_pkg->recur_show_zero
+ ? ' (printed on invoices)'
+ : ''
+ ),
align=>'left',
}
: ()
),
],
- ( map {
+ [ { data => ' ' }, ], # Line 4: empty
+ ( $part_pkg->adjourn_months ?
+ [ # Line 5: Adjourn months
+ { data => mt('After [quant,_1,month], suspend the package.',
+ $part_pkg->adjourn_months),
+ align => 'left',
+ size => -1,
+ colspan => 2,
+ }
+ ] : ()
+ ),
+ ( $part_pkg->contract_end_months ?
+ [ # Line 6: Contract end months
+ { data => mt('After [quant,_1,month], contract ends.',
+ $part_pkg->contract_end_months),
+ align => 'left',
+ size => -1,
+ colspan => 2,
+ }
+ ] : ()
+ ),
+ ( $part_pkg->expire_months ?
+ [ # Line 7: Expire months and automatic transfer
+ { data => $part_pkg->change_to_pkgpart ?
+ mt('After [quant,_1,month], change to ',
+ $part_pkg->expire_months) .
+ qq() . $part_pkg->change_to_pkg->pkg . qq() . '.'
+ : mt('After [quant,_1,month], cancel the package.',
+ $part_pkg->expire_months)
+ ,
+ align => 'left',
+ size => -1,
+ colspan => 2,
+ }
+ ] : ()
+ ),
+ ( # Usage prices
+ map { my $amount = $_->amount / ($_->target_info->{multiplier} || 1);
+ my $label = $_->target_info->{label};
+ [
+ { data => "Plus $money_char". $_->price. ' '.
+ ( $_->action eq 'increment' ? 'per' : 'for' ).
+ " $amount $label",
+ align => 'center', #left?
+ colspan => 2,
+ },
+ ];
+ }
+ $part_pkg->part_pkg_usageprice
+ ),
+ ( # Supplementals
+ map { my $dst_pkg = $_->dst_pkg;
+ [
+ { data => 'Supplemental: '.
+ '' .
+ $dst_pkg->pkg . '',
+ align=> 'center',
+ colspan => 2,
+ }
+ ]
+ }
+ $part_pkg->supp_part_pkg_link
+ ),
+ ( # Billing add-ons/bundle packages
+ map {
my $dst_pkg = $_->dst_pkg;
[
{ data => 'Add-on: '.$dst_pkg->pkg_comment,
@@ -222,7 +385,30 @@ push @fields, sub {
}
$part_pkg->bill_part_pkg_link
),
- ];
+ ( # Discounts available
+ scalar(@discounts)
+ ? [
+ { data => 'Discounts',
+ align=>'center', #?
+ colspan=>2,
+ }
+ ]
+ : ()
+ ),
+ ( scalar(@discounts)
+ ? map {
+ [
+ { data => $_->months. ':',
+ align => 'right',
+ },
+ { data => $_->amount ? '$'. $_->amount : $_->percent. '%'
+ }
+ ]
+ }
+ @discounts
+ : ()
+ ),
+ ]; # end of "middle column"
# $plan_labels{$part_pkg->plan}.'
'.
# $money_char.sprintf('%.2f setup
', $part_pkg->option('setup_fee') ).
@@ -233,6 +419,51 @@ push @fields, sub {
# $part_pkg->freq_pretty; #.'
'
};
+push @header, 'Cost tracking';
+$align .= 'r'; #?
+push @fields, sub {
+ my $part_pkg = shift;
+ #(my $plan = $plan_labels{$part_pkg->plan} ) =~ s/ / /g;
+ my $is_recur = ( $part_pkg->freq ne '0' );
+
+ [
+ [
+ { data => ' ', # $plan,
+ align=>'center',
+ colspan=>2,
+ },
+ ],
+ [
+ { data =>$money_char.
+ sprintf('%.2f ', $part_pkg->setup_cost ),
+ align=>'right'
+ },
+ { data => ( $is_recur ? ' setup' : ' one-time' ),
+ align=>'left',
+ },
+ ],
+ [
+ { data=>(
+ $is_recur
+ ? $money_char. sprintf('%.2f', $part_pkg->recur_cost)
+ : '(no recurring)' #$part_pkg->freq_pretty
+ ),
+ align=> ( $is_recur ? 'right' : 'center' ),
+ colspan=> ( $is_recur ? 1 : 2 ),
+ },
+ ( $is_recur
+ ? { data => ( $is_recur
+ ? ' '. $part_pkg->freq_pretty
+ : ''
+ ),
+ align=>'left',
+ }
+ : ()
+ ),
+ ],
+ ];
+};
+
###
# Agent goes here if displayed
###
@@ -240,7 +471,8 @@ push @fields, sub {
#agent type
if ( $acl_edit_global ) {
#really we just want a count, but this is fine unless someone has tons
- my @all_agent_types = map {$_->typenum} qsearch('agent_type',{});
+ my @all_agent_types = map {$_->typenum}
+ qsearch('agent_type', { 'disabled'=>'' });
if ( scalar(@all_agent_types) > 1 ) {
push @header, 'Agent types';
my $typelink = $p. 'edit/agent_type.cgi?';
@@ -267,24 +499,20 @@ if ( $acl_edit_global ) {
#if ( $cgi->param('active') ) {
push @header, 'Customer
packages';
- my %col = (
- 'active' => '00CC00',
- 'suspended' => 'FF9900',
- 'cancelled' => 'FF0000',
- #'one-time charge' => '000000',
- 'charge' => '000000',
- );
+ my %col = %{ FS::cust_pkg->statuscolors };
my $cust_pkg_link = $p. 'search/cust_pkg.cgi?pkgpart=';
push @fields, sub { my $part_pkg = shift;
- [
- map {
+ [
+ map( {
my $magic = $_;
my $label = $_;
if ( $magic eq 'active' && $part_pkg->freq == 0 ) {
$magic = 'inactive';
- #$label = 'one-time charge',
- $label = 'charge',
+ #$label = 'one-time charge';
+ $label = 'charge';
}
+ $label= 'not yet billed' if $magic eq 'not_yet_billed';
+ $label= 'on hold' if $magic eq 'on_hold';
[
{
@@ -309,8 +537,24 @@ if ( $acl_edit_global ) {
),
},
],
- } (qw( active suspended cancelled ))
- ]; };
+ } (qw( on_hold not_yet_billed active suspended cancelled ))
+ ),
+ ($acl_config ?
+ [ {},
+ { 'data' => '[ '.
+ include('/elements/popup_link.html',
+ 'label' => 'change',
+ 'action' => "${p}edit/bulk-cust_pkg.html?".
+ 'pkgpart='.$part_pkg->pkgpart,
+ 'actionlabel' => 'Change Packages',
+ 'width' => 569,
+ 'height' => 210,
+ ).' ]',
+ 'align' => 'left',
+ }
+ ] : () ),
+ ];
+ };
$align .= 'r';
#}
@@ -320,6 +564,10 @@ if ( $taxclasses ) {
$align .= 'l';
}
+# make a table of report class optionnames => the actual
+my %report_optionname_name = map { 'report_option_'.$_->num, $_->name }
+ qsearch('part_pkg_report_option', { disabled => '' });
+
push @header, 'Plan options',
'Services';
#'Service', 'Quan', 'Primary';
@@ -330,10 +578,20 @@ push @fields,
if ( $part_pkg->plan ) {
my %options = $part_pkg->options;
-
- [ map {
+ # gather any options that are really report options,
+ # convert them to their user-friendly names,
+ # and sort them (I think?)
+ my @report_options =
+ sort { $a cmp $b }
+ map { $report_optionname_name{$_} }
+ grep { $options{$_}
+ and exists($report_optionname_name{$_}) }
+ keys %options;
+
+ my @rows = (
+ map {
[
- { 'data' => $_,
+ { 'data' => "$_: ",
'align' => 'right',
},
{ 'data' => $part_pkg->format($_,$options{$_}),
@@ -341,12 +599,32 @@ push @fields,
},
];
}
+ sort
grep { $options{$_} =~ /\S/ }
- grep { $_ !~ /^(setup|recur)_fee$/ }
+ grep { $_ !~ /^(setup|recur)_fee$/
+ and $_ !~ /^report_option_\d+$/ }
keys %options
- ];
+ );
+ if ( @report_options ) {
+ push @rows,
+ [ { 'data' => 'Report classes',
+ 'align' => 'center',
+ 'style' => 'font-weight: bold',
+ 'colspan' => 2
+ } ];
+ foreach (@report_options) {
+ push @rows, [
+ { 'data' => $_,
+ 'align' => 'center',
+ 'colspan' => 2
+ }
+ ];
+ } # foreach @report_options
+ } # if @report_options
+
+ return \@rows;
- } else {
+ } else { # should never happen...
[ map { [
{ 'data' => uc($_),
@@ -367,6 +645,8 @@ push @fields,
sub {
my $part_pkg = shift;
+ my @part_pkg_usage = sort { $a->priority <=> $b->priority }
+ $part_pkg->part_pkg_usage;
[
(map {
@@ -409,7 +689,27 @@ push @fields,
]
}
$part_pkg->svc_part_pkg_link
- )
+ ),
+ ( scalar(@part_pkg_usage) ?
+ [ { data => 'Usage minutes',
+ align => 'center',
+ colspan => 2,
+ data_style => 'b',
+ link => $p.'browse/part_pkg_usage.html#pkgpart'.
+ $part_pkg->pkgpart
+ } ]
+ : ()
+ ),
+ ( map {
+ [ { data => $_->minutes,
+ align => 'right'
+ },
+ { data => $_->description,
+ align => 'left'
+ },
+ ]
+ } @part_pkg_usage
+ ),
];
};
@@ -424,4 +724,25 @@ $extra_count = ( $count_extra_sql ? ' AND ' : ' WHERE ' ). $extra_count
if $extra_count;
my $count_query = "SELECT COUNT(*) FROM part_pkg $count_extra_sql $extra_count";
+my $html_form = '';
+my $html_foot = '';
+if ( $acl_edit_bulk ) {
+ # insert a checkbox column
+ push @header, '';
+ push @fields, sub {
+ '';
+ };
+ push @links, '';
+ $align .= 'c';
+ $html_form = qq!';
+}
+
+my @menubar;
+# show this if there are any voip_cdr packages defined
+if ( FS::part_pkg->count("plan = 'voip_cdr'") ) {
+ push @menubar, 'Per-package usage minutes' => $p.'browse/part_pkg_usage.html';
+}
%init>