summaryrefslogtreecommitdiff
path: root/httemplate/graph
diff options
context:
space:
mode:
Diffstat (limited to 'httemplate/graph')
-rw-r--r--httemplate/graph/cust_bill_pkg.cgi151
-rw-r--r--httemplate/graph/cust_bill_pkg_detail.cgi137
-rw-r--r--httemplate/graph/cust_pkg.cgi63
-rw-r--r--httemplate/graph/cust_pkg_cost.cgi61
-rw-r--r--httemplate/graph/elements/monthly.html139
-rw-r--r--httemplate/graph/elements/report.html296
-rw-r--r--httemplate/graph/money_time.cgi98
-rw-r--r--httemplate/graph/report_cust_bill_pkg.html57
-rw-r--r--httemplate/graph/report_cust_bill_pkg_detail.html48
-rw-r--r--httemplate/graph/report_cust_pkg.html28
-rw-r--r--httemplate/graph/report_cust_pkg_cost.html26
-rw-r--r--httemplate/graph/report_money_time.html43
-rw-r--r--httemplate/graph/report_signupdate.html30
-rw-r--r--httemplate/graph/signupdate.cgi60
14 files changed, 1237 insertions, 0 deletions
diff --git a/httemplate/graph/cust_bill_pkg.cgi b/httemplate/graph/cust_bill_pkg.cgi
new file mode 100644
index 000000000..03e29b901
--- /dev/null
+++ b/httemplate/graph/cust_bill_pkg.cgi
@@ -0,0 +1,151 @@
+<% include('elements/monthly.html',
+ #Dumper(
+ 'title' => $title,
+ 'graph_type' => 'Mountain',
+ 'items' => \@items,
+ 'params' => \@params,
+ 'labels' => \@labels,
+ 'graph_labels' => \@labels,
+ 'colors' => \@colors,
+ 'links' => \@links,
+ 'remove_empty' => 1,
+ 'bottom_total' => 1,
+ 'bottom_link' => $bottom_link,
+ 'agentnum' => $agentnum,
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+my $link = "${p}search/cust_bill_pkg.cgi?nottax=1";
+my $bottom_link = "$link;";
+
+my $use_override = $cgi->param('use_override') ? 1 : 0;
+my $use_usage = $cgi->param('use_usage') ? 1 : 0;
+my $average_per_cust_pkg = $cgi->param('average_per_cust_pkg') ? 1 : 0;
+
+#XXX or virtual
+my( $agentnum, $sel_agent, $all_agent ) = ('', '', '');
+if ( $cgi->param('agentnum') eq 'all' ) {
+ $agentnum = 0;
+ $all_agent = 'ALL';
+}
+elsif ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+ $bottom_link .= "agentnum=$agentnum;";
+ $sel_agent = qsearchs('agent', { 'agentnum' => $agentnum } );
+ die "agentnum $agentnum not found!" unless $sel_agent;
+}
+my $title = $sel_agent ? $sel_agent->agent.' ' : '';
+$title .= 'Sales Report (Gross)';
+$title .= ', average per customer package' if $average_per_cust_pkg;
+
+#classnum (here)
+# 0: all classes
+# not specified: empty class
+# N: classnum
+#classnum (link)
+# not specified: all classes
+# 0: empty class
+# N: classnum
+
+#false lazinessish w/FS::cust_pkg::search_sql (previously search/cust_pkg.cgi)
+my $classnum = 0;
+my @pkg_class = ();
+my $all_class = '';
+if ( $cgi->param('classnum') eq 'all' ) {
+ $all_class = 'ALL';
+ @pkg_class = ('');
+}
+elsif ( $cgi->param('classnum') =~ /^(\d*)$/ ) {
+ $classnum = $1;
+ if ( $classnum ) { #a specific class
+
+ @pkg_class = ( qsearchs('pkg_class', { 'classnum' => $classnum } ) );
+ die "classnum $classnum not found!" unless $pkg_class[0];
+ $title .= ' '.$pkg_class[0]->classname.' ';
+ $bottom_link .= "classnum=$classnum;";
+
+ } elsif ( $classnum eq '' ) { #the empty class
+
+ $title .= 'Empty class ';
+ @pkg_class = ( '(empty class)' );
+ $bottom_link .= "classnum=0;";
+
+ } elsif ( $classnum eq '0' ) { #all classes
+
+ @pkg_class = qsearch('pkg_class', {} ); # { 'disabled' => '' } );
+ push @pkg_class, '(empty class)';
+
+ }
+}
+#eslaf
+
+my $hue = 0;
+#my $hue_increment = 170;
+#my $hue_increment = 145;
+my $hue_increment = 125;
+
+my @items = ();
+my @params = ();
+my @labels = ();
+my @colors = ();
+my @links = ();
+
+foreach my $agent ( $all_agent || $sel_agent || qsearch('agent', { 'disabled' => '' } ) ) {
+
+ my $col_scheme = Color::Scheme->new
+ ->from_hue($hue) #->from_hex($agent->color)
+ ->scheme('analogic')
+ ;
+ my @recur_colors = ();
+ my @onetime_colors = ();
+
+ ### fixup the color handling for package classes...
+ ### and usage
+ my $n = 0;
+
+ foreach my $pkg_class ( @pkg_class ) {
+ foreach my $component ( $use_usage ? ('recurring', 'usage') : ('') ) {
+
+ push @items, 'cust_bill_pkg';
+
+ push @labels,
+ ( $all_agent || $sel_agent ? '' : $agent->agent.' ' ).
+ ( $classnum eq '0'
+ ? ( ref($pkg_class) ? $pkg_class->classname : $pkg_class )
+ : ''
+ ).
+ " $component";
+
+ my $row_classnum = ref($pkg_class) ? $pkg_class->classnum : 0;
+ my $row_agentnum = $all_agent || $agent->agentnum;
+ push @params, [ ($all_class ? () : ('classnum' => $row_classnum) ),
+ ($all_agent ? () : ('agentnum' => $row_agentnum) ),
+ 'use_override' => $use_override,
+ 'use_usage' => $component,
+ 'average_per_cust_pkg' => $average_per_cust_pkg,
+ ];
+
+ push @links, "$link;".($all_agent ? '' : "agentnum=$row_agentnum;").
+ ($all_class ? '' : "classnum=$row_classnum;").
+ "use_override=$use_override;use_usage=$component;";
+
+ @recur_colors = ($col_scheme->colors)[0,4,8,1,5,9]
+ unless @recur_colors;
+ @onetime_colors = ($col_scheme->colors)[2,6,10,3,7,11]
+ unless @onetime_colors;
+ push @colors, shift @recur_colors;
+
+ }
+ }
+
+ $hue += $hue_increment;
+
+}
+
+#use Data::Dumper;
+
+</%init>
diff --git a/httemplate/graph/cust_bill_pkg_detail.cgi b/httemplate/graph/cust_bill_pkg_detail.cgi
new file mode 100644
index 000000000..642a9ecf3
--- /dev/null
+++ b/httemplate/graph/cust_bill_pkg_detail.cgi
@@ -0,0 +1,137 @@
+<% include('elements/monthly.html',
+ 'title' => $title. 'Rated Call Sales Report (Gross)',
+ 'graph_type' => 'Mountain',
+ 'items' => \@items,
+ 'params' => \@params,
+ 'labels' => \@labels,
+ 'graph_labels' => \@labels,
+ 'colors' => \@colors,
+ 'remove_empty' => 1,
+ 'bottom_total' => 1,
+ 'agentnum' => $agentnum,
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+#XXX or virtual
+my( $agentnum, $sel_agent ) = ('', '');
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+ $sel_agent = qsearchs('agent', { 'agentnum' => $agentnum } );
+ die "agentnum $agentnum not found!" unless $sel_agent;
+}
+my $title = $sel_agent ? $sel_agent->agent.' ' : '';
+
+#false lazinessish w/FS::cust_pkg::search_sql (previously search/cust_pkg.cgi)
+my $classnum = '';
+if ( $cgi->param('classnum') =~ /^(\d*)$/ ) {
+ $classnum = $1;
+
+ if ( $classnum ) { #a specific class
+
+ my $pkg_class = ( qsearchs('pkg_class', { 'classnum' => $classnum } ) );
+ die "classnum $classnum not found!" unless $pkg_class;
+ $title .= $pkg_class->classname.' ';
+
+ } elsif ( $classnum eq '' ) { #the empty class
+
+ $title .= 'Empty class ';
+ # FS::Report::Table::Monthly.pm has the converse view
+ $classnum = 0;
+
+ } elsif ( $classnum eq '0' ) { #all classes
+
+ # FS::Report::Table::Monthly.pm has the converse view
+ $classnum = '';
+ }
+}
+#eslaf
+
+my $use_override = 0;
+$use_override = 1 if ( $cgi->param('use_override') );
+
+my $usageclass = 0;
+my @usage_class = ();
+if ( $cgi->param('usageclass') =~ /^(\d*)$/ ) {
+ $usageclass = $1;
+
+ if ( $usageclass ) { #a specific class
+
+ @usage_class = ( qsearchs('usage_class', { 'classnum' => $usageclass } ) );
+ die "usage class $usageclass not found!" unless $usage_class[0];
+ $title .= $usage_class[0]->classname.' ';
+
+ } elsif ( $usageclass eq '' ) { #the empty class -- legacy
+
+ $title .= 'Empty usage class ';
+ @usage_class = ( '(empty usage class)' );
+
+ } elsif ( $usageclass eq '0' ) { #all classes
+
+ @usage_class = qsearch('usage_class', {} ); # { 'disabled' => '' } );
+ push @usage_class, '(empty usage class)';
+
+ }
+}
+#eslaf
+
+my $hue = 0;
+#my $hue_increment = 170;
+#my $hue_increment = 145;
+my $hue_increment = 125;
+
+my @items = ();
+my @params = ();
+my @labels = ();
+my @colors = ();
+
+foreach my $agent ( $sel_agent || qsearch('agent', { 'disabled' => '' } ) ) {
+
+ my $col_scheme = Color::Scheme->new
+ ->from_hue($hue) #->from_hex($agent->color)
+ ->scheme('analogic')
+ ;
+ my @recur_colors = ();
+ my @onetime_colors = ();
+
+ ### fixup the color handling for usage classes...
+ my $n = 0;
+
+ foreach my $usage_class ( @usage_class ) {
+
+ push @items, 'cust_bill_pkg_detail';
+
+ push @labels,
+ ( $sel_agent ? '' : $agent->agent.' ' ).
+ ( $usageclass eq '0'
+ ? ( ref($usage_class) ? $usage_class->classname : $usage_class )
+ : ''
+ );
+
+ my $row_classnum = ref($usage_class) ? $usage_class->classnum : 0;
+ my $row_agentnum = $agent->agentnum;
+ push @params, [ 'usageclass' => $row_classnum,
+ 'agentnum' => $row_agentnum,
+ 'use_override' => $use_override,
+ 'classnum' => $classnum,
+ ];
+
+ @recur_colors = ($col_scheme->colors)[0,4,8,1,5,9]
+ unless @recur_colors;
+ @onetime_colors = ($col_scheme->colors)[2,6,10,3,7,11]
+ unless @onetime_colors;
+ push @colors, shift @recur_colors;
+
+ }
+
+ $hue += $hue_increment;
+
+}
+
+#use Data::Dumper;
+#warn Dumper(\@items);
+
+</%init>
diff --git a/httemplate/graph/cust_pkg.cgi b/httemplate/graph/cust_pkg.cgi
new file mode 100644
index 000000000..21ce07d21
--- /dev/null
+++ b/httemplate/graph/cust_pkg.cgi
@@ -0,0 +1,63 @@
+<% include('elements/monthly.html',
+ 'title' => $agentname. 'Package Churn',
+ 'items' => \@items,
+ 'labels' => \%label,
+ 'graph_labels' => \%graph_label,
+ 'colors' => \%color,
+ 'links' => \%link,
+ 'agentnum' => $agentnum,
+ 'sprintf' => '%u',
+ 'disable_money' => 1,
+ )
+%>
+<%init>
+
+#XXX use a different ACL for package churn?
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+#false laziness w/money_time.cgi, cust_bill_pkg.cgi
+
+#XXX or virtual
+my( $agentnum, $agent ) = ('', '');
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+ $agent = qsearchs('agent', { 'agentnum' => $agentnum } );
+ die "agentnum $agentnum not found!" unless $agent;
+}
+
+my $agentname = $agent ? $agent->agent.' ' : '';
+
+my @items = qw( setup_pkg susp_pkg cancel_pkg );
+
+my %label = (
+ 'setup_pkg' => 'New orders',
+ 'susp_pkg' => 'Suspensions',
+# 'unsusp' => 'Unsuspensions',
+ 'cancel_pkg' => 'Cancellations',
+);
+my %graph_label = %label;
+
+my %color = (
+ 'setup_pkg' => '00cc00', #green
+ 'susp_pkg' => 'ff9900', #yellow
+ #'unsusp' => '', #light green?
+ 'cancel_pkg' => 'cc0000', #red ? 'ff0000'
+);
+
+my %link = (
+ 'setup_pkg' => { 'link' => "${p}search/cust_pkg.cgi?agentnum=$agentnum;",
+ 'fromparam' => 'setup_begin',
+ 'toparam' => 'setup_end',
+ },
+ 'susp_pkg' => { 'link' => "${p}search/cust_pkg.cgi?agentnum=$agentnum;",
+ 'fromparam' => 'susp_begin',
+ 'toparam' => 'susp_end',
+ },
+ 'cancel_pkg' => { 'link' => "${p}search/cust_pkg.cgi?agentnum=$agentnum;",
+ 'fromparam' => 'cancel_begin',
+ 'toparam' => 'cancel_end',
+ },
+);
+
+</%init>
diff --git a/httemplate/graph/cust_pkg_cost.cgi b/httemplate/graph/cust_pkg_cost.cgi
new file mode 100644
index 000000000..0aa7e3262
--- /dev/null
+++ b/httemplate/graph/cust_pkg_cost.cgi
@@ -0,0 +1,61 @@
+<% include('elements/monthly.html',
+ 'title' => $agentname.
+ 'Package Costs Report',
+ 'graph_type' => 'Lines',
+ 'items' => \@items,
+ 'labels' => \%label,
+ 'graph_labels' => \%label,
+ 'colors' => \%color,
+ 'links' => \%link,
+ 'agentnum' => $agentnum,
+ 'nototal' => scalar($cgi->param('12mo')),
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+#XXX or virtual
+my( $agentnum, $agent ) = ('', '');
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+ $agent = qsearchs('agent', { 'agentnum' => $agentnum } );
+ die "agentnum $agentnum not found!" unless $agent;
+}
+
+my $agentname = $agent ? $agent->agent.' ' : '';
+
+my @items = qw( cust_pkg_setup_cost cust_pkg_recur_cost );
+if ( $cgi->param('12mo') == 1 ) {
+ @items = map $_.'_12mo', @items;
+}
+
+my %label = (
+ 'cust_pkg_setup_cost' => 'Setup Costs',
+ 'cust_pkg_recur_cost' => 'Recurring Costs',
+);
+
+$label{$_.'_12mo'} = $label{$_}. " (prev 12 months)"
+ foreach keys %label;
+
+my %color = (
+ 'cust_pkg_setup_cost' => '0000cc',
+ 'cust_pkg_recur_cost' => '00cc00',
+);
+$color{$_.'_12mo'} = $color{$_}
+ foreach keys %color;
+
+my %link = (
+ 'cust_pkg_setup_cost' => { 'link' => "${p}search/cust_pkg.cgi?agentnum=$agentnum;",
+ 'fromparam' => 'setup_begin',
+ 'toparam' => 'setup_end',
+ },
+ 'cust_pkg_recur_cost' => { 'link' => "${p}search/cust_pkg.cgi?agentnum=$agentnum;",
+ 'fromparam' => 'active_begin',
+ 'toparam' => 'active_end',
+ },
+);
+# XXX link 12mo?
+
+</%init>
diff --git a/httemplate/graph/elements/monthly.html b/httemplate/graph/elements/monthly.html
new file mode 100644
index 000000000..7b1b98a61
--- /dev/null
+++ b/httemplate/graph/elements/monthly.html
@@ -0,0 +1,139 @@
+<%doc>
+
+Example:
+
+ include('elements/monthly.html',
+ #required
+ 'title' => 'Page title',
+ 'items' => \@items,
+ 'labels' => \@labels, # or \%labels (keys are items)
+
+ #required?
+ 'colors' => \@colors, # or \%colors,
+
+ #recommended
+ 'graph_labels' => \@graph_labels, # or \%graph_labels,
+
+ #optional
+ 'params' => \@params, # opt,
+ 'links' => \@links, # or \%link, #opt
+ 'link_fromparam' => 'param_from', #defaults to 'begin'
+ 'link_toparam' => 'param_to', #defaults to 'end'
+
+ #optional, pulled from CGI params if not specified
+ 'start_month' => $smonth,
+ 'start_year' => $syear,
+ 'end_month' => $emonth,
+ 'end_year' => $eyear,
+
+ #optional
+ 'agentnum' => $agentnum,
+ 'nototal' => 1,
+ 'graph_type' => 'LinesPoints',
+ 'remove_empty' => 1,
+ 'bottom_total' => 1,
+ 'sprintf' => '%u', #sprintf format, overrides default %.2f
+ 'disable_money' => 1,
+ );
+
+</%doc>
+<% include('report.html',
+ 'items' => $data->{'items'},
+ 'data' => $data->{'data'},
+ 'row_labels' => $data->{'item_labels'},
+ 'graph_labels' => $opt{'graph_labels'} || $data->{'item_labels'},
+ 'col_labels' => [ map { my $m = $_; $m =~ s/^(\d+)\//$mon[$1-1] / ; $m }
+ @{$data->{label}} ],
+ 'axis_labels' => $data->{label},
+ 'colors' => $data->{colors},
+ 'links' => \@links,
+ 'bottom_link' => \@bottom_link,
+ map { $_, $opt{$_} } (qw(title
+ nototal
+ graph_type
+ bottom_total
+ sprintf
+ disable_money)),
+ ) %>
+<%init>
+
+my(%opt) = @_;
+
+my $conf = new FS::Conf;
+my $money_char = $opt{'disable_money'} ? '' : $conf->config('money_char');
+
+my $fromparam = $opt{'link_fromparam'} || 'begin';
+my $toparam = $opt{'link_toparam'} || 'end';
+
+my @items = @{ $opt{'items'} };
+
+foreach my $other (qw( labels graph_labels colors links )) {
+ if ( ref($opt{$other}) eq 'HASH' ) {
+ $opt{$other} = [ map $opt{$other}{$_}, @items ];
+ }
+}
+
+my @mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
+
+#find first month
+$opt{'start_month'} ||= $cgi->param('start_month'); # || $curmon+1;
+$opt{'start_year'} ||= $cgi->param('start_year'); # || 1899+$curyear;
+
+#find last month
+$opt{'end_month'} ||= $cgi->param('end_month'); # || $curmon+1;
+$opt{'end_year'} ||= $cgi->param('end_year'); # || 1900+$curyear;
+
+my $report = new FS::Report::Table::Monthly (
+
+ 'items' => \@items,
+ 'params' => $opt{'params'},
+ 'item_labels' => ( $cgi->param('_type') =~ /^(png)$/
+ ? $opt{'graph_labels'}
+ : $opt{'labels'}
+ ),
+ 'colors' => $opt{'colors'},
+ 'links' => $opt{'links'},
+
+ 'start_month' => $opt{'start_month'},
+ 'start_year' => $opt{'start_year'},
+ 'end_month' => $opt{'end_month'},
+ 'end_year' => $opt{'end_year'},
+
+ 'agentnum' => $opt{'agentnum'},
+ 'remove_empty' => $opt{'remove_empty'},
+);
+my $data = $report->data;
+
+my @links;
+foreach my $link (@{ $data->{'links'} }) {
+ my @speriod = @{$data->{'speriod'}};
+ my @eperiod = @{$data->{'eperiod'}};
+ my ($begin, $end) = ($fromparam, $toparam);
+
+ my @new = ( $link );
+ if(ref($link)) {
+ $begin = $link->{'fromparam'};
+ $end = $link->{'toparam'};
+ @new = ( $link->{'link'} );
+ }
+ while(@speriod) {
+ push @new, "$begin=". shift(@speriod).";$end=".shift(@eperiod);
+ }
+ if(! $opt{'nototal'}) {
+ push @new, "$begin=". $data->{'speriod'}[0] . ";$end=". $data->{'eperiod'}[-1];
+ }
+ push @links, \@new;
+}
+
+my @bottom_link;
+if($opt{'bottom_link'}) {
+ my @speriod = (@{$data->{'speriod'}}, $data->{'speriod'}[0]);
+ my @eperiod = (@{$data->{'eperiod'}}, $data->{'eperiod'}[-1]);
+
+ push @bottom_link, $opt{'bottom_link'};
+ while(@speriod) {
+ push @bottom_link, "$fromparam=". shift(@speriod). ";$toparam=". shift(@eperiod);
+ }
+}
+
+</%init>
diff --git a/httemplate/graph/elements/report.html b/httemplate/graph/elements/report.html
new file mode 100644
index 000000000..b831f3a4f
--- /dev/null
+++ b/httemplate/graph/elements/report.html
@@ -0,0 +1,296 @@
+<%doc>
+
+Example:
+
+ include('elements/report.html',
+ #required
+ 'title' => 'Page title',
+ 'items' => \@items,
+ 'data' => [ \@item1 \@item2 ... ],
+
+ #these run parallel to items, and can be given as hashes
+ 'row_labels' => \@row_labels, #required
+ 'colors' => \@colors, #required
+ 'graph_labels' => \@graph_labels, #defaults to row_labels
+
+ 'links' => \@links, #optional
+
+ #these run parallel to the elements of each @item
+ 'col_labels' => \@col_labels, #required
+ 'axis_labels' => \@axis_labels, #defaults to col_labels
+
+ #optional
+ 'nototal' => 1,
+ 'graph_type' => 'LinesPoints',
+ 'bottom_total' => 1,
+ 'sprintf' => '%u', #sprintf format, overrides default %.2f
+ 'disable_money' => 1,
+ );
+
+About @links: Each element must be an arrayref, corresponding to an element of
+@items. Within the array, the first element is a URL prefix, and the rest
+are suffixes corresponding to data elements. These will be joined without
+any delimiter and linked from the elements in @data.
+
+</%doc>
+% if ( $cgi->param('_type') =~ /^(csv)$/ ) {
+%
+% #http_header('Content-Type' => 'text/comma-separated-values' ); #IE chokes
+% #http_header('Content-Type' => 'text/plain' );
+% http_header('Content-Type' => 'text/csv');
+% http_header('Content-Disposition' => "attachment;filename=$filename.csv");
+%
+% my $csv = new Text::CSV_XS { 'always_quote' => 1,
+% 'eol' => "\n", #"\015\012", #"\012"
+% };
+%
+% $csv->combine('', @col_labels, $opt{'nototal'} ? () : 'Total');
+%
+<% $csv->string %>
+%
+% my @bottom_total = ();
+% foreach ( @items ) {
+%
+% my $col = 0;
+% my $total = 0;
+% $csv->combine(
+% shift( @row_labels ),
+% map { $total += $_; $bottom_total[$col++] += $_; sprintf($sprintf, $_); }
+% ( @{ shift( @data ) } ),
+% ( $opt{'nototal'} ? () : sprintf($sprintf, $total) ),
+% );
+% unless ( $opt{'nototal'} ) {
+% $bottom_total[$col++] += $total;
+% }
+<% $csv->string %>
+%
+% }
+%
+% if ( $opt{'bottom_total'} ) {
+% $csv->combine(
+% 'Total',
+% map { sprintf($sprintf, $_) } @bottom_total,
+% );
+%
+<% $csv->string %>
+%
+% }
+%
+% } elsif ( $cgi->param('_type') =~ /(xls)$/ ) {
+%
+% #http_header('Content-Type' => 'application/excel' ); #eww
+% http_header('Content-Type' => 'application/vnd.ms-excel' );
+% #http_header('Content-Type' => 'application/msexcel' ); #alas
+% http_header('Content-Disposition' => "attachment;filename=$filename.xls");
+%
+% my $output = '';
+% my $XLS = new IO::Scalar \$output;
+% my $workbook = Spreadsheet::WriteExcel->new($XLS)
+% or die "Error opening .xls file: $!";
+%
+% my $worksheet = $workbook->add_worksheet(substr($opt{'title'},0,31));
+%
+% my($r,$c) = (0,0);
+%
+% foreach ('', @col_labels, ($opt{'nototal'} ? () : 'Total') ) {
+% my $header = $_;
+% $worksheet->write($r, $c++, $header)
+% }
+%
+% my @bottom_total = ();
+% foreach ( @items ) {
+% $r++;
+% $c = 0;
+% my $total = 0;
+% $worksheet->write( $r, $c++, shift( @row_labels ) );
+% foreach ( @{ shift( @data ) } ) {
+% $total += $_;
+% $bottom_total[$c] += $_;
+% $worksheet->write($r, $c++, sprintf($sprintf, $_) );
+% }
+% unless ( $opt{'nototal'} ) {
+% $bottom_total[$c] += $total;
+% $worksheet->write($r, $c++, sprintf($sprintf, $total) );
+% }
+% }
+%
+% $c = 0;
+% if ( $opt{'bottom_total'} ) {
+% $r++;
+% $worksheet->write($r, $c++, 'Total');
+% $worksheet->write($r, $c++, sprintf($sprintf, $_)) foreach @bottom_total;
+% }
+%
+% $workbook->close();# or die "Error creating .xls file: $!";
+%
+% http_header('Content-Length' => length($output) );
+%
+<% $output %>
+% } elsif ( $cgi->param('_type') eq 'png' ) {
+%
+% my $graph_type = 'LinesPoints';
+% if ( $opt{'graph_type'} =~ /^(LinesPoints|Mountain|Bars)$/ ) {
+% $graph_type = $1;
+% }
+% my $class = "Chart::$graph_type";
+%
+% my $chart = $class->new(976,384);
+%
+% my $d = 0;
+% $chart->set(
+% #'min_val' => 0,
+% 'legend' => 'bottom',
+% 'colors' => { (
+% map { my $color = $_;
+% 'dataset'.$d++ =>
+% [ map hex($_), unpack 'a2a2a2', $color ]
+% }
+% @{ $opt{'colors'} }
+% ),
+% 'grey_background' => 'white',
+% 'background' => [ 0xe8, 0xe8, 0xe8 ], #grey
+% },
+% 'legend_labels' => $opt{'graph_labels'},
+% 'brush_size' => 4,
+% );
+%
+% http_header('Content-Type' => 'image/png' );
+%
+% $chart->_set_colors();
+%
+<% $chart->scalar_png([ $opt{'axis_labels'}, @data ]) %>
+%
+% } else {
+%
+<% include('/elements/header.html', $opt{'title'} ) %>
+% $cgi->param('_type', 'png');
+
+<IMG SRC="<% $cgi->self_url %>" WIDTH="976" HEIGHT="384">
+<P ALIGN="right">
+
+% unless ( $opt{'disable_download'} ) {
+% $cgi->param('_type', "xls" );
+ Download full results<BR>
+ as <A HREF="<% $cgi->self_url %>">Excel spreadsheet</A><BR>
+% $cgi->param('_type', 'csv');
+ as <A HREF="<% $cgi->self_url %>">CSV file</A></P>
+% $cgi->param('_type', "html" );
+% }
+%
+</P>
+<% include('/elements/table.html', 'f8f8f8') %>
+
+<TR>
+
+ <TD></TD>
+
+% foreach my $column ( @col_labels ) {
+% $column =~ s/ /\<BR\>/; # working on a smarter way to do this
+ <TH><% $column %></TH>
+% }
+
+% unless ( $opt{'nototal'} ) {
+ <TH>Total</TH>
+% }
+
+</TR>
+
+% my @bottom_total = ();
+% foreach my $row ( @items ) {
+%
+% my $color = shift( @{ $opt{'colors'} } );
+% my @links = @{ shift( @{ $opt{'links'} } ) };
+% # $opt{'links'} is an array parallel to items.
+% # Each element of that is an array containing a prefix,
+% # followed by suffixes matched to the cells of the table.
+% my $link_prefix = shift @links;
+% $link_prefix = $link_prefix ? qq(<A HREF="$link_prefix) : ''; #"
+% my $label = shift( @row_labels );
+
+ <TR>
+
+ <TH>
+ <FONT COLOR="#<% $color %>"><% $label %></FONT>
+ </TH>
+
+% my $total = 0;
+% my $col = 0;
+% foreach my $column ( @{ shift( @data ) } ) {
+
+ <TD ALIGN="right" BGCOLOR="#ffffff">
+ <% $link_prefix ? $link_prefix . shift(@links) . '">' : '' %><FONT COLOR="#<% $color %>"><% $money_char %><% sprintf($sprintf,, $column) %></FONT><% $link_prefix ? '</A>' : '' %>
+ </TD>
+%
+% $total += $column;
+% $bottom_total[$col++] += $column;
+%
+% }
+
+% unless ( $opt{'nototal'} ) {
+ <TD ALIGN="right" BGCOLOR="#f5f6be">
+ <% $link_prefix ? $link_prefix. shift(@links) . '">' : '' %><FONT COLOR="#<% $color %>"><% $money_char %><% sprintf($sprintf, $total) %></FONT><% $link_prefix ? '</A>' : '' %>
+ </TD>
+% $bottom_total[$col++] += $total;
+% }
+
+ </TR>
+
+% }
+
+% if ( $opt{'bottom_total'} ) {
+ <TR>
+ <TH>Total</TH>
+% my @bottom_links = $opt{'bottom_link'} ? @{ $opt{'bottom_link'} } : ();
+% my $prefix = shift(@bottom_links);
+% pop @bottom_links if $opt{'nototal'};
+% foreach my $total ( @bottom_total ) {
+
+ <TD ALIGN="right" BGCOLOR="#f5f6be">
+ <% $prefix
+ ? '<A HREF="'. $prefix .shift(@bottom_links). '">'
+ : ''
+ %><% $money_char %><% sprintf($sprintf, $total) %><% $prefix ? '</A>' : '' %>
+
+ </TD>
+
+% }
+
+ </TR>
+
+% }
+
+</TABLE>
+
+<% include('/elements/footer.html') %>
+% }
+<%once>
+
+</%once>
+<%init>
+
+my(%opt) = @_;
+
+my $sprintf = $opt{'sprintf'} || '%.2f';
+
+my $conf = new FS::Conf;
+my $money_char = $opt{'disable_money'} ? '' : $conf->config('money_char');
+
+my @items = @{ $opt{'items'} };
+
+foreach my $other (qw( col_labels row_labels graph_labels axis_labels colors links )) {
+ if ( ref($opt{$other}) eq 'HASH' ) {
+ $opt{$other} = [ map $opt{$other}{$_}, @items ];
+ }
+}
+
+my @col_labels = @{$opt{'col_labels'}};
+my @row_labels = @{$opt{'row_labels'}};
+my @data = @{$opt{'data'}};
+
+$opt{'axis_labels'} ||= $opt{'col_labels'};
+$opt{'graph_labels'} ||= $opt{'row_labels'};
+
+my $filename = $cgi->url(-relative => 1);
+$filename =~ s/\.(cgi|html)$//;
+
+</%init>
diff --git a/httemplate/graph/money_time.cgi b/httemplate/graph/money_time.cgi
new file mode 100644
index 000000000..cde71be76
--- /dev/null
+++ b/httemplate/graph/money_time.cgi
@@ -0,0 +1,98 @@
+<% include('elements/monthly.html',
+ 'title' => $agentname.
+ 'Sales, Credits and Receipts Summary',
+ 'items' => \@items,
+ 'labels' => \%label,
+ 'graph_labels' => \%graph_label,
+ 'colors' => \%color,
+ 'links' => \%link,
+ 'agentnum' => $agentnum,
+ 'nototal' => scalar($cgi->param('12mo')),
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+#XXX or virtual
+my( $agentnum, $agent ) = ('', '');
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+ $agent = qsearchs('agent', { 'agentnum' => $agentnum } );
+ die "agentnum $agentnum not found!" unless $agent;
+}
+
+my $agentname = $agent ? $agent->agent.' ' : '';
+
+my @items = qw( invoiced netsales
+ credits netcredits
+ payments receipts
+ refunds netrefunds
+ cashflow netcashflow
+ );
+if ( $cgi->param('12mo') == 1 ) {
+ @items = map $_.'_12mo', @items;
+}
+
+my %label = (
+ 'invoiced' => 'Gross Sales',
+ 'netsales' => 'Net Sales',
+ 'credits' => 'Gross Credits',
+ 'netcredits' => 'Net Credits',
+ 'payments' => 'Gross Receipts',
+ 'receipts' => 'Net Receipts',
+ 'refunds' => 'Gross Refunds',
+ 'netrefunds' => 'Net Refunds',
+ 'cashflow' => 'Gross Cashflow',
+ 'netcashflow' => 'Net Cashflow',
+);
+
+my %graph_suffix = (
+ 'invoiced' => ' (invoiced)',
+ 'netsales' => ' (invoiced - applied credits)',
+ 'credits' => ' (credited)',
+ 'netcredits' => ' (applied credits)',
+ 'payments' => ' (payments)',
+ 'receipts' => ' (applied payments)',
+ 'refunds' => ' (refunds)',
+ 'netrefunds' => ' (applied refunds)',
+ 'cashflow' => ' (payments - refunds)',
+ 'netcashflow' => ' (applied payments - applied refunds)',
+);
+my %graph_label = map { $_ => $label{$_}.$graph_suffix{$_} } keys %label;
+
+$label{$_.'_12mo'} = $label{$_}. " (prev 12 months)"
+ foreach keys %label;
+
+$graph_label{$_.'_12mo'} = $graph_label{$_}. " (prev 12 months)"
+ foreach keys %graph_label;
+
+my %color = (
+ 'invoiced' => '9999ff', #light blue
+ 'netsales' => '0000cc', #blue
+ 'credits' => 'ff9999', #light red
+ 'netcredits' => 'cc0000', #red
+ 'payments' => '99cc99', #light green
+ 'receipts' => '00cc00', #green
+ 'refunds' => 'ffcc99', #light orange
+ 'netrefunds' => 'ff9900', #orange
+ 'cashflow' => '99cc33', #light olive
+ 'netcashflow' => '339900', #olive
+);
+$color{$_.'_12mo'} = $color{$_}
+ foreach keys %color;
+
+my %link = (
+ 'invoiced' => "${p}search/cust_bill.html?agentnum=$agentnum;",
+ 'netsales' => "${p}search/cust_bill.html?agentnum=$agentnum;net=1;",
+ 'credits' => "${p}search/cust_credit.html?agentnum=$agentnum;",
+ 'netcredits' => "${p}search/cust_credit_bill.html?agentnum=$agentnum;",
+ 'payments' => "${p}search/cust_pay.html?magic=_date;agentnum=$agentnum;",
+ 'receipts' => "${p}search/cust_bill_pay.html?agentnum=$agentnum;",
+ 'refunds' => "${p}search/cust_refund.html?magic=_date;agentnum=$agentnum;",
+ 'netrefunds' => "${p}search/cust_credit_refund.html?agentnum=$agentnum;",
+);
+# XXX link 12mo?
+
+</%init>
diff --git a/httemplate/graph/report_cust_bill_pkg.html b/httemplate/graph/report_cust_bill_pkg.html
new file mode 100644
index 000000000..348746514
--- /dev/null
+++ b/httemplate/graph/report_cust_bill_pkg.html
@@ -0,0 +1,57 @@
+<% include('/elements/header.html', 'Sales Report' ) %>
+
+<FORM ACTION="cust_bill_pkg.cgi" METHOD="GET">
+
+<TABLE>
+
+<% include('/elements/tr-select-from_to.html' ) %>
+
+<% include('/elements/tr-select-agent.html',
+ 'label' => 'For agent: ',
+ 'disable_empty' => 0,
+ 'pre_options' => [ 'all' => 'all (aggregate)' ],
+ 'empty_label' => 'all (breakdown)',
+ )
+%>
+
+<% include('/elements/tr-select-pkg_class.html',
+ 'pre_options' => [ 'all' => 'all (aggregate)',
+ '0' => 'all (breakdown)' ],
+ 'empty_label' => '(empty class)',
+ )
+%>
+
+<!--
+<TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="separate_0freq" VALUE="1"></TD>
+ <TD>Separate one-time vs. recurring sales</TD>
+</TR>
+-->
+
+<TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="use_override" VALUE="1"></TD>
+ <TD>Separate sub-packages from parents</TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="use_usage" VALUE="1"></TD>
+ <TD>Separate rated usage from recurring fees</TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="average_per_cust_pkg" VALUE="1"></TD>
+ <TD>Average per customer package</TD>
+</TR>
+
+</TABLE>
+
+<BR><INPUT TYPE="submit" VALUE="Display">
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/graph/report_cust_bill_pkg_detail.html b/httemplate/graph/report_cust_bill_pkg_detail.html
new file mode 100644
index 000000000..3b85d521c
--- /dev/null
+++ b/httemplate/graph/report_cust_bill_pkg_detail.html
@@ -0,0 +1,48 @@
+<% include('/elements/header.html', 'Usage Sales Report' ) %>
+
+<FORM ACTION="cust_bill_pkg_detail.cgi" METHOD="GET">
+
+<TABLE>
+
+<% include('/elements/tr-select-from_to.html' ) %>
+
+<% include('/elements/tr-select-agent.html',
+ 'label' => 'For agent: ',
+ 'disable_empty' => 0,
+ )
+%>
+
+<% include('/elements/tr-select-pkg_class.html',
+ 'pre_options' => [ '0' => 'all' ],
+ 'empty_label' => '(empty class)',
+ )
+%>
+
+<TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="use_override" VALUE="1"></TD>
+ <TD>Separate sub-packages from parents</TD>
+</TR>
+
+<% include('/elements/tr-select-table.html',
+ 'label' => 'Usage class: ',
+ 'element_name' => 'usageclass',
+ 'table' => 'usage_class',
+ 'name_col' => 'classname',
+ 'hashref' => { 'disabled' => '' },
+ 'pre_options' => [ '0' => 'all' ],
+ 'empty_label' => '(empty class)',
+ )
+%>
+
+</TABLE>
+
+<BR><INPUT TYPE="submit" VALUE="Display">
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/graph/report_cust_pkg.html b/httemplate/graph/report_cust_pkg.html
new file mode 100644
index 000000000..22ccd5def
--- /dev/null
+++ b/httemplate/graph/report_cust_pkg.html
@@ -0,0 +1,28 @@
+<% include('/elements/header.html', 'Package Churn Summary' ) %>
+
+<FORM ACTION="cust_pkg.cgi" METHOD="GET">
+
+<TABLE>
+
+<% include('/elements/tr-select-from_to.html' ) %>
+
+<% include('/elements/tr-select-agent.html',
+ 'curr_value' => scalar($cgi->param('agentnum')),
+ 'label' => 'For agent: ',
+ 'disable_empty' => 0,
+ )
+%>
+
+</TABLE>
+
+<BR><INPUT TYPE="submit" VALUE="Display">
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+#XXX use a different ACL for package churn?
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/graph/report_cust_pkg_cost.html b/httemplate/graph/report_cust_pkg_cost.html
new file mode 100644
index 000000000..553db096b
--- /dev/null
+++ b/httemplate/graph/report_cust_pkg_cost.html
@@ -0,0 +1,26 @@
+<% include('/elements/header.html', 'Package Costs Report' ) %>
+
+<FORM ACTION="cust_pkg_cost.cgi" METHOD="GET">
+
+<TABLE>
+
+<% include('/elements/tr-select-from_to.html' ) %>
+
+<% include('/elements/tr-select-agent.html',
+ 'label' => 'For agent: ',
+ 'disable_empty' => 0,
+ )
+%>
+
+</TABLE>
+
+<BR><INPUT TYPE="submit" VALUE="Display">
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/graph/report_money_time.html b/httemplate/graph/report_money_time.html
new file mode 100644
index 000000000..b85bb6552
--- /dev/null
+++ b/httemplate/graph/report_money_time.html
@@ -0,0 +1,43 @@
+<% include('/elements/header.html', 'Sales, Credits and Receipts Summary' ) %>
+
+<FORM ACTION="money_time.cgi" METHOD="GET">
+
+<!--
+<INPUT TYPE="checkbox" NAME="ar">
+ Accounts receivable (invoices - applied credits)<BR>
+<INPUT TYPE="checkbox" NAME="charged">
+ Just Invoices<BR>
+<INPUT TYPE="checkbox" NAME="defer">
+ Accounts receivable, with deferred revenue (invoices - applied credits, with charges for annual/semi-annual/quarterly/etc. services deferred over applicable time period) (there has got to be a shorter description for this)<BR>
+<INPUT TYPE="checkbox" NAME="cash">
+ Cashflow (payments - refunds)<BR>
+<BR>
+-->
+
+<TABLE>
+
+<% include('/elements/tr-select-from_to.html' ) %>
+
+<% include('/elements/tr-select-agent.html',
+ 'label' => 'For agent: ',
+ 'disable_empty' => 0,
+ )
+%>
+
+<TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="12mo" VALUE="1"></TD>
+ <TD>Show 12 month totals instead of monthly values</TD>
+</TR>
+
+</TABLE>
+
+<BR><INPUT TYPE="submit" VALUE="Display">
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/graph/report_signupdate.html b/httemplate/graph/report_signupdate.html
new file mode 100644
index 000000000..7c22f112d
--- /dev/null
+++ b/httemplate/graph/report_signupdate.html
@@ -0,0 +1,30 @@
+<% include('/elements/header.html', 'Customer Signups by Date/Time' ) %>
+
+<FORM ACTION="signupdate.cgi" METHOD="GET">
+
+<TABLE>
+
+<% include('/elements/tr-select-from_to.html' ) %>
+
+<% include('/elements/tr-select-agent.html',
+ 'curr_value' => scalar($cgi->param('agentnum')),
+ 'label' => 'For agent: ',
+ 'disable_empty' => 0,
+ )
+%>
+
+<% include('/elements/tr-select-user.html') %>
+
+</TABLE>
+
+<BR><INPUT TYPE="submit" VALUE="Display">
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+#XXX use a different ACL for package churn?
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/graph/signupdate.cgi b/httemplate/graph/signupdate.cgi
new file mode 100644
index 000000000..5b7075868
--- /dev/null
+++ b/httemplate/graph/signupdate.cgi
@@ -0,0 +1,60 @@
+<% include('elements/report.html',
+ 'title' => $agentname . 'Customer signups by time of day',
+ 'items' => [ 'signupdate' ],
+ 'data' => [ \@count ],
+ 'row_labels' => [ 'New customers' ],
+ 'colors' => [ '00cc00' ], #green
+ 'col_labels' => [ map { "$_:00" } @hours ],
+ 'links' => [ \@links ],
+ 'graph_type' => 'Bars',
+ 'nototal' => 0,
+ 'sprintf' => '%u',
+ 'disable_money' => 1,
+ ) %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+#XXX or virtual
+my( $agentnum, $agent ) = ('', '');
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+ $agent = qsearchs('agent', { 'agentnum' => $agentnum } );
+ die "agentnum $agentnum not found!" unless $agent;
+}
+
+my $agentname = $agent ? $agent->agent.' ' : '';
+my $usernum = $cgi->param('usernum');
+
+my @hours = (0..23);
+my @count = (0) x 24;
+my %where;
+$where{'agentnum'} = $agentnum if $agentnum;
+$where{'usernum'} = $usernum if $usernum;
+my $sdate = $cgi->param('start_year').
+ '-'.
+ $cgi->param('start_month').
+ '-01';
+my $edate = ($cgi->param('end_year') +
+ ($cgi->param('end_month')==12)).
+ '-'.
+ ($cgi->param('end_month') % 12 + 1).
+ '-01'; # first day of the next month
+
+my $sql = "AND signupdate >= ".str2time($sdate).
+ " AND signupdate < ".str2time($edate);
+
+foreach my $cust (qsearch({ table => 'cust_main',
+ hashref => \%where,
+ extra_sql => $sql } )) {
+ next if !$cust->signupdate;
+ my $hour = time2str('%H',$cust->signupdate);
+ $count[$hour]++;
+}
+
+my @links = ("${p}search/cust_main.html?" .
+ join (';', map {$_.'='.$where{$_}} (keys(%where))) ).
+ ";signupdate_beginning=$sdate;signupdate_ending=$edate";
+push @links, map { ";signuphour=$_" } @hours;
+</%init>