summaryrefslogtreecommitdiff
path: root/httemplate
diff options
context:
space:
mode:
Diffstat (limited to 'httemplate')
-rw-r--r--httemplate/docs/about.html2
-rw-r--r--httemplate/edit/cust_main/billing.html4
-rwxr-xr-xhttemplate/edit/svc_acct.cgi7
-rw-r--r--httemplate/elements/validate_password.html10
-rw-r--r--httemplate/misc/confirm-address_standardize.html24
-rw-r--r--httemplate/misc/confirm-censustract.html4
-rw-r--r--httemplate/misc/email-customers.html4
-rw-r--r--httemplate/misc/xmlhttp-validate_password.html25
-rwxr-xr-xhttemplate/search/cust_main.cgi10
-rwxr-xr-xhttemplate/search/cust_pkg.cgi2
-rw-r--r--httemplate/search/customer_accounting_summary.html131
-rw-r--r--httemplate/search/customer_cdr_profit.html137
-rw-r--r--httemplate/search/elements/grid-report.html189
-rw-r--r--httemplate/view/cust_main/billing.html4
14 files changed, 269 insertions, 284 deletions
diff --git a/httemplate/docs/about.html b/httemplate/docs/about.html
index 8ed4089db..651918017 100644
--- a/httemplate/docs/about.html
+++ b/httemplate/docs/about.html
@@ -28,7 +28,7 @@
% } else {
<FONT SIZE="-1">
% }
-&copy; 2015 Freeside Internet Services, Inc.<BR>
+&copy; 2016 Freeside Internet Services, Inc.<BR>
All rights reserved.<BR>
Licensed under the terms of the<BR>
GNU <b>Affero</b> General Public License.<BR>
diff --git a/httemplate/edit/cust_main/billing.html b/httemplate/edit/cust_main/billing.html
index c4b66c8d5..519f2f8f2 100644
--- a/httemplate/edit/cust_main/billing.html
+++ b/httemplate/edit/cust_main/billing.html
@@ -597,9 +597,9 @@ function toggle(obj) {
<INPUT TYPE="hidden" NAME="squelch_cdr" VALUE="<% $cust_main->squelch_cdr %>">
% }
-% if ( $conf->config('voip-cdr_email_attach') ) {
+% if ( my $attach = $conf->config('voip-cdr_email_attach') ) {
<TR>
- <TD COLSPAN="2"><INPUT TYPE="checkbox" NAME="email_csv_cdr" VALUE="Y" <% $cust_main->email_csv_cdr eq "Y" ? 'CHECKED' : '' %>> <% mt('Attach CDRs as CSV to emailed invoices') |h %></TD>
+ <TD COLSPAN="2"><INPUT TYPE="checkbox" NAME="email_csv_cdr" VALUE="Y" <% $cust_main->email_csv_cdr eq "Y" ? 'CHECKED' : '' %>> <% mt('Attach CDRs as '. uc($attach). ' to emailed invoices') |h %></TD>
</TR>
% } else {
<INPUT TYPE="hidden" NAME="email_csv_cdr" VALUE="<% $cust_main->email_csv_cdr %>">
diff --git a/httemplate/edit/svc_acct.cgi b/httemplate/edit/svc_acct.cgi
index ff8e31615..42660462d 100755
--- a/httemplate/edit/svc_acct.cgi
+++ b/httemplate/edit/svc_acct.cgi
@@ -52,9 +52,10 @@
<INPUT TYPE="text" ID="clear_password" NAME="clear_password" VALUE="<% $password %>" SIZE=<% $pmax2 %> MAXLENGTH=<% $pmax %>>
<& /elements/random_pass.html, 'clear_password' &><BR>
<DIV ID="clear_password_result" STYLE="font-size: smaller"></DIV>
- <& '/elements/validate_password.html',
- 'fieldid' => 'clear_password',
- 'svcnum' => $svcnum
+ <& /elements/validate_password.html,
+ 'fieldid' => 'clear_password',
+ 'svcnum' => $svcnum ,
+ 'pkgnum' => $pkgnum,
&>
</TD>
</TR>
diff --git a/httemplate/elements/validate_password.html b/httemplate/elements/validate_password.html
index a488c4f16..f067ad8fc 100644
--- a/httemplate/elements/validate_password.html
+++ b/httemplate/elements/validate_password.html
@@ -5,8 +5,9 @@ To validate passwords via javascript/xmlhttp:
<INPUT ID="password_field" TYPE="text">
<DIV ID="password_field_result">
<& '/elements/validate_password.html',
- fieldid => 'password_field',
- svcnum => $svcnum
+ fieldid => 'password_field',
+ svcnum => $svcnum,
+ pkgnum => $pkgnum, # used if the service doesn't exist yet
&>
The ID of the input field can be anything; the ID of the DIV in which to display results
@@ -27,7 +28,10 @@ function add_password_validation (fieldid) {
var resultfield = document.getElementById(fieldid);
if (this.value) {
resultfield.innerHTML = '<SPAN STYLE="color: blue;">Validating password...</SPAN>';
- validate_password('fieldid',fieldid,'svcnum','<% $opt{'svcnum'} %>','password',this.value,
+ validate_password('fieldid',fieldid,
+ 'svcnum',<% $opt{'svcnum'} |js_string %>,
+ 'pkgnum',<% $opt{'pkgnum'} |js_string %>,
+ 'password',this.value,
function (result) {
result = JSON.parse(result);
var resultfield = document.getElementById(result.fieldid);
diff --git a/httemplate/misc/confirm-address_standardize.html b/httemplate/misc/confirm-address_standardize.html
index 0a05c70bd..9d1a5c135 100644
--- a/httemplate/misc/confirm-address_standardize.html
+++ b/httemplate/misc/confirm-address_standardize.html
@@ -34,18 +34,18 @@ Confirm address standardization
</TR>
<TR>
% if ( $old{$pre.'company'} ) {
- <TD><% $old{$pre.'company'} %></TD>
+ <TD><% $old{$pre.'company'} |h %></TD>
% }
</TR>
<TR>
- <TD><% $old{$pre.'address1'} %></TD>
+ <TD><% $old{$pre.'address1'} |h %></TD>
<TD ROWSPAN=3><FONT COLOR="#ff0000"><B><% $new{$pre.'error'} %></B></FONT></TD>
</TR>
<TR>
- <TD><% $old{$pre.'address2'} %></TD>
+ <TD><% $old{$pre.'address2'} |h %></TD>
</TR>
<TR>
- <TD><% $old{$pre.'city'} %>, <% $old{$pre.'state'} %> <% $old{$pre.'zip'} %></TD>
+ <TD><% $old{$pre.'city'} |h %>, <% $old{$pre.'state'} |h %> <% $old{$pre.'zip'} |h %></TD>
</TR>
% } else { # not an error
% $rows++ if !$new{$pre.'addr_clean'};
@@ -68,21 +68,21 @@ Confirm address standardization
<TR>
% if ( $old{$pre.'company'} ) {
<TR>
- <TD><% $old{$pre.'company'} %></TD>
- <TD><% $new{$pre.'company'} %></TD>
+ <TD><% $old{$pre.'company'} |h %></TD>
+ <TD><% $new{$pre.'company'} |h %></TD>
</TR>
% }
<TR>
- <TD><% $old{$pre.'address1'} %></TD>
- <TD><% $new{$pre.'address1'} %></TD>
+ <TD><% $old{$pre.'address1'} |h %></TD>
+ <TD><% $new{$pre.'address1'} |h %></TD>
</TR>
<TR>
- <TD><% $old{$pre.'address2'} %></TD>
- <TD><% $new{$pre.'address2'} %></TD>
+ <TD><% $old{$pre.'address2'} |h %></TD>
+ <TD><% $new{$pre.'address2'} |h %></TD>
</TR>
<TR>
- <TD><% $old{$pre.'city'} %>, <% $old{$pre.'state'} %> <% $old{$pre.'zip'} %></TD>
- <TD><% $new{$pre.'city'} %>, <% $new{$pre.'state'} %> <% $new{$pre.'zip'} %></TD>
+ <TD><% $old{$pre.'city'} |h %>, <% $old{$pre.'state'} |h %> <% $old{$pre.'zip'} |h %></TD>
+ <TD><% $new{$pre.'city'} |h %>, <% $new{$pre.'state'} |h %> <% $new{$pre.'zip'} |h %></TD>
</TR>
% } # if error
diff --git a/httemplate/misc/confirm-censustract.html b/httemplate/misc/confirm-censustract.html
index 024bc17c4..10ae91812 100644
--- a/httemplate/misc/confirm-censustract.html
+++ b/httemplate/misc/confirm-censustract.html
@@ -13,8 +13,8 @@ Census tract error
Confirm census tract
% }
</B><BR>
-<% $location{address1} %> <% $location{address2} %><BR>
-<% $location{city} %>, <% $location{state} %> <% $location{zip} %><BR>
+<% $location{address1} |h %> <% $location{address2} |h %><BR>
+<% $location{city} |h %>, <% $location{state} |h %> <% $location{zip} |h %><BR>
<BR>
% my $querystring = "census_year=$year&latitude=".$cache->get('latitude').'&longitude='.$cache->get('longitude');
<A HREF="http://maps.ffiec.gov/FFIECMapper/TGMapSrv.aspx?<% $querystring %>"
diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html
index 09ff93cca..cd4c92f23 100644
--- a/httemplate/misc/email-customers.html
+++ b/httemplate/misc/email-customers.html
@@ -80,14 +80,14 @@ should be used to set msgnum or from/subject/html_body cgi params
<% include('/elements/tr-fixed.html',
'field' => 'from',
'label' => 'From:',
- 'value' => scalar( $from ),
+ 'value' => $from,
)
%>
<% include('/elements/tr-fixed.html',
'field' => 'subject',
'label' => 'Subject:',
- 'value' => scalar( $subject ),
+ 'value' => $subject,
)
%>
diff --git a/httemplate/misc/xmlhttp-validate_password.html b/httemplate/misc/xmlhttp-validate_password.html
index 28dbf6460..1efb4aaa3 100644
--- a/httemplate/misc/xmlhttp-validate_password.html
+++ b/httemplate/misc/xmlhttp-validate_password.html
@@ -1,13 +1,14 @@
<%doc>
-Requires cgi params 'password' (plaintext) and 'sub' ('validate_password' is only
-acceptable value.) Also accepts 'svcnum' (for svc_acct, will otherwise create an
-empty dummy svc_acct) and 'fieldid' (for html post-processing, passed along in
-results for convenience.)
-
-Returns a json-encoded hashref with keys of 'valid' (set to 1 if object is valid),
-'error' (error text if password is invalid) or 'syserror' (error text if password
-could not be validated.) Only one of these keys will be set. Will also set
-'fieldid' if it was passed.
+Requires cgi params 'password' (plaintext) and 'sub' ('validate_password' is
+only acceptable value.) Also accepts 'svcnum' (for svc_acct, will otherwise
+create an empty dummy svc_acct), 'pkgnum' (for when the svc_acct isn't yet
+inserted), and 'fieldid' (for html post-processing, passed along in results
+for convenience.)
+
+Returns a json-encoded hashref with keys of 'valid' (set to 1 if object is
+valid), 'error' (error text if password is invalid) or 'syserror' (error text
+if password could not be validated.) Only one of these keys will be set.
+Will also set 'fieldid' if it was passed.
</%doc>
<% encode_json($result) %>
@@ -32,9 +33,13 @@ my $validate_password = sub {
$result{'syserror'} = 'Invalid svcnum' unless $svcnum =~ /^\d*$/;
return \%result if $result{'syserror'};
+ my $pkgnum = $arg{'pkgnum'};
+ $result{'syserror'} = 'Invalid pkgnum' unless $pkgnum =~ /^\d*$/;
+ return \%result if $result{'syserror'};
+
my $svc_acct = $svcnum
? qsearchs('svc_acct',{'svcnum' => $svcnum})
- : (new FS::svc_acct {});
+ : FS::svc_acct->new({ 'pkgnum' => $pkgnum });
$result{'syserror'} = 'Could not find service' unless $svc_acct;
return \%result if $result{'syserror'};
diff --git a/httemplate/search/cust_main.cgi b/httemplate/search/cust_main.cgi
index ba80f0275..38ec4f453 100755
--- a/httemplate/search/cust_main.cgi
+++ b/httemplate/search/cust_main.cgi
@@ -501,8 +501,13 @@ my $pkgs_method = $conf->exists('hidecancelledpackages')
: 'all_pkgs';
#false laziness w/httemplate/view/cust_main/packages.html
-my $select = '*, setup_option.optionvalue AS _opt_setup_fee, '.
- 'recur_option.optionvalue AS _opt_recur_fee',
+my $select = join(',',
+ 'cust_pkg.*',
+ 'part_pkg.*',
+ 'setup_option.optionvalue AS _opt_setup_fee',
+ 'recur_option.optionvalue AS _opt_recur_fee',
+ );
+
my $addl_from = qq{
LEFT JOIN part_pkg USING ( pkgpart )
LEFT JOIN part_pkg_option AS setup_option
@@ -513,6 +518,7 @@ my $addl_from = qq{
AND recur_option.optionname = 'recur_fee' )
};
+local($FS::cust_pkg::cache_enabled) = 1; #for $cust_pkg->part_pkg
my %all_pkgs = map { $_->custnum =>
[ $_->$pkgs_method({ select => $select,
addl_from => $addl_from,
diff --git a/httemplate/search/cust_pkg.cgi b/httemplate/search/cust_pkg.cgi
index f1e686a83..dbd346dba 100755
--- a/httemplate/search/cust_pkg.cgi
+++ b/httemplate/search/cust_pkg.cgi
@@ -44,7 +44,7 @@
},
sub { my $c = shift;
sprintf( $money_char.'%.2f',
- $c->part_pkg->base_recur($c)
+ $c->base_recur
);
},
sub { FS::part_pkg::freq_pretty(shift); },
diff --git a/httemplate/search/customer_accounting_summary.html b/httemplate/search/customer_accounting_summary.html
index 744b313f9..a8e503324 100644
--- a/httemplate/search/customer_accounting_summary.html
+++ b/httemplate/search/customer_accounting_summary.html
@@ -1,120 +1,8 @@
-% if ( $cgi->param('_type') =~ /(xls)$/ ) {
-<%perl>
- # egregious false laziness w/ search/report_tax-xls.cgi
- my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format;
- my $filename = $cgi->url(-relative => 1);
- $filename =~ s/\.html$//;
- $filename .= $format->{extension};
- http_header('Content-Type' => $format->{mime_type});
- http_header('Content-Disposition' => qq!attachment;filename="$filename"!);
-
- my $output = '';
- my $XLS = IO::String->new($output);
- my $workbook = $format->{class}->new($XLS)
- or die "Error opening .xls file: $!";
-
- my $worksheet = $workbook->add_worksheet('Summary');
-
- my %format = (
- header => {
- size => 11,
- bold => 1,
- align => 'center',
- valign => 'vcenter',
- text_wrap => 1,
- },
- money => {
- size => 11,
- align => 'right',
- valign => 'bottom',
- num_format=> 8,
- },
- '' => {},
- );
- my %default = (
- font => 'Calibri',
- border => 1,
- );
- foreach (keys %format) {
- my %f = (%default, %{$format{$_}});
- $format{$_} = $workbook->add_format(%f);
- $format{"m_$_"} = $workbook->add_format(%f);
- }
-
- my ($r, $c) = (0, 0);
- for my $row (@rows) {
- $c = 0;
- my $thisrow = shift @cells;
- for my $cell (@$thisrow) {
- if (!ref($cell)) {
- # placeholder, so increment $c so that we write to the correct place
- $c++;
- next;
- }
- # format name
- my $f = '';
- $f = 'header' if $row->{header} or $cell->{header};
- $f = 'money' if $cell->{format} eq 'money';
- if ( $cell->{rowspan} > 1 or $cell->{colspan} > 1 ) {
- my $range = xl_range_formula(
- 'Summary',
- $r, $r - 1 + ($cell->{rowspan} || 1),
- $c, $c - 1 + ($cell->{colspan} || 1)
- );
- #warn "merging $range\n";
- $worksheet->merge_range($range, $cell->{value}, $format{"m_$f"});
- } else {
- #warn "writing ".xl_rowcol_to_cell($r, $c)."\n";
- $worksheet->write( $r, $c, $cell->{value}, $format{$f} );
- }
- $c++;
- } #$cell
- $r++;
- } #$row
- $workbook->close;
-
- http_header('Content-Length' => length($output));
- $m->print($output);
-</%perl>
-% } else {
-<& /elements/header.html, $title &>
-% my $myself = $cgi->self_url;
-<P ALIGN="right" CLASS="noprint">
-Download full reports<BR>
-as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR>
-% # as <A HREF="<% "$myself;_type=csv" %>">CSV file</A> # is this still needed?
-</P>
-<style type="text/css">
-.report * {
- background-color: #f8f8f8;
- border: 1px solid #999999;
- padding: 2px;
-}
-.report td {
- text-align: right;
-}
-.total * { background-color: #f5f6be; }
-.shaded * { background-color: #c8c8c8; }
-.totalshaded * { background-color: #bfc094; }
-</style>
-<table class="report" width="100%" cellspacing=0>
-% foreach my $rowinfo (@rows) {
- <tr<% $rowinfo->{class} ? ' class="'.$rowinfo->{class}.'"' : ''%>>
-% my $thisrow = shift @cells;
-% foreach my $cell (@$thisrow) {
-% next if !ref($cell); # placeholders
-% my $td = $cell->{header} ? 'th' : 'td';
-% my $style = '';
-% $style .= " rowspan=".$cell->{rowspan} if $cell->{rowspan} > 1;
-% $style .= " colspan=".$cell->{colspan} if $cell->{colspan} > 1;
- <<%$td%><%$style%>><% $cell->{value} |h %></<%$td%>>
-% }
- </tr>
-% }
-</table>
-
-<& /elements/footer.html &>
-% }
+<& elements/grid-report.html,
+ title => $title,
+ rows => \@rows,
+ cells => \@cells,
+&>
<%init>
die "access denied"
@@ -224,17 +112,14 @@ my @cells; # arrayrefs of cell info
$rows[0] = {};
$cells[0] = [
{ header => 1, rowspan => 2, colspan => ($setuprecur ? 4 : 3) },
- ($setuprecur ? '' : ()),
map {
{ header => 1, colspan => ($grossdiscount ? 3 : 2), value => time2str('%b %Y', $_) },
- ''
} @{ $data->{speriod} }
];
my $ncols = scalar(@{ $data->{speriod} });
$rows[1] = {};
-$cells[1] = [ '',
- ($setuprecur ? '' : ()),
+$cells[1] = [
map {
( ($grossdiscount
? (
@@ -270,8 +155,6 @@ foreach my $cust_main (@cust_main) { # correspond to cross_params
rowspan => ($setuprecur ? 2 : 1),
},
;
- } else {
- push @thisrow, '';
}
if ( $setuprecur ) {
# subheading
@@ -310,8 +193,6 @@ for my $subrow (0..($setuprecur ? 1 : 0)) {
header => 1,
colspan => 3,
rowspan => ($setuprecur ? 2 : 1), };
- } else {
- push @thisrow, '';
}
if ( $setuprecur ) {
push @thisrow,
diff --git a/httemplate/search/customer_cdr_profit.html b/httemplate/search/customer_cdr_profit.html
index 8dc06636a..c5351094f 100644
--- a/httemplate/search/customer_cdr_profit.html
+++ b/httemplate/search/customer_cdr_profit.html
@@ -1,121 +1,11 @@
-% if ( $cgi->param('_type') =~ /(xls)$/ ) {
-<%perl>
- # egregious false laziness w/ search/report_tax-xls.cgi
- my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format;
- my $filename = $cgi->url(-relative => 1);
- $filename =~ s/\.html$//;
- $filename .= $format->{extension};
- http_header('Content-Type' => $format->{mime_type});
- http_header('Content-Disposition' => qq!attachment;filename="$filename"!);
-
- my $output = '';
- my $XLS = IO::String->new($output);
- my $workbook = $format->{class}->new($XLS)
- or die "Error opening .xls file: $!";
-
- my $worksheet = $workbook->add_worksheet('Summary');
-
- my %format = (
- header => {
- size => 11,
- bold => 1,
- align => 'center',
- valign => 'vcenter',
- text_wrap => 1,
- },
- money => {
- size => 11,
- align => 'right',
- valign => 'bottom',
- num_format=> 8,
- },
- '' => {},
- );
- my %default = (
- font => 'Calibri',
- border => 1,
- );
- foreach (keys %format) {
- my %f = (%default, %{$format{$_}});
- $format{$_} = $workbook->add_format(%f);
- $format{"m_$_"} = $workbook->add_format(%f);
- }
-
- my ($r, $c) = (0, 0);
- for my $row (@rows) {
- $c = 0;
- my $thisrow = shift @cells;
- for my $cell (@$thisrow) {
- if (!ref($cell)) {
- # placeholder, so increment $c so that we write to the correct place
- $c++;
- next;
- }
- # format name
- my $f = '';
- $f = 'header' if $row->{header} or $cell->{header};
- $f = 'money' if $cell->{format} eq 'money';
- if ( $cell->{rowspan} > 1 or $cell->{colspan} > 1 ) {
- my $range = xl_range_formula(
- 'Summary',
- $r, $r - 1 + ($cell->{rowspan} || 1),
- $c, $c - 1 + ($cell->{colspan} || 1)
- );
- #warn "merging $range\n";
- $worksheet->merge_range($range, $cell->{value}, $format{"m_$f"});
- } else {
- #warn "writing ".xl_rowcol_to_cell($r, $c)."\n";
- $worksheet->write( $r, $c, $cell->{value}, $format{$f} );
- }
- $c += $cell->{colspan} || 1;
- } #$cell
- $r++;
- } #$row
- $workbook->close;
-
- http_header('Content-Length' => length($output));
- $m->print($output);
-</%perl>
-% } else {
-<& /elements/header.html, $title &>
-% my $myself = $cgi->self_url;
-<P ALIGN="right" CLASS="noprint">
-Download full reports<BR>
-as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A>
-</P>
-<style type="text/css">
-.report * {
- background-color: #f8f8f8;
- border: 1px solid #999999;
- padding: 2px;
-}
-.report td {
- text-align: right;
-}
-.total { background-color: #f5f6be; }
-.shaded { background-color: #c8c8c8; }
-.totalshaded { background-color: #bfc094; }
-</style>
-<table class="report" width="100%" cellspacing=0>
-% foreach my $rowinfo (@rows) {
- <tr<% $rowinfo->{class} ? ' class="'.$rowinfo->{class}.'"' : ''%>>
-% my $thisrow = shift @cells;
-% foreach my $cell (@$thisrow) {
-% next if !ref($cell); # placeholders
-% my $td = $cell->{header} ? 'th' : 'td';
-% my $style = '';
-% $style .= ' class="'.$cell->{class}.'"' if $cell->{class};
-% $style .= " rowspan=".$cell->{rowspan} if $cell->{rowspan} > 1;
-% $style .= " colspan=".$cell->{colspan} if $cell->{colspan} > 1;
-% $style .= ' style="color: red"' if $cell->{value} < 0;
- <<%$td%><%$style%>><% $cell->{value} |h %></<%$td%>>
-% }
- </tr>
-% }
-</table>
-
-<& /elements/footer.html &>
-% }
+<& elements/grid-report.html,
+ title => $title,
+ rows => \@rows,
+ cells => \@cells,
+ head => $head,
+ # would be better handled with Mason inheritance? consider this. easy enough
+ # to change it at this point.
+&>
<%init>
die "access denied"
@@ -213,7 +103,11 @@ foreach my $cust_main (@cust_main) { # correspond to cross_params
for my $item (0..3) { # recur/recur_cost/usage/usage_cost
my $value = $data->{data}[$item][$col][$row];
$skip = 0 if abs($value) > 0.005;
- push @thisrow, { value => sprintf('%0.2f', $value), format => 'money' };
+ push @thisrow, {
+ value => sprintf('%0.2f', $value),
+ format => 'money',
+ class => ($value < 0 ? 'negative' : ''),
+ };
$total[$col * 5 + $item] += $value;
$profit += (($item % 2) ? -1 : 1) * $value;
} #item
@@ -250,4 +144,9 @@ for my $col (0..($ncols * 5)-1) { # month and recur/recur_cost/usage/usage_cost/
}
push @cells, \@thisrow;
+my $head = q[
+<style>
+ .negative { color: red }
+</style>
+];
</%init>
diff --git a/httemplate/search/elements/grid-report.html b/httemplate/search/elements/grid-report.html
new file mode 100644
index 000000000..98e81785f
--- /dev/null
+++ b/httemplate/search/elements/grid-report.html
@@ -0,0 +1,189 @@
+<%doc>
+
+Simple display front-end for reports that produce some kind of data table,
+which the user can request as an Excel spreadsheet. /elements/header.html
+and /elements/footer.html are included automatically, so don't include them
+again.
+
+This element defines "total", "shaded", and "totalshaded" CSS classes. For
+anything else, insert a <style> element via the 'head' argument.
+
+Usage:
+
+<& elements/grid-report.html,
+ title => 'My Report',
+ rows => [
+ { header => 1, },
+ ...
+ ],
+ cells => [
+ [ # row 0
+ { value => '123.45',
+ # optional
+ format => 'money',
+ header => 1,
+ rowspan => 2,
+ colspan => 3,
+ class => 'shaded',
+ },
+ ...
+ ],
+ ],
+ head => q[<div>Thing to insert before the table</div>],
+ foot => q[<span>That's all folks!</span>].
+&>
+</%doc>
+% if ( $cgi->param('_type') =~ /(xls)$/ ) {
+<%perl>
+ # egregious false laziness w/ search/report_tax-xls.cgi
+ # and search/customer_cdr_profit.html
+ my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format;
+ my $filename = $cgi->url(-relative => 1);
+ $filename =~ s/\.html$//;
+ $filename .= $format->{extension};
+ http_header('Content-Type' => $format->{mime_type});
+ http_header('Content-Disposition' => qq!attachment;filename="$filename"!);
+
+ my $output = '';
+ my $XLS = IO::String->new($output);
+ my $workbook = $format->{class}->new($XLS)
+ or die "Error opening .xls file: $!";
+
+ my $worksheet = $workbook->add_worksheet('Summary');
+
+ my %format = (
+ header => {
+ size => 11,
+ bold => 1,
+ align => 'center',
+ valign => 'vcenter',
+ text_wrap => 1,
+ },
+ money => {
+ size => 11,
+ align => 'right',
+ valign => 'bottom',
+ num_format=> 8,
+ },
+ '' => {},
+ );
+ my %default = (
+ font => 'Calibri',
+ border => 1,
+ );
+ foreach (keys %format) {
+ my %f = (%default, %{$format{$_}});
+ $format{$_} = $workbook->add_format(%f);
+ $format{"m_$_"} = $workbook->add_format(%f);
+ }
+
+ my ($r, $c) = (0, 0);
+ # indices in these correspond to column positions
+ my @rowspans;
+ my @widths;
+
+ for my $row (@rows) {
+ $c = 0;
+ my $thisrow = shift @cells;
+ for my $cell (@$thisrow) {
+ # skip over cells that are occupied by rowspans above them
+ while ($rowspans[$c]) {
+ $rowspans[$c]--;
+ $c++;
+ }
+
+ # skip this cell if it's empty, also
+ next if !ref($cell);
+ # format name
+ my $f = '';
+ $f = 'header' if $row->{header} or $cell->{header};
+ $f = 'money' if $cell->{format} eq 'money';
+ if ( $cell->{rowspan} > 1 or $cell->{colspan} > 1 ) {
+ my $range = xl_range_formula(
+ 'Summary',
+ $r, $r - 1 + ($cell->{rowspan} || 1),
+ $c, $c - 1 + ($cell->{colspan} || 1)
+ );
+ #warn "merging $range\n";
+ $worksheet->merge_range($range, $cell->{value}, $format{"m_$f"});
+ } else {
+ #warn "writing ".xl_rowcol_to_cell($r, $c)."\n";
+ $worksheet->write( $r, $c, $cell->{value}, $format{$f} );
+ }
+
+ # estimate column width, as in search-xls, but without date formats
+ my $width = length($cell->{value}) / ($cell->{colspan} || 1);
+ $width *= 1.1 if $f eq 'header';
+ $width++ if $f eq 'money'; # for money symbol
+ $width += 2; # pad it
+
+ for (1 .. ($cell->{colspan} || 1)) {
+ # adjust minimum widths to allow for this cell's contents
+ $widths[$c] = $width if $width > ($widths[$c] || 0);
+
+ # and if this cell has a rowspan, block off that many rows below it
+ if ( $cell->{rowspan} > 1 ) {
+ $rowspans[$c] = $cell->{rowspan} - 1;
+ }
+ $c++;
+ }
+ } #$cell
+ $r++;
+ } #$row
+
+ $c = 0;
+ for my $c (0 .. scalar(@widths) - 1) {
+ $worksheet->set_column($c, $c, $widths[$c]);
+ }
+ $workbook->close;
+
+ http_header('Content-Length' => length($output));
+ $m->print($output);
+</%perl>
+% } else {
+<& /elements/header.html, $title &>
+<% $head %>
+% my $myself = $cgi->self_url;
+<P ALIGN="right" CLASS="noprint">
+Download full reports<BR>
+as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR>
+</P>
+<style type="text/css">
+.report * {
+ background-color: #f8f8f8;
+ border: 1px solid #999999;
+ padding: 2px;
+}
+.report td {
+ text-align: right;
+}
+.total { background-color: #f5f6be; }
+.shaded { background-color: #c8c8c8; }
+.totalshaded { background-color: #bfc094; }
+</style>
+<table class="report" width="100%" cellspacing=0>
+% foreach my $rowinfo (@rows) {
+ <tr<% $rowinfo->{class} ? ' class="'.$rowinfo->{class}.'"' : ''%>>
+% my $thisrow = shift @cells;
+% foreach my $cell (@$thisrow) {
+% next if !ref($cell); # placeholders
+% my $td = $cell->{header} ? 'th' : 'td';
+% my $style = '';
+% $style .= " rowspan=".$cell->{rowspan} if $cell->{rowspan} > 1;
+% $style .= " colspan=".$cell->{colspan} if $cell->{colspan} > 1;
+% $style .= ' class="' . $cell->{class} . '"' if $cell->{class};
+ <<%$td%><%$style%>><% $cell->{value} |h %></<%$td%>>
+% }
+ </tr>
+% }
+</table>
+<% $foot %>
+<& /elements/footer.html &>
+% }
+<%args>
+$title
+@rows
+@cells
+$head => ''
+$foot => ''
+</%args>
diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html
index 39f032499..3d0983e67 100644
--- a/httemplate/view/cust_main/billing.html
+++ b/httemplate/view/cust_main/billing.html
@@ -385,9 +385,9 @@
</TR>
% }
-% if ( $conf->config('voip-cdr_email_attach') ) {
+% if ( my $attach = $conf->config('voip-cdr_email_attach') ) {
<TR>
- <TD ALIGN="right"><% mt('Email CDRs as CSV') |h %></TD>
+ <TD ALIGN="right"><% mt('Email CDRs as '.uc($attach)) |h %></TD>
<TD BGCOLOR="#ffffff"><% $cust_main->email_csv_cdr ? $yes : $no %></TD>
</TR>
% }