diff options
author | Ivan Kohler <ivan@freeside.biz> | 2015-08-07 22:01:31 -0700 |
---|---|---|
committer | Ivan Kohler <ivan@freeside.biz> | 2015-08-07 22:01:31 -0700 |
commit | 0c76afbb717e1716e6126bc4a120b8d9471614a0 (patch) | |
tree | 9a398e455a7767372588077470685d25ef8d82b3 /httemplate | |
parent | 7beec7068e00be5ae1b2599fdf2b494bc19e31d0 (diff) | |
parent | 3e2c2ad8aff1bd361ca07495b2255538c8231079 (diff) |
Merge branch 'FREESIDE_3_BRANCH' of git.freeside.biz:/home/git/freeside into FREESIDE_3_BRANCH
Diffstat (limited to 'httemplate')
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 © 2005-2014 Freeside Internet Services, Inc.<BR> +Copyright © 2005-2015 Freeside Internet Services, Inc.<BR> Copyright © 2000-2005 Ivan Kohler<BR> Copyright © 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> </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> - - <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 %>"> <% 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 = ' ' + 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)) // + document.createTextNode(String.fromCharCode(160) + (values ? values.balance : '')) // ); 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 %>"> <% 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 = ' ' + 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, &>></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", + "− $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 & 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 .= ' '. '<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‑apply: ' + . ($cust_pay->no_auto_apply ? 'no' : 'yes') + . ' | ' + . 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>'; |