summaryrefslogtreecommitdiff
path: root/httemplate
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2015-08-07 22:01:31 -0700
committerIvan Kohler <ivan@freeside.biz>2015-08-07 22:01:31 -0700
commit0c76afbb717e1716e6126bc4a120b8d9471614a0 (patch)
tree9a398e455a7767372588077470685d25ef8d82b3 /httemplate
parent7beec7068e00be5ae1b2599fdf2b494bc19e31d0 (diff)
parent3e2c2ad8aff1bd361ca07495b2255538c8231079 (diff)
Merge branch 'FREESIDE_3_BRANCH' of git.freeside.biz:/home/git/freeside into FREESIDE_3_BRANCH
Diffstat (limited to 'httemplate')
-rw-r--r--httemplate/browse/addr_block.cgi136
-rwxr-xr-xhttemplate/browse/cust_attachment.html2
-rw-r--r--httemplate/browse/part_device.html7
-rw-r--r--httemplate/browse/part_virtual_field.html2
-rw-r--r--httemplate/docs/about.html2
-rw-r--r--httemplate/docs/credits.html1
-rw-r--r--httemplate/docs/license.html10
-rwxr-xr-xhttemplate/edit/cust_main.cgi7
-rw-r--r--httemplate/edit/cust_main/top_misc.html7
-rwxr-xr-xhttemplate/edit/cust_pay.cgi10
-rw-r--r--httemplate/edit/invoice_conf.html5
-rw-r--r--httemplate/edit/part_device.html2
-rw-r--r--httemplate/edit/process/change-cust_pkg.html59
-rw-r--r--httemplate/edit/process/cust_pay-no_auto_apply.cgi48
-rwxr-xr-xhttemplate/edit/process/cust_pay.cgi1
-rwxr-xr-xhttemplate/edit/process/part_pkg.cgi5
-rw-r--r--httemplate/edit/process/svc_circuit.html4
-rw-r--r--httemplate/elements/bill.html2
-rw-r--r--httemplate/elements/city.html5
-rw-r--r--httemplate/elements/columnstart.html2
-rw-r--r--httemplate/elements/create_uri_query2
-rw-r--r--httemplate/elements/customer-statement.html45
-rw-r--r--httemplate/elements/customer-table.html595
-rw-r--r--httemplate/elements/freeside.css18
-rw-r--r--httemplate/elements/input-fcc_options.html2
-rw-r--r--httemplate/elements/location.html8
-rw-r--r--httemplate/elements/menu.html11
-rw-r--r--httemplate/elements/pickcolor.html2
-rw-r--r--httemplate/elements/popup_link_onclick.html2
-rw-r--r--httemplate/elements/progress-init.html2
-rw-r--r--httemplate/elements/select.html3
-rw-r--r--httemplate/elements/standardize_locations.js4
-rw-r--r--httemplate/elements/tr-input-mask.html2
-rw-r--r--httemplate/elements/tr-pkg_svc.html13
-rw-r--r--httemplate/graph/cust_bill_pkg.cgi57
-rw-r--r--httemplate/graph/elements/monthly.html5
-rw-r--r--httemplate/graph/elements/report.html2
-rw-r--r--httemplate/graph/money_time.cgi50
-rw-r--r--httemplate/graph/money_time_daily.cgi31
-rw-r--r--httemplate/graph/report_cust_bill_pkg.html6
-rw-r--r--httemplate/graph/report_money_time.html23
-rw-r--r--httemplate/graph/report_money_time_daily.html12
-rw-r--r--httemplate/misc/batch-cust_pay.html96
-rwxr-xr-xhttemplate/misc/change_pkg.cgi13
-rw-r--r--httemplate/misc/email-customer-statement.html95
-rw-r--r--httemplate/misc/email-customers.html98
-rw-r--r--httemplate/misc/process/batch-cust_pay.cgi7
-rw-r--r--httemplate/misc/process/delete-cust_pay_batch.cgi37
-rw-r--r--httemplate/misc/process/email-customer-statement.html9
-rw-r--r--httemplate/misc/process/nms-add_iface.html2
-rwxr-xr-xhttemplate/search/cust_bill.html141
-rw-r--r--httemplate/search/cust_bill_pay_pkg.html36
-rw-r--r--httemplate/search/cust_bill_pkg.cgi62
-rwxr-xr-xhttemplate/search/cust_pay_batch.cgi34
-rw-r--r--httemplate/search/cust_pkg_summary.cgi3
-rw-r--r--httemplate/search/customer_accounting_summary.html46
-rw-r--r--httemplate/search/elements/cust_main_dayranges.html5
-rw-r--r--httemplate/search/elements/cust_pay_batch_top.html2
-rwxr-xr-xhttemplate/search/elements/cust_pay_or_refund.html36
-rw-r--r--httemplate/search/elements/grouped-search.html49
-rw-r--r--httemplate/search/elements/grouped-search/core162
-rw-r--r--httemplate/search/elements/grouped-search/html149
-rw-r--r--httemplate/search/elements/grouped-search/html-print81
-rw-r--r--httemplate/search/elements/grouped-search/xls61
-rw-r--r--httemplate/search/elements/match-classnum12
-rw-r--r--httemplate/search/elements/report_cust_pay_or_refund.html63
-rw-r--r--httemplate/search/elements/search-html.html675
-rw-r--r--httemplate/search/elements/search-xls.html73
-rw-r--r--httemplate/search/elements/search.html106
-rw-r--r--httemplate/search/report_cust_bill.html10
-rwxr-xr-xhttemplate/search/report_customer_accounting_summary.html8
-rw-r--r--httemplate/search/report_rt_cust.html40
-rw-r--r--httemplate/search/rt_cust.html174
-rw-r--r--httemplate/search/sql.html5
-rwxr-xr-xhttemplate/search/svc_acct.cgi32
-rw-r--r--httemplate/search/svc_circuit.cgi4
-rwxr-xr-xhttemplate/view/cust_bill-tex.cgi51
-rw-r--r--httemplate/view/cust_main/billing.html69
-rw-r--r--httemplate/view/cust_main/contacts.html4
-rw-r--r--httemplate/view/cust_main/packages/status.html4
-rw-r--r--httemplate/view/cust_main/payment_history.html15
-rw-r--r--httemplate/view/cust_main/payment_history/payment.html103
-rw-r--r--httemplate/view/svc_circuit.cgi (renamed from httemplate/view/svc_circuit.html)0
-rw-r--r--httemplate/view/svc_phone.cgi4
84 files changed, 2758 insertions, 1130 deletions
diff --git a/httemplate/browse/addr_block.cgi b/httemplate/browse/addr_block.cgi
index 46b12e65f..ba40bfd43 100644
--- a/httemplate/browse/addr_block.cgi
+++ b/httemplate/browse/addr_block.cgi
@@ -1,71 +1,71 @@
-<% include('elements/browse.html',
- 'title' => 'Address Blocks',
- 'name' => 'address block',
- 'html_init' => $html_init,
- 'html_foot' => $html_foot,
- 'query' => { 'table' => 'addr_block',
- 'hashref' => {},
- 'extra_sql' => $extra_sql,
- 'order_by' => $order_by,
- },
- 'count_query' => "SELECT count(*) from addr_block $count_sql",
- 'header' => [ 'Address Block',
- 'Router',
- 'Action(s)',
- '',
- '',
- '',
- ],
- 'fields' => [ 'NetAddr',
- sub { my $block = shift;
- my $router = $block->router;
- my $result = '';
- if ($router) {
- $result .= $router->routername. ' (';
- $result .= scalar($block->svc_broadband). ' services)';
- }
- $result;
- },
- $allocate_text,
- sub { shift->router ? '' : '<FONT SIZE="-2">(split)</FONT>' },
- sub { '<FONT SIZE="-2">('. (shift->manual_flag ? 'allow' : 'prevent'). ' automatic ip assignment)</FONT>' },
- sub {
- my $block = shift;
- if(!$block->router && scalar($block->svc_broadband) == 0) {
- return '<FONT SIZE="-2">(delete)</FONT>';
- }
- '';
- },
- ],
- 'links' => [ '',
- '',
- [ 'javascript:void(0)', '' ],
- $split_link,
- $autoassign_link,
- sub {
- my $block = shift;
- if(!$block->router && scalar($block->svc_broadband) == 0) {
- [ "${p}misc/delete-addr_block.html?", 'blocknum' ];
- }
- },
- ],
- 'link_onclicks' => [ '',
- '',
- $allocate_link,
- '',
- '',
- ],
- 'cell_styles' => [ '',
- '',
- 'border-right:none;',
- 'border-left:none;',
- '',
- ],
- 'agent_virt' => 1,
- 'agent_null_right' => 'Broadband global configuration',
- 'agent_pos' => 1,
- )
-%>
+<& elements/browse.html,
+ 'title' => 'Address Blocks',
+ 'name' => 'address block',
+ 'html_init' => $html_init,
+ 'html_foot' => $html_foot,
+ 'query' => { 'table' => 'addr_block',
+ 'hashref' => {},
+ 'extra_sql' => $extra_sql,
+ 'order_by' => $order_by,
+ },
+ 'count_query' => "SELECT count(*) from addr_block $count_sql",
+ 'header' => [ 'Address Block',
+ 'Router',
+ 'Action(s)',
+ '',
+ '',
+ '',
+ ],
+ 'fields' => [ 'NetAddr',
+ sub { my $block = shift;
+ my $router = $block->router;
+ my $result = '';
+ if ($router) {
+ $result .= $router->routername. ' (';
+ $result .= scalar($block->svc_broadband). ' services)';
+ }
+ $result;
+ },
+ $allocate_text,
+ sub { shift->router ? '' : '<FONT SIZE="-2">(split)</FONT>' },
+ sub { '<FONT SIZE="-2">('. (shift->manual_flag ? 'allow' : 'prevent'). ' automatic ip assignment)</FONT>' },
+ sub {
+ my $block = shift;
+ if(!$block->router && scalar($block->svc_broadband) == 0) {
+ return '<FONT SIZE="-2">(delete)</FONT>';
+ }
+ '';
+ },
+ ],
+ 'sort_fields' => [],
+ 'links' => [ '',
+ '',
+ [ 'javascript:void(0)', '' ],
+ $split_link,
+ $autoassign_link,
+ sub {
+ my $block = shift;
+ if(!$block->router && scalar($block->svc_broadband) == 0) {
+ [ "${p}misc/delete-addr_block.html?", 'blocknum' ];
+ }
+ },
+ ],
+ 'link_onclicks' => [ '',
+ '',
+ $allocate_link,
+ '',
+ '',
+ ],
+ 'cell_styles' => [ '',
+ '',
+ 'border-right:none;',
+ 'border-left:none;',
+ '',
+ ],
+ 'agent_virt' => 1,
+ 'agent_null_right' => 'Broadband global configuration',
+ 'agent_pos' => 1,
+&>
<%init>
die "access denied"
diff --git a/httemplate/browse/cust_attachment.html b/httemplate/browse/cust_attachment.html
index 9d62e5609..f81ec1b6b 100755
--- a/httemplate/browse/cust_attachment.html
+++ b/httemplate/browse/cust_attachment.html
@@ -101,7 +101,7 @@ my $orderby = $cgi->param('orderby') || 'custnum';
my $sub_cust = sub {
my $c = qsearchs('cust_main', { custnum => shift->custnum } );
- return $c ? $c->name : '<FONT COLOR="red"><B>(not found)</B></FONT>';
+ return $c ? encode_entities($c->name) : '<FONT COLOR="red"><B>(not found)</B></FONT>';
};
my $sub_date = sub {
diff --git a/httemplate/browse/part_device.html b/httemplate/browse/part_device.html
index 69387dd16..12c19ed90 100644
--- a/httemplate/browse/part_device.html
+++ b/httemplate/browse/part_device.html
@@ -8,7 +8,10 @@
],
'query' => { 'table' => 'part_device', },
'count_query' => 'SELECT COUNT(*) FROM part_device',
- 'header' => [ '#', 'Device type', 'Inventory Class', ],
+ 'header' => [ '#',
+ 'Device type',
+ 'Inventory Class',
+ 'External name', ],
'fields' => [ 'devicepart',
'devicename',
sub {
@@ -18,10 +21,12 @@
if $inventory_class;
'';
},
+ 'title',
],
'links' => [ $link,
$link,
'',
+ '',
],
)
%>
diff --git a/httemplate/browse/part_virtual_field.html b/httemplate/browse/part_virtual_field.html
index 1d8fad4c6..5e3876285 100644
--- a/httemplate/browse/part_virtual_field.html
+++ b/httemplate/browse/part_virtual_field.html
@@ -30,6 +30,6 @@
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+ unless $FS::CurrentUser::CurrentUser->access_right('Edit custom fields');
</%init>
diff --git a/httemplate/docs/about.html b/httemplate/docs/about.html
index b61e237fc..8ed4089db 100644
--- a/httemplate/docs/about.html
+++ b/httemplate/docs/about.html
@@ -56,7 +56,7 @@ GNU <b>Affero</b> General Public License.<BR>
% unless ( $agentnum ) {
<CENTER>
- <FONT SIZE="-3">"What good is spilling blood? It will not grow a thing." - R. Hunter</FONT>
+ <FONT SIZE="-3">"All the years combine; they melt into a dream" - R. Hunter</FONT>
</CENTER>
% }
diff --git a/httemplate/docs/credits.html b/httemplate/docs/credits.html
index f3b7b35d8..9e7782829 100644
--- a/httemplate/docs/credits.html
+++ b/httemplate/docs/credits.html
@@ -28,7 +28,6 @@
<H3>version <% $FS::VERSION %></H3>
<BR>
-<BR>
<H3>Core Team</H3>
Jeremy Davis<BR>
diff --git a/httemplate/docs/license.html b/httemplate/docs/license.html
index 7a69572a7..cebea72c8 100644
--- a/httemplate/docs/license.html
+++ b/httemplate/docs/license.html
@@ -6,7 +6,7 @@
<P>
-Copyright &copy; 2005-2014 Freeside Internet Services, Inc.<BR>
+Copyright &copy; 2005-2015 Freeside Internet Services, Inc.<BR>
Copyright &copy; 2000-2005 Ivan Kohler<BR>
Copyright &copy; 1999 Silicon Interactive Software Design<BR>
All rights reserved<BR>
@@ -30,10 +30,10 @@ All rights reserved<BR>
<P>
At your option, you may also redistribute and/or modify the files in the
- fs_selfservice/ directory (but not the rest of the software) under the
- terms of the GNU General Public License as published by the Free Software
- Foundation, either version 3 of the License, or (at your option) any later
- version.
+ fs_selfservice/ and ng_selfservice/ directories (but not the rest of the
+ software) under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or (at your
+ option) any later version.
<P>
At your option, you may also redistribute and/or modify the
diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi
index 7343a9d16..e9d131a78 100755
--- a/httemplate/edit/cust_main.cgi
+++ b/httemplate/edit/cust_main.cgi
@@ -84,13 +84,12 @@
&>
% unless ($conf->exists('invoice-ship_address')) { #it's always on, so hide per-cust config
<TR>
- <TD>&nbsp;</TD>
- <TD COLSPAN="7">
+ <TD COLSPAN="8">
<% include('/elements/checkbox.html',
'field' => 'invoice_ship_address',
'value' => 'Y',
'curr_value' => $cust_main->invoice_ship_address,
- 'postfix' => emt('included on invoices'),
+ 'postfix' => emt('Include service address on invoices'),
) %>
</TD>
</TR>
@@ -113,7 +112,7 @@ function samechanged(what) {
t1.style.visibility = 'visible'
}
}
-//samechanged(document.getElementById('same'));
+samechanged(document.getElementById('same'));
</SCRIPT>
<BR>
diff --git a/httemplate/edit/cust_main/top_misc.html b/httemplate/edit/cust_main/top_misc.html
index 8f8a96a0c..c19f347c3 100644
--- a/httemplate/edit/cust_main/top_misc.html
+++ b/httemplate/edit/cust_main/top_misc.html
@@ -40,7 +40,7 @@
var ship_locked_agents = <% encode_json(\%ship_locked_agents) %>;
var ship_fields = [
- 'locationname', 'address1', 'city', 'state', 'zip', 'country',
+ 'locationname', 'address1',<% $conf->exists('cust_main-no_city_in_address') ? '' : q( 'city',) %> 'state', 'zip', 'country',
'latitude', 'longitude', 'district'
];
@@ -276,7 +276,10 @@ foreach (qsearch('agent',{})) {
my $agent_ship_location = $cust_main->ship_location;
$ship_locked_agents{$agentnum} = +{
map { $_ => $agent_ship_location->$_ }
- qw(locationname address1 city state zip country latitude longitude district)
+ (
+ qw(locationname address1 state zip country latitude longitude district),
+ ($conf->exists('cust_main-no_city_in_address') ? () : 'city')
+ )
};
}
diff --git a/httemplate/edit/cust_pay.cgi b/httemplate/edit/cust_pay.cgi
index 888335fbb..5d74365e7 100755
--- a/httemplate/edit/cust_pay.cgi
+++ b/httemplate/edit/cust_pay.cgi
@@ -87,15 +87,17 @@
<TD ALIGN="right"><% mt('Auto-apply to invoices') |h %></TD>
<TD COLSPAN=2>
<SELECT NAME="apply">
- <OPTION VALUE="yes" SELECTED><% mt('yes') |h %>
- <OPTION><% mt('no') |h %></SELECT>
- </TD>
+ <OPTION VALUE="yes" SELECTED><% mt('yes') |h %></OPTION>
+ <OPTION VALUE=""><% mt('not now') |h %></OPTION>
+ <OPTION VALUE="never"><% mt('never') |h %></OPTION>
+ </SELECT>
+ </TD>
% } elsif ( $link eq 'invnum' ) {
<TD ALIGN="right"><% mt('Apply to') |h %></TD>
<TD COLSPAN=2 BGCOLOR="#ffffff">Invoice #<B><% $linknum %></B> only</TD>
- <INPUT TYPE="hidden" NAME="apply" VALUE="no">
+ <INPUT TYPE="hidden" NAME="apply" VALUE="">
% }
</TR>
diff --git a/httemplate/edit/invoice_conf.html b/httemplate/edit/invoice_conf.html
index 7122653f2..861114b1c 100644
--- a/httemplate/edit/invoice_conf.html
+++ b/httemplate/edit/invoice_conf.html
@@ -49,6 +49,7 @@ my @fields = (
'Footer',
'Summary header',
'Return address',
+ 'Watermark',
'Small footer',
'Enable coupon',
),
@@ -59,6 +60,7 @@ my @fields = (
{ field => 'latexfooter', %textarea },
{ field => 'latexsummary', %textarea },
{ field => 'latexreturnaddress', %textarea },
+ { field => 'latexwatermark', %textarea },
{ field => 'latexsmallfooter', %textarea },
{ field => 'with_latexcoupon', type => 'checkbox', value => 'Y' },
@@ -68,6 +70,7 @@ my @fields = (
{ field => 'htmlfooter', %textarea },
{ field => 'htmlsummary', %textarea },
{ field => 'htmlreturnaddress', %textarea },
+ { field => 'htmlwatermark', %textarea },
# logo
{ type => 'columnend' },
@@ -87,12 +90,14 @@ my %labels = (
latexfooter
latexsummary
latexreturnaddress
+ latexwatermark
with_latexcoupon
latexsmallfooter
htmlnotes
htmlfooter
htmlsummary
htmlreturnaddress
+ htmlwatermark
) ),
);
diff --git a/httemplate/edit/part_device.html b/httemplate/edit/part_device.html
index 1317c8d1c..75e6b0aa5 100644
--- a/httemplate/edit/part_device.html
+++ b/httemplate/edit/part_device.html
@@ -5,6 +5,7 @@
'devicepart' => 'Part number',
'devicename' => 'Device name',
'inventory_classnum' => 'Inventory class',
+ 'title' => 'External name',
},
'fields' => \@fields,
'viewall_dir' => 'browse',
@@ -34,6 +35,7 @@ foreach my $inventory_class ( @inventory_classes ) {
my @fields;
push @fields, 'devicename',
+ 'title',
{ field => 'inventory_classnum',
type => 'select',
options => \@inventory_classnums,
diff --git a/httemplate/edit/process/change-cust_pkg.html b/httemplate/edit/process/change-cust_pkg.html
index 96175e1e4..046a9795c 100644
--- a/httemplate/edit/process/change-cust_pkg.html
+++ b/httemplate/edit/process/change-cust_pkg.html
@@ -41,34 +41,43 @@ if ( $cgi->param('locationnum') == -1 ) {
}
my $error;
-if ( $cgi->param('delay') ) {
- my $date = parse_datetime($cgi->param('start_date'));
- if (!$date) {
- $error = "Invalid change date '".$cgi->param('start_date')."'.";
- } elsif ( $date < time ) {
- $error = "Change date ".$cgi->param('start_date')." is in the past.";
- } else {
- # schedule the change
- $change{'start_date'} = $date;
- $error = $cust_pkg->change_later(\%change);
- }
-} else {
- # special case: if there's a package change scheduled, and it matches
- # the parameters the user requested this time, then change to the existing
- # future package.
- if ( $cust_pkg->change_to_pkgnum ) {
- my $change_to = FS::cust_pkg->by_key($cust_pkg->change_to_pkgnum);
- if ( $change_to->pkgpart == $change{'pkgpart'} and
- $change_to->locationnum == $change{'locationnum'} ) {
-
- %change = ( 'cust_pkg' => $change_to );
+my $now = time;
+if (defined($cgi->param('contract_end'))) {
+ $change{'contract_end'} = parse_datetime($cgi->param('contract_end'));
+}
+unless ($error) {
+ if ( $cgi->param('delay') ) {
+ my $date = parse_datetime($cgi->param('start_date'));
+ if (!$date) {
+ $error = "Invalid change date '".$cgi->param('start_date')."'.";
+ } elsif ( $date < $now ) {
+ $error = "Change date ".$cgi->param('start_date')." is in the past.";
+ } else {
+ # schedule the change
+ $change{'start_date'} = $date;
+ $error = $cust_pkg->change_later(\%change);
+ }
+ } else {
+ # special case: if there's a package change scheduled, and it matches
+ # the parameters the user requested this time, then change to the existing
+ # future package.
+ if ( $cust_pkg->change_to_pkgnum ) {
+ my $change_to = FS::cust_pkg->by_key($cust_pkg->change_to_pkgnum);
+ if (
+ $change_to->pkgpart == $change{'pkgpart'} and
+ $change_to->locationnum == $change{'locationnum'} and
+ $change_to->quantity == $change{'quantity'} and
+ $change_to->contract_end == $change{'contract_end'}
+ ) {
+ %change = ( 'cust_pkg' => $change_to );
+ }
}
- }
- # do a package change right now
- my $pkg_or_error = $cust_pkg->change( \%change );
- $error = ref($pkg_or_error) ? '' : $pkg_or_error;
+ # do a package change right now
+ my $pkg_or_error = $cust_pkg->change( \%change );
+ $error = ref($pkg_or_error) ? '' : $pkg_or_error;
+ }
}
</%init>
diff --git a/httemplate/edit/process/cust_pay-no_auto_apply.cgi b/httemplate/edit/process/cust_pay-no_auto_apply.cgi
new file mode 100644
index 000000000..ccbd2d7b5
--- /dev/null
+++ b/httemplate/edit/process/cust_pay-no_auto_apply.cgi
@@ -0,0 +1,48 @@
+<%doc>
+Quick process for toggling no_auto_apply field in cust_pay.
+
+Requires paynum and no_auto_apply ('Y' or '') in cgi.
+
+Requires 'Apply payment' acl.
+</%doc>
+
+% if ($error) {
+
+<P STYLE="color: #FF0000"><% emt($error) %></P>
+
+% } else {
+
+<P STYLE="font-weight: bold;"><% emt($message) %></P>
+<P><% emt('Please wait while the page reloads.') %></P>
+<SCRIPT TYPE="text/javascript">
+window.top.location.reload();
+</SCRIPT>
+
+% }
+
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Apply payment');
+
+my $paynum = $cgi->param('paynum');
+my $noauto = $cgi->param('no_auto_apply');
+
+my $error = '';
+my $message = '';
+my $cust_pay = qsearchs('cust_pay',{ paynum => $paynum });
+if ($cust_pay) {
+ if (($noauto eq 'Y') || (defined($noauto) && (length($noauto) == 0))) {
+ $cust_pay->no_auto_apply($noauto);
+ $error = $cust_pay->replace;
+ $message = $noauto ?
+ q(Payment will not be automatically applied to open invoices, must be applied manually) :
+ q(Payment will be automatically applied to open invoices the next time this customer's payments are processed);
+ } else {
+ $error = 'no_auto_apply not specified';
+ }
+} else {
+ $error .= 'Payment could not be found in database';
+}
+
+
+</%init>
diff --git a/httemplate/edit/process/cust_pay.cgi b/httemplate/edit/process/cust_pay.cgi
index a002fa181..56d3f2ff1 100755
--- a/httemplate/edit/process/cust_pay.cgi
+++ b/httemplate/edit/process/cust_pay.cgi
@@ -50,6 +50,7 @@ else {
my $new = new FS::cust_pay ( {
$field => $linknum,
_date => $_date,
+ no_auto_apply => ($cgi->param('apply') eq 'never') ? 'Y' : '',
map {
$_, scalar($cgi->param($_));
} qw( paid payby payinfo paybatch
diff --git a/httemplate/edit/process/part_pkg.cgi b/httemplate/edit/process/part_pkg.cgi
index 8e8be853d..3ffd5fc23 100755
--- a/httemplate/edit/process/part_pkg.cgi
+++ b/httemplate/edit/process/part_pkg.cgi
@@ -128,8 +128,11 @@ my $args_callback = sub {
my @svcparts = map { $_->svcpart } qsearch('part_svc', {});
my %pkg_svc = map { $_ => scalar($cgi->param("pkg_svc$_")) } @svcparts;
my %hidden_svc = map { $_ => scalar($cgi->param("hidden$_")) } @svcparts;
+ my %provision_hold = map { $_ => scalar($cgi->param("provision_hold$_" )) } @svcparts;
- push @args, 'pkg_svc' => \%pkg_svc, 'hidden_svc' => \%hidden_svc;
+ push @args, 'pkg_svc' => \%pkg_svc,
+ 'hidden_svc' => \%hidden_svc,
+ 'provision_hold' => \%provision_hold;
###
# cust_pkg and custnum_ref (inserts only)
diff --git a/httemplate/edit/process/svc_circuit.html b/httemplate/edit/process/svc_circuit.html
index 27f43db38..318aaa4df 100644
--- a/httemplate/edit/process/svc_circuit.html
+++ b/httemplate/edit/process/svc_circuit.html
@@ -1,7 +1,7 @@
<& elements/svc_Common.html,
table => 'svc_circuit',
- edit_ext => 'html',
- redirect => popurl(3)."view/svc_circuit.html?",
+ edit_ext => 'cgi',
+ redirect => popurl(3)."view/svc_circuit.cgi?",
precheck_callback => $precheck,
&>
<%init>
diff --git a/httemplate/elements/bill.html b/httemplate/elements/bill.html
index 64a1a6d2c..420a7489b 100644
--- a/httemplate/elements/bill.html
+++ b/httemplate/elements/bill.html
@@ -45,7 +45,7 @@ my $label = $opt{'label'};
# formname no longer needs to be passed from outside, but we still
# need one and it needs to be unique
my $formname = $opt{'formname'} ||
- 'bill'.sprintf('%04d',int(rand(10000))).$custnum;
+ 'bill'.sprintf('%04d',random_id(4)).$custnum;
my $url = $opt{'url'} || '';
my $message = $opt{'message'} || 'Finished!';
my $bill_opts = $opt{'bill_opts'} || {};
diff --git a/httemplate/elements/city.html b/httemplate/elements/city.html
index 5f4d4e09e..4e9a60940 100644
--- a/httemplate/elements/city.html
+++ b/httemplate/elements/city.html
@@ -159,6 +159,11 @@ my $disable_select = 1 if $conf->config('tax_district_method');
$opt{'disable_empty'} = 1 unless exists($opt{'disable_empty'});
+if ($conf->exists('cust_main-no_city_in_address')) {
+ $opt{'disable_text'} = 1;
+ $disable_select = 1;
+}
+
my $text_style = $opt{'style'} ? [ @{ $opt{'style'} } ] : [];
my $select_style = $opt{'style'} ? [ @{ $opt{'style'} } ] : [];
diff --git a/httemplate/elements/columnstart.html b/httemplate/elements/columnstart.html
index 1ffbcb9e8..245c308a7 100644
--- a/httemplate/elements/columnstart.html
+++ b/httemplate/elements/columnstart.html
@@ -10,7 +10,7 @@
Pass 'aligned' => 1 to have corresponding rows in the columns line up.
</%doc>
-% my $id = sprintf('table%08d', rand(100000000));
+% my $id = sprintf('table%08d', random_id(8));
<TR>
<TD CLASS="background" COLSPAN=99>
<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=0 id="<%$id%>">
diff --git a/httemplate/elements/create_uri_query b/httemplate/elements/create_uri_query
index ce6249e0e..414d53ba4 100644
--- a/httemplate/elements/create_uri_query
+++ b/httemplate/elements/create_uri_query
@@ -18,7 +18,7 @@ my $query = $cgi->query_string;
if ( length($query) > 1920 || $opt{secure} ) { #stupid IE 2083 URL limit
- my $session = int(rand(4294967296)); #XXX
+ my $session = random_id(9);
my $pref = new FS::access_user_pref({
'usernum' => $FS::CurrentUser::CurrentUser->usernum,
'prefname' => "redirect$session",
diff --git a/httemplate/elements/customer-statement.html b/httemplate/elements/customer-statement.html
new file mode 100644
index 000000000..63c21cba3
--- /dev/null
+++ b/httemplate/elements/customer-statement.html
@@ -0,0 +1,45 @@
+<%doc>
+
+Formats customer payment history into a table.
+
+ include('/elements/customer-statement.html',
+ 'history' => \@history
+ );
+
+Option 'history' should be of the form returned by $cust_main->payment_history.
+This element might be used directly by selfservice, so it does not (and should not)
+pull data from the database.
+
+</%doc>
+
+% my $style = 'text-align: left; margin: 0; padding: 0 1em 0 0;';
+% my $moneystyle = 'text-align: right; margin: 0; padding: 0 1em 0 0;';
+
+<TABLE STYLE="margin: 0;" CELLSPACING="0">
+ <TR>
+ <TH STYLE="<% $style %> background: #ff9999;">Date</TH>
+ <TH STYLE="<% $style %> background: #ff9999;">Description</TH>
+ <TH STYLE="<% $moneystyle %> background: #ff9999;">Amount</TH>
+ <TH STYLE="<% $moneystyle %> background: #ff9999;">Balance</TH>
+ </TR>
+
+% my $col1 = "#ffffff";
+% my $col2 = "#dddddd";
+% my $col = $col1;
+% foreach my $item (@{$opt{'history'}}) {
+ <TR>
+ <TD STYLE="<% $style %> background: <% $col %>;"><% $$item{'date_pretty'} %></TD>
+ <TD STYLE="<% $style %> background: <% $col %>;"><% $$item{'description'} %></TD>
+ <TD STYLE="<% $moneystyle %> background: <% $col %>;"><% $$item{'amount_pretty'} %></TD>
+ <TD STYLE="<% $moneystyle %> background: <% $col %>;"><% $$item{'balance_pretty'} %></TD>
+ </TR>
+% $col = $col eq $col1 ? $col2 : $col1;
+% }
+
+</TABLE>
+
+<%init>
+my %opt = @_;
+
+die "Invalid type for history" unless ref($opt{'history'}) eq 'ARRAY';
+</%init>
diff --git a/httemplate/elements/customer-table.html b/httemplate/elements/customer-table.html
index 83abad010..76a7f12f6 100644
--- a/httemplate/elements/customer-table.html
+++ b/httemplate/elements/customer-table.html
@@ -37,6 +37,34 @@ Example:
)
+Some incomplete notes for javascript programmers:
+
+On page load, existing rows are initialized by passing values to addRow
+based on existing cgi values. An empty row (marked with the 'emptyrow'
+attribute) is created by invoking addRow without values. After that,
+to keep the non-empty row count (totalrows) accurate, use newEmptyRow to
+create the next row. There should only be one empty row at a time.
+
+Global vars:
+total_el - element for displaying total number of rows
+totalrows - total number of non-empty rows
+rownum - really more of a "next row" value, used by addRow
+allrows - array of tr elements, one for each row
+
+Don't confuse the global rownum with the element attribute rownum
+that is set as a reference point on some of the elements generated
+by this script. They have different values.
+
+Some of the functions:
+updateTotalRow() - updates total_el based on value of totalrows
+addDeleteButton(searchrow) - adds delete button to searchrow
+newEmptyRow() - replaces old empty row
+deleteRow() - removes the row specified by this.rownum
+addRow(values) - adds a new row (marked as empty if values aren't specified)
+
+This mason element is currently only used by misc/batch-cust_pay.html,
+and probably should be cleaned up more before being used by anything else.
+
</%doc>
<SCRIPT TYPE="text/javascript">
@@ -112,7 +140,7 @@ Example:
}
}
- function <% $opt{prefix} %>search_invnum() {
+ function search_invnum() {
this.style.color = '#000000'
@@ -128,8 +156,8 @@ Example:
return;
}
- if ( ( <% $opt{prefix} %>rownum - searchrow ) == 1 ) {
- <% $opt{prefix} %>addRow();
+ if ( document.getElementById('row'+searchrow).emptyrow ) {
+ newEmptyRow(searchrow);
}
var customer = document.getElementById('customer'+searchrow);
customer.value = 'searching...';
@@ -153,7 +181,7 @@ Example:
update_customer(searchrow, customerArray);
% if ( $opt{invnum_update_callback} ) {
- <% $opt{invnum_update_callback} %>(searchrow, '<% $opt{prefix} %>')
+ <% $opt{invnum_update_callback} %>(searchrow)
% }
}
@@ -162,7 +190,7 @@ Example:
}
- function <% $opt{prefix} %>search_custnum() {
+ function search_custnum() {
this.style.color = '#000000'
@@ -179,8 +207,8 @@ Example:
return;
}
- if ( ( <% $opt{prefix} %>rownum - searchrow ) == 1 ) {
- <% $opt{prefix} %>addRow();
+ if ( document.getElementById('row'+searchrow).emptyrow ) {
+ newEmptyRow(searchrow);
}
var customer_obj = document.getElementById('customer'+searchrow);
@@ -214,7 +242,7 @@ Example:
update_customer(searchrow, customerArrayArray[0]);
% if ( $opt{custnum_update_callback} ) {
- <% $opt{custnum_update_callback} %>(searchrow, '<% $opt{prefix} %>')
+ <% $opt{custnum_update_callback} %>(searchrow)
% }
} else {
@@ -248,7 +276,7 @@ Example:
}
- function <% $opt{prefix} %>search_customer() {
+ function search_customer() {
var customer_obj = this;
var searchrow = this.getAttribute('rownum');
@@ -262,8 +290,8 @@ Example:
return;
}
- if ( ( <% $opt{prefix} %>rownum - searchrow ) == 1 ) {
- <% $opt{prefix} %>addRow();
+ if ( document.getElementById('row'+searchrow).emptyrow ) {
+ newEmptyRow(searchrow);
}
var invnum = document.getElementById('invnum'+searchrow);
@@ -292,7 +320,7 @@ Example:
update_customer(searchrow, customerArrayArray[0]);
% if ( $opt{custnum_update_callback} ) {
- <% $opt{custnum_update_callback} %>(searchrow, '<% $opt{prefix} %>')
+ <% $opt{custnum_update_callback} %>(searchrow)
% }
} else {
@@ -355,7 +383,7 @@ Example:
update_customer(searchrow, JSON.parse(custnum_balance_status));
% if ( $opt{custnum_update_callback} ) {
- <% $opt{custnum_update_callback} %>(searchrow, '<% $opt{prefix} %>')
+ <% $opt{custnum_update_callback} %>(searchrow)
% }
}
@@ -385,255 +413,104 @@ Example:
}
function update_num_open(rownum, newval) {
+ document.getElementById('num_open'+rownum).value = newval;
num_open_invoices[rownum] = newval;
}
+ // updates display of total rows based on value of totalrows
+ function updateTotalRow () {
+ if ( totalrows == 1 ) {
+ total_el.innerHTML =
+ 'Total '
+ + totalrows
+ + ' <% $opt{name_singular} || 'customer' %>';
+ } else {
+ total_el.innerHTML =
+ 'Total '
+ + totalrows
+ + ' <% PL($opt{name_singular} || 'customer') %>';
+ }
+ }
-</SCRIPT>
-
-<TABLE ID="<% $opt{prefix} %>OneTrueTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
-
-<TR>
- <TH>Inv #</TH>
- <TH>Cust #</TH>
- <TH>Status</TH>
- <TH>Customer</TH>
- <TH>Balance</TH>
-% foreach my $header ( @{$opt{header}} ) {
- <TH><% $header %></TH>
-% }
-</TR>
-% my $row = 0;
-% for ( $row = 0; exists($param->{"custnum$row"}); $row++ ) {
-
- <TR id="row<%$row%>" rownum="<%$row%>">
- <TD>
- <INPUT TYPE = "text"
- NAME = "invnum<% $row %>"
- ID = "invnum<% $row %>"
- SIZE = 8
- MAXLENGTH = 12
- STYLE = "text-align:right;"
- VALUE = "<% $param->{"invnum$row"} %>"
- rownum = "<% $row %>"
- >
- <SCRIPT TYPE="text/javascript">
- var invnum_input<% $row %> = document.getElementById("invnum<% $row %>");
- invnum_input<% $row %>.onfocus = clearhint_invnum;
- invnum_input<% $row %>.onchange = <% $opt{prefix} %>search_invnum;
- </SCRIPT>
- </TD>
-
- <TD>
- <INPUT TYPE = "text"
- NAME = "display_custnum<% $row %>"
- ID = "display_custnum<% $row %>"
- SIZE = 8
- MAXLENGTH = 12
- STYLE = "text-align:right;"
- VALUE = "<% $param->{"display_custnum$row"} %>"
- rownum = "<% $row %>"
- >
- <INPUT TYPE = "hidden"
- NAME = "custnum<% $row %>"
- ID = "custnum<% $row %>"
- VALUE = "<% $param->{"custnum$row"} %>"
- rownum = "<% $row %>"
- >
- <SCRIPT TYPE="text/javascript">
- var custnum_input<% $row %> = document.getElementById("display_custnum<% $row %>");
- custnum_input<% $row %>.onfocus = clearhint_custnum;
- custnum_input<% $row %>.onchange = <% $opt{prefix} %>search_custnum;
- </SCRIPT>
- </TD>
-
- <TD STYLE="text-align: center">
- <SPAN
- ID = "status<% $row %>_text"
- rownum = "<% $row %>"
- STYLE = "font-weight: bold;
- color: <%$param->{"statuscolor$row"} || '#000000'%>"
-
- ><% $param->{"status$row"} %></SPAN>
- <INPUT TYPE = "hidden"
- NAME = "status<% $row %>"
- ID = "status<% $row %>"
- VALUE = "<% $param->{"status$row"} %>"
- rownum = "<% $row %>"
- >
- <INPUT TYPE = "hidden"
- NAME = "statuscolor<% $row %>"
- ID = "statuscolor<% $row %>"
- VALUE = "<% $param->{"statuscolor$row"} %>"
- rownum = "<% $row %>"
- >
- </TD>
-
- <TD>
- <INPUT TYPE="text" NAME="customer<% $row %>" ID="customer<% $row %>" SIZE=64 VALUE="<% $param->{"customer$row"} %>" rownum="<% $row %>">
- <SCRIPT TYPE="text/javascript">
- var customer_input<% $row %> = document.getElementById("customer<% $row %>");
- customer_input<% $row %>.onfocus = clearhint_customer;
- customer_input<% $row %>.onclick = clearhint_customer;
- customer_input<% $row %>.onchange = <% $opt{prefix} %>search_customer;
- </SCRIPT>
- <SELECT NAME="cust_select<% $row %>" ID="cust_select<% $row %>" rownum="<% $row %>" STYLE="color:#ff0000; display:none">
- </SELECT>
- <SCRIPT TYPE="text/javascript">
- var customer_select<% $row %> = document.getElementById("cust_select<% $row %>");
- customer_select<% $row %>.onchange = select_customer;
- </SCRIPT>
- </TD>
-
- <TD STYLE="text-align:right">
- <% $money_char %>
- <SPAN
- ID = "balance<% $row %>_text"
- rownum = "<% $row %>"
- ><% $param->{"balance$row"} %></SPAN>
- &nbsp;
- <INPUT TYPE = "hidden"
- NAME = "balance<% $row %>"
- ID = "balance<% $row %>"
- VALUE = "<% $param->{"balance$row"} %>"
- rownum = "<% $row %>"
- >
- </TD>
+ var total_el, rownum, totalrows, allrows;
+
+ function addDeleteButton (searchrow) {
+ var td_delete = document.getElementById('delete'+searchrow);
+ var button_delete = document.createElement('INPUT');
+ button_delete.setAttribute('rownum', searchrow);
+ button_delete.setAttribute('type', 'button');
+ button_delete.setAttribute('value', 'X');
+ button_delete.onclick = deleteRow;
+ button_delete.style.color = '#ff0000';
+ button_delete.style.fontWeight = 'bold';
+ button_delete.style.paddingLeft = '2px';
+ button_delete.style.paddingRight = '2px';
+ td_delete.appendChild(button_delete);
+ }
-% my $col = 0;
-% foreach my $field ( @{$opt{fields}} ) {
-% my $value;
-% if ( ref($field) eq 'CODE' ) {
-% $value = &{$field}($row,$param);
-% } else {
-% $value = $param->{"$field$row"};
-% }
-% my $name = (ref($field) eq 'CODE') ? "column${col}_$row" : "$field$row";
-% my $align = $align{ $opt{align}->[$col] || 'l' };
-% my $size = $sizes->[$col] || 10;
-% my $color = $opt{color}->[$col];
-% my $font = $color ? qq(<FONT COLOR="$color">) : '';
-% my $onchange = '';
-% if ( $opt{onchange}->[$col] ) {
-% $onchange = 'onchange="'.$opt{onchange}->[$col].'"';
-% }
-% elsif ( $opt{footer}->[$col] eq '_TOTAL' ) {
-% $total[$col] += $value;
-% $onchange = $opt{prefix}. "calc_total$col();";
-% $onchange = qq(onchange="$onchange" onkeyup="$onchange");
-% }
- <TD ALIGN="<% $align %>">
-% my $type = $types->[$col] || 'text';
-% if ($type eq 'text' or $type eq 'checkbox') {
- <INPUT TYPE = "<% $type %>"
- NAME = "<% $name %>"
- ID = "<% $name %>"
- SIZE = "<% $size %>"
- STYLE = "text-align: <% $align %>;"
- VALUE = "<% $value %>"
- rownum = "<% $row %>"
- <% $onchange %>
- >
-% } elsif ($types->[$col] eq 'immutable') {
- <% $font %><% $value %><% $font ? '</FONT>' : '' %>
- <INPUT TYPE="hidden" ID="<% $name %>" NAME="<% $name %>" VALUE="<% $value %>" >
-% } else {
- Cannot represent unknown type: <% $types->[$col] %>
-% }
- </TD>
-% $col++;
-% }
- </TR>
-% }
+ function newEmptyRow (searchrow) {
+ // add delete button to current row
+ addDeleteButton(searchrow);
+ // mark current row as non-empty
+ var oldemptyrow = document.getElementById('row'+searchrow);
+ oldemptyrow.emptyrow = false;
+ // update totalrows
+ totalrows++
+ updateTotalRow();
+ // add a new empty row
+ addRow();
+ }
-<TR id="row_total">
- <TH COLSPAN=5 ID="<% $opt{'prefix'} %>_TOTAL_TOTAL">
- Total <% $row ? $row-1 : 0 %>
- <% PL($opt{name_singular} || 'customer', ( $row ? $row-1 : 0 ) ) %>
- </TH>
-% my $col = 0;
-% foreach my $footer ( @{$opt{footer}} ) {
-% my $align = $align{ $opt{'footer_align'}->[$col] || 'c' };
-% if ($footer eq '_TOTAL' ) {
-% my $id = $opt{'fields'}->[$col];
-% $id = ref($id) ? "column${col}_TOTAL" : "${id}_TOTAL";
- <TH ALIGN="<% $align %>" ID="<% $id %>">&nbsp;<% sprintf('%.2f', $total[$col] ) %></TH>
-% } else {
- <TH ALIGN="<% $align %>"><% $footer %></TH>
-% }
-% $col++;
+ function deleteRow() {
+ var thisrownum = this.getAttribute('rownum');
+% if ( $opt{delete_row_callback} ) {
+ // callback
+ <% $opt{delete_row_callback} %>(thisrownum);
% }
-</TR>
-
-</TABLE>
-
-<SCRIPT TYPE="text/javascript">
+ // remove the actual row
+ var thisrow = document.getElementById('row'+thisrownum);
+ thisrow.parentNode.removeChild(thisrow);
+ // remove row from tally of all rows
+ var newrows = [];
+ for (i = 0; i < allrows.length; i++) {
+ if (allrows[i] == thisrownum) continue;
+ newrows.push(allrows[i]);
+ }
+ allrows = newrows;
+ totalrows--; // should never be deleting empty rows
+ updateTotalRow();
+ // recalculate column totals, if any
% my $col = 0;
% foreach my $footer ( @{$opt{footer}} ) {
% if ($footer eq '_TOTAL' ) {
-% my $name = $opt{fields}->[$col];
-% $name = ref($name) ? "column$col" : $name;
- var <% $opt{prefix}.$name %>_CACHE = new Array ();
- var <% $opt{prefix} %>th_el = document.getElementById("<%$name%>_TOTAL");
- function <% $opt{prefix} %>calc_total<% $col %>() {
- var row = 0;
- var total = 0;
- for ( var row = 0;
-
- ( <% $opt{prefix}.$name%>_CACHE[row] =
- <% $opt{prefix}.$name%>_CACHE[row]
- || document.getElementById("<%$name%>"+row)
- ) != null;
-
- row++
- )
- {
- var value = <%$name%>_CACHE[row].value;
- value = parseFloat(value);
- if ( ! isNaN(value) ) {
- total = total + value;
- }
- }
- <% $opt{prefix} %>th_el.innerHTML = '&nbsp;' + total.toFixed(2);
-
- }
+ calc_total<% $col %>()
% }
% $col++;
% }
-</SCRIPT>
-
-<% include('/elements/xmlhttp.html',
- 'url' => $p. 'misc/xmlhttp-cust_main-search.cgi',
- 'subs' => [qw( custnum_search smart_search invnum_search )],
- )
-%>
-
-<SCRIPT TYPE="text/javascript">
-
- var <% $opt{prefix} %>total_el =
- document.getElementById("<% $opt{'prefix'} %>_TOTAL_TOTAL");
-
- var <% $opt{prefix} %>rownum = <% $row %>;
+ }
- function <% $opt{prefix} %>addRow() {
+ function addRow(values) {
- var table = document.getElementById('<% $opt{prefix} %>OneTrueTable');
+ var table = document.getElementById('OneTrueTable');
var tablebody = table.getElementsByTagName('tbody').item(0);
var row = table.insertRow(table.rows.length - 1);
- row.setAttribute('id', 'row'+rownum);
+ var thisrownum = values ? values.rownum : rownum;
+ row.setAttribute('id', 'row'+thisrownum);
+ row.emptyrow = values ? false : true;
var invnum_cell = document.createElement('TD');
var invnum_input = document.createElement('INPUT');
- invnum_input.setAttribute('name', 'invnum'+<% $opt{prefix} %>rownum);
- invnum_input.setAttribute('id', 'invnum'+<% $opt{prefix} %>rownum);
+ invnum_input.setAttribute('name', 'invnum'+thisrownum);
+ invnum_input.setAttribute('id', 'invnum'+thisrownum);
invnum_input.style.textAlign = 'right';
invnum_input.setAttribute('size', 8);
invnum_input.setAttribute('maxlength', 12);
- invnum_input.setAttribute('rownum', <% $opt{prefix} %>rownum);
+ invnum_input.setAttribute('rownum', thisrownum);
+ invnum_input.value = values ? values.invnum : '';
invnum_input.onfocus = clearhint_invnum;
- invnum_input.onchange = <% $opt{prefix} %>search_invnum;
+ invnum_input.onchange = search_invnum;
invnum_cell.appendChild(invnum_input);
row.appendChild(invnum_cell);
@@ -641,21 +518,23 @@ Example:
var custnum_cell = document.createElement('TD');
var display_custnum_input = document.createElement('INPUT');
- display_custnum_input.setAttribute('name', 'display_custnum'+<% $opt{prefix} %>rownum);
- display_custnum_input.setAttribute('id', 'display_custnum'+<% $opt{prefix} %>rownum);
+ display_custnum_input.setAttribute('name', 'display_custnum'+thisrownum);
+ display_custnum_input.setAttribute('id', 'display_custnum'+thisrownum);
display_custnum_input.style.textAlign = 'right';
display_custnum_input.setAttribute('size', 8);
display_custnum_input.setAttribute('maxlength', 12);
- display_custnum_input.setAttribute('rownum', <% $opt{prefix} %>rownum);
+ display_custnum_input.setAttribute('rownum', thisrownum);
+ display_custnum_input.value = values ? values.custnum : '';
display_custnum_input.onfocus = clearhint_custnum;
- display_custnum_input.onchange = <% $opt{prefix} %>search_custnum;
+ display_custnum_input.onchange = search_custnum;
custnum_cell.appendChild(display_custnum_input);
var custnum_input = document.createElement('INPUT');
custnum_input.type = 'hidden';
- custnum_input.setAttribute('name', 'custnum'+<% $opt{prefix} %>rownum);
- custnum_input.setAttribute('id', 'custnum'+<% $opt{prefix} %>rownum);
- custnum_input.setAttribute('rownum', <% $opt{prefix} %>rownum);
+ custnum_input.setAttribute('name', 'custnum'+thisrownum);
+ custnum_input.setAttribute('id', 'custnum'+thisrownum);
+ custnum_input.setAttribute('rownum', thisrownum);
+ custnum_input.value = values ? values.custnum : '';
custnum_cell.appendChild(custnum_input);
row.appendChild(custnum_cell);
@@ -664,23 +543,29 @@ Example:
status_cell.style.textAlign = 'center';
var status_span = document.createElement('SPAN');
- status_span.setAttribute('id', 'status'+<% $opt{prefix} %>rownum+'_text');
+ status_span.setAttribute('id', 'status'+thisrownum+'_text');
status_span.style.fontWeight = 'bold';
- status_span.setAttribute('rownum', <% $opt{prefix} %>rownum);
+ status_span.style.color = values ? values.statuscolor : '';
+ status_span.setAttribute('rownum', thisrownum);
+ status_span.appendChild(
+ document.createTextNode(values ? values.status : '')
+ );
status_cell.appendChild(status_span);
var status_input = document.createElement('INPUT');
status_input.setAttribute('type', 'hidden');
- status_input.setAttribute('name', 'status'+<% $opt{prefix} %>rownum);
- status_input.setAttribute('id', 'status'+<% $opt{prefix} %>rownum);
- status_input.setAttribute('rownum', <% $opt{prefix} %>rownum);
+ status_input.setAttribute('name', 'status'+thisrownum);
+ status_input.setAttribute('id', 'status'+thisrownum);
+ status_input.setAttribute('rownum', thisrownum);
+ status_input.value = values ? values.status : '';
status_cell.appendChild(status_input);
var statuscolor_input = document.createElement('INPUT');
statuscolor_input.setAttribute('type', 'hidden');
- statuscolor_input.setAttribute('name', 'statuscolor'+<% $opt{prefix} %>rownum);
- statuscolor_input.setAttribute('id', 'statuscolor'+<% $opt{prefix} %>rownum);
- statuscolor_input.setAttribute('rownum', <% $opt{prefix} %>rownum);
+ statuscolor_input.setAttribute('name', 'statuscolor'+thisrownum);
+ statuscolor_input.setAttribute('id', 'statuscolor'+thisrownum);
+ statuscolor_input.setAttribute('rownum', thisrownum);
+ statuscolor_input.value = values ? values.statuscolor : '';
status_cell.appendChild(statuscolor_input);
row.appendChild(status_cell);
@@ -688,20 +573,21 @@ Example:
var customer_cell = document.createElement('TD');
var customer_input = document.createElement('INPUT');
- customer_input.setAttribute('name', 'customer'+<% $opt{prefix} %>rownum);
- customer_input.setAttribute('id', 'customer'+<% $opt{prefix} %>rownum);
+ customer_input.setAttribute('name', 'customer'+thisrownum);
+ customer_input.setAttribute('id', 'customer'+thisrownum);
customer_input.setAttribute('size', 64);
customer_input.setAttribute('value', '(last name or company)' );
- customer_input.setAttribute('rownum', <% $opt{prefix} %>rownum);
+ customer_input.setAttribute('rownum', thisrownum);
+ customer_input.value = values ? values.customer : '';
customer_input.onfocus = clearhint_customer;
customer_input.onclick = clearhint_customer;
- customer_input.onchange = <% $opt{prefix} %>search_customer;
+ customer_input.onchange = search_customer;
customer_cell.appendChild(customer_input);
var customer_select = document.createElement('SELECT');
- customer_select.setAttribute('name', 'cust_select'+<% $opt{prefix} %>rownum);
- customer_select.setAttribute('id', 'cust_select'+<% $opt{prefix} %>rownum);
- customer_select.setAttribute('rownum', <% $opt{prefix} %>rownum);
+ customer_select.setAttribute('name', 'cust_select'+thisrownum);
+ customer_select.setAttribute('id', 'cust_select'+thisrownum);
+ customer_select.setAttribute('rownum', thisrownum);
customer_select.style.color = '#ff0000';
customer_select.style.display = 'none';
customer_select.onchange = select_customer;
@@ -715,21 +601,29 @@ Example:
balance_cell.appendChild(document.createTextNode('<%$money_char%>'));
var balance_span = document.createElement('SPAN');
- balance_span.setAttribute('id', 'balance'+<% $opt{prefix} %>rownum+'_text');
- balance_span.setAttribute('rownum', <% $opt{prefix} %>rownum);
+ balance_span.setAttribute('id', 'balance'+thisrownum+'_text');
+ balance_span.setAttribute('rownum', thisrownum);
balance_cell.appendChild(balance_span);
balance_cell.appendChild(
- document.createTextNode(String.fromCharCode(160)) //&nbsp;
+ document.createTextNode(String.fromCharCode(160) + (values ? values.balance : '')) //&nbsp;
);
var balance_input = document.createElement('INPUT');
balance_input.setAttribute('type', 'hidden');
- balance_input.setAttribute('name', 'balance'+<% $opt{prefix} %>rownum);
- balance_input.setAttribute('id', 'balance'+<% $opt{prefix} %>rownum);
- balance_input.setAttribute('rownum', <% $opt{prefix} %>rownum);
+ balance_input.setAttribute('name', 'balance'+thisrownum);
+ balance_input.setAttribute('id', 'balance'+thisrownum);
+ balance_input.setAttribute('rownum', thisrownum);
+ balance_input.value = values ? values.balance : '';
balance_cell.appendChild(balance_input);
+ var num_open_input = document.createElement('INPUT');
+ num_open_input.setAttribute('type', 'hidden');
+ num_open_input.setAttribute('name', 'num_open'+thisrownum);
+ num_open_input.setAttribute('id', 'num_open'+thisrownum);
+ num_open_input.setAttribute('rownum', thisrownum);
+ balance_cell.appendChild(num_open_input);
+
row.appendChild(balance_cell);
% my $col = 0;
@@ -737,37 +631,35 @@ Example:
var my_cell = document.createElement('TD');
my_cell.setAttribute('align', '<% $align{ $opt{align}->[$col] || 'l' } %>');
+% if ($opt{'color'}->[$col]) {
+ my_cell.style.color = '<% $opt{color}->[$col] %>';
+% }
% if ($types->[$col] eq 'immutable') {
-% my $value;
-% if ( ref($field) eq 'CODE' ) {
-% $value = &{$field}($row,$param);
-% } else {
-% $value = $param->{"$field$row"};
-% }
- var my_text = document.createTextNode(<% $value |js_string %>);
+ var my_text = document.createTextNode(values ? values.<% $field %> : '');
my_cell.appendChild(my_text);
% }
% my $name = (ref($field) eq 'CODE') ? "column${col}_" : $field;
var my_input = document.createElement('INPUT');
- my_input.setAttribute('name', '<% $name %>'+<% $opt{prefix} %>rownum);
- my_input.setAttribute('id', '<% $name %>'+<% $opt{prefix} %>rownum);
+ my_input.setAttribute('name', '<% $name %>'+thisrownum);
+ my_input.setAttribute('id', '<% $name %>'+thisrownum);
my_input.style.textAlign = '<% $align{ $opt{align}->[$col] || 'l' } %>';
my_input.setAttribute('size', <% $sizes->[$col] || 10 %>);
- my_input.setAttribute('rownum', <% $opt{prefix} %>rownum);
+ my_input.setAttribute('rownum', thisrownum);
% if ( $types->[$col] eq 'immutable' ) {
- my_input.setAttribute('type', 'hidden');
-% }
-% elsif ( $types->[$col] eq 'checkbox' ) {
- my_input.setAttribute('type', 'checkbox');
+ my_input.setAttribute('type', 'hidden');
+% } elsif ( $types->[$col] eq 'checkbox' ) {
+ my_input.setAttribute('type', 'checkbox');
+ my_input.checked = (values && values.<% $field %>) ? true : false;
% }
+ my_input.value = (values && values.<% $field %>) || '';
% if ( $opt{onchange}->[$col] ) {
my_input.onchange = <% $opt{onchange}->[$col] %>;
% }
% elsif ( $opt{footer}->[$col] eq '_TOTAL' ) {
- my_input.onchange = <% $opt{prefix} %>calc_total<%$col%>;
- my_input.onkeyup = <% $opt{prefix} %>calc_total<%$col%>;
+ my_input.onchange = calc_total<%$col%>;
+ my_input.onkeyup = calc_total<%$col%>;
% }
my_cell.appendChild(my_input);
@@ -776,41 +668,144 @@ Example:
% $col++;
% }
- //update the total # of rows display
- if ( <% $opt{prefix} %>rownum == 1 ) {
- <% $opt{prefix} %>total_el.innerHTML =
- 'Total '
- + <% $opt{prefix} %>rownum
- + ' <% $opt{name_singular} || 'customer' %>';
- } else {
- <% $opt{prefix} %>total_el.innerHTML =
- 'Total '
- + <% $opt{prefix} %>rownum
- + ' <% PL($opt{name_singular} || 'customer') %>';
+ var td_delete = document.createElement('TD');
+ td_delete.setAttribute('id', 'delete'+thisrownum);
+ row.appendChild(td_delete);
+ if (values) {
+ addDeleteButton(thisrownum);
}
+ update_num_open(thisrownum, (values ? values.num_open : '0'));
+
% if ( $opt{add_row_callback} ) {
- <% $opt{add_row_callback} %>(<% $opt{prefix} %>rownum,
- '<% $opt{prefix} %>');
+ <% $opt{add_row_callback} %>(thisrownum, values);
% }
- <% $opt{prefix} %>rownum++;
+ // update the total number of rows display
+ allrows.push(thisrownum);
+ if (values) totalrows++;
+ updateTotalRow();
- }
+ // update the next available row number
+ if (thisrownum >= rownum) {
+ rownum = thisrownum + 1;
+ }
+
+ } // end of addRow
+
+
+</SCRIPT>
+
+<TABLE ID="OneTrueTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
-% unless ($cgi->param('error')) {
- <% $opt{prefix} %>addRow();
+<TR>
+ <TH>Inv #</TH>
+ <TH>Cust #</TH>
+ <TH>Status</TH>
+ <TH>Customer</TH>
+ <TH>Balance</TH>
+% foreach my $header ( @{$opt{header}} ) {
+ <TH><% $header %></TH>
+% }
+</TR>
+
+% my @rownums = sort { $a <=> $b } map /^custnum(\d+)$/, keys %$param;
+<TR id="row_total">
+ <TH COLSPAN=5 ID="_TOTAL_TOTAL">
+ Total <% @rownums || 0 %>
+ <% PL($opt{name_singular} || 'customer', ( @rownums || 0 ) ) %>
+ </TH>
+% my $col = 0;
+% foreach my $footer ( @{$opt{footer}} ) {
+% my $align = $align{ $opt{'footer_align'}->[$col] || 'c' };
+% if ($footer eq '_TOTAL' ) {
+% my $id = $opt{'fields'}->[$col];
+% $id = ref($id) ? "column${col}_TOTAL" : "${id}_TOTAL";
+ <TH ALIGN="<% $align %>" ID="<% $id %>">&nbsp;<% sprintf('%.2f', $total[$col] ) %></TH>
+% } else {
+ <TH ALIGN="<% $align %>"><% $footer %></TH>
+% }
+% $col++;
+% }
+</TR>
+
+</TABLE>
+
+<SCRIPT TYPE="text/javascript">
+
+total_el =
+ document.getElementById("_TOTAL_TOTAL");
+
+rownum = 1; // really more of a "next row", used by addrow
+totalrows = 0; // will not include empty rows
+allrows = []; // will include empty rows
+
+% foreach my $row ( @rownums ) {
+% if ( grep($param->{$_.$row},qw(invnum display_custnum custnum status statuscolor customer balance),@{$opt{fields}} ) ) {
+
+addRow({
+ rownum:<% $row %>,
+ num_open:<% $param->{"num_open$row"} |js_string %>,
+ invnum:<% $param->{"invnum$row"} |js_string %>,
+ display_custnum:<% $param->{"display_custnum$row"} |js_string %>,
+ custnum:<% $param->{"custnum$row"} |js_string %>,
+ status:<% $param->{"status$row"} |js_string %>,
+ statuscolor:<% $param->{"statuscolor$row"} |js_string %>,
+ customer:<% $param->{"customer$row"} |js_string %>,
+ balance:<% $param->{"balance$row"} |js_string %>,
+% my $col = 0;
+% foreach my $field ( @{$opt{fields}} ) {
+% my $value;
+% if ( ref($field) eq 'CODE' ) {
+% $value = &{$field}($row,$param) || '';
+% } else {
+% $value = $param->{"$field$row"} || '';
+% }
+% my $name = (ref($field) eq 'CODE') ? "column${col}" : "$field";
+ <% $name %>:<% $value |js_string %>,
+% $col++;
+% }
+});
+% }
+% }
+
+addRow();
+
+% my $col = 0;
+% foreach my $footer ( @{$opt{footer}} ) {
+% if ($footer eq '_TOTAL' ) {
+% my $name = $opt{fields}->[$col];
+% $name = ref($name) ? "column$col" : $name;
+ var th_el = document.getElementById("<%$name%>_TOTAL");
+ function calc_total<% $col %>() {
+ var row = 0;
+ var total = 0;
+ for (i = 0; i < allrows.length; i++) {
+ var value = document.getElementById("<%$name%>"+allrows[i]).value;
+ value = parseFloat(value);
+ if ( ! isNaN(value) ) {
+ total = total + value;
+ }
+ }
+ th_el.innerHTML = '&nbsp;' + total.toFixed(2);
+ }
+ calc_total<% $col %>()
+% }
+% $col++;
% }
</SCRIPT>
+<% include('/elements/xmlhttp.html',
+ 'url' => $p. 'misc/xmlhttp-cust_main-search.cgi',
+ 'subs' => [qw( custnum_search smart_search invnum_search )],
+ )
+%>
+
<%init>
my(%opt) = @_;
my $conf = new FS::Conf;
-$opt{prefix} = '' unless defined $opt{prefix};
-$opt{prefix} .= '_' if $opt{prefix};
-
my $types = $opt{'type'} ? [ @{$opt{'type'}} ] : [];
my $sizes = $opt{'size'} ? [ @{$opt{'size'}} ] : [];
diff --git a/httemplate/elements/freeside.css b/httemplate/elements/freeside.css
index 4ba0f3f7d..f6d73c2de 100644
--- a/httemplate/elements/freeside.css
+++ b/httemplate/elements/freeside.css
@@ -278,6 +278,24 @@ td.grid {
empty-cells: show;
}
+tr.row0 {
+ background-color: #eeeeee;
+}
+
+tr.row1 {
+ background-color: #ffffff;
+}
+
+.grid tfoot tr {
+ background-color: #dddddd;
+ font-style: italic;
+}
+
+/* border at the top of the footer, but not between footer rows */
+.grid tfoot tr:first-child td {
+ border-top: 1px dashed black;
+}
+
table.inv { border: none }
th.inv { border: none }
td.inv { border: none }
diff --git a/httemplate/elements/input-fcc_options.html b/httemplate/elements/input-fcc_options.html
index 064c647fc..080b40f88 100644
--- a/httemplate/elements/input-fcc_options.html
+++ b/httemplate/elements/input-fcc_options.html
@@ -9,7 +9,7 @@
% }
% unless ($opt{html_only}) {
% my $popup = $fsurl.'misc/part_pkg_fcc_options.html?id=';
-% my $popup_name = 'popup-'.time. "-$$-". rand() * 2**32;
+% my $popup_name = 'popup-'.random_id();
<SCRIPT TYPE="text/javascript">
function edit_fcc_options() {
var id = this.dataset['target'];
diff --git a/httemplate/elements/location.html b/httemplate/elements/location.html
index 214a7d5f2..b50509aea 100644
--- a/httemplate/elements/location.html
+++ b/httemplate/elements/location.html
@@ -169,7 +169,11 @@ Example:
<TR>
- <<%$th%> ALIGN="right"><%$r%><% mt('City') |h %></<%$th%>>
+ <<%$th%> ALIGN="right">
+% unless ($conf->exists('cust_main-no_city_in_address')) {
+ <% $r %><% mt('City') |h %>
+% }
+ </<%$th%>>
<TD WIDTH="1"><% include('/elements/city.html', %select_hash, 'text_style' => \@style ) %></TD>
<<%$th%> ALIGN="right" WIDTH="1" ID="<%$pre%>countylabel" <%$county_style%>><%$r%>County</<%$th%>>
<TD WIDTH="1"><% include('/elements/select-county.html', %select_hash ) %></TD>
@@ -285,7 +289,7 @@ Example:
var clear_coords_on_change = [
'<%$pre%>address1',
'<%$pre%>address2',
- '<%$pre%>city',
+ <% $conf->exists('cust_main-no_city_in_address') ? '' : qq('${pre}city',) %>
'<%$pre%>state',
'<%$pre%>zip',
'<%$pre%>country'
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index 34bba7e8c..f96c05ea5 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -290,7 +290,8 @@ $report_rating{'Unrateable CDRs'} = [ $fsurl.'search/cdr.html?freesidestatus=fai
if $curuser->access_right("Usage: Unrateable CDRs");
if ( $curuser->access_right("Usage: Time worked") ) {
$report_rating{'Time worked'} = [ $fsurl.'search/report_rt_transaction.html', '' ];
- $report_rating{'Time worked summary'} = [ $fsurl.'search/report_rt_ticket.html', '' ];
+ $report_rating{'Time worked summary per ticket'} = [ $fsurl.'search/report_rt_ticket.html', '' ];
+ $report_rating{'Time worked summary per customer'} = [ $fsurl.'search/report_rt_cust.html', '' ];
}
tie my %report_ticketing_statistics, 'Tie::IxHash',
@@ -737,10 +738,10 @@ $config_misc{'Message templates'} = [ $fsurl.'browse/msg_template.html', 'Templa
$config_misc{'Advertising sources'} = [ $fsurl.'browse/part_referral.html', 'Where a customer heard about your service.' ]
if $curuser->access_right('Edit advertising sources')
|| $curuser->access_right('Edit global advertising sources');
-if ( $curuser->access_right('Configuration') ) {
- $config_misc{'Custom fields'} = [ $fsurl.'browse/part_virtual_field.html', 'Locally defined fields', ];
- $config_misc{'Translation strings'} = [ $fsurl.'browse/msgcat.html', 'Translations and other customizable labels for each locale' ];
-}
+$config_misc{'Custom fields'} = [ $fsurl.'browse/part_virtual_field.html', 'Locally defined fields', ]
+ if $curuser->access_right('Edit custom fields');
+$config_misc{'Translation strings'} = [ $fsurl.'browse/msgcat.html', 'Translations and other customizable labels for each locale' ]
+ if $curuser->access_right('Configuration');
$config_misc{'Inventory classes and inventory'} = [ $fsurl.'browse/inventory_class.html', 'Setup inventory classes and stock inventory' ]
if $curuser->access_right('Edit inventory')
|| $curuser->access_right('Edit global inventory')
diff --git a/httemplate/elements/pickcolor.html b/httemplate/elements/pickcolor.html
index d410ebfc7..2b0647fbf 100644
--- a/httemplate/elements/pickcolor.html
+++ b/httemplate/elements/pickcolor.html
@@ -38,7 +38,7 @@ my %opt = @_;
my $value = length($opt{curr_value}) ? $opt{curr_value} : $opt{value};
-my $unum = int(rand(100000));
+my $unum = random_id(5);
my $id = $opt{'id'} || $opt{'field'}.$unum;
diff --git a/httemplate/elements/popup_link_onclick.html b/httemplate/elements/popup_link_onclick.html
index 5173115a5..09ce93e7a 100644
--- a/httemplate/elements/popup_link_onclick.html
+++ b/httemplate/elements/popup_link_onclick.html
@@ -62,7 +62,7 @@ $scrolling = $params->{'scrolling'} if exists $params->{'scrolling'};
#stupid safari is caching the "location" of popup iframs, and submitting them
#instead of displaying them. this should prevent that.
-my $popup_name = 'popup-'.time. "-$$-". rand() * 2**32;
+my $popup_name = 'popup-'.random_id();
my $onclick =
"overlib( OLiframeContent($action, $width, $height, '$popup_name', 0, '$scrolling' ), ".
diff --git a/httemplate/elements/progress-init.html b/httemplate/elements/progress-init.html
index 5b42aa1a8..2728240ef 100644
--- a/httemplate/elements/progress-init.html
+++ b/httemplate/elements/progress-init.html
@@ -170,6 +170,6 @@ $progress_url->query_form(
#stupid safari is caching the "location" of popup iframs, and submitting them
#instead of displaying them. this should prevent that.
-my $popup_name = 'popup-'.time. "-$$-". rand() * 2**32;
+my $popup_name = 'popup-'.random_id();
</%init>
diff --git a/httemplate/elements/select.html b/httemplate/elements/select.html
index 67ef51418..4492681de 100644
--- a/httemplate/elements/select.html
+++ b/httemplate/elements/select.html
@@ -4,6 +4,7 @@
ID = "<% $opt{id} %>"
previousValue = "<% $curr_value %>"
previousText = "<% $labels->{$curr_value} || $curr_value %>"
+ <% $multiple %>
<% $size %>
<% $style %>
<% $opt{disabled} %>
@@ -74,4 +75,6 @@ my $style = scalar(@style) ? 'STYLE="'. join(';', @style). '"' : '';
my $size = $opt{'size'} ? 'SIZE='.$opt{'size'} : '';
+my $multiple = $opt{'multiple'} ? 'MULTIPLE' : '';
+
</%init>
diff --git a/httemplate/elements/standardize_locations.js b/httemplate/elements/standardize_locations.js
index f114e341c..56b2be990 100644
--- a/httemplate/elements/standardize_locations.js
+++ b/httemplate/elements/standardize_locations.js
@@ -13,7 +13,7 @@ function form_address_info() {
% }
% for my $pre (@prefixes) {
% # normal case
-% for my $field (qw(address1 address2 city state zip country)) {
+% for my $field (qw(address1 address2 state zip country), ($conf->exists('cust_main-no_city_in_address') ? () : 'city')) {
returnobj['<% $pre %><% $field %>'] = cf.elements['<% $pre %><% $field %>'].value;
% } #for $field
% if ( $withcensus ) {
@@ -145,7 +145,7 @@ function replace_address() {
var clean = newaddr['<% $pre %>addr_clean'] == 'Y';
var error = newaddr['<% $pre %>error'];
if ( clean ) {
-% foreach my $field (qw(address1 address2 city state zip addr_clean )) {
+% foreach my $field (qw(address1 address2 state zip addr_clean ),($conf->exists('cust_main-no_city_in_address') ? () : 'city')) {
cf.elements['<% $pre %><% $field %>'].value = newaddr['<% $pre %><% $field %>'];
% } #foreach $field
diff --git a/httemplate/elements/tr-input-mask.html b/httemplate/elements/tr-input-mask.html
index fdd20962d..93e322c6c 100644
--- a/httemplate/elements/tr-input-mask.html
+++ b/httemplate/elements/tr-input-mask.html
@@ -68,7 +68,7 @@ my $init = 0;
<%init>
my %opt = @_;
# must have a DOM id
-my $id = $opt{id} || sprintf('input%04d',int(rand(10000)));
+my $id = $opt{id} || sprintf('input%04d',random_id(4));
my $value = length($opt{curr_value}) ? $opt{curr_value} : $opt{value} || '';
my $clipboard_hack = $FS::CurrentUser::CurrentUser->option('enable_mask_clipboard_hack');
diff --git a/httemplate/elements/tr-pkg_svc.html b/httemplate/elements/tr-pkg_svc.html
index 8acbca118..b3bf80212 100644
--- a/httemplate/elements/tr-pkg_svc.html
+++ b/httemplate/elements/tr-pkg_svc.html
@@ -32,6 +32,13 @@
% $quan = $pkg_svc->quantity;
% }
%
+% my $provision_hold = '';
+% if ( grep { $_ eq "provision_hold$svcpart" } $cgi->param ) {
+% $provision_hold = $cgi->param("hidden_svc$svcpart");
+% } else {
+% $provision_hold = $pkg_svc->provision_hold;
+% }
+%
% my @exports = $pkg_svc->part_svc->part_export;
% foreach my $export ( @exports ) {
% push @possible_exports, $export if $export->can('external_pkg_map');
@@ -53,6 +60,11 @@
<TD>
<INPUT TYPE="checkbox" NAME="hidden<% $svcpart %>" VALUE="Y"<% $pkg_svc->hidden =~ /^Y/i ? ' CHECKED' : ''%>>
</TD>
+
+ <TD ALIGN="center">
+ <INPUT TYPE="checkbox" NAME="provision_hold<% $svcpart %>" VALUE="Y"<% $provision_hold =~ /^Y/i ? ' CHECKED' : ''%>>
+ </TD>
+
</TR>
% foreach ( 1 .. $columns-1 ) {
% if ( $count == int( $_ * scalar(@part_svc) / $columns ) ) {
@@ -106,6 +118,7 @@ my $thead = "\n\n". ntable('#cccccc', 2).
'<TH BGCOLOR="#dcdcdc"><FONT SIZE=-2>Primary</FONT></TH>'.
'<TH BGCOLOR="#dcdcdc">Service</TH>'.
'<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Hide</FONT></TH>'.
+ '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Hold<BR>Until<BR>Provision</FONT></TH>'.
'</TR>';
my $part_pkg = $opt{'object'};
diff --git a/httemplate/graph/cust_bill_pkg.cgi b/httemplate/graph/cust_bill_pkg.cgi
index ea7fee932..b5486f4af 100644
--- a/httemplate/graph/cust_bill_pkg.cgi
+++ b/httemplate/graph/cust_bill_pkg.cgi
@@ -27,6 +27,8 @@ my $bottom_link = "$link;";
my $use_usage = $cgi->param('use_usage') || 0;
my $use_setup = $cgi->param('use_setup') || 0;
+my $use_discount = $cgi->param('use_discount') || 2;
+
my $use_override = $cgi->param('use_override') ? 1 : 0;
my $average_per_cust_pkg = $cgi->param('average_per_cust_pkg') ? 1 : 0;
my $distribute = $cgi->param('distribute') ? 1 : 0;
@@ -41,11 +43,13 @@ if ( $average_per_cust_pkg ) {
}
my %charge_labels = (
+ 'SRU'=> 'setup + recurring',
'SR' => 'setup + recurring',
'RU' => 'recurring',
'S' => 'setup',
'R' => 'recurring',
'U' => 'usage',
+ 'D' => 'discount',
);
#XXX or virtual
@@ -186,6 +190,10 @@ elsif ( $use_usage == 2 ) {
$components[-1] =~ s/U//;
}
+if ( $use_discount == 1 ) {
+ push @components, 'D';
+} # else leave discounts off entirely; never combine them with setup/recur
+
# Categorization of line items goes
# Agent -> Referral -> Package class -> Component (setup/recur/usage)
# If per-agent totals are enabled, they go under the Agent level.
@@ -240,21 +248,26 @@ foreach my $agent ( $all_agent || $sel_agent || $FS::CurrentUser::CurrentUser->a
'charges' => $component,
);
- # XXX this is very silly. we should cache it server-side and
- # just put a cache identifier in the link
- my $rowlink = "$link;".
- ($all_agent ? '' : "agentnum=$row_agentnum;").
+ my $row_link = "$link;".
+ "charges=$component;".
+ "distribute=$distribute;";
+
+ if ( $component eq 'D' ) {
+ # discounts ignore 'charges' and 'distribute'
+ $row_link = "${p}search/cust_bill_pkg_discount.html?";
+ }
+
+ $row_link .= ($all_agent ? '' : "agentnum=$row_agentnum;").
($all_part_referral ? '' : "refnum=$row_refnum;").
(join('',map {"cust_classnum=$_;"} @cust_classnums)).
- "distribute=$distribute;".
- "use_override=$use_override;charges=$component;";
- $rowlink .= "$class_param=$_;" foreach @classnums;
+ "use_override=$use_override;";
+ $row_link .= "$class_param=$_;" foreach @classnums;
if ( $all_report_options ) {
push @row_params, 'all_report_options', 1;
- $rowlink .= 'all_report_options=1';
+ $row_link .= 'all_report_options=1';
}
push @params, \@row_params;
- push @links, $rowlink;
+ push @links, $row_link;
@colorbuf = @agent_colors unless @colorbuf;
push @colors, shift @colorbuf;
@@ -293,13 +306,22 @@ foreach my $agent ( $all_agent || $sel_agent || $FS::CurrentUser::CurrentUser->a
($all_part_referral ? () : ('refnum' => $row_refnum)),
'charges' => $component,
);
+
my $row_link = "$link;".
- ($all_agent ? '' : "agentnum=$row_agentnum;").
+ "charges=$component;".
+ "distribute=$distribute;";
+
+ if ( $component eq 'D' ) {
+ # discounts ignore 'charges' and 'distribute'
+ $row_link ="${p}search/cust_bill_pkg_discount.html?";
+ }
+
+ $row_link .= ($all_agent ? '' : "agentnum=$row_agentnum;").
($all_part_referral ? '' : "refnum=$row_refnum;").
(join('',map {"cust_classnum=$_;"} @cust_classnums)).
"$class_param=$row_classnum;".
- "distribute=$distribute;".
- "use_override=$use_override;charges=$component;";
+ "use_override=$use_override;";
+
if ( $class_param eq 'report_optionnum' ) {
push @row_params,
'all_report_options' => 1,
@@ -366,17 +388,6 @@ foreach my $agent ( $all_agent || $sel_agent || $FS::CurrentUser::CurrentUser->a
}
-# may be useful at some point...
-#if ( $average_per_cust_pkg ) {
-# @items = map { ('cust_bill_pkg', 'cust_bill_pkg_count_pkgnum') } @items;
-# @labels = map { $_, "Packages" } @labels;
-# @params = map { $_, $_ } @params;
-# @links = map { $_, $_ } @links;
-# @colors = map { $_, $_ } @colors;
-# @no_graph = map { $_, 1 } @no_graph;
-#}
-#
-
#use Data::Dumper;
if ( $cgi->param('debug') == 1 ) {
$FS::Report::Table::DEBUG = 1;
diff --git a/httemplate/graph/elements/monthly.html b/httemplate/graph/elements/monthly.html
index 4b988f166..1a9428115 100644
--- a/httemplate/graph/elements/monthly.html
+++ b/httemplate/graph/elements/monthly.html
@@ -27,7 +27,7 @@ Example:
'start_year' => $syear,
'end_month' => $emonth,
'end_year' => $eyear,
-
+ '12mo' => 0,
#optional, pulled from CGI params if not specified,
#only if 'daily' option is given
@@ -96,6 +96,8 @@ $opt{'start_year'} ||= $cgi->param('start_year'); # || 1899+$curyear;
$opt{'end_month'} ||= $cgi->param('end_month'); # || $curmon+1;
$opt{'end_year'} ||= $cgi->param('end_year'); # || 1900+$curyear;
+$opt{'12mo'} ||= $cgi->param('12mo') ? 1 : 0;
+
$opt{'projection'} ||= $cgi->param('projection') ? 1 : 0;
if ( $opt{'daily'} ) { # daily granularity
@@ -119,6 +121,7 @@ my %reportopts = (
'end_day' => $opt{'end_day'},
'end_month' => $opt{'end_month'},
'end_year' => $opt{'end_year'},
+ '12mo' => $opt{'12mo'},
'projection' => $opt{'projection'},
'agentnum' => $opt{'agentnum'},
'refnum' => $opt{'refnum'},
diff --git a/httemplate/graph/elements/report.html b/httemplate/graph/elements/report.html
index cffc82816..f1b0d166d 100644
--- a/httemplate/graph/elements/report.html
+++ b/httemplate/graph/elements/report.html
@@ -331,7 +331,7 @@ if ( $cgi->param('session') =~ /^(\d+)$/ ) {
%opt = %{ $m->cache->get($session) };
}
else {
- $session = sprintf("%010d%06d", time, int(rand(1000000)));
+ $session = sprintf("%010d", random_id(10));
$m->cache->set($session, \%opt, '1h');
}
diff --git a/httemplate/graph/money_time.cgi b/httemplate/graph/money_time.cgi
index 9071fc7b9..aa17eb2f4 100644
--- a/httemplate/graph/money_time.cgi
+++ b/httemplate/graph/money_time.cgi
@@ -39,33 +39,37 @@ if ( $cgi->param('refnum') =~ /^(\d+)$/ ) {
}
my $referralname = $part_referral ? $part_referral->referral.' ' : '';
-
-my @items = qw( invoiced netsales
+# need to clean this up. the false symmetry of "gross" and "net" everything
+# makes it aesthetically hard to make this report more useful.
+my @items = ($cgi->param('exclude_discount') ? 'invoiced' : 'gross');
+push @items,
+ qw( discounted 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',
+ 'gross' => 'Gross Sales',
+ 'invoiced' => 'Invoiced Sales',
+ 'netsales' => 'Net Sales',
+ 'discounted' => 'Discounts',
'credits' => 'Gross Credits',
- 'netcredits' => 'Net Credits',
+ 'netcredits' => 'Net Credits',
'payments' => 'Gross Receipts',
- 'receipts' => 'Net Receipts',
+ 'receipts' => 'Net Receipts',
'refunds' => 'Gross Refunds',
- 'netrefunds' => 'Net Refunds',
+ 'netrefunds' => 'Net Refunds',
'cashflow' => 'Gross Cashflow',
- 'netcashflow' => 'Net Cashflow',
+ 'netcashflow' => 'Net Cashflow',
);
my %graph_suffix = (
- 'invoiced' => ' (invoiced)',
+ 'gross' => ' (invoiced + discounts)',
+ 'invoiced' => '',
'netsales' => ' (invoiced - applied credits)',
+ 'discounted' => '',
'credits' => ' (credited)',
'netcredits' => ' (applied credits)',
'payments' => ' (payments)',
@@ -77,13 +81,8 @@ my %graph_suffix = (
);
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 = (
+ 'gross' => '9999ff', #light blue
'invoiced' => '9999ff', #light blue
'netsales' => '0000cc', #blue
'credits' => 'ff9999', #light red
@@ -94,6 +93,7 @@ my %color = (
'netrefunds' => 'ff9900', #orange
'cashflow' => '99cc33', #light olive
'netcashflow' => '339900', #olive
+ 'discounted' => 'cc33cc', #purple-ish?
);
$color{$_.'_12mo'} = $color{$_}
foreach keys %color;
@@ -102,7 +102,8 @@ my $ar = "agentnum=$agentnum;refnum=$refnum";
$ar .= ";cust_classnum=$_" foreach @classnums;
my %link = (
- 'invoiced' => "${p}search/cust_bill.html?$ar;",
+ 'gross' => "${p}search/cust_bill.html?$ar;",
+ 'invoiced' => "${p}search/cust_bill.html?$ar;invoiced=1;",
'netsales' => "${p}search/cust_bill.html?$ar;net=1;",
'credits' => "${p}search/cust_credit.html?$ar;",
'netcredits' => "${p}search/cust_credit_bill.html?$ar;",
@@ -110,7 +111,16 @@ my %link = (
'receipts' => "${p}search/cust_bill_pay.html?$ar;",
'refunds' => "${p}search/cust_refund.html?magic=_date;$ar;",
'netrefunds' => "${p}search/cust_credit_refund.html?$ar;",
+ 'discounted' => "${p}search/cust_bill_pkg_discount.html?$ar;",
);
# XXX link 12mo?
+if ( $cgi->param('12mo') ) {
+ $label{$_} .= " (prev 12 months)"
+ foreach keys %label;
+
+ $graph_label{$_} .= " (prev 12 months)"
+ foreach keys %graph_label;
+}
+
</%init>
diff --git a/httemplate/graph/money_time_daily.cgi b/httemplate/graph/money_time_daily.cgi
index 0fdbd895e..95617f6f4 100644
--- a/httemplate/graph/money_time_daily.cgi
+++ b/httemplate/graph/money_time_daily.cgi
@@ -8,7 +8,6 @@
'links' => \%link,
'agentnum' => $agentnum,
'cust_classnum'=> \@classnums,
- 'nototal' => scalar($cgi->param('12mo')),
'daily' => 1,
'start_day' => $smday,
'start_month' => $smon+1,
@@ -44,19 +43,20 @@ my ($ssec,$smin,$shour,$smday,$smon,$syear,$swday,$syday,$sisdst)
my ($esec,$emin,$ehour,$emday,$emon,$eyear,$ewday,$eyday,$eisdst)
= localtime($ending);
-my @items = qw( invoiced netsales
+my @items = ($cgi->param('exclude_discount') ? 'invoiced' : 'gross');
+push @items,
+ qw( discounted netsales
credits netcredits
payments receipts
refunds netrefunds
cashflow netcashflow
);
-if ( $cgi->param('12mo') == 1 ) {
- @items = map $_.'_12mo', @items;
-}
my %label = (
- 'invoiced' => 'Gross Sales',
+ 'gross' => 'Gross Sales',
+ 'invoiced' => 'Invoiced Sales',
'netsales' => 'Net Sales',
+ 'discounted' => 'Discounts',
'credits' => 'Gross Credits',
'netcredits' => 'Net Credits',
'payments' => 'Gross Receipts',
@@ -68,8 +68,10 @@ my %label = (
);
my %graph_suffix = (
- 'invoiced' => ' (invoiced)',
+ 'gross' => ' (invoiced + discounts)',
+ 'invoiced' => '',
'netsales' => ' (invoiced - applied credits)',
+ 'discounted' => '',
'credits' => ' (credited)',
'netcredits' => ' (applied credits)',
'payments' => ' (payments)',
@@ -81,13 +83,8 @@ my %graph_suffix = (
);
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 = (
+ 'gross' => '9999ff', #light blue
'invoiced' => '9999ff', #light blue
'netsales' => '0000cc', #blue
'credits' => 'ff9999', #light red
@@ -98,12 +95,12 @@ my %color = (
'netrefunds' => 'ff9900', #orange
'cashflow' => '99cc33', #light olive
'netcashflow' => '339900', #olive
+ 'discounted' => 'cc33cc', #purple-ish?
);
-$color{$_.'_12mo'} = $color{$_}
- foreach keys %color;
my %link = (
- 'invoiced' => "${p}search/cust_bill.html?agentnum=$agentnum;",
+ 'gross' => "${p}search/cust_bill.html?agentnum=$agentnum;",
+ 'invoiced' => "${p}search/cust_bill.html?agentnum=$agentnum;invoiced=1;",
'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;",
@@ -111,7 +108,7 @@ my %link = (
'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;",
+ 'discounted' => "${p}search/cust_bill_pkg_discount.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
index 76d3a6ca3..e99671454 100644
--- a/httemplate/graph/report_cust_bill_pkg.html
+++ b/httemplate/graph/report_cust_bill_pkg.html
@@ -190,6 +190,12 @@ window.onload = class_mode_changed;
'onchange'=> 'enable_agent_totals',
&>
% }
+<& /elements/tr-select.html,
+ 'label' => 'Discounts',
+ 'field' => 'use_discount',
+ 'options' => [ 2, 1 ], # 3.x only: make 2 the default
+ 'labels' => { 1 => 'Separate', 2 => 'Do not show' },
+&>
<TR>
<TD ALIGN="right">Colors</TD>
diff --git a/httemplate/graph/report_money_time.html b/httemplate/graph/report_money_time.html
index 315d31bc5..2f6dd3844 100644
--- a/httemplate/graph/report_money_time.html
+++ b/httemplate/graph/report_money_time.html
@@ -36,9 +36,26 @@
)
%>
-<TR>
- <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="12mo" VALUE="1"></TD>
- <TD>Show 12 month totals instead of monthly values</TD>
+<tr>
+ <td />
+ <td>
+ <& /elements/checkbox.html,
+ field => '12mo',
+ value => 1,
+ &>
+ <% emt('Show 12 month totals instead of monthly values') %>
+ </td>
+</tr>
+<tr>
+ <td />
+ <td>
+ <& /elements/checkbox.html,
+ field => 'exclude_discount',
+ value => 1,
+ curr_value => 1, #3.x only
+ &>
+ <% emt('Exclude discounts from total sales') %>
+ </td>
</TR>
</TABLE>
diff --git a/httemplate/graph/report_money_time_daily.html b/httemplate/graph/report_money_time_daily.html
index e80f5862c..5daa305f0 100644
--- a/httemplate/graph/report_money_time_daily.html
+++ b/httemplate/graph/report_money_time_daily.html
@@ -21,6 +21,18 @@
'multiple' => 1,
&>
+<tr>
+ <td />
+ <td>
+ <& /elements/checkbox.html,
+ field => 'exclude_discount',
+ value => 1,
+ curr_value => 1, #3.x only
+ &>
+ <% emt('Exclude discounts from total sales') %>
+ </td>
+</TR>
+
</TABLE>
<BR><INPUT TYPE="submit" VALUE="Display">
diff --git a/httemplate/misc/batch-cust_pay.html b/httemplate/misc/batch-cust_pay.html
index cc1a26a0e..9f2540cc7 100644
--- a/httemplate/misc/batch-cust_pay.html
+++ b/httemplate/misc/batch-cust_pay.html
@@ -26,11 +26,30 @@ function warnUnload() {
}
window.onbeforeunload = warnUnload;
-function add_row_callback(rownum, prefix) {
- document.getElementById('enable_app'+rownum).disabled = true;
+function add_row_callback(rownum, values) {
+ if (values) {
+ custnum_update_callback(rownum);
+ } else {
+ document.getElementById('enable_app'+rownum).disabled = true;
+ }
+}
+
+function delete_row_callback(rownum) {
+ var i = 0;
+ var delbutton = document.getElementById('delete'+rownum+'.'+i);
+ var delrows = [];
+ while (delbutton) {
+ delrows[i] = delbutton;
+ i++;
+ delbutton = document.getElementById('delete'+rownum+'.'+i);
+ }
+ delrows = delrows.reverse();
+ for (i = 0; i < delrows.length; i++) {
+ delrows[i].onclick();
+ }
}
-function custnum_update_callback(rownum, prefix) {
+function custnum_update_callback(rownum) {
var custnum = document.getElementById('custnum'+rownum).value;
// if there is a custnum and more than one open invoice, enable
// (and check) the box
@@ -39,17 +58,17 @@ function custnum_update_callback(rownum, prefix) {
enable_app_checkbox.disabled = show_applications;
% if ( $use_discounts ) {
- select_discount_term(rownum, prefix);
+ select_discount_term(rownum);
% }
}
-function invnum_update_callback(rownum, prefix) {
- custnum_update_callback(rownum, prefix);
+function invnum_update_callback(rownum) {
+ custnum_update_callback(rownum);
}
-function select_discount_term(row, prefix) {
- var custnum_obj = document.getElementById('custnum'+prefix+row);
- var select_obj = document.getElementById('discount_term'+prefix+row);
+function select_discount_term(row) {
+ var custnum_obj = document.getElementById('custnum'+row);
+ var select_obj = document.getElementById('discount_term'+row);
var value = '';
if (select_obj.type == 'hidden') {
@@ -313,17 +332,17 @@ function create_application_row(rownum, appnum) {
%# for error handling--ugly, but the alternative is translating the whole
%# process of creating rows into Mason
-var row_array = <% encode_json(\@rows) %>;
+var row_obj = <% encode_json(\%rows) %>;
function preload() {
var rownum;
var appnum;
- for (rownum=0; rownum < row_array.length; rownum++) {
- if ( row_array[rownum].length ) {
+ for (rownum in row_obj) {
+ if ( row_obj[rownum].length ) {
var enable = document.getElementById('enable_app'+rownum);
enable.checked = true;
var preload_row = function(r) {//continuation from toggle_application_row
- for (appnum=0; appnum < row_array[r].length; appnum++) {
- this_app = row_array[r][appnum];
+ for (appnum=0; appnum < row_obj[r].length; appnum++) {
+ this_app = row_obj[r][appnum];
var x = r + '.' + appnum;
//set invnum
var select_invnum = document.getElementById('invnum'+x);
@@ -345,7 +364,7 @@ function preload() {
} //for appnum
}; //preload_row function
toggle_application_row.call(enable, null, preload_row);
- } // if row_array[rownum].length
+ } // if (row_obj[rownum].length
} //for rownum
}
@@ -380,6 +399,7 @@ function preload() {
custnum_update_callback => 'custnum_update_callback',
invnum_update_callback => 'invnum_update_callback',
add_row_callback => 'add_row_callback',
+ delete_row_callback => 'delete_row_callback',
&>
<BR>
@@ -387,13 +407,12 @@ function preload() {
</FORM>
-%if ( $cgi->param('error') ) {
-<SCRIPT TYPE="text/javascript">
-% for ( my $row = 0; defined($cgi->param("custnum$row")); $row++ ) {
- select_discount_term(<% $row %>, '');
-% }
-</SCRIPT>
-%}
+% #XXX I think this can go away completely, but need to test with $use_discount
+% ###not perl <SCRIPT TYPE="text/javascript">
+% #foreach my $row ( keys %rows ) {
+% ###not perl select_discount_term(<% $row %>, '');
+% #}
+% ###not perl </SCRIPT>
<% include('/elements/footer.html') %>
@@ -417,6 +436,7 @@ my @footer_align = ( 'r', 'r' );
my @onchange = ( '', '' );;
my $use_discounts = '';
+# Not entirely sure this works anymore...
if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) {
#push @header, 'Discount';
push @header, '';
@@ -441,6 +461,16 @@ push @footer, '';
push @footer_align, '';
push @onchange, 'toggle_application_row';
+push @header, 'No Auto Allocate';
+push @fields, 'no_auto_apply';
+push @types, 'checkbox';
+push @align, 'c';
+push @sizes, '0';
+push @colors, '';
+push @footer, '';
+push @footer_align, '';
+push @onchange, '';
+
#push @header, 'Error';
push @header, '';
push @fields, 'error';
@@ -455,29 +485,31 @@ push @onchange, '';
$m->comp('/elements/handle_uri_query');
# set up for preloading
-my @rows;
-my @row_errors;
+my %rows;
+my %row_errors;
if ( $cgi->param('error') ) {
my $param = $cgi->Vars;
my $enum = 0; #errors numbered separately
- for( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
- $rows[$row] = [];
- $row_errors[$row] = $param->{"error$enum"};
+ my @invrows = grep /^invnum\d+\.\d+$/, keys %$param; #pare down possibilities
+ foreach my $row ( sort { $a <=> $b } map /^custnum(\d+)$/, keys %$param ) {
+# for( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
+ $rows{$row} = [];
+ $row_errors{$row} = $param->{"error$enum"};
$enum++;
- for( my $app = 0; exists($param->{"invnum$row.$app"}); $app++ ) {
+ foreach my $app ( map /^invnum$row\.(\d+)$/, @invrows ) {
next if !$param->{"invnum$row.$app"};
my %this_app = map { $_ => ($param->{$_.$row.'.'.$app} || '') }
qw( invnum amount );
$this_app{'error'} = $param->{"error$enum"} || '';
$param->{"error$enum"} = ''; # don't pass this error through
- $rows[$row][$app] = \%this_app;
+ $rows{$row}[$app] = \%this_app;
$enum++;
}
}
- for( my $row = 0; $row < @row_errors; $row++ ) {
- $param->{"error$row"} = $row_errors[$row];
+ foreach my $row (keys %rows) {
+ $param->{"error$row"} = $row_errors{$row};
}
}
-#warn Dumper {rows => \@rows, row_errors => \@row_errors };
+#warn Dumper {rows => \%rows, row_errors => \%row_errors };
</%init>
diff --git a/httemplate/misc/change_pkg.cgi b/httemplate/misc/change_pkg.cgi
index 1b4a94e81..e74747e82 100755
--- a/httemplate/misc/change_pkg.cgi
+++ b/httemplate/misc/change_pkg.cgi
@@ -28,6 +28,14 @@
'curr_value' => $cust_pkg->quantity
&>
+% if ($use_contract_end) {
+ <& /elements/tr-input-date-field.html, {
+ 'name' => 'contract_end',
+ 'value' => ($cgi->param('contract_end') || $cust_pkg->get('contract_end')),
+ 'label' => '<B>Contract End</B>',
+ } &>
+% }
+
</TABLE>
<BR>
@@ -124,6 +132,8 @@ my $part_pkg = $cust_pkg->part_pkg;
my $title = "Change Package";
+my $use_contract_end = $cust_pkg->get('contract_end') ? 1 : 0;
+
# if there's already a package change ordered, preload it
if ( $cust_pkg->change_to_pkgnum ) {
my $change_to = FS::cust_pkg->by_key($cust_pkg->change_to_pkgnum);
@@ -131,6 +141,9 @@ if ( $cust_pkg->change_to_pkgnum ) {
foreach(qw( start_date pkgpart locationnum quantity )) {
$cgi->param($_, $change_to->get($_));
}
+ if ($use_contract_end) {
+ $cgi->param('contract_end', $change_to->get('contract_end'));
+ }
$title = "Edit Scheduled Package Change";
}
</%init>
diff --git a/httemplate/misc/email-customer-statement.html b/httemplate/misc/email-customer-statement.html
new file mode 100644
index 000000000..c0ed88031
--- /dev/null
+++ b/httemplate/misc/email-customer-statement.html
@@ -0,0 +1,95 @@
+
+ <% include('email-customers.html',
+ 'form_action' => 'email-customer-statement.html',
+ 'title' => 'Send statement to customer',
+ 'no_search_fields' => [ 'start_date', 'end_date' ],
+ 'alternate_form' => $alternate_form,
+ 'post_search_hook' => $post_search_hook,
+ 'acl' => $acl,
+ 'process_url' => 'process/email-customer-statement.html',
+ )
+ %>
+
+<%init>
+
+my $acl = 'Resend invoices';
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right($acl);
+
+my $alternate_form = sub {
+ # this could maaaybe be a separate element, for cleanliness
+ # but it's really only for use by this page, and it's not overly complicated
+ my $noinit = 0;
+ return join("\n",
+ '<TABLE BORDER="0">',
+ (
+ map {
+ my $label = ucfirst($_);
+ $label =~ s/_/ /;
+ include('/elements/tr-input-date-field.html',{
+ 'name' => $_,
+ 'value' => $cgi->param($_) || '',
+ 'label' => $label,
+ 'noinit' => $noinit++
+ });
+ }
+ qw( start_date end_date )
+ ),
+ '</TABLE>',
+ '<INPUT TYPE="hidden" NAME="action" VALUE="preview">',
+ '<INPUT TYPE="submit" VALUE="Preview notice">',
+ );
+};
+
+my $post_search_hook = sub {
+ my %opt = @_;
+ return unless $cgi->param('action') eq 'preview';
+ my $cust_main = qsearchs('cust_main',$opt{'search'})
+ or die "Could not find customer";
+
+ # so that the statement indicates the latest date
+ my $date_format = $opt{'conf'}->config('date_format') || '%m/%d/%Y';
+ $cgi->param('end_date', time2str($date_format, time))
+ unless $cgi->param('end_date');
+
+ # set from/subject/html_body based on date range
+
+ $cgi->param('from',
+ $opt{'conf'}->config('invoice_from')
+ );
+
+ # shortcut for common text
+ my $summary_text = $cust_main->name_short .
+ ($cgi->param('start_date') ? ' from ' : '') .
+ $cgi->param('start_date') .
+ ($cgi->param('end_date') ? ' through ' : '') .
+ $cgi->param('end_date');
+
+ $cgi->param('subject',
+ $opt{'conf'}->config('company_name') .
+ ' statement for ' .
+ $summary_text
+ );
+
+ $cgi->param('html_body',
+ '<P>' .
+ $opt{'conf'}->config('company_name') .
+ ' statement of charges and payments for ' .
+ $summary_text .
+ "</P>" .
+ include('/elements/customer-statement.html',
+ 'history' => [
+ $cust_main->payment_history(
+ map {
+ $_ => parse_datetime(scalar($cgi->param($_)))
+ }
+ qw( start_date end_date ),
+ ),
+ ],
+ )
+ );
+};
+
+</%init>
+
diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html
index 83e86158f..09ff93cca 100644
--- a/httemplate/misc/email-customers.html
+++ b/httemplate/misc/email-customers.html
@@ -1,3 +1,35 @@
+<%doc>
+
+Allows emailing one or more customers, based on a search for customers. Search can
+be specified either through cust_main fields as cgi params, or through a base64 encoded
+frozen hash in the 'search' cgi param. Form allows selecting an existing msg_template,
+or creating a custom message, and shows a preview of the message before sending.
+If linked to as a popup, include the cgi parameter 'popup' for proper header handling.
+
+This may also be used as an element in other pages, enabling you to provide an
+alternate initial form while using this for search freezing/thawing and
+preview/send actions, with the following options:
+
+acl - the access right to use (defaults to 'Bulk send customer notices')
+
+form_action - the URL to submit the form to
+
+process_url - the URL for starting the JSRPC process
+
+title - the title of the page
+
+no_search_fields - arrayref of additional fields that are not search parameters
+
+alternate_form - subroutine that returns alternate html for the initial form,
+replaces msgnum/from/subject/html_body/action inputs and submit button,
+not used if an action is specified
+
+post_search_hook - sub hook for additional processing after search has been processed from cgi,
+gets passed options 'conf' and 'search' (a reference to the unfrozen %search hash),
+should be used to set msgnum or from/subject/html_body cgi params
+
+</%doc>
+
% if ($popup) {
<% include('/elements/header-popup.html', $title) %>
% } else {
@@ -5,7 +37,7 @@
% }
-<FORM NAME="OneTrueForm" ACTION="email-customers.html" METHOD="POST">
+<FORM NAME="OneTrueForm" ACTION="<% $form_action %>" METHOD="POST">
<INPUT TYPE="hidden" NAME="table" VALUE="<% $table %>">
%# Mixing search params with from address, subject, etc. required special-case
%# handling of those, risked name conflicts, and caused massive problems with
@@ -19,13 +51,12 @@
<FONT SIZE="+2">Sending notice</FONT>
- <% include('/elements/progress-init.html',
+ <& /elements/progress-init.html,
'OneTrueForm',
[ qw( search table from subject html_body text_body msgnum ) ],
- 'process/email-customers.html',
+ $process_url,
$pdest,
- )
- %>
+ &>
% } elsif ( $cgi->param('action') eq 'preview' ) {
@@ -36,7 +67,7 @@
% if ( $cgi->param('action') ) {
<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
- <INPUT TYPE="hidden" NAME="msgnum" VALUE="<% $cgi->param('msgnum') %>">
+ <INPUT TYPE="hidden" NAME="msgnum" VALUE="<% scalar($cgi->param('msgnum')) %>">
% if ( $msg_template ) {
<% include('/elements/tr-fixed.html',
@@ -62,7 +93,7 @@
<INPUT TYPE="hidden" NAME="html_body" VALUE="<% $html_body |h %>">
<TR>
- <TD ALIGN="right" VALIGN="top">Message (HTML display): </TD>
+ <TH ALIGN="right" VALIGN="top">Message (HTML display): </TD>
<TD CLASS="background" ALIGN="left"><% $html_body %></TD>
</TR>
@@ -73,13 +104,16 @@
% );
<INPUT TYPE="hidden" NAME="text_body" VALUE="<% $text_body |h %>">
<TR>
- <TD ALIGN="right" VALIGN="top">Message (Text display): </TD>
- <TD CLASS="background" STYLE="background-color:white" ALIGN="left"><PRE><% $text_body %></PRE></TD>
+ <TH ALIGN="right" VALIGN="top">Message (Text display): </TD>
+ <TD CLASS="background" STYLE="background-color:white" ALIGN="left">
+ <a href="javascript:void(0)" onclick="this.style.display='none'; document.getElementById('email-message-text').style.display=''">click to view</a>
+ <PRE id="email-message-text" style="display: none;"><% $text_body %></PRE>
+ </TD>
</TR>
</TABLE>
-% if ( $cgi->param('action') eq 'preview' ) {
+% if ( $cgi->param('action') eq 'preview' ) {
<SCRIPT>
function areyousure(href) {
@@ -93,6 +127,10 @@
% }
+% } elsif ($opt{'alternate_form'}) {
+
+<% &{$opt{'alternate_form'}}() %>
+
% } else {
<SCRIPT TYPE="text/javascript">
@@ -121,12 +159,11 @@ Template:
'size' => 20,
&>&gt;</TD>
- <% include('/elements/tr-input-text.html',
+ <& /elements/tr-input-text.html,
'field' => 'subject',
'label' => 'Subject:',
'size' => 50,
- )
- %>
+ &>
<TR>
<TD ALIGN="right" VALIGN="top" STYLE="padding-top:3px">Message: </TD>
@@ -144,7 +181,7 @@ Template:
<INPUT TYPE="hidden" NAME="action" VALUE="preview">
<INPUT TYPE="submit" VALUE="Preview notice">
-% }
+% } #end not action or alternate form
</FORM>
@@ -154,14 +191,24 @@ Template:
</SCRIPT>
% }
-<% include('/elements/footer.html') %>
+<& /elements/footer.html &>
<%init>
+my %opt = @_;
+
+$opt{'acl'} ||= 'Bulk send customer notices';
+
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices');
+ unless $FS::CurrentUser::CurrentUser->access_right($opt{'acl'});
my $conf = FS::Conf->new;
+my @no_search_fields = qw( action table from subject html_body text_body popup url );
+
+my $form_action = $opt{'form_action'} || 'email-customers.html';
+my $process_url = $opt{'process_url'} || 'process/email-customers.html';
+my $title = $opt{'title'} || 'Send customer notices';
+push( @no_search_fields, @{$opt{'no_search_fields'}} ) if $opt{'no_search_fields'};
my $table = $cgi->param('table') or die "'table' required";
my $agent_virt_agentnum = $cgi->param('agent_virt_agentnum') || '';
@@ -173,19 +220,22 @@ $pdest->{'url'} = $cgi->param('url') if $url;
my %search;
if ( $cgi->param('search') ) {
- %search = %{ thaw(decode_base64($cgi->param('search'))) };
+ %search = %{ thaw(decode_base64( $cgi->param('search') )) };
}
else {
%search = $cgi->Vars;
- delete $search{$_} for qw( action table from subject html_body text_body popup url );
+ delete $search{$_} for @no_search_fields;
# FS::$table->search is expected to know which parameters might be
# multi-valued, and to accept scalar values for them also. No good
# solution to this since CGI can't tell whether a parameter _might_
# have had multiple values, only whether it does.
@search{keys %search} = map { /\0/ ? [ split /\0/, $_ ] : $_ } values %search;
-}
+}
-my $title = 'Send customer notices';
+&{$opt{'post_search_hook'}}(
+ 'conf' => $conf,
+ 'search' => \%search,
+) if $opt{'post_search_hook'};
my $num_cust;
my $from = '';
@@ -203,6 +253,7 @@ my $html_body = $cgi->param('html_body') || '';
my $msg_template = '';
if ( $cgi->param('action') eq 'preview' ) {
+
my $sql_query = "FS::$table"->search(\%search);
my $count_query = delete($sql_query->{'count_query'});
my $count_sth = dbh->prepare($count_query)
@@ -214,17 +265,18 @@ if ( $cgi->param('action') eq 'preview' ) {
if ( $cgi->param('msgnum') ) {
$msg_template = qsearchs('msg_template',
- { msgnum => $cgi->param('msgnum') } )
+ { msgnum => scalar($cgi->param('msgnum')) } )
or die "template not found: ".$cgi->param('msgnum');
$sql_query->{'extra_sql'} .= ' LIMIT 1';
$sql_query->{'select'} = "$table.*";
$sql_query->{'order_by'} = '';
my $object = qsearchs($sql_query);
my $cust = $object->cust_main;
- my %message = $msg_template->prepare(
+ my %msgopts = (
'cust_main' => $cust,
- 'object' => $object
+ 'object' => $object,
);
+ my %message = $msg_template->prepare(%msgopts);
($from, $subject, $html_body) = @message{'from', 'subject', 'html_body'};
}
}
diff --git a/httemplate/misc/process/batch-cust_pay.cgi b/httemplate/misc/process/batch-cust_pay.cgi
index 1105af943..ff7886239 100644
--- a/httemplate/misc/process/batch-cust_pay.cgi
+++ b/httemplate/misc/process/batch-cust_pay.cgi
@@ -12,7 +12,8 @@ my $paybatch = time2str('webbatch-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time);
my @cust_pay = ();
#my $row = 0;
#while ( exists($param->{"custnum$row"}) ) {
-for ( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
+my @invrows = grep(/^invnum\d+\.\d+$/, keys %$param);
+foreach my $row ( map /^custnum(\d+)$/, keys %$param ) {
my $custnum = $param->{"custnum$row"};
my $cust_main;
if ( $custnum =~ /^(\d+)$/ and $1 <= 2147483647 ) {
@@ -39,6 +40,7 @@ for ( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
'payinfo' => $param->{"payinfo$row"},
'discount_term' => $param->{"discount_term$row"},
'paybatch' => $paybatch,
+ 'no_auto_apply' => exists($param->{"no_auto_apply$row"}) ? 'Y' : '',
}
if $param->{"custnum$row"}
|| $param->{"paid$row"}
@@ -48,7 +50,8 @@ for ( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
# payment applications, if any
my @cust_bill_pay = ();
- for ( my $app = 0; exists($param->{"invnum$row.$app"}); $app++ ) {
+ foreach my $app ( sort {$a <=> $b} map /^invnum$row\.(\d+)$/, @invrows ) {
+# for ( my $app = 0; exists($param->{"invnum$row.$app"}); $app++ ) {
next if !$param->{"invnum$row.$app"};
push @cust_bill_pay, new FS::cust_bill_pay {
'invnum' => $param->{"invnum$row.$app"},
diff --git a/httemplate/misc/process/delete-cust_pay_batch.cgi b/httemplate/misc/process/delete-cust_pay_batch.cgi
new file mode 100644
index 000000000..4937b58e2
--- /dev/null
+++ b/httemplate/misc/process/delete-cust_pay_batch.cgi
@@ -0,0 +1,37 @@
+<% $server->process %>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('Process batches')
+ || $curuser->access_right('Process global batches');
+
+# look up paybatch using agentnums_sql & status constraints
+# to validate access for this particular cust_pay_batch,
+# similar to how it's done in cust_pay_batch.cgi
+
+my %arg = $cgi->param('arg');
+my $paybatchnum = $arg{'paybatchnum'};
+$paybatchnum =~ /^\d+$/ or die "Illegal paybatchnum";
+my @search = ();
+push @search, 'cust_pay_batch.paybatchnum = ' . $paybatchnum;
+push @search, '(cust_pay_batch.status = \'\' OR cust_pay_batch.status IS NULL)';
+push @search, 'pay_batch.status = \'O\'';
+push @search, $curuser->agentnums_sql({ table => 'cust_main' });
+push @search, $curuser->agentnums_sql({ table => 'pay_batch',
+ null_right => 'Process global batches',
+ });
+my $search = ' WHERE ' . join(' AND ', @search);
+die "permission denied" unless qsearchs({
+ 'table' => 'cust_pay_batch',
+ 'hashref' => {},
+ 'addl_from' => 'LEFT JOIN pay_batch USING ( batchnum ) '.
+ 'LEFT JOIN cust_main USING ( custnum ) '.
+ 'LEFT JOIN cust_pay USING ( batchnum, custnum ) ',
+ 'extra_sql' => $search
+});
+
+my $server = new FS::UI::Web::JSRPC 'FS::cust_pay_batch::process_unbatch_and_delete', $cgi;
+
+</%init>
diff --git a/httemplate/misc/process/email-customer-statement.html b/httemplate/misc/process/email-customer-statement.html
new file mode 100644
index 000000000..40a8a702a
--- /dev/null
+++ b/httemplate/misc/process/email-customer-statement.html
@@ -0,0 +1,9 @@
+<% $server->process %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Resend invoices');
+
+my $server = new FS::UI::Web::JSRPC 'FS::cust_main_Mixin::process_email_search_result', $cgi;
+
+</%init>
diff --git a/httemplate/misc/process/nms-add_iface.html b/httemplate/misc/process/nms-add_iface.html
index d21b3792a..79e685686 100644
--- a/httemplate/misc/process/nms-add_iface.html
+++ b/httemplate/misc/process/nms-add_iface.html
@@ -1,4 +1,4 @@
-<& /elements/header-popup.html, 'Interface added') &>
+<& /elements/header-popup.html, 'Interface added' &>
<SCRIPT TYPE="text/javascript">
window.top.location.reload();
</SCRIPT>
diff --git a/httemplate/search/cust_bill.html b/httemplate/search/cust_bill.html
index bd302c64f..62f5f7afa 100755
--- a/httemplate/search/cust_bill.html
+++ b/httemplate/search/cust_bill.html
@@ -1,61 +1,82 @@
<& elements/search.html,
- 'title' => emt('Invoice Search Results'),
- 'html_init' => $html_init,
- 'menubar' => $menubar,
- 'name' => 'invoices',
- 'query' => $sql_query,
- 'count_query' => $count_query,
- 'count_addl' => $count_addl,
- 'redirect' => $link,
- 'header' => [ emt('Invoice #'),
- emt('Balance'),
- emt('Net Amount'),
- emt('Gross Amount'),
- emt('Date'),
- FS::UI::Web::cust_header(),
- ],
- 'fields' => [
- 'display_invnum',
- sub { sprintf($money_char.'%.2f', shift->get('owed') ) },
- sub { sprintf($money_char.'%.2f', shift->get('net') ) },
- sub { sprintf($money_char.'%.2f', shift->charged ) },
- sub { time2str('%b %d %Y', shift->_date ) },
- \&FS::UI::Web::cust_fields,
- ],
- 'sort_fields' => [
- 'COALESCE( agent_invid, invnum )',
- FS::cust_bill->owed_sql,
- FS::cust_bill->net_sql,
- 'charged',
- '_date',
- ],
- 'align' => 'rrrrl'.FS::UI::Web::cust_aligns(),
- 'links' => [
- $link,
- $link,
- $link,
- $link,
- $link,
- ( map { $_ ne 'Cust. Status' ? $clink : '' }
- FS::UI::Web::cust_header()
- ),
- ],
- 'color' => [
- '',
- '',
- '',
- '',
- '',
- FS::UI::Web::cust_colors(),
- ],
- 'style' => [
- '',
- '',
- '',
- '',
- '',
- FS::UI::Web::cust_styles(),
- ],
+ 'title' => emt('Invoice Search Results'),
+ 'html_init' => $html_init,
+ 'menubar' => $menubar,
+ 'name' => 'invoices',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'count_addl' => $count_addl,
+ 'redirect' => $link,
+ 'header' => [ emt('Invoice #'),
+ emt($invoiced ? 'Charged' : 'Gross Amount'),
+ emt('Discount'),
+ emt('Credits'),
+ emt('Net Amount'),
+ emt('Balance'),
+ emt('Date'),
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [
+ 'display_invnum',
+ $invoiced ? 'charged' : 'gross',
+ 'discounted',
+ 'credited',
+ 'net',
+ 'owed',
+ sub { time2str('%b %d %Y', shift->_date ) },
+ \&FS::UI::Web::cust_fields,
+ ],
+ 'sort_fields' => [
+ 'COALESCE( agent_invid, invnum )',
+ $invoiced ? 'charged' : 'gross',
+ 'discounted',
+ 'credited',
+ 'net',
+ 'owed',
+ '_date',
+ ],
+ 'format' => [
+ '',
+ $money_char.'%.2f',
+ $money_char.'%.2f',
+ $money_char.'%.2f',
+ $money_char.'%.2f',
+ $money_char.'%.2f',
+ '',
+ ],
+ 'align' => 'rrrrrrl'.FS::UI::Web::cust_aligns(),
+ 'links' => [
+ $link,
+ $link,
+ $link,
+ $link,
+ $link,
+ $link,
+ $link,
+ ( map { $_ ne 'Cust. Status' ? $clink : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'color' => [
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
&>
<%init>
@@ -66,6 +87,9 @@ my( $count_query, $sql_query );
my $count_addl = '';
my %search = ();
+# show invoiced amount (charged) instead of gross sales
+my $invoiced = $cgi->param('invoiced') ? 1 : 0;
+
if ( $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/ ) {
my $join_cust_main = FS::UI::Web::join_cust_main('cust_bill');
@@ -107,11 +131,12 @@ if ( $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/ ) {
($search{'days'}, my $field) = ($2, $3);
$field = "_date" if $field eq 'date';
$search{'order_by'} = "cust_bill.$field";
+ $search{'invoiced'} = 1; # preserve old behavior under 3.x
}
#scalars
for (qw( agentnum custnum cust_status refnum invnum_min invnum_max
- open net newest_percust
+ open net newest_percust invoiced
))
{
diff --git a/httemplate/search/cust_bill_pay_pkg.html b/httemplate/search/cust_bill_pay_pkg.html
index a6738f32c..5a3be7551 100644
--- a/httemplate/search/cust_bill_pay_pkg.html
+++ b/httemplate/search/cust_bill_pay_pkg.html
@@ -10,6 +10,7 @@
'Amount',
'By',
'#',
+ 'Card Type',
#payment
'Date',
@@ -20,6 +21,7 @@
# line item
'Description',
+ 'Package class',
'Location',
@post_desc_header,
@@ -36,7 +38,10 @@
$cust_pay->payby =~ /^(CARD|CHEK)$/
? $cust_pay->paymask : $cust_pay->payinfo;
},
-
+ sub { my $cust_pay = shift->cust_bill_pay->cust_pay;
+ $cust_pay->payby =~ /^CARD$/
+ ? cardtype($cust_pay->paymask) : '';
+ },
sub { time2str('%b %d %Y', shift->get('cust_pay_date') ) },
sub { shift->cust_bill_pay->cust_pay->otaker },
@@ -46,6 +51,7 @@
? $_[0]->get('pkg') # possibly use override.pkg
: $_[0]->get('itemdesc') # but i think this correct
},
+ 'classname', #package class
$location_sub,
@post_desc,
'invnum',
@@ -55,10 +61,13 @@
'sort_fields' => [
'paid',
'', #payby
+ '', #payinfo/paymask
+ '', #cardtype
'cust_pay_date',
'', #'otaker',
'', #amount
'', #line item description
+ '', #package class
'', #location
@post_desc_null,
'invnum',
@@ -74,6 +83,8 @@
'',
'',
'',
+ '',
+ '',
@post_desc_null,
$ilink,
$ilink,
@@ -81,7 +92,10 @@
FS::UI::Web::cust_header()
),
],
- 'align' => 'rcrrlrlllrrcl'.
+ 'align' => 'rcrlrlrlll',
+#original value before cardtype & package were added
+#why are there 13 cols?
+#'rcrrlrlllrrcl'.
$post_desc_align.
'rr'.
FS::UI::Web::cust_aligns(),
@@ -94,6 +108,8 @@
'',
'',
'',
+ '',
+ '',
@post_desc_null,
'',
'',
@@ -108,6 +124,8 @@
'',
'',
'',
+ '',
+ '',
@post_desc_null,
'',
'',
@@ -415,6 +433,7 @@ if ( $cgi->param('nottax') ) {
$join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum )
LEFT JOIN part_pkg USING ( pkgpart )
+ LEFT JOIN pkg_class USING ( classnum )
LEFT JOIN part_pkg AS override
ON pkgpart_override = override.pkgpart ';
$join_pkg .= ' LEFT JOIN cust_location USING ( locationnum ) '
@@ -454,7 +473,8 @@ if ( $cgi->param('nottax') ) {
#warn "neither nottax nor istax parameters specified";
#same as before?
$join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum )
- LEFT JOIN part_pkg USING ( pkgpart ) ';
+ LEFT JOIN part_pkg USING ( pkgpart )
+ LEFT JOIN pkg_class USING ( classnum )';
}
@@ -477,7 +497,7 @@ my @select = ( 'cust_bill_pay_pkg.*',
'cust_pay._date AS cust_pay_date',
'cust_bill._date',
);
-push @select, 'part_pkg.pkg' unless $cgi->param('istax');
+push @select, 'part_pkg.pkg, pkg_class.classname' unless $cgi->param('istax');
push @select, 'cust_main.custnum',
FS::UI::Web::cust_sql_fields();
@@ -493,10 +513,10 @@ if ( $conf->exists('enable_taxclasses') && ! $cgi->param('istax') ) {
push @select, 'part_pkg.taxclass'; # or should this use override?
}
-warn "$join_cust_bill_pkg
- $join_pkg
- $join_pay
- $join_cust";
+#warn "$join_cust_bill_pkg
+# $join_pkg
+# $join_pay
+# $join_cust";
my $query = {
'table' => 'cust_bill_pay_pkg',
diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi
index cbe37bcc4..a5424033d 100644
--- a/httemplate/search/cust_bill_pkg.cgi
+++ b/httemplate/search/cust_bill_pkg.cgi
@@ -186,8 +186,8 @@ my $conf = new FS::Conf;
my $money_char = $conf->config('money_char') || '$';
my @select = ( 'cust_bill_pkg.*', 'cust_bill._date' );
-my @total = ( 'COUNT(*)', 'SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)');
-my @total_desc = ( $money_char.'%.2f total' ); # sprintf strings
+my @total = ( 'COUNT(*)' );
+my @total_desc = ();
my @peritem = ( 'setup', 'recur' );
my @peritem_desc = ( 'Setup charges', 'Recurring charges' );
@@ -216,7 +216,7 @@ if ( $conf->exists('enable_taxclasses') ) {
}
# used in several places
-my $itemdesc = 'COALESCE(part_fee.itemdesc, part_pkg.pkg, cust_bill_pkg.itemdesc)';
+my $itemdesc = 'COALESCE(cust_bill_pkg.itemdesc, part_fee.itemdesc, part_pkg.pkg, cust_bill_pkg.itemdesc)';
# valid in both the tax and non-tax cases
my $join_cust =
@@ -291,14 +291,14 @@ if ( $use_override ) {
$part_pkg = 'override';
}
push @select, "$part_pkg.pkgpart", "$part_pkg.pkg";
+push @select, "($itemdesc) AS itemdesc"; # available in all report modes
+
push @select, "COALESCE($part_pkg.taxclass, part_fee.taxclass) AS taxclass"
if $conf->exists('enable_taxclasses');
# the non-tax case
if ( $cgi->param('nottax') ) {
- push @select, "($itemdesc) AS itemdesc";
-
push @where,
'(cust_bill_pkg.pkgnum > 0 OR cust_bill_pkg.feepart IS NOT NULL)';
@@ -489,14 +489,32 @@ if ( $cgi->param('nottax') ) {
# setup/recur/usage separation
my %charges = map { $_ => 1 } split('', $cgi->param('charges') || 'SRU');
- if ( $charges{R} and $charges{U} ) {
+ if ( $charges{S} and $charges{R} and $charges{U} ) {
+ # in this case, show discounts
- # default, don't change @peritem or @total
- if ( !$charges{S} ) {
- push @where, 'cust_bill_pkg.recur > 0';
- $total[1] = "SUM(cust_bill_pkg.recur)";
- $total_desc[0] = "$money_char%.2f recurring";
- }
+ $join_pkg .= ' JOIN (
+ SELECT billpkgnum, COALESCE(SUM(amount), 0) AS discounted
+ FROM cust_bill_pkg_discount RIGHT JOIN cust_bill_pkg USING (billpkgnum)
+ GROUP BY billpkgnum
+ ) AS _discount ON (cust_bill_pkg.billpkgnum = _discount.billpkgnum)
+ ';
+ push @select, '_discount.discounted';
+
+ push @peritem, 'discounted';
+ push @peritem_desc, 'Discount';
+ push @total, 'SUM(cust_bill_pkg.setup + cust_bill_pkg.recur + discounted)',
+ 'SUM(discounted)',
+ 'SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)';
+ push @total_desc, "$money_char%.2f gross sales",
+ "&minus; $money_char%.2f discounted",
+ "= $money_char%.2f invoiced";
+
+ } elsif ( $charges{R} and $charges{U} ) {
+
+ # hide rows with no recurring fee, and show the sum of recurring fees only
+ push @where, 'cust_bill_pkg.recur > 0';
+ push @total, "SUM(cust_bill_pkg.recur)";
+ push @total_desc, "$money_char%.2f recurring";
} elsif ( $charges{R} and !$charges{U} ) {
@@ -505,8 +523,8 @@ if ( $cgi->param('nottax') ) {
push @select, "($recur_no_usage) AS recur_no_usage";
$peritem[1] = 'recur_no_usage';
$peritem_desc[1] = 'Recurring charges (excluding usage)';
- $total[1] = "SUM($recur_no_usage)";
- $total_desc[0] = "$money_char%.2f recurring";
+ push @total, "SUM($recur_no_usage)";
+ push @total_desc, "$money_char%.2f recurring";
if ( !$charges{S} ) {
push @where, "($recur_no_usage) > 0";
}
@@ -518,8 +536,8 @@ if ( $cgi->param('nottax') ) {
# there's already a method named 'usage'
$peritem[1] = '_usage';
$peritem_desc[1] = 'Usage charge';
- $total[1] = "SUM($usage)";
- $total_desc[0] = "$money_char%.2f usage charges";
+ push @total, "SUM($usage)";
+ push @total_desc, "$money_char%.2f usage charges";
if ( !$charges{S} ) {
push @where, "($usage) > 0";
}
@@ -527,8 +545,8 @@ if ( $cgi->param('nottax') ) {
} elsif ( $charges{S} ) {
push @where, "cust_bill_pkg.setup > 0";
- $total[1] = "SUM(cust_bill_pkg.setup)";
- $total_desc[0] = "$money_char%.2f setup";
+ push @total, "SUM(cust_bill_pkg.setup)";
+ push @total_desc, "$money_char%.2f setup";
} # else huh? you have to have SOME charges
@@ -551,10 +569,11 @@ if ( $cgi->param('nottax') ) {
}
}
- $total[1] = 'SUM(
+ push @total, 'SUM(
COALESCE(cust_bill_pkg_tax_rate_location.amount,
cust_bill_pkg.setup + cust_bill_pkg.recur)
)';
+ push @total_desc, "$money_char%.2f total";
} else { # the internal-tax case
@@ -564,8 +583,9 @@ if ( $cgi->param('nottax') ) {
';
# don't double-count the components of consolidated taxes
- $total[0] = 'COUNT(DISTINCT cust_bill_pkg.billpkgnum)';
- $total[1] = 'SUM(cust_bill_pkg_tax_location.amount)';
+ @total = ( 'COUNT(DISTINCT cust_bill_pkg.billpkgnum)',
+ 'SUM(cust_bill_pkg_tax_location.amount)' );
+ @total_desc = "$money_char%.2f total";
# taxclass
if ( $cgi->param('taxclassNULL') ) {
diff --git a/httemplate/search/cust_pay_batch.cgi b/httemplate/search/cust_pay_batch.cgi
index b47aaf37c..6f5db2d96 100755
--- a/httemplate/search/cust_pay_batch.cgi
+++ b/httemplate/search/cust_pay_batch.cgi
@@ -14,6 +14,7 @@
'Exp',
'Amount',
'Status',
+ '', # delete link
'', # error_message
],
'fields' => [ 'paybatchnum',
@@ -31,9 +32,10 @@
sprintf('%.02f', $_[0]->amount)
},
'status',
+ $sub_unbatch,
'error_message',
],
- 'align' => 'rrrlllcrll',
+ 'align' => 'rrrlllcrlll',
'links' => [ '',
["${p}view/cust_bill.cgi?", 'invnum'],
(["${p}view/cust_main.cgi?", 'custnum']) x 2,
@@ -129,6 +131,36 @@ my $sub_receipt = sub {
);
};
+my $sub_unbatch = '';
+if ( ($pay_batch && ($pay_batch->status eq 'O'))
+ && ( $curuser->access_right('Process batches')
+ || $curuser->access_right('Process global batches') )
+) {
+ $sub_unbatch = sub {
+ my $self = shift;
+ return '' if $self->status; # sanity check, shouldn't happen
+ my $batchnum = $self->batchnum;
+ my $paybatchnum = $self->paybatchnum;
+ my $out = <<EOF;
+<FORM name="delete_cust_pay_batch_$paybatchnum">
+<INPUT TYPE="hidden" name="paybatchnum" value="$paybatchnum">
+</FORM>
+EOF
+ $out .= include('/elements/progress-init.html',
+ "delete_cust_pay_batch_$paybatchnum",
+ [ 'paybatchnum' ],
+ $p.'misc/process/delete-cust_pay_batch.cgi',
+ $p.'search/cust_pay_batch.cgi?' . $cgi->query_string,
+ "paybatchnum$paybatchnum",
+ );
+ my $onclick = 'if ( confirm(\'';
+ $onclick .= emt('Are you sure you want to delete batch payment ') . $self->paybatchnum;
+ $onclick .= emt(' from payment batch ') . $self->batchnum;
+ $onclick .= '\') ) { paybatchnum' . $paybatchnum . 'process() }';
+ return $out . '<A HREF="javascript:void(0)" ONCLICK="' . $onclick . '">delete</A>';
+ };
+}
+
my $html_init = '';
if ( $pay_batch ) {
$html_init = include('elements/cust_pay_batch_top.html',
diff --git a/httemplate/search/cust_pkg_summary.cgi b/httemplate/search/cust_pkg_summary.cgi
index c0eb69920..76ca8956b 100644
--- a/httemplate/search/cust_pkg_summary.cgi
+++ b/httemplate/search/cust_pkg_summary.cgi
@@ -56,7 +56,8 @@ foreach my $column (keys %conds) {
my $count_query = $h_search->{count_query};
# push a select expression for the total packages with pkgpart=main.pkgpart
- push @select, "($count_query AND h_cust_pkg.pkgpart = main.pkgpart) AS $column";
+ # (have to quote $column, otherwise mysql thinks before/after are keywords)
+ push @select, "($count_query AND h_cust_pkg.pkgpart = main.pkgpart) AS \"$column\"";
# and query the total packages with pkgpart=any of the main.pkgparts
my $total = FS::Record->scalar_sql($count_query .
diff --git a/httemplate/search/customer_accounting_summary.html b/httemplate/search/customer_accounting_summary.html
index 0dab7cecf..744b313f9 100644
--- a/httemplate/search/customer_accounting_summary.html
+++ b/httemplate/search/customer_accounting_summary.html
@@ -141,9 +141,20 @@ $title .= 'Customer Accounting Summary Report';
my @items = ('netsales', 'cashflow');
my @params = ( [], [] );
-my $setuprecur = '';
-if ( $cgi->param('setuprecur') ) {
- $setuprecur = 1;
+my $grossdiscount = $cgi->param('grossdiscount');
+my $setuprecur = $cgi->param('setuprecur');
+if ($setuprecur && $grossdiscount) {
+ #see blocks below for more details on each option
+ @items = ('gross', 'discounted', 'receipts', 'gross', 'discounted', 'receipts');
+ @params = (
+ [ setuprecur => 'setup' ],
+ [ setuprecur => 'setup' ],
+ [ setuprecur => 'setup' ],
+ [ setuprecur => 'recur' ],
+ [ setuprecur => 'recur' ],
+ [ setuprecur => 'recur' ],
+ );
+} elsif ($setuprecur) {
# instead of 'cashflow' (payments - refunds), use 'receipts'
# (applied payments), because it's divisible into setup and recur.
@items = ('netsales', 'receipts', 'netsales', 'receipts');
@@ -153,7 +164,14 @@ if ( $cgi->param('setuprecur') ) {
[ setuprecur => 'recur' ],
[ setuprecur => 'recur' ],
);
+} elsif ($grossdiscount) {
+ # instead of 'netsales' (invoiced - netcredits)
+ # use 'gross' (invoiced + discounted) and 'discounted' (sum of discounts on invoices)
+ @items = ('gross', 'discounted', 'cashflow');
+ @params = ( [], [], [] );
}
+
+
my @labels = ();
my @cross_params = ();
@@ -208,7 +226,7 @@ $cells[0] = [
{ header => 1, rowspan => 2, colspan => ($setuprecur ? 4 : 3) },
($setuprecur ? '' : ()),
map {
- { header => 1, colspan => 2, value => time2str('%b %Y', $_) },
+ { header => 1, colspan => ($grossdiscount ? 3 : 2), value => time2str('%b %Y', $_) },
''
} @{ $data->{speriod} }
];
@@ -218,8 +236,14 @@ $rows[1] = {};
$cells[1] = [ '',
($setuprecur ? '' : ()),
map {
- ( { header => 1, value => mt('Billed') },
- { header => 1, value => mt('Paid') }
+ ( ($grossdiscount
+ ? (
+ { header => 1, value => mt('Gross') },
+ { header => 1, value => mt('Discount') }
+ )
+ : { header => 1, value => mt('Billed') }
+ ),
+ { header => 1, value => mt('Paid') },
) } (1..$ncols)
];
@@ -256,12 +280,12 @@ foreach my $cust_main (@cust_main) { # correspond to cross_params
header => 1 };
}
for my $col (0..$ncols-1) { # the month
- for my $subcol (0..1) { # the billed/paid axis
- my $item = $subrow * 2 + $subcol;
+ for my $subcol (0..($grossdiscount ? 2 : 1)) { # the billed/paid or gross/discount/paid axis
+ my $item = $subrow * ($grossdiscount ? 3 : 2) + $subcol;
my $value = $data->{data}[$item][$col][$row];
$skip = 0 if abs($value) > 0.005;
push @thisrow, { value => sprintf('%0.2f', $value), format => 'money' };
- $total[( ($ncols * $subrow) + $col ) * 2 + $subcol] += $value;
+ $total[( ($ncols * $subrow) + $col ) * ($grossdiscount ? 3 : 2) + $subcol] += $value;
} #subcol
} #col
push @cells, \@thisrow;
@@ -294,8 +318,8 @@ for my $subrow (0..($setuprecur ? 1 : 0)) {
{ value => $subrow ? mt('recurring') : mt('setup'),
header => 1 };
}
- for my $col (0..($ncols * 2)-1) { # month and billed/paid axis
- my $value = $total[($subrow * $ncols * 2) + $col];
+ for my $col (0..($ncols * ($grossdiscount ? 3 : 2))-1) { # month and billed/paid or gross/discount/paid axis
+ my $value = $total[($subrow * $ncols * ($grossdiscount ? 3 : 2)) + $col];
push @thisrow, { value => sprintf('%0.2f', $value), format => 'money' };
}
push @cells, \@thisrow;
diff --git a/httemplate/search/elements/cust_main_dayranges.html b/httemplate/search/elements/cust_main_dayranges.html
index e5b1f478c..951eff29d 100644
--- a/httemplate/search/elements/cust_main_dayranges.html
+++ b/httemplate/search/elements/cust_main_dayranges.html
@@ -21,7 +21,7 @@ Example:
'query' => $sql_query,
'count_query' => $count_sql,
'header' => [
- @act_blank,
+ @act_head,
@cust_header,
'0-30',
'30-60',
@@ -248,7 +248,7 @@ if($opt{'payment_links'} && $curuser->access_right('Process payment') && @payby)
@payby );
}
-my (@act_blank, @act_fields, $act_align, $html_foot);
+my (@act_head, @act_blank, @act_fields, $act_align, $html_foot);
if (delete($opt{'email_checkboxes'})) {
my $email_link = q!var url = toCGIString(); !;
$email_link .= q/if (!url) { alert('No customers selected'); return false; }; /;
@@ -271,6 +271,7 @@ if (delete($opt{'email_checkboxes'})) {
qq!<input type="checkbox" name="custnum" value="$custnum">!;
};
$act_align = 'l';
+ push @act_head, {nodownload => 1};
push @act_blank, '';
}
diff --git a/httemplate/search/elements/cust_pay_batch_top.html b/httemplate/search/elements/cust_pay_batch_top.html
index 2dbf62019..626d7c3ea 100644
--- a/httemplate/search/elements/cust_pay_batch_top.html
+++ b/httemplate/search/elements/cust_pay_batch_top.html
@@ -19,7 +19,7 @@ function format_changed() {
% or ( $status eq 'I' and $curuser->access_right('Reprocess batches') )
% or ( $status eq 'R' and $curuser->access_right('Redownload resolved batches') )
% ) {
-<FORM ACTION="<%$p%>misc/download-batch.cgi" NAME="download" METHOD="POST">
+<FORM ACTION="<%$p%>misc/download-batch.cgi" NAME="download" METHOD="GET">
<INPUT TYPE="hidden" NAME="batchnum" VALUE="<%$batchnum%>">
% if ( $fixed ) {
<INPUT TYPE="hidden" NAME="format" VALUE="<%$fixed%>">
diff --git a/httemplate/search/elements/cust_pay_or_refund.html b/httemplate/search/elements/cust_pay_or_refund.html
index 0e3fce303..5808e5f3e 100755
--- a/httemplate/search/elements/cust_pay_or_refund.html
+++ b/httemplate/search/elements/cust_pay_or_refund.html
@@ -39,7 +39,7 @@ Examples:
)
</%doc>
-<& search.html,
+<& grouped-search.html,
'title' => $title, # XXX: translate
'name_singular' => $name_singular,
'query' => $sql_query,
@@ -54,6 +54,17 @@ Examples:
'link_onclicks' => \@link_onclicks,
'color' => \@color,
'style' => \@style,
+
+ 'group_column' => 'payby',
+ 'group_label' => 'payby_name',
+ 'subtotal' => { $opt{name_verb} => "sum($amount_field)" },
+ 'subtotal_row' => [ 'Subtotal',
+ sub { sprintf($money, $_[0]->$amount_field) },
+ ],
+ 'total_row' => [ '<B>Total</B>',
+ sub { sprintf("<B>$money</B>", $_[0]->$amount_field) },
+ ],
+ 'show_combined' => 1,
&>
<%init>
@@ -61,6 +72,9 @@ my %opt = @_;
my $curuser = $FS::CurrentUser::CurrentUser;
+my $conf = FS::Conf->new;
+my $money = ($conf->config('money_char') || '$') . '%.2f';
+
die "access denied"
unless $curuser->access_right('Financial reports');
@@ -165,16 +179,16 @@ push @header, "\u$name_singular",
$align .= 'rr';
push @links, '', '';
push @fields, 'payby_payinfo_pretty',
- sub { sprintf('$%.2f', shift->$amount_field() ) },
+ sub { sprintf($money, shift->$amount_field() ) },
;
push @link_onclicks, $sub_receipt, '';
-push @sort_fields, '', $amount_field;
+push @sort_fields, 'paysort', $amount_field;
if ( $unapplied ) {
push @header, emt('Unapplied');
$align .= 'r';
push @links, '';
- push @fields, sub { sprintf('$%.2f', shift->unapplied_amount) };
+ push @fields, sub { sprintf($money, shift->unapplied_amount) };
push @sort_fields, '';
}
@@ -230,6 +244,7 @@ if ( $cgi->param('magic') ) {
my @search = ();
my @select = (
"$table.*",
+ "( $table.payby || ' ' || coalesce($table.paymask, $table.payinfo) ) AS paysort",
FS::UI::Web::cust_sql_fields(),
'cust_main.custnum AS cust_main_custnum',
);
@@ -252,14 +267,10 @@ if ( $cgi->param('magic') ) {
$title = $part_referral->referral. " $title";
}
- # cust_classnum (false laziness w/ elements/cust_main_dayranges.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
- if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
- my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
- push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
- join(',', map { $_ || '0' } @classnums ).
- ' )'
- if @classnums;
- }
+ # cust_classnum - standard matching
+ push @search, $m->comp('match-classnum',
+ param => 'cust_classnum', field => 'cust_main.classnum'
+ );
if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
push @search, "$table.custnum = $1";
@@ -321,7 +332,6 @@ if ( $cgi->param('magic') ) {
} elsif ( $cardtype eq 'Discover' ) {
- my $conf = new FS::Conf;
my $country = $conf->config('countrydefault') || 'US';
$search =
diff --git a/httemplate/search/elements/grouped-search.html b/httemplate/search/elements/grouped-search.html
new file mode 100644
index 000000000..56fc88d38
--- /dev/null
+++ b/httemplate/search/elements/grouped-search.html
@@ -0,0 +1,49 @@
+<%doc>
+
+<& elements/grouped-search/html,
+
+ # required
+ 'title' => 'Page title',
+
+ 'name_singular' => 'item',
+
+ 'query' => {
+ 'table' => 'tablename',
+ 'hashref' => { 'field' => 'value', ... }, # optional
+ 'select' => '*',
+ 'addl_from' => '',
+ 'extra_sql' => '',
+ #'order_by' is not allowed
+ },
+ 'group_column' => 'classnum', # must be a single field
+ # 'group_table' => 'mytable', # optional; defaults to the one in the query
+ 'group_label' => 'classname',
+ 'subtotal' => { amount => 'sum(amount)', ... }
+ # The subtotal row will be generated as an FS::* object of the same type
+ # as all the detail rows.
+ # The only fields present will be the grouping column and any subtotals
+ # defined in 'subtotal'. Key is a field name (in the FS::* object),
+ # value is an SQL aggregate expression.
+
+ # How to display that object: arrayref of literal strings or coderefs
+ # (the subtotal object will be passed). These will be placed in table
+ # cells, and can contain HTML.
+ 'subtotal_row' =>
+ [ 'Total'
+ sub { sprintf('%.2f', $_[0]->amount) },
+ ],
+
+ 'order_by' => '_date', # SQL ordering expression for the detail rows
+
+ 'header' => [ ... ], # per search.html
+ 'fields' => [ ... ],
+
+&>
+</%doc>
+<%init>
+my $type = 'html';
+if ($cgi->param('type') =~ /^(html|html-print|xls)$/) {
+ $type = $1;
+}
+</%init>
+<& "grouped-search/$type", @_ &>
diff --git a/httemplate/search/elements/grouped-search/core b/httemplate/search/elements/grouped-search/core
new file mode 100644
index 000000000..ffa8cee39
--- /dev/null
+++ b/httemplate/search/elements/grouped-search/core
@@ -0,0 +1,162 @@
+<%doc>
+
+my $group_data = $m->comp( 'elements/grouped-search/core',
+
+ 'query' => {
+ 'table' => 'tablename',
+ 'hashref' => { 'field' => 'value', ... }, # optional
+ 'select' => '*',
+ 'addl_from' => '',
+ 'extra_sql' => '',
+ #'order_by' is not allowed
+ },
+ 'group_column' => 'classnum', # must be a single field
+ # 'group_table' => 'mytable', # optional; defaults to the one in the query
+ 'group_label' => 'classname',
+ 'subtotal' => { amount => 'sum(amount)', ... }
+ # The subtotal row will be generated as an FS::* object of the same type
+ # as all the detail rows.
+ # The only fields present will be the grouping column and any subtotals
+ # defined in 'subtotal'. Key is a field name (in the FS::* object),
+ # value is an SQL aggregate expression.
+
+ # How to display that object: arrayref of literal strings or coderefs
+ # (the subtotal object will be passed). These will be placed in table
+ # cells, and can contain HTML.
+ 'subtotal_row' =>
+ [ 'Subtotal', sub { sprintf('%.2f', $_[0]->amount) }, ],
+
+ # The subtotal aggregates will also be calculated for the entire data
+ # set, without grouping. How to display those (if not present, will
+ # not be displayed);
+ 'total_row' =>
+ [ 'Total', sub { sprintf('%.2f', $_[0]->amount) }, ],
+
+ 'order_by' => '_date', # SQL ordering expression for the detail rows
+
+ 'header' => [ ... ], # per search.html
+ 'fields' => [ ... ],
+);
+
+returns:
+{
+ num => number of groups,
+ groups => [ group summary objects,
+ with group key columns, subtotals, and "num_rows" ],
+ group_labels => [ label strings ],
+ group_footers => [ formatted subtotal row arrayrefs ],
+ queries => [ FS::Query objects to evaluate to get detail rows ],
+}
+
+</%doc>
+<%shared>
+my $conf = FS::Conf->new;
+</%shared>
+<%init>
+my %opt = @_;
+
+my $base_query = FS::Query->new( $opt{'query'} );
+my $main_table = $base_query->{table};
+
+# $opt{subtotal} is a hashref of field names => SQL aggregate expressions.
+
+my @subtotal_names = sort keys(%{ $opt{'subtotal'} });
+my @subtotal_exprs = map { $opt{'subtotal'}->{$_} } @subtotal_names;
+
+# qualify the group key to the main table unless otherwise specified
+my $group_table = $opt{group_table} || $main_table;
+my $group_key = $group_table . '.' . $opt{group_column};
+
+my @select = (
+ $group_key, # need this to identify groups
+ 'COUNT(*) as num_rows', # and this for pagination
+ map { '(' . $subtotal_exprs[$_] . ') AS "' . $subtotal_names[$_] . '"' }
+ 0 .. (scalar(@subtotal_names) - 1)
+);
+
+my $group_query = $base_query->clone;
+$group_query->{order_by} = "GROUP BY $group_key";
+$group_query->{select} = join(',', @select);
+# keep table, addl_from, extra_sql, and hashref as they are
+
+#warn Dumper($group_query); #DEBUG
+
+# run the group query and create a tab label for each group
+my @groups = $group_query->qsearch;
+
+# also run it with no grouping, and just get overall totals
+my $total_query = $base_query->clone;
+shift @select; # remove $group_key
+$total_query->{select} = join(',', @select);
+$total_query->{order_by} = '';
+
+my $totals = $total_query->qsearchs;
+
+my $order_by = '';
+if ( $cgi->param('order_by') =~ /^(\w+\.?\w*(?: DESC)?)$/ ) {
+ $order_by = $1;
+}
+if ( $opt{order_by} ) {
+ $order_by .= ', ' if length($order_by); # user selection takes priority
+ $order_by .= $opt{order_by};
+}
+$order_by = "ORDER BY $order_by " if $order_by;
+$base_query->{order_by} = $order_by;
+
+my $group_label = $opt{group_label} || $opt{group_column};
+my (@group_labels, @group_footers, @queries);
+for my $i (0 .. scalar(@groups) - 1) {
+ my $label = $groups[$i]->$group_label . ' (' . $groups[$i]->num_rows . ')';
+ push @group_labels, $label;
+
+ my @footer;
+ if ($opt{'subtotal_row'}) {
+ for( my $col = 0;
+ exists($opt{'subtotal_row'}[$col]) or exists($opt{'header'}[$col]);
+ $col++
+ ) {
+ my $value = $opt{'subtotal_row'}[$col] || '';
+ $value = &$value( $groups[$i] ) if ref($value) eq 'CODE';
+ $footer[$col] = $value;
+ }
+ }
+ push @group_footers, \@footer;
+
+ my $detail_query = $base_query->clone;
+ my $group_key_value = $groups[$i]->get( $opt{group_column} );
+ $group_key_value = dbh->quote($group_key_value)
+ unless looks_like_number($group_key_value);
+ $detail_query->and_where("$group_key = $group_key_value");
+ push @queries, $detail_query;
+}
+
+if ( $opt{show_combined} ) {
+ # set up group 0 as a combined view
+ unshift @groups, $totals;
+ unshift @group_labels, 'All ' . PL($opt{name_singular}) .
+ ' (' . $totals->num_rows . ')';
+ unshift @group_footers, []; # the total footer will suffice
+ unshift @queries, $base_query->clone;
+}
+
+my @total_footer;
+if ($opt{'total_row'}) {
+ for( my $col = 0;
+ exists($opt{'total_row'}[$col]) or exists($opt{'header'}[$col]);
+ $col++
+ ) {
+ my $value = $opt{'total_row'}[$col] || '';
+ $value = &$value( $totals ) if ref($value) eq 'CODE';
+ $total_footer[$col] = $value;
+ }
+}
+
+return {
+ num => scalar(@groups),
+ groups => \@groups,
+ group_labels => \@group_labels,
+ group_footers => \@group_footers,
+ queries => \@queries,
+ total_footer => \@total_footer,
+};
+</%init>
diff --git a/httemplate/search/elements/grouped-search/html b/httemplate/search/elements/grouped-search/html
new file mode 100644
index 000000000..df1471a52
--- /dev/null
+++ b/httemplate/search/elements/grouped-search/html
@@ -0,0 +1,149 @@
+<%shared>
+my $conf = FS::Conf->new;
+</%shared>
+<%init>
+my %opt = @_;
+$opt{'name'} ||= PL($opt{'name_singular'});
+
+my $group_info = $m->comp('core', %opt);
+
+my $redirect;
+
+if ( $group_info->{num} == 0 ) {
+ $redirect = $opt{'redirect_empty'};
+ if ($redirect) {
+ $redirect = &$redirect($cgi) if ref($redirect) eq 'CODE';
+ redirect( $redirect );
+ } else { # just print this stuff and exit
+ $m->comp('/elements/header.html', $opt{'title'});
+ $m->print('<BR><BR>No matching ' . $opt{'name'} . ' found.<BR>');
+ $m->comp('/elements/footer.html');
+ $m->abort;
+ }
+}
+
+# this mode has a concept of "current group"
+my $curr_group = 0;
+if ($cgi->param('group') =~ /^(\d+)$/) {
+ $curr_group = $1;
+}
+
+my $group = $group_info->{groups}[$curr_group];
+my $query = $group_info->{queries}[$curr_group];
+my $footer = $group_info->{group_footers}[$curr_group];
+my $total_footer = $group_info->{total_footer} || [];
+# pagination
+my ($limit, $offset);
+my $maxrecords = $conf->config('maxsearchrecordsperpage') || 50;
+if ( $cgi->param('maxrecords') =~ /^(\d+)$/ ) {
+ $maxrecords = $1;
+}
+if ( $maxrecords ) {
+ $limit = "LIMIT $maxrecords";
+ if ( $cgi->param('offset') =~ /^(\d+)$/ ) {
+ $offset = $1;
+ $limit .= " OFFSET $offset";
+ }
+}
+$query->{order_by} .= $limit if $limit;
+
+#warn Dumper($query); #DEBUG
+
+# run the query
+my @rows = $query->qsearch;
+
+#warn Dumper(\@rows); #DEBUG
+
+my $pager = '';
+# show pager if needed
+if ( $group->num_rows > scalar(@rows) ) {
+ $pager = include( '/elements/pager.html',
+ 'offset' => $offset,
+ 'num_rows' => scalar(@rows),
+ 'total' => $group->num_rows,
+ 'maxrecords' => $maxrecords,
+ );
+}
+
+# set up tab bar
+my @menubar;
+for (my $i = 0; $i < $group_info->{num}; $i++) {
+ push @menubar, $group_info->{group_labels}[$i], ";group=$i";
+}
+
+# not enabled yet; if we need this at some point, enable it on a per-report
+# basis and then disable it for search/cust_pay.html, because it's redundant
+# to see "Check Check #130108", "Credit card Card #401...", etc.
+
+## if this is the combined view, add a column for the group key
+#if ( $curr_group == 0 and $opt{'show_combined'} ) {
+# unshift @{$opt{'header'}}, '';
+# unshift @{$opt{'fields'}}, $opt{group_label};
+# unshift @{$opt{'sort_fields'}}, $opt{group_column} if $opt{'sort_fields'};
+# $opt{'align'} = 'c'.$opt{'align'};
+# foreach (qw(header2 links link_onclicks color size style cell_style xls_format)) {
+# if ( $opt{$_} ) {
+# unshift @{$opt{$_}}, '';
+# }
+# }
+#}
+
+</%init>
+
+<& /elements/header.html, $opt{title} &>
+
+%# tab bar
+% $cgi->delete('group');
+% $cgi->delete('offset');
+% $cgi->delete('type');
+<& /elements/menubar.html,
+ { newstyle => 1,
+ url_base => $cgi->self_url,
+ selected => $group_info->{group_labels}[$curr_group] },
+ @menubar
+&>
+
+<DIV CLASS="fstabcontainer">
+%# download links
+<P><% emt('Download full results') %><BR>
+% $cgi->param('type', 'xls');
+<A HREF="<% $cgi->self_url %>"><% emt('as Excel spreadsheet') %></A><BR>
+% $cgi->param('type', 'html-print');
+<A HREF="<% $cgi->self_url %>"><% emt('as printable copy') %></A><BR>
+% $cgi->delete('type');
+</P>
+
+<% $pager %>
+
+<STYLE>
+ table.grid {
+ border-spacing: 0;
+ }
+</STYLE>
+<table class="grid">
+ <thead>
+ <& /search/elements/search-html.html:header_row,
+ 'header' => $opt{'header'},
+ 'header2' => $opt{'header2'},
+ 'sort_fields' => ($opt{'sort_fields'} || $opt{'fields'}),
+ &>
+ </thead>
+ <tbody>
+ <& /search/elements/search-html.html:data_rows,
+ 'rows' => \@rows,
+ 'opt' => \%opt,
+ &>
+ </tbody>
+ <tfoot>
+ <& /search/elements/search-html.html:footer_row, row => $footer, opt => \%opt &>
+% if ( scalar @$total_footer ) {
+ <& /search/elements/search-html.html:footer_row, row => $total_footer, opt => \%opt &>
+% }
+ </tfoot>
+</table>
+
+<% $pager %>
+</DIV>
+
+<& /elements/footer.html &>
+
diff --git a/httemplate/search/elements/grouped-search/html-print b/httemplate/search/elements/grouped-search/html-print
new file mode 100644
index 000000000..6d9521ba3
--- /dev/null
+++ b/httemplate/search/elements/grouped-search/html-print
@@ -0,0 +1,81 @@
+<%doc>
+
+The "printable" view (all groups on one page).
+</%doc>
+<%init>
+my %opt = @_;
+
+my $group_info = $m->comp('core', %opt,
+ 'show_combined' => 0
+);
+my $ncols = scalar(@{ $opt{header} });
+
+my $total_footer = $group_info->{total_footer} || [];
+</%init>
+<& /elements/header-popup.html, $opt{title} &>
+
+<STYLE>
+.grouphead {
+ text-align: left;
+ font-size: 120%;
+ padding: 1ex 0 0.5ex 0.1ex;
+ border-top: 1px solid black;
+}
+.subtotal td {
+ background-color: #dddddd;
+ font-style: italic;
+ border-top: 1px dashed black;
+}
+.total td {
+ background-color: #dddddd;
+ font-style: italic;
+ font-weight: bold;
+ border-top: 2px solid black !important;
+}
+</STYLE>
+<& /elements/table-grid.html &>
+<THEAD>
+<& /search/elements/search-html.html:header_row,
+ 'header' => $opt{'header'},
+ 'header2' => $opt{'header2'},
+ 'sort_fields' => ($opt{'sort_fields'} || $opt{'fields'}),
+&>
+</THEAD>
+
+% for (my $curr_group = 0; $curr_group < $group_info->{num}; $curr_group++) {
+% my $group = $group_info->{groups}[$curr_group];
+% my $query = $group_info->{queries}[$curr_group];
+% my $footer = $group_info->{group_footers}[$curr_group];
+% my $label = $group_info->{group_labels}[$curr_group];
+% # run the query
+% my @rows = $query->qsearch;
+% #warn Dumper(\@rows); #DEBUG
+
+<TBODY>
+<TR><TH CLASS="grouphead" COLSPAN="<% $ncols %>">
+ <% $label %>
+</TH></TR>
+
+<& /search/elements/search-html.html:data_rows,
+ rows => \@rows,
+ opt => \%opt,
+&>
+</TBODY>
+<TBODY CLASS="subtotal">
+<& /search/elements/search-html.html:footer_row,
+ row => $footer,
+ opt => \%opt,
+&>
+</TBODY>
+% } # for $curr_group
+
+% if ( scalar @$total_footer ) {
+<TFOOT CLASS="total">
+<& /search/elements/search-html.html:footer_row,
+ row => $total_footer,
+ opt => \%opt,
+&>
+</TFOOT>
+% }
+</TABLE>
+<& /elements/footer.html &>
diff --git a/httemplate/search/elements/grouped-search/xls b/httemplate/search/elements/grouped-search/xls
new file mode 100644
index 000000000..8540dcf06
--- /dev/null
+++ b/httemplate/search/elements/grouped-search/xls
@@ -0,0 +1,61 @@
+<%doc>
+
+Excel spreadsheet view.
+
+</%doc>
+<%init>
+my %opt = @_;
+
+my $group_info = $m->comp('core', %opt);
+
+# minor false laziness with search-xls.html
+my $override = '';
+$override = 'XLSX' if grep { $_->num_rows >= 65536 }
+ @{ $group_info->{groups} };
+
+my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format($override);
+
+my $filename = $opt{'name'} || PL($opt{'name_singular'});
+$filename .= $format->{extension};
+
+http_header('Content-Type' => $format->{mime_type} );
+http_header('Content-Disposition' => qq!attachment;filename="$filename"! );
+$HTML::Mason::Commands::r->headers_out->{'Cache-control'} = 'max-age=0';
+
+my $data = '';
+my $XLS = new IO::Scalar \$data;
+my $workbook = $format->{class}->new($XLS)
+ or die "Error opening Excel file: $!";
+
+my $title = $opt{'title'};
+$title =~ s/[\[\]\:\*\?\/\/]//g;
+$title = substr($title, 0, 31);
+
+for (my $curr_group = 0; $curr_group < $group_info->{num}; $curr_group++) {
+ my $group = $group_info->{groups}[$curr_group];
+ my $query = $group_info->{queries}[$curr_group];
+ my $footer = $group_info->{group_footers}[$curr_group];
+ my $label = $group_info->{group_labels}[$curr_group];
+ # run the query
+ my @rows = $query->qsearch;
+ #warn Dumper(\@rows); #DEBUG
+
+ # pass arrayrefs to write_row to write multiple rows
+ $opt{footer} = [ List::MoreUtils::pairwise { [ $a, $b ] }
+ @$footer,
+ @{$group_info->{total_footer}}
+ ];
+ $m->comp('/search/elements/search-xls.html:worksheet',
+ workbook => $workbook,
+ title => $label,
+ header => $opt{header},
+ opt => \%opt,
+ rows => \@rows,
+ );
+}
+
+$workbook->close();
+
+$m->clear_buffer();
+$m->print($data);
+</%init>
diff --git a/httemplate/search/elements/match-classnum b/httemplate/search/elements/match-classnum
new file mode 100644
index 000000000..ed1efd53c
--- /dev/null
+++ b/httemplate/search/elements/match-classnum
@@ -0,0 +1,12 @@
+<%args>
+$param # name of the form field containing the classnum
+$field => $param # SQL field that must match that form field
+</%args>
+<%init>
+my @values = grep /^(\d+)$/, $cgi->param( $param );
+if (@values) {
+ return "COALESCE($field, 0) IN (" . join(',', @values) . ')';
+} else {
+ return;
+}
+</%init>
diff --git a/httemplate/search/elements/report_cust_pay_or_refund.html b/httemplate/search/elements/report_cust_pay_or_refund.html
index 9c4ca2761..70727c007 100644
--- a/httemplate/search/elements/report_cust_pay_or_refund.html
+++ b/httemplate/search/elements/report_cust_pay_or_refund.html
@@ -27,36 +27,14 @@ Examples:
</TH>
</TR>
- <TR>
- <TD ALIGN="right"><% ucfirst(PL($name_singular)) %> of type: </TD>
- <TD>
- <SELECT NAME="payby" SIZE=16 MULTIPLE>
-
-%# <OPTION VALUE=""><% mt('all') |h %></OPTION>
-%# <OPTION VALUE="CARD"><% mt('credit card (all)') |h %></OPTION>
-
- <OPTION VALUE="CARD-VisaMC" SELECTED><% mt('credit card (Visa/MasterCard)') |h %></OPTION>
- <OPTION VALUE="CARD-Amex" SELECTED><% mt('credit card (American Express)') |h %></OPTION>
- <OPTION VALUE="CARD-Discover" SELECTED><% mt('credit card (Discover)') |h %></OPTION>
- <OPTION VALUE="CARD-Maestro" SELECTED><% mt('credit card (Maestro/Switch/Solo)') |h %></OPTION>
- <OPTION VALUE="CARD-Tokenized" SELECTED><% mt('credit card (Tokenized)') |h %></OPTION>
-
- <OPTION VALUE="CHEK" SELECTED><% mt('electronic check / ACH') |h %></OPTION>
- <OPTION VALUE="BILL" SELECTED><% mt('check') |h %></OPTION>
- <OPTION VALUE="CASH" SELECTED><% mt('cash') |h %></OPTION>
- <OPTION VALUE="PPAL" SELECTED><% mt('Paypal') |h %></OPTION>
- <OPTION VALUE="APPL" SELECTED><% mt('Apple Store') |h %></OPTION>
- <OPTION VALUE="ANRD" SELECTED><% mt('Android Market') |h %></OPTION>
-
- <OPTION VALUE="PREP" SELECTED><% mt('prepaid card') |h %></OPTION>
- <OPTION VALUE="WIRE" SELECTED><% mt('wire transfer') |h %></OPTION>
- <OPTION VALUE="WEST" SELECTED><% mt('Western Union') |h %></OPTION>
- <OPTION VALUE="EDI" SELECTED><% mt('Electronic Debit (EDI)') |h %></OPTION>
- <OPTION VALUE="MCRD" SELECTED><% mt('manual credit card') |h %></OPTION>
- <OPTION VALUE="MCHK" SELECTED><% mt('manual electronic check') |h %></OPTION>
- </SELECT>
- </TD>
- </TR>
+ <& /elements/tr-select.html,
+ label => ucfirst(PL($name_singular)) . ' of type:',
+ field => 'payby',
+ options => [ keys(\%payby) ],
+ labels => \%payby,
+ multiple => 1,
+ size => 16
+ &>
<TR>
<TD ALIGN="right"><% mt('Check #:') |h %> </TD>
@@ -137,8 +115,7 @@ Examples:
'label' => emt('Customer class'),
'field' => 'cust_classnum',
'multiple' => 1,
- 'pre_options' => [ '' => emt('(none)') ],
- 'all_selected' => 1,
+ 'pre_options' => [ 0 => emt('(none)') ],
&>
</TABLE>
@@ -189,4 +166,26 @@ my $title = $void ? "Voided $name_singular report" :
"\u$name_singular report" ;
$table .= '_void' if $void;
+tie (my %payby, 'Tie::IxHash',
+ 'CARD-VisaMC' => 'credit card (Visa/MasterCard)',
+ 'CARD-Amex' => 'credit card (American Express)',
+ 'CARD-Discover' => 'credit card (Discover)',
+ 'CARD-Maestro' => 'credit card (Maestro/Switch/Solo)',
+ 'CARD-Tokenized' => 'credit card (Tokenized)',
+
+ 'CHEK' => 'electronic check / ACH',
+ 'BILL' => 'check',
+ 'CASH' => 'cash',
+ 'PPAL' => 'Paypal',
+ 'APPL' => 'Apple Store',
+ 'ANRD' => 'Android Market',
+
+ 'PREP' => 'prepaid card',
+ 'WIRE' => 'wire transfer',
+ 'WEST' => 'Western Union',
+ 'EDI' => 'Electronic Debit (EDI)',
+ 'MCRD' => 'manual credit card',
+ 'MCHK' => 'manual electronic check',
+);
+
</%init>
diff --git a/httemplate/search/elements/search-html.html b/httemplate/search/elements/search-html.html
index 10cc95539..12f6c1e04 100644
--- a/httemplate/search/elements/search-html.html
+++ b/httemplate/search/elements/search-html.html
@@ -181,300 +181,13 @@
% }
- <% include('/elements/table-grid.html') %>
-
- <TR>
-% my $h2 = 0;
-% my $colspan = 0;
-% my @fields = @{ $opt{'sort_fields'} || $opt{'fields'} || [] };
-% my $order_by = $cgi->param('order_by');
-% foreach my $header ( @{ $opt{header} } ) {
-%
-% my $field = shift @fields;
-%
-% $colspan-- if $colspan > 0;
-% next if $colspan;
-%
-% my $label = ref($header) ? $header->{label} : $header;
-% unless ( ref($field) || !$field ) {
-% if ( $order_by eq $field ) {
-% $cgi->param('order_by', "$field DESC");
-% } else {
-% $cgi->param('order_by', $field);
-% }
-% $label = qq(<A HREF="$self_url?). $cgi->query_string.
-% qq(">$label</A>);
-% }
-%
-% $colspan = ref($header) ? $header->{colspan} : 0;
-% my $rowspan = 1;
-% my $style = '';
-% if ( $opt{header2} ) {
-% if ( !length($opt{header2}->[$h2]) ) {
-% $rowspan = 2;
-% splice @{ $opt{header2} }, $h2, 1;
-% } else {
-% $h2++;
-% $style = 'STYLE="border-bottom: none"'
-% }
-% }
- <TH CLASS = "grid"
- BGCOLOR = "#cccccc"
- ROWSPAN = "<% $rowspan %>"
- <% $colspan ? 'COLSPAN = "'.$colspan.'"' : '' %>
- <% $style %>
-
- >
- <% $label %>
- </TH>
-% }
- </TR>
-
-% if ( $opt{header2} ) {
- <TR>
-% foreach my $header ( @{ $opt{header2} } ) {
-% my $label = ref($header) ? $header->{label} : $header;
- <TH CLASS="grid" BGCOLOR="#cccccc">
- <FONT SIZE="-1"><% $label %></FONT>
- </TH>
-% }
- </TR>
-% }
-
-% my $bgcolor1 = '#eeeeee';
-% my $bgcolor2 = '#ffffff';
-% my $bgcolor;
-%
-% foreach my $row ( @$rows ) {
-%
-% if ( $bgcolor eq $bgcolor1 ) {
-% $bgcolor = $bgcolor2;
-% } else {
-% $bgcolor = $bgcolor1;
-% }
-
-% my $rowstyle = '';
-% if ( $row eq $opt{'footer_data'} ) {
-% $rowstyle = ' STYLE="border-top: dashed 1px black; font-style: italic"';
-% $bgcolor = '#dddddd';
-% }
-
-% my $trid = '';
-% if ( $opt{'link_field' } ) {
-% my $link_field = $opt{'link_field'};
-% if ( ref($link_field) eq 'CODE' ) {
-% $trid = &{$link_field}($row);
-% } else {
-% $trid = $row->$link_field();
-% }
-% }
- <TR ID="<%$trid |h%>"<%$rowstyle%>>
-
-
-% if ( $opt{'fields'} ) {
-%
-% my $links = $opt{'links'} ? [ @{$opt{'links'}} ] : '';
-% my $onclicks = $opt{'link_onclicks'} ? [ @{$opt{'link_onclicks'}} ] : [];
-% my $tooltips = $opt{'tooltips'} ? [ @{$opt{'tooltips'}} ] : [];
-% my $aligns = $opt{'align'} ? [ @{$opt{'align'}} ] : '';
-% my $colors = $opt{'color'} ? [ @{$opt{'color'}} ] : [];
-% my $sizes = $opt{'size'} ? [ @{$opt{'size'}} ] : [];
-% my $styles = $opt{'style'} ? [ @{$opt{'style'}} ] : [];
-% my $cstyles = $opt{'cell_style'} ? [ @{$opt{'cell_style'}} ] : [];
-%
-% foreach my $field (
-%
-% map {
-% if ( ref($_) eq 'ARRAY' ) {
-%
-% my $tableref = $_;
-%
-% '<TABLE CLASS="inv" CELLSPACING=0 CELLPADDING=0 WIDTH="100%">'.
-%
-% join('', map {
-%
-% my $rowref = $_;
-%
-% '<tr>'.
-%
-% join('', map {
-%
-% my $e = $_;
-%
-% '<TD '.
-% join(' ', map {
-% uc($_).'="'. $e->{$_}. '"';
-% }
-% grep exists($e->{$_}),
-% qw( align bgcolor colspan rowspan
-% style valign width )
-% ).
-% '>'.
-%
-% ( $e->{'link'}
-% ? '<A HREF="'. $e->{'link'}. '">'
-% : ''
-% ).
-% ( $e->{'onclick'} # don't use with 'link'
-% ? '<A HREF="#" onclick="' .
-% $e->{'onclick'}.'">'
-% : ''
-% ).
-% ( $e->{'size'}
-% ? '<FONT SIZE="'.uc($e->{'size'}).'">'
-% : ''
-% ).
-% ( $e->{'data_style'}
-% ? '<'. uc($e->{'data_style'}). '>'
-% : ''
-% ).
-% $e->{'data'}.
-% ( $e->{'data_style'}
-% ? '</'. uc($e->{'data_style'}). '>'
-% : ''
-% ).
-% ( $e->{'size'} ? '</FONT>' : '' ).
-% ( $e->{'link'} || $e->{'onclick'}
-% ? '</A>'
-% : '' ).
-% '</td>';
-%
-% } @$rowref ).
-%
-% '</tr>';
-% } @$tableref ).
-%
-% '</table>';
-%
-% } else {
-% $_;
-% }
-% }
-%
-% map {
-% if ( ref($_) eq 'CODE' ) {
-% &{$_}($row);
-% } elsif ( ref($row) eq 'ARRAY' and
-% $_ =~ /^\d+$/ ) {
-% # for the 'straight SQL' case: specify fields
-% # by position
-% encode_entities($row->[$_]);
-% } else {
-% encode_entities($row->$_());
-% }
-% }
-% @{$opt{'fields'}}
-%
-% ) {
-%
-%# my $class = ( $field =~ /^<TABLE/i ) ? 'inv' : 'grid';
-% my $class = 'grid';
-%
-% my $align = $aligns ? shift @$aligns : '';
-% $align = " ALIGN=$align" if $align;
-%
-% my $a = '';
-% if ( $links ) {
-% my $link = shift @$links;
-% my $onclick = shift @$onclicks;
-% my $tooltip = shift @$tooltips;
-%
-% if ( ! $opt{'agent_virt'}
-% || ( $null_link && ! $row->agentnum )
-% || grep { $row->agentnum == $_ }
-% @link_agentnums
-% ) {
-%
-% $link = &{$link}($row)
-% if ref($link) eq 'CODE';
-%
-% $onclick = &{$onclick}($row)
-% if ref($onclick) eq 'CODE';
-% $onclick = qq( onClick="$onclick") if $onclick;
-%
-% $tooltip = &{$tooltip}($row)
-% if ref($tooltip) eq 'CODE';
-% $tooltip = qq! id="a$id" !.
-% qq! onmouseover="return overlib(!.
-% $m->interp->apply_escapes($tooltip, 'h', 'js_string').
-% qq!, FGCLASS, 'tooltip', REF, 'a$id', !.
-% qq!REFC, 'LL', REFP, 'UL')"! if $tooltip;
-%
-% if ( $link ) {
-% my( $url, $method ) = @{$link};
-% if ( ref($method) eq 'CODE' ) {
-% $a = $url. &{$method}($row);
-% } else {
-% $a = $url. $row->$method();
-% }
-% $a = qq(<A HREF="$a"$onclick$tooltip>);
-% }
-% elsif ( $onclick ) {
-% $a = qq(<A HREF="javascript:void(0);"$onclick>);
-% }
-% elsif ( $tooltip ) {
-% $a = qq(<A $tooltip>);
-% }
-% $id++;
-
-% }
-%
-% }
-%
-% my $font = '';
-% my $color = shift @$colors;
-% $color = &{$color}($row) if ref($color) eq 'CODE';
-% my $size = shift @$sizes;
-% $size = &{$size}($row) if ref($size) eq 'CODE';
-% if ( $color || $size ) {
-% $font = '<FONT '.
-% ( $color ? "COLOR=#$color " : '' ).
-% ( $size ? qq(SIZE="$size" ) : '' ).
-% '>';
-% }
-%
-% my($s, $es) = ( '', '' );
-% my $style = shift @$styles;
-% $style = &{$style}($row) if ref($style) eq 'CODE';
-% if ( $style ) {
-% $s = join( '', map "<$_>", split('', $style) );
-% $es = join( '', map "</$_>", split('', $style) );
-% }
-%
-% my $cstyle = shift @$cstyles;
-% $cstyle = &{$cstyle}($row) if ref($cstyle) eq 'CODE';
-% $cstyle = qq(STYLE="$cstyle")
-% if $cstyle;
-
- <TD CLASS="<% $class %>" BGCOLOR="<% $bgcolor %>" <% $align %> <% $cstyle %>><% $a %><% $font %><% $s %><% $field %><% $es %><% $font ? '</FONT>' : '' %><% $a ? '</A>' : '' %></TD>
-
-% }
-%
-% } else {
-%
-% foreach ( @$row ) {
- <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $_ %></TD>
-% }
-%
-% }
-
- </TR>
-
-% }
-
-% if ( $opt{'footer'} ) {
-
- <TR>
-
-% foreach my $footer ( @{ $opt{'footer'} } ) {
-% $footer = &{$footer}() if ref($footer) eq 'CODE';
- <TD CLASS="grid" BGCOLOR="#dddddd" STYLE="border-top: dashed 1px black;"><i><% $footer %></i></TD>
-% }
-
- </TR>
-% }
-
- </TABLE>
+ <& SELF:data_table,
+ rows => $rows,
+ null_link => $null_link,
+ link_agentnums => \@link_agentnums,
+ self_url => $self_url,
+ %opt
+ &>
<% $pager %>
@@ -521,14 +234,374 @@ my $confmax = $args{'confmax'};
my $maxrecords = $args{'maxrecords'};
my $offset = $args{'offset'};
my %opt = %{ $args{'opt'} };
-my $self_url = $opt{'url'} || $cgi->url('-path_info' => 1, '-full' =>1);
-my $count_sth = dbh->prepare($opt{'count_query'})
- or die "Error preparing $opt{'count_query'}: ". dbh->errstr;
-$count_sth->execute
- or die "Error executing $opt{'count_query'}: ". $count_sth->errstr;
-my $count_arrayref = $count_sth->fetchrow_arrayref;
+# must be an arrayref of the row count, followed by any other totals
+my $count_arrayref = $args{'totals'};
my $total = $count_arrayref->[0];
-my $id = 0;
+# there used to be an option to override this, for highly dubious reasons
+my $self_url = $cgi->url('-path_info' => 1, '-full' =>1);
+
</%init>
+<%method data_table>
+% my %opt = @_;
+% my $rows = delete $opt{rows};
+% my $self_url = delete $opt{self_url};
+<& /elements/table-grid.html &>
+
+<THEAD>
+<& SELF:header_row,
+ 'header' => $opt{'header'},
+ 'header2' => $opt{'header2'},
+ 'sort_fields' => ($opt{'sort_fields'} || $opt{'fields'}),
+&>
+</THEAD>
+
+<TBODY>
+<& SELF:data_rows, rows => $rows, opt => \%opt &>
+</TBODY>
+
+% if ( $opt{'footer'} ) {
+<TFOOT>
+<& SELF:footer_row, row => $opt{'footer'}, opt => \%opt &>
+</TFOOT>
+% }
+</TABLE>
+</%method>
+<%method header_row>
+<%args>
+@sort_fields
+@header
+@header2 => ()
+</%args>
+ <TR>
+% my $h2 = 0;
+% my $colspan = 0;
+% my $order_by = $cgi->param('order_by');
+% my $self_url = $cgi->url('-path_info' => 1, '-full' =>1);
+% foreach my $header ( @header ) {
+%
+% my $field = shift @sort_fields;
+%
+% $colspan-- if $colspan > 0;
+% next if $colspan;
+%
+% my $label = ref($header) ? $header->{label} : $header;
+% unless ( ref($field) || !$field ) {
+% if ( $order_by eq $field ) {
+% $cgi->param('order_by', "$field DESC");
+% } else {
+% $cgi->param('order_by', $field);
+% }
+% $label = qq(<A HREF="$self_url?). $cgi->query_string.
+% qq(">$label</A>);
+% }
+%
+% $colspan = ref($header) ? $header->{colspan} : 0;
+% my $rowspan = 1;
+% my $style = '';
+% if ( @header2 ) {
+% if ( !length($header2[$h2]) ) {
+% $rowspan = 2;
+% splice @header2, $h2, 1;
+% } else {
+% $h2++;
+% $style = 'STYLE="border-bottom: none"'
+% }
+% }
+ <TH CLASS = "grid"
+ BGCOLOR = "#cccccc"
+ ROWSPAN = "<% $rowspan %>"
+ <% $colspan ? 'COLSPAN = "'.$colspan.'"' : '' %>
+ <% $style %>
+
+ >
+ <% $label %>
+ </TH>
+% }
+ </TR>
+
+% if ( @header2 ) {
+ <TR>
+% foreach my $header ( @header2 ) {
+% my $label = ref($header) ? $header->{label} : $header;
+ <TH CLASS="grid" BGCOLOR="#cccccc">
+ <FONT SIZE="-1"><% $label %></FONT>
+ </TH>
+% }
+ </TR>
+% }
+</%method>
+<%method data_rows>
+<%args>
+$rows => []
+%opt
+</%args>
+% my %align = (
+% 'l' => 'left',
+% 'r' => 'right',
+% 'c' => 'center',
+% ' ' => '',
+% '.' => '',
+% );
+% if ( $opt{align} and !ref($opt{align}) ) {
+% $opt{align} = [ map $align{$_}, split(//, $opt{align}) ];
+% }
+
+% my $i = 0; # for row striping # XXX CSS - nth-child
+% my $id = 0;
+% foreach my $row ( @$rows ) {
+%
+% my $rowstyle = '';
+% if ( $row eq $opt{'footer_data'} ) { # XXX CSS - tfoot
+% $rowstyle = ' STYLE="border-top: dashed 1px black; font-style: italic background-color=#dddddd"';
+% }
+%
+% my $trid = '';
+% if ( $opt{'link_field' } ) {
+% my $link_field = $opt{'link_field'};
+% if ( ref($link_field) eq 'CODE' ) {
+% $trid = &{$link_field}($row);
+% } else {
+% $trid = $row->$link_field();
+% }
+% }
+ <TR ID="<%$trid |h%>" CLASS="row<% $i % 2 %>"<%$rowstyle%>>
+
+% if ( $opt{'fields'} ) {
+%
+% my $links = $opt{'links'} ? [ @{$opt{'links'}} ] : '';
+% my $onclicks = $opt{'link_onclicks'} ? [ @{$opt{'link_onclicks'}} ] : [];
+% my $tooltips = $opt{'tooltips'} ? [ @{$opt{'tooltips'}} ] : [];
+% my $aligns = $opt{'align'} ? [ @{$opt{'align'}} ] : '';
+% my $colors = $opt{'color'} ? [ @{$opt{'color'}} ] : [];
+% my $sizes = $opt{'size'} ? [ @{$opt{'size'}} ] : [];
+% my $styles = $opt{'style'} ? [ @{$opt{'style'}} ] : [];
+% my $cstyles = $opt{'cell_style'} ? [ @{$opt{'cell_style'}} ] : [];
+% my $formats = $opt{'format'} ? [ @{$opt{'format'}} ] : [];
+%
+% foreach my $field (
+%
+% # if the value of the field is an arrayref, then construct a table in
+% # the cell.
+% # if it's a (non-empty) scalar, and a format has been specified, then
+% # format the scalar with that.
+% # otherwise, just output the value.
+% # XXX we should also do date formats like this
+% map {
+% if ( ref($_) eq 'ARRAY' ) {
+%
+% my $tableref = $_;
+%
+% '<TABLE CLASS="inv" CELLSPACING=0 CELLPADDING=0 WIDTH="100%">'.
+%
+% join('', map {
+%
+% my $rowref = $_;
+%
+% '<tr>'.
+%
+% join('', map {
+%
+% my $e = $_;
+%
+% '<TD '.
+% join(' ', map {
+% uc($_).'="'. $e->{$_}. '"';
+% }
+% grep exists($e->{$_}),
+% qw( align bgcolor colspan rowspan
+% style valign width )
+% ).
+% '>'.
+%
+% ( $e->{'link'}
+% ? '<A HREF="'. $e->{'link'}. '">'
+% : ''
+% ).
+% ( $e->{'onclick'} # don't use with 'link'
+% ? '<A HREF="#" onclick="' .
+% $e->{'onclick'}.'">'
+% : ''
+% ).
+% ( $e->{'size'}
+% ? '<FONT SIZE="'.uc($e->{'size'}).'">'
+% : ''
+% ).
+% ( $e->{'data_style'}
+% ? '<'. uc($e->{'data_style'}). '>'
+% : ''
+% ).
+% $e->{'data'}.
+% ( $e->{'data_style'}
+% ? '</'. uc($e->{'data_style'}). '>'
+% : ''
+% ).
+% ( $e->{'size'} ? '</FONT>' : '' ).
+% ( $e->{'link'} || $e->{'onclick'}
+% ? '</A>'
+% : '' ).
+% '</td>';
+%
+% } @$rowref ).
+%
+% '</tr>';
+% } @$tableref ).
+%
+% '</table>';
+%
+% } else {
+% if ( length($_) > 0 and my $format = shift @$formats ) {
+% $_ = sprintf($format, $_);
+% }
+% $_;
+% }
+% }
+%
+% # get the value of the field spec:
+% # - if the spec is a coderef, evaluate the coderef
+% # - if the spec is a string, call that string as a method
+% # - if the spec is an integer, get the field in that position
+% map {
+% if ( ref($_) eq 'CODE' ) {
+% &{$_}($row);
+% } elsif ( ref($row) eq 'ARRAY' and
+% $_ =~ /^\d+$/ ) {
+% # for the 'straight SQL' case: specify fields
+% # by position
+% encode_entities($row->[$_]);
+% } else {
+% encode_entities($row->$_());
+% }
+% }
+% @{$opt{'fields'}}
+%
+% ) {
+%
+% my $class = ( $field =~ /^<TABLE/i ) ? 'inv' : 'grid';
+% my $class = 'grid';
+%
+% my $align = $aligns ? shift @$aligns : '';
+% $align = " ALIGN=$align" if $align;
+%
+% my $a = '';
+% if ( $links ) {
+% my $link = shift @$links;
+% my $onclick = shift @$onclicks;
+% my $tooltip = shift @$tooltips;
+%
+% if ( ! $opt{'agent_virt'}
+% || ( $opt{'null_link'} && ! $row->agentnum )
+% || grep { $row->agentnum == $_ }
+% @{ $opt{link_agentnums} }
+% ) {
+%
+% $link = &{$link}($row)
+% if ref($link) eq 'CODE';
+%
+% $onclick = &{$onclick}($row)
+% if ref($onclick) eq 'CODE';
+% $onclick = qq( onClick="$onclick") if $onclick;
+%
+% $tooltip = &{$tooltip}($row)
+% if ref($tooltip) eq 'CODE';
+% $tooltip = qq! id="a$id" !.
+% qq! onmouseover="return overlib(!.
+% $m->interp->apply_escapes($tooltip, 'h', 'js_string').
+% qq!, FGCLASS, 'tooltip', REF, 'a$id', !.
+% qq!REFC, 'LL', REFP, 'UL')"! if $tooltip;
+%
+% if ( $link ) {
+% my( $url, $method ) = @{$link};
+% if ( ref($method) eq 'CODE' ) {
+% $a = $url. &{$method}($row);
+% } else {
+% $a = $url. $row->$method();
+% }
+% $a = qq(<A HREF="$a"$onclick$tooltip>);
+% }
+% elsif ( $onclick ) {
+% $a = qq(<A HREF="javascript:void(0);"$onclick>);
+% }
+% elsif ( $tooltip ) {
+% $a = qq(<A $tooltip>);
+% }
+% $id++;
+
+% }
+%
+% }
+%
+% my $font = '';
+% my $color = shift @$colors;
+% $color = &{$color}($row) if ref($color) eq 'CODE';
+% my $size = shift @$sizes;
+% $size = &{$size}($row) if ref($size) eq 'CODE';
+% if ( $color || $size ) {
+% $font = '<FONT '.
+% ( $color ? "COLOR=#$color " : '' ).
+% ( $size ? qq(SIZE="$size" ) : '' ).
+% '>';
+% }
+%
+% my($s, $es) = ( '', '' );
+% my $style = shift @$styles;
+% $style = &{$style}($row) if ref($style) eq 'CODE';
+% if ( $style ) {
+% $s = join( '', map "<$_>", split('', $style) );
+% $es = join( '', map "</$_>", split('', $style) );
+% }
+%
+% my $cstyle = shift @$cstyles;
+% $cstyle = &{$cstyle}($row) if ref($cstyle) eq 'CODE';
+% $cstyle = qq(STYLE="$cstyle")
+% if $cstyle;
+
+ <TD CLASS="<% $class %>" <% $align %> <% $cstyle %>><% $a %><% $font %><% $s %><% $field %><% $es %><% $font ? '</FONT>' : '' %><% $a ? '</A>' : '' %></TD>
+
+% }
+%
+% } else { # not $opt{'fields'}
+%
+% foreach ( @$row ) {
+ <TD CLASS="grid"><% $_ %></TD>
+% }
+%
+% }
+
+ </TR>
+
+% $i++;
+%
+% } # foreach $row
+</%method>
+<%method footer_row>
+<%args>
+$row
+%opt
+</%args>
+%# don't try to respect all the styling options, just the ones that are
+%# hard to replicate with CSS
+% my %align = (
+% 'l' => 'left',
+% 'r' => 'right',
+% 'c' => 'center',
+% ' ' => '',
+% '.' => '',
+% );
+% if ( $opt{align} and !ref($opt{align}) ) {
+% $opt{align} = [ map $align{$_}, split(//, $opt{align}) ];
+% }
+% my @aligns = @{ $opt{align} };
+
+<TR>
+% foreach my $footer ( @$row ) {
+% $footer = &{$footer}() if ref($footer) eq 'CODE';
+% my $align = shift @aligns;
+% my $style = '';
+% $style .= "text-align: $align;" if $align;
+ <TD CLASS="grid" STYLE="<% $style %>"><% $footer %></TD>
+% }
+</TR>
+</%method>
+
diff --git a/httemplate/search/elements/search-xls.html b/httemplate/search/elements/search-xls.html
index 8334497d2..c4265e8c8 100644
--- a/httemplate/search/elements/search-xls.html
+++ b/httemplate/search/elements/search-xls.html
@@ -1,13 +1,10 @@
<%init>
my %args = @_;
-my $type = $args{'type'};
my $header = $args{'header'};
my $rows = $args{'rows'};
my %opt = %{ $args{'opt'} };
-my $style = $opt{'style'};
-
my $override = scalar(@$rows) >= 65536 ? 'XLSX' : '';
my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format($override);
@@ -35,9 +32,38 @@ my $workbook = $format->{class}->new($XLS)
my $title = $opt{'title'};
$title =~ s/[\[\]\:\*\?\/\/]//g;
$title = substr($title, 0, 31);
+
+# append a single worksheet
+$m->comp( 'SELF:worksheet',
+ workbook => $workbook,
+ title => $title,
+ opt => \%opt,
+ header => $header,
+ rows => $rows
+);
+
+$workbook->close();# or die "Error creating .xls file: $!";
+
+http_header('Content-Length' => length($data) );
+$m->clear_buffer();
+$m->print($data);
+
+</%init>
+<%method worksheet>
+<%args>
+$workbook
+$title
+%opt
+$header
+$rows
+</%args>
+<%perl>
+
my $worksheet = $workbook->add_worksheet($title);
-$worksheet->protect();
+#$worksheet->protect();
+
+my $style = $opt{style};
my($r,$c) = (0,0);
@@ -63,12 +89,29 @@ xl_parse_date_init();
my %bold_format;
-my $writer = sub {
+my @widths;
+
+my $writer;
+$writer = sub {
# Wrapper for $worksheet->write.
# Do any massaging of the value/format here.
my ($r, $c, $value, $format) = @_;
#warn "writer called with format $format\n";
+ if ( ref $value eq 'ARRAY' ) {
+ # imitate the write_row() method: write the array into a column starting
+ # with $r.
+ # (currently only used in the footer; to use it anywhere else we'd need
+ # some way to return the number of rows written)
+ foreach my $v (@$value) {
+ $writer->($r, $c, $v, $format);
+ $r++;
+ }
+ return;
+ }
+
+ my $bold = 0;
+ my $date = 0;
if ( $style->[$c] eq 'b' or $value =~ /<b>/i ) { # the only one in common use
$value =~ s[</?b>][]ig;
if ( !exists($bold_format{$format}) ) {
@@ -77,6 +120,7 @@ my $writer = sub {
$bold_format{$format}->set_bold();
}
$format = $bold_format{$format};
+ $bold = 1;
}
# convert HTML entities
@@ -106,6 +150,7 @@ my $writer = sub {
$date_format{$format}->set_num_format('mmm dd yyyy');
}
$format = $date_format{$format};
+ $date = 1;
}
else {
# String: replace line breaks with newlines
@@ -113,6 +158,14 @@ my $writer = sub {
}
#warn "writing with format $format\n";
$worksheet->write($r, $c, $value, $format);
+
+ # estimate width
+ # use Font::TTFMetrics; # would work, but we can't redistribute the font...
+ my $width = length($value);
+ $width = 11 if $date;
+ $width *= 1.1 if $bold;
+ $width += 1; # pad it a little
+ $widths[$c] = $width if $width > ($widths[$c] || 0);
};
$writer->( $r, $c++, $_, $header_format ) foreach @$header;
@@ -170,9 +223,9 @@ if ( $opt{'footer'} ) {
}
}
-$workbook->close();# or die "Error creating .xls file: $!";
-
-http_header('Content-Length' => length($data) );
-$m->print($data);
+for ( my $x = 0; $x < scalar @widths; $x++ ) {
+ $worksheet->set_column($x, $x, $widths[$x]);
+}
-</%init>
+</%perl>
+</%method>
diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html
index 053da4787..beb017300 100644
--- a/httemplate/search/elements/search.html
+++ b/httemplate/search/elements/search.html
@@ -43,7 +43,7 @@ Example:
#listref of column labels, <TH>
#recommended unless 'query' is an SQL query string
- # (if not specified the database column names will be used)
+ # (if not specified the database column names will be used) (XXX this is not currently working either)
'header' => [ '#',
'Item',
{ 'label' => 'Another Item',
@@ -52,7 +52,7 @@ Example:
],
#listref - each item is a literal column name (or method) or coderef
- #if not specified all columns will be shown
+ #if not specified all columns will be shown (XXX this is not currently working?)
'fields' => [
'column',
sub { my $row = shift; $row->column; },
@@ -106,7 +106,8 @@ Example:
'disable_maxselect' => '', # set true to disable record/page selection
'disable_nonefound' => '', # set true to disable the "No matching Xs found"
# message
-
+ 'nohtmlheader' => '', # set true to remove the header and menu bar
+
#handling "disabled" fields in the records
'disableable' => 1, # set set to 1 (or column position for "disabled"
# status col) to enable if this table has a "disabled"
@@ -130,7 +131,7 @@ Example:
'agent_pos' => 3, # optional position (starting from 0) to
# insert an Agent column (query needs to be a
# qsearch hashref and header & fields need to
- # be defined)
+ # be defined)cust_pkg_susp.html
# sort, link & display properties for fields
@@ -141,7 +142,7 @@ Example:
# or a listref of link and method name to append,
# or a listref of link and coderef to run and append
# or a coderef that returns such a listref
- 'links' => [],`
+ 'links' => [],
#listref - each item is the empty string,
# or a string onClick handler for the corresponding link
@@ -200,6 +201,7 @@ Example:
confmax => $confmax,
maxrecords => $maxrecords,
offset => $offset,
+ totals => $totals,
opt => \%opt
)
%>
@@ -220,6 +222,7 @@ if ( !$curuser->access_right('Download report data') ) {
$type = 'html';
}
+# split/map aligns here before doing anything else
my %align = (
'l' => 'left',
'r' => 'right',
@@ -227,6 +230,7 @@ my %align = (
' ' => '',
'.' => '',
);
+
$opt{align} = [ map $align{$_}, split(//, $opt{align}) ],
unless !$opt{align} || ref($opt{align});
@@ -253,6 +257,12 @@ $opt{disable_download} = 0
$opt{disable_download} = 1
if $opt{really_disable_download};
+# get our queries ready
+my $query = $opt{query} or die "query required";
+my $count_query = $opt{count_query} or die "count_query required";
+# there was a default count_query but it hasn't worked in about ten years
+
+# set up agent restriction
my @link_agentnums = ();
my $null_link = '';
if ( $opt{'agent_virt'} ) {
@@ -264,16 +274,22 @@ if ( $opt{'agent_virt'} ) {
my $agentnums_sql = $curuser->agentnums_sql(
'null' => $opt{'agent_null'},
'null_right' => $opt{'agent_null_right'},
- 'table' => $opt{'query'}{'table'},
+ 'table' => $query->{'table'},
);
- $opt{'query'}{'extra_sql'} .=
- ( $opt{'query'}{'extra_sql'} =~ /WHERE/i || keys %{$opt{'query'}{'hashref'}}
- ? ' AND '
- : ' WHERE ' ). $agentnums_sql;
+ # this is ridiculous, but we do have searches where $query has constraints
+ # and $count_query doesn't, or vice versa.
+ if ( $query->{'extra_sql'} =~ /\bWHERE\b/i or keys %{$query->{hashref}} ) {
+ $query->{'extra_sql'} .= " AND $agentnums_sql";
+ } else {
+ $query->{'extra_sql'} .= " WHERE $agentnums_sql";
+ }
- $opt{'count_query'} .=
- ( $opt{'count_query'} =~ /WHERE/i ? ' AND ' : ' WHERE ' ). $agentnums_sql;
+ if ( $count_query =~ /\bWHERE\b/i ) {
+ $count_query .= " AND $agentnums_sql";
+ } else {
+ $count_query .= " WHERE $agentnums_sql";
+ }
if ( $opt{'agent_pos'} || $opt{'agent_pos'} eq '0'
and scalar($curuser->agentnums) > 1 ) {
@@ -304,13 +320,13 @@ if ( $opt{'disableable'} ) {
unless ( $cgi->param('showdisabled') ) { #modify searches
- $opt{'query'}{'hashref'}{'disabled'} = '';
- $opt{'query'}{'extra_sql'} =~ s/^\s*WHERE/ AND/i;
+ $query->{'hashref'}{'disabled'} = '';
+ $query->{'extra_sql'} =~ s/^\s*\bWHERE\b/ AND/i;
- my $table = $opt{'query'}{'table'};
+ my $table = $query->{'table'};
- $opt{'count_query'} .=
- ( $opt{'count_query'} =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
+ $count_query .=
+ ( $count_query =~ /\bWHERE\b/i ? ' AND ' : ' WHERE ' ).
"( $table.disabled = '' OR $table.disabled IS NULL )";
} elsif ( $opt{'disabled_statuspos'}
@@ -357,10 +373,6 @@ my($confmax, $maxrecords, $offset );
unless ( $type =~ /^(csv|xml|\w*.xls)$/) {
# html mode
- unless (exists($opt{count_query}) && length($opt{count_query})) {
- ( $opt{count_query} = $opt{query} ) =~
- s/^\s*SELECT\s*(.*?)\s+FROM\s/SELECT COUNT(*) FROM /i; #silly vim:/
- }
unless ( $type eq 'html-print' ) {
@@ -393,41 +405,36 @@ $order_by = $cgi->param('order_by') if $cgi->param('order_by');
my $header = [ map { ref($_) ? $_->{'label'} : $_ } @{$opt{header}} ];
my $rows;
-if ( ref($opt{query}) ) {
+if ( ref $query ) {
my @query;
- if (ref($opt{query}) eq 'HASH') {
- @query = ( $opt{query} );
+ if (ref($query) eq 'HASH') {
+ @query = $query;
if ( $order_by ) {
- if ( $opt{query}->{'order_by'} ) {
- if ( $opt{query}->{'order_by'} =~ /^(\s*ORDER\s+BY\s+)?(\S.*)$/is ) {
- $opt{query}->{'order_by'} = "ORDER BY $order_by, $2";
+ if ( $query->{'order_by'} ) {
+ if ( $query->{'order_by'} =~ /^(\s*ORDER\s+BY\s+)?(\S.*)$/is ) {
+ $query->{'order_by'} = "ORDER BY $order_by, $2";
} else {
- warn "unparsable query order_by: ". $opt{query}->{'order_by'};
- die "unparsable query order_by: ". $opt{query}->{'order_by'};
+ warn "unparsable query order_by: ". $query->{'order_by'};
+ die "unparsable query order_by: ". $query->{'order_by'};
}
} else {
- $opt{query}->{'order_by'} = "ORDER BY $order_by";
+ $query->{'order_by'} = "ORDER BY $order_by";
}
}
- $opt{query}->{'order_by'} .= " $limit";
+ $query->{'order_by'} .= " $limit";
- } elsif (ref($opt{query}) eq 'ARRAY') {
- @query = @{ $opt{query} };
+ } elsif (ref($query) eq 'ARRAY') {
+ # do we still use this? it was for the old 477 report.
+ @query = @{ $query };
} else {
die "invalid query reference";
}
- if ( $opt{disableable} && ! $cgi->param('showdisabled') ) {
- #%search = ( 'disabled' => '' );
- $opt{'query'}->{'hashref'}->{'disabled'} = '';
- $opt{'query'}->{'extra_sql'} =~ s/^\s*WHERE/ AND/i;
- }
-
#eval "use FS::$opt{'query'};";
- my @param = qw( select table addl_from hashref extra_sql order_by );
+ my @param = qw( select table addl_from hashref extra_sql order_by debug );
$rows = [ qsearch( [ map { my $query = $_;
({ map { $_ => $query->{$_} } @param });
}
@@ -436,18 +443,25 @@ if ( ref($opt{query}) ) {
#'order_by' => $opt{order_by}. " ". $limit,
)
];
-} else {
- my $sth = dbh->prepare("$opt{'query'} $limit")
- or die "Error preparing $opt{'query'}: ". dbh->errstr;
+
+} else { # not ref $query; plain SQL (still used as of 07/2015)
+
+ $query .= " $limit";
+ my $sth = dbh->prepare($query)
+ or die "Error preparing $query: ". dbh->errstr;
$sth->execute
- or die "Error executing $opt{'query'}: ". $sth->errstr;
+ or die "Error executing $query: ". $sth->errstr;
- #can get # of rows without fetching them all?
$rows = $sth->fetchall_arrayref;
-
$header ||= $sth->{NAME};
}
+# run the count query to get number of rows and other totals
+my $count_sth = dbh->prepare($count_query);
+$count_sth->execute
+ or die "Error executing '$count_query': ".$count_sth->errstr;
+my $totals = $count_sth->fetchrow_arrayref;
+
push @$rows, $opt{'footer_data'} if $opt{'footer_data'};
</%init>
diff --git a/httemplate/search/report_cust_bill.html b/httemplate/search/report_cust_bill.html
index 8734467a4..92f619b79 100644
--- a/httemplate/search/report_cust_bill.html
+++ b/httemplate/search/report_cust_bill.html
@@ -29,8 +29,7 @@
label => mt('Customer Class'),
field => 'cust_classnum',
multiple => 1,
- 'pre_options' => [ '' => emt('(none)') ],
- 'all_selected' => 1,
+ 'pre_options' => [ 0 => emt('(none)') ],
&>
% if ( $cust_main ) {
@@ -100,7 +99,12 @@
<TR>
<TD ALIGN="right"><INPUT TYPE="checkbox" NAME="open" VALUE="1" CHECKED></TD>
<TD><% mt('Show only open invoices') |h %></TD>
- </TR>
+</TR>
+
+<TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="invoiced" VALUE="1" CHECKED></TD>
+ <TD><% emt('Exclude discounts from gross amount billed'), %></TD>
+</TR>
% unless ( $custnum ) {
<TR>
diff --git a/httemplate/search/report_customer_accounting_summary.html b/httemplate/search/report_customer_accounting_summary.html
index 8206f34ca..9edbd0fd7 100755
--- a/httemplate/search/report_customer_accounting_summary.html
+++ b/httemplate/search/report_customer_accounting_summary.html
@@ -37,6 +37,14 @@
'value' => 1,
&>
+ <& /elements/tr-checkbox.html,
+ 'label' => 'Show Gross &amp; Discounted',
+ 'field' => 'grossdiscount',
+ 'value' => 1,
+ 'curr_value' => defined($cgi->param('grossdiscount'))
+ ? scalar($cgi->param('grossdiscount'))
+ : undef, #default to off in v3
+ &>
</TABLE>
diff --git a/httemplate/search/report_rt_cust.html b/httemplate/search/report_rt_cust.html
new file mode 100644
index 000000000..07d497fc5
--- /dev/null
+++ b/httemplate/search/report_rt_cust.html
@@ -0,0 +1,40 @@
+<& /elements/header.html, 'Time worked per-customer summary' &>
+
+<FORM ACTION="rt_cust.html" METHOD="GET">
+
+<TABLE>
+
+ <& /elements/tr-select-cust_main-status.html,
+ 'label' => emt('Status'),
+ &>
+
+ <& /elements/tr-input-beginning_ending.html &>
+
+<!--
+ <& /elements/tr-select.html,
+ label => 'Time category:',
+ field => 'category',
+ options => [ '', 'development', 'support' ],
+ option_labels => { '' => 'all' },
+ curr_value => 'development',
+ &>
+
+ <& /elements/tr-select-otaker.html &>
+-->
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Search">
+
+</FORM>
+
+<& /elements/footer.html &>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List rating data');
+
+my $conf = new FS::Conf;
+
+</%init>
diff --git a/httemplate/search/rt_cust.html b/httemplate/search/rt_cust.html
new file mode 100644
index 000000000..7c31e976b
--- /dev/null
+++ b/httemplate/search/rt_cust.html
@@ -0,0 +1,174 @@
+<& elements/search.html,
+ 'title' => 'Time worked per-customer summary',
+ 'name_singular' => 'customer',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'header' => [ FS::UI::Web::cust_header(
+ $cgi->param('cust_fields')
+ ),
+ @extra_headers,
+ 'Support time',
+ #'Development time',
+ 'Unclassified time',
+ ],
+ 'fields' => [
+ \&FS::UI::Web::cust_fields,
+ @extra_fields,
+ $support_time_sub,
+ $unclass_time_sub,
+ ],
+ 'color' => [ FS::UI::Web::cust_colors(),
+ map '', @extra_fields
+ ],
+ 'style' => [ FS::UI::Web::cust_styles(),
+ map '', @extra_fields
+ ],
+ 'align' => [ FS::UI::Web::cust_aligns(),
+ map '', @extra_fields
+ ],
+ 'links' => [ ( map { $_ ne 'Cust. Status' ? $link : '' }
+ FS::UI::Web::cust_header(
+ $cgi->param('cust_fields')
+ )
+ ),
+ map '', @extra_fields
+ ],
+
+&>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List rating data')
+;
+
+#false laziness w/cust_main.html (we're really only filtering on status for now)
+
+my %search_hash = ();
+
+#$search_hash{'query'} = $cgi->keywords;
+
+#scalars
+my @scalars = qw (
+ agentnum salesnum status address city county state zip country
+ invoice_terms
+ no_censustract with_geocode with_email tax no_tax POST no_POST
+ custbatch usernum
+ cancelled_pkgs
+ cust_fields flattened_pkgs
+ all_tags
+ all_pkg_classnums
+ any_pkg_status
+);
+
+for my $param ( @scalars ) {
+ $search_hash{$param} = scalar( $cgi->param($param) )
+ if length($cgi->param($param));
+}
+
+#lists
+for my $param (qw( classnum refnum tagnum pkg_classnum )) {
+ $search_hash{$param} = [ $cgi->param($param) ];
+}
+
+###
+# etc
+###
+
+my $sql_query = FS::cust_main::Search->search(\%search_hash);
+my $count_query = delete($sql_query->{'count_query'});
+my @extra_headers = @{ delete($sql_query->{'extra_headers'}) };
+my @extra_fields = @{ delete($sql_query->{'extra_fields'}) };
+
+my $link = [ "${p}view/cust_main.cgi?", 'custnum' ];
+
+#eofalse (cust_main.html)
+
+#false laziness / cribbed from search/rt_ticket.html
+
+my $twhere = "
+ WHERE Transactions.ObjectType = 'RT::Ticket'
+"; #AND Transactions.ObjectId = Tickets.Id
+
+my $transaction_time = "
+CASE transactions.type when 'Set'
+ THEN (to_number(newvalue,'999999')-to_number(oldvalue, '999999')) * 60
+ ELSE timetaken*60
+END";
+
+$twhere .= "
+ AND ( ( Transactions.Type = 'Set'
+ AND Transactions.Field = 'TimeWorked'
+ AND Transactions.NewValue != Transactions.OldValue )
+ OR ( Transactions.Type IN ( 'Create', 'Comment', 'Correspond', 'Touch' )
+ AND Transactions.TimeTaken > 0
+ )
+ )";
+
+my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+# TIMESTAMP is Pg-specific... ?
+if ( $beginning > 0 ) {
+ $beginning = "TIMESTAMP '". time2str('%Y-%m-%d %X', $beginning). "'";
+ $twhere .= " AND Transactions.Created >= $beginning ";
+}
+if ( $ending < 4294967295 ) {
+ $ending = "TIMESTAMP '". time2str('%Y-%m-%d %X', $ending). "'";
+ $twhere .= " AND Transactions.Created <= $ending ";
+}
+
+my $transactions = "FROM Transactions $twhere";
+
+#eofalse (rt_ticket.html)
+
+my $support_time_sub = sub {
+ my $cust_main = shift;
+ my $sec = 0;
+ foreach my $ticket ($cust_main->tickets) {
+
+ my $TimeType = FS::Record->scalar_sql(
+ "SELECT Content FROM ObjectCustomFieldValues
+ JOIN CustomFields
+ ON (ObjectCustomFieldValues.CustomField = CustomFields.Id)
+ WHERE CustomFields.Name = 'TimeType'
+ AND ObjectCustomFieldValues.ObjectType = 'RT::Ticket'
+ AND ObjectCustomFieldValues.Disabled = 0
+ AND ObjectId = ". $ticket->{id}
+ );
+ next unless $TimeType eq 'support';
+
+ $sec += FS::Record->scalar_sql(
+ "SELECT SUM($transaction_time) $transactions ".
+ " AND Transactions.ObjectId = ". $ticket->{id}
+ );
+ }
+
+ (($sec < 0) ? '-' : '' ). int(abs($sec)/3600)."h".sprintf("%02d",(abs($sec)%3600)/60)."m";
+
+};
+
+my $unclass_time_sub = sub {
+ my $cust_main = shift;
+ my $sec = 0;
+ foreach my $ticket ($cust_main->tickets) {
+
+ my $TimeType = FS::Record->scalar_sql(
+ "SELECT Content FROM ObjectCustomFieldValues
+ JOIN CustomFields
+ ON (ObjectCustomFieldValues.CustomField = CustomFields.Id)
+ WHERE CustomFields.Name = 'TimeType'
+ AND ObjectCustomFieldValues.ObjectType = 'RT::Ticket'
+ AND ObjectCustomFieldValues.Disabled = 0
+ AND ObjectId = ". $ticket->{id}
+ );
+ next unless $TimeType eq '';
+
+ $sec += FS::Record->scalar_sql(
+ "SELECT SUM($transaction_time) $transactions ".
+ " AND Transactions.ObjectId = ". $ticket->{id}
+ );
+ }
+
+ (($sec < 0) ? '-' : '' ). int(abs($sec)/3600)."h".sprintf("%02d",(abs($sec)%3600)/60)."m";
+
+};
+
+</%init>
diff --git a/httemplate/search/sql.html b/httemplate/search/sql.html
index 71aa00671..54d6c2d79 100644
--- a/httemplate/search/sql.html
+++ b/httemplate/search/sql.html
@@ -2,7 +2,7 @@
'title' => 'Query Results',
'name' => 'rows',
'query' => "SELECT $sql",
-
+ 'count_query' => $count,
&>
<%init>
@@ -12,4 +12,7 @@ die "access denied"
my $sql = $cgi->param('sql') or errorpage('Empty query');
$sql =~ s/;+\s*$//; #remove trailing ;
+my $count = $sql;
+$count =~ s/.* FROM /SELECT COUNT(*) FROM /i;
+
</%init>
diff --git a/httemplate/search/svc_acct.cgi b/httemplate/search/svc_acct.cgi
index b9e5a7cc9..58764f881 100755
--- a/httemplate/search/svc_acct.cgi
+++ b/httemplate/search/svc_acct.cgi
@@ -148,10 +148,34 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
if ( $sortby eq 'seconds' ) {
my $tot_time = 0;
push @header, emt('Time');
- push @fields, sub { my $svc_acct = shift;
- $tot_time += $svc_acct->seconds;
- format_time($svc_acct->seconds);
- };
+
+ if ( $conf->exists('svc_acct-display_paid_time_remaining') ) {
+ push @fields, sub { my $svc_acct = shift;
+ my $seconds = $svc_acct->seconds;
+ my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+ my $part_pkg = $cust_pkg->part_pkg;
+
+ $tot_time += $svc_acct->seconds;
+
+ $timepermonth = $part_pkg->option('seconds');
+ $timepermonth = $timepermonth / $part_pkg->freq
+ if $part_pkg->freq =~ /^\d+$/ && $part_pkg->freq != 0;
+ my $recur = $part_pkg->base_recur($cust_pkg);
+
+ return format_time($seconds)
+ unless $timepermonth && $recur;
+
+ format_time($seconds).
+ sprintf(' (%.2fx monthly)', $seconds / $timepermonth );
+
+ };
+ } else {
+ push @fields, sub { my $svc_acct = shift;
+ $tot_time += $svc_acct->seconds;
+ format_time($svc_acct->seconds);
+ };
+ }
+
push @links, '';
$align .= 'r';
push @color, '';
diff --git a/httemplate/search/svc_circuit.cgi b/httemplate/search/svc_circuit.cgi
index c14c55fdc..8f05e0488 100644
--- a/httemplate/search/svc_circuit.cgi
+++ b/httemplate/search/svc_circuit.cgi
@@ -3,7 +3,7 @@
'name' => 'circuit services',
'query' => $query,
'count_query' => $query->{'count_query'},
- 'redirect' => [ popurl(2). "view/svc_circuit.html?", 'svcnum' ],
+ 'redirect' => [ popurl(2). "view/svc_circuit.cgi?", 'svcnum' ],
'header' => [ '#',
'Provider',
'Type',
@@ -60,6 +60,6 @@ if ( $cgi->param('magic') eq 'unlinked' ) {
my $query = FS::svc_circuit->search(\%search_hash);
-my $link = [ $p.'view/svc_circuit.html?', 'svcnum' ];
+my $link = [ $p.'view/svc_circuit.cgi?', 'svcnum' ];
</%init>
diff --git a/httemplate/view/cust_bill-tex.cgi b/httemplate/view/cust_bill-tex.cgi
new file mode 100755
index 000000000..813376957
--- /dev/null
+++ b/httemplate/view/cust_bill-tex.cgi
@@ -0,0 +1,51 @@
+<% $tex %>
+<%init>
+
+use File::Slurp 'slurp';
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('View invoices');
+
+my( $invnum, $mode, $template, $notice_name );
+my($query) = $cgi->keywords;
+if ( $query =~ /^((.+)-)?(\d+)(.pdf)?$/ ) { #probably not necessary anymore?
+ $template = $2;
+ $invnum = $3;
+ $notice_name = 'Invoice';
+} else {
+ $invnum = $cgi->param('invnum');
+ $invnum =~ s/\.pdf//i; #probably not necessary anymore
+ $template = $cgi->param('template');
+ $notice_name = ( $cgi->param('notice_name') || 'Invoice' );
+ $mode = $cgi->param('mode');
+}
+
+my $conf = new FS::Conf;
+
+my %opt = (
+ 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
+ 'template' => $template,
+ 'notice_name' => $notice_name,
+ 'no_coupon' => ($cgi->param('no_coupon') || 0)
+);
+
+my $cust_bill = qsearchs({
+ 'select' => 'cust_bill.*',
+ 'table' => 'cust_bill',
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => { 'invnum' => $invnum },
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+});
+die "Invoice #$invnum not found!" unless $cust_bill;
+
+$cust_bill->set(mode => $mode);
+
+my ($file) = $cust_bill->print_latex(\%opt);
+my $tex = slurp("$file.tex");
+
+http_header('Content-Type' => 'text/plain' );
+http_header('Content-Disposition' => "filename=$invnum.tex" );
+http_header('Content-Length' => length($tex) );
+http_header('Cache-control' => 'max-age=60' );
+
+</%init>
diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html
index 78d51d07f..ca48a7068 100644
--- a/httemplate/view/cust_main/billing.html
+++ b/httemplate/view/cust_main/billing.html
@@ -23,6 +23,75 @@
<TD BGCOLOR="#ffffff"><B><% $balance %></B></TD>
</TR>
+% #54: just an arbitrary number i pulled out of my goober. ideally we'd like
+% # to consider e.g. a histogram of num_ncancelled_packages for the entire
+% # customer base, and compare it to a graph of the overhead for generating this
+% # information. (and optimize it better, we could get it more from SQL)
+% if ( $cust_main->num_ncancelled_pkgs < 54 ) {
+% my $sth = dbh->prepare("
+% SELECT DISTINCT freq FROM cust_pkg LEFT JOIN part_pkg USING (pkgpart)
+% WHERE freq IS NOT NULL AND freq != '0'
+% AND ( cancel IS NULL OR cancel = 0 )
+% AND custnum = ?
+% ") or die $DBI::errstr;
+%
+% $sth->execute($cust_main->custnum) or die $sth->errstr;
+
+% #not really a numeric sort because freqs can actually be all sorts of things
+% # but good enough for the 99% cases of ordering monthly quarterly annually
+% my @freqs = sort { $a <=> $b } map { $_->[0] } @{ $sth->fetchall_arrayref };
+%
+% foreach my $freq (@freqs) {
+% my @cust_pkg = qsearch({
+% 'table' => 'cust_pkg',
+% 'addl_from' => 'LEFT JOIN part_pkg USING (pkgpart)',
+% 'hashref' => { 'custnum' => $cust_main->custnum, },
+% 'extra_sql' => 'AND ( cancel IS NULL OR cancel = 0 )
+% AND freq = '. dbh->quote($freq),
+% }) or next;
+%
+% my $freq_pretty = $cust_pkg[0]->part_pkg->freq_pretty;
+%
+% my $amount = 0;
+% foreach my $cust_pkg (@cust_pkg) {
+% my $part_pkg = $cust_pkg->part_pkg;
+% next if $cust_pkg->susp
+% && ! $cust_pkg->option('suspend_bill')
+% && ( ! $part_pkg->option('suspend_bill')
+% || $cust_pkg->option('no_suspend_bill')
+% );
+%
+% my $pkg_amount = 0;
+%
+% #add recurring amounts for this package and its billing add-ons
+% foreach my $l_part_pkg ( $part_pkg->self_and_bill_linked ) {
+% $pkg_amount += $l_part_pkg->base_recur($cust_pkg);
+% }
+%
+% #subtract amounts for any active discounts
+% #(there should only be one at the moment, otherwise this makes no sense)
+% foreach my $cust_pkg_discount ( $cust_pkg->cust_pkg_discount_active ) {
+% my $discount = $cust_pkg_discount->discount;
+% #and only one of these for each
+% $pkg_amount -= $discount->amount;
+% $pkg_amount -= $amount * $discount->percent/100;
+% }
+%
+% $pkg_amount *= ( $cust_pkg->quantity || 1 );
+%
+% $amount += $pkg_amount;
+%
+% }
+
+ <TR>
+ <TD ALIGN="right"><% emt( ucfirst($freq_pretty). ' recurring' ) %></TD>
+ <TD BGCOLOR="#ffffff"><% $money_char. sprintf('%.2f', $amount) %></TD>
+ </TD>
+ </TR>
+% }
+
+% }
+
% if ( $conf->exists('cust_main-select-prorate_day') ) {
<TR>
<TD ALIGN="right"><% mt('Prorate day of month') |h %></TD>
diff --git a/httemplate/view/cust_main/contacts.html b/httemplate/view/cust_main/contacts.html
index e810d6f5d..136f634a4 100644
--- a/httemplate/view/cust_main/contacts.html
+++ b/httemplate/view/cust_main/contacts.html
@@ -93,8 +93,12 @@
% }
<TR>
+% if ( $location->city ) {
<TD ALIGN="right"><% mt('City') |h %></TD>
<TD BGCOLOR="#ffffff"><% $location->city |h %></TD>
+% } else {
+ <TD COLSPAN="2"></TD>
+% }
% if ( $location->county ) {
<TD ALIGN="right"><% mt('County') |h %></TD>
<TD BGCOLOR="#ffffff"><% $location->county |h %></TD>
diff --git a/httemplate/view/cust_main/packages/status.html b/httemplate/view/cust_main/packages/status.html
index 047abda0b..3063e3fc8 100644
--- a/httemplate/view/cust_main/packages/status.html
+++ b/httemplate/view/cust_main/packages/status.html
@@ -417,6 +417,10 @@ sub pkg_status_row_expire {
} elsif ( $cust_pkg->change_to_pkg->locationnum != $cust_pkg->locationnum )
{
$title = mt('Will <b>change location</b> on');
+ } elsif (( $cust_pkg->change_to_pkg->quantity != $cust_pkg->quantity ) ||
+ ( $cust_pkg->change_to_pkg->contract_end != $cust_pkg->contract_end ))
+ {
+ $title = mt('Will change on');
} else {
# FS::cust_pkg->change_later should have prevented this, but
# just so that we can display _something_
diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html
index d79d84365..69571c78e 100644
--- a/httemplate/view/cust_main/payment_history.html
+++ b/httemplate/view/cust_main/payment_history.html
@@ -151,10 +151,19 @@
%# invoice reports, combined statement
% if ( $curuser->access_right('List invoices') ) {
-% if ( $num_cust_bill > 0 ) {
- <A HREF="<% $p %>view/cust_main_statement-pdf.cgi?<% $custnum %>"><%
- mt('Download typeset statement PDF') |h %></A>
+% if ( $curuser->access_right('Resend invoices')
+% && $cust_main->invoicing_list_emailonly ) {
+
+ <A HREF="<% $p %>misc/email-customer-statement.html?table=cust_main;agent_virt_agentnum=<% $cust_main->agentnum %>;custnum=<% $custnum %>"><% mt('Email statement to this customer') |h %></A>
<BR>
+
+% }
+% if ( $num_cust_bill > 0
+% && $curuser->access_right('View legacy typeset statements')
+% ) {
+ <A HREF="<% $p %>view/cust_main_statement-pdf.cgi?<% $custnum %>"><%
+ mt('Download typeset statement PDF') |h %></A>
+ <BR>
% }
<A HREF="<% $p %>search/report_cust_bill.html?custnum=<% $custnum %>"><% mt('Invoice reports') |h %></A>
% }
diff --git a/httemplate/view/cust_main/payment_history/payment.html b/httemplate/view/cust_main/payment_history/payment.html
index 4ec9271ef..bf88a6607 100644
--- a/httemplate/view/cust_main/payment_history/payment.html
+++ b/httemplate/view/cust_main/payment_history/payment.html
@@ -9,6 +9,7 @@ my $date_format = $opt{'date_format'} || '%m/%d/%Y';
my @cust_bill_pay = $cust_pay->cust_bill_pay;
my @cust_pay_refund = $cust_pay->cust_pay_refund;
+my $unapplied = $cust_pay->unapplied;
my ($payby,$payinfo) = translate_payinfo($cust_pay);
my $target = "$payby$payinfo";
@@ -50,39 +51,14 @@ if ( scalar(@cust_bill_pay) == 0
$payment = emt("Unapplied Payment by [_1]",$otaker);
$payment =~ s/$otaker/<i>$otaker<\/i>/ if $italicize_otaker;
$payment = '<B><FONT COLOR="#FF0000">'.$payment.'</FONT></B>';
- if ( $opt{'Apply payment'} ) {
- if ( $opt{total_owed} > 0 ) {
- $apply = ' ('.
- include( '/elements/popup_link.html',
- 'label' => emt('apply'),
- 'action' => "${p}edit/cust_bill_pay.cgi?".
- $cust_pay->paynum,
- 'actionlabel' => emt('Apply payment'),
- %cust_bill_pay_width,
- %cust_bill_pay_height,
- ).
- ')';
- }
- if ( $opt{total_unapplied_refunds} > 0 ) {
- $apply.= ' ('.
- include( '/elements/popup_link.html',
- 'label' => emt('apply to refund'),
- 'action' => "${p}edit/cust_pay_refund.cgi?".
- $cust_pay->paynum,
- 'actionlabel' => emt('Apply payment to refund'),
- 'width' => 392,
- ).
- ')';
- }
- }
} elsif ( scalar(@cust_bill_pay) == 1
&& scalar(@cust_pay_refund) == 0
- && $cust_pay->unapplied == 0 ) {
+ && $unapplied == 0 ) {
#applied to one invoice, the usual situation
$desc .= ' '. $cust_bill_pay[0]->applied_to_invoice;
} elsif ( scalar(@cust_bill_pay) == 0
&& scalar(@cust_pay_refund) == 1
- && $cust_pay->unapplied == 0 ) {
+ && $unapplied == 0 ) {
#applied to one refund
$desc .= emt(" refunded on [_1]", time2str($date_format, $cust_pay_refund[0]->_date) );
} else {
@@ -101,40 +77,59 @@ if ( scalar(@cust_bill_pay) == 0
die "$app is not a FS::cust_bill_pay or FS::cust_pay_refund";
}
}
- if ( $cust_pay->unapplied > 0 ) {
+ if ( $unapplied > 0 ) {
$desc .= '&nbsp;&nbsp;'.
'<B><FONT COLOR="#FF0000">'.
- emt("[_1][_2] unapplied", $opt{money_char}, $cust_pay->unapplied).
+ emt("[_1][_2] unapplied", $opt{money_char}, $unapplied).
'</FONT></B>';
- if ( $opt{'Apply payment'} ) {
- if ( $opt{total_owed} > 0 ) {
- $apply = ' ('.
- include( '/elements/popup_link.html',
- 'label' => emt('apply'),
- 'action' => "${p}edit/cust_bill_pay.cgi?".
- $cust_pay->paynum,
- 'actionlabel' => emt('Apply payment'),
- %cust_bill_pay_width,
- %cust_bill_pay_height,
- ).
- ')';
- }
- if ( $opt{total_unapplied_refunds} > 0 ) {
- $apply.= ' ('.
- include( '/elements/popup_link.html',
- 'label' => emt('apply to refund'),
- 'action' => "${p}edit/cust_pay_refund.cgi?".
- $cust_pay->paynum,
- 'actionlabel' => emt('Apply payment to refund'),
- 'width' => 392,
- ).
- ')';
- }
- }
$desc .= '<BR>';
}
}
+if ($unapplied > 0) {
+ if ( $opt{'Apply payment'} ) {
+ if ( $opt{total_owed} > 0 ) {
+ $apply = ' ('.
+ include( '/elements/popup_link.html',
+ 'label' => emt('apply'),
+ 'action' => "${p}edit/cust_bill_pay.cgi?".
+ $cust_pay->paynum,
+ 'actionlabel' => emt('Apply payment'),
+ %cust_bill_pay_width,
+ %cust_bill_pay_height,
+ ).
+ ')';
+ }
+ if ( $opt{total_unapplied_refunds} > 0 ) {
+ $apply.= ' ('.
+ include( '/elements/popup_link.html',
+ 'label' => emt('apply to refund'),
+ 'action' => "${p}edit/cust_pay_refund.cgi?".
+ $cust_pay->paynum,
+ 'actionlabel' => emt('Apply payment to refund'),
+ 'width' => 392,
+ ).
+ ')';
+ }
+ $apply .= ' (auto&#8209;apply:&nbsp;'
+ . ($cust_pay->no_auto_apply ? 'no' : 'yes')
+ . '&nbsp;|&nbsp;'
+ . include( '/elements/popup_link.html',
+ 'label' => emt($cust_pay->no_auto_apply ? 'yes' : 'no'),
+ 'action' => "${p}edit/process/cust_pay-no_auto_apply.cgi?paynum="
+ . $cust_pay->paynum
+ . '&no_auto_apply='
+ . ($cust_pay->no_auto_apply ? '' : 'Y'),
+ 'actionlabel' => emt('Toggle Auto-Apply'),
+ 'width' => 392,
+ 'height' => 200,
+ )
+ . ')';
+ } else { # end if $opt('Apply payment')
+ $apply .= ' (no auto-apply)' if $cust_pay->no_auto_apply;
+ }
+} # end if $unapplied > 0
+
my $view =
' ('. include('/elements/popup_link.html',
'label' => emt('view receipt'),
diff --git a/httemplate/view/svc_circuit.html b/httemplate/view/svc_circuit.cgi
index 42bfc4b21..42bfc4b21 100644
--- a/httemplate/view/svc_circuit.html
+++ b/httemplate/view/svc_circuit.cgi
diff --git a/httemplate/view/svc_phone.cgi b/httemplate/view/svc_phone.cgi
index aca412969..ab69c4f6f 100644
--- a/httemplate/view/svc_phone.cgi
+++ b/httemplate/view/svc_phone.cgi
@@ -56,7 +56,7 @@ if ( $conf->exists('svc_phone-lnp') ) {
$labels{circuit_label} = mt('Circuit');
push @fields, { field => 'circuit_label',
- link => [ $p.'view/svc_circuit.html?', 'circuit_svcnum' ]
+ link => [ $p.'view/svc_circuit.cgi?', 'circuit_svcnum' ]
};
push @fields, 'sip_server';
@@ -146,7 +146,7 @@ my $html_foot = sub {
###
$e911.
- $devices.
+ #$devices.
$status.
join(' | ', @links ). '<BR>'.
join(' | ', @ilinks). '<BR>';