From: Mark Wells Date: Sat, 27 Jun 2015 22:51:29 +0000 (-0700) Subject: reports with row grouping for payment/refund search, #25944 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=062f38f2f5d0da64c6fd0702d4df2e805473a1f5 reports with row grouping for payment/refund search, #25944 --- diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 6163197fa..f05ac0339 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -57,7 +57,7 @@ if ( -e $addl_handler_use_file ) { use CGI::Cookie; use List::Util qw( max min sum ); use List::MoreUtils qw( first_index uniq ); - use Scalar::Util qw( blessed ); + use Scalar::Util qw( blessed looks_like_number ); use Data::Dumper; use Date::Format; use Time::Local; @@ -156,6 +156,8 @@ if ( -e $addl_handler_use_file ) { use FS::Locales; use FS::Maketext qw( mt emt js_mt ); + use FS::Query; + use FS::agent; use FS::agent_type; use FS::domain_record; diff --git a/FS/FS/Query.pm b/FS/FS/Query.pm new file mode 100644 index 000000000..8ecf1c49c --- /dev/null +++ b/FS/FS/Query.pm @@ -0,0 +1,118 @@ +package FS::Query; + +use strict; +use FS::Record; # don't import qsearch +use Storable 'dclone'; + +=head1 NAME + +FS::Query - A thin wrapper around qsearch argument hashes. + +=head1 DESCRIPTION + +This module exists because we pass qsearch argument lists around a lot, +and add new joins or WHERE expressions in several stages, and I got tired +of doing this: + + my $andwhere = "mycolumn IN('perl','python','javascript')"; + if ( ($search->{hashref} and keys( %{$search->{hashref}} )) + or $search->{extra_sql} =~ /^\s*WHERE/ ) { + $search->{extra_sql} .= " AND $andwhere"; + } else { + $search->{extra_sql} = " WHERE $andwhere "; + } + +and then having it fail under some conditions if it's done wrong (as the above +example is, obviously). + +We may eventually switch over to SQL::Abstract or something for this, but for +now it's a couple of crude manipulations and a wrapper to qsearch. + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Turns HASHREF (a qsearch argument list) into an FS::Query object. None of +the params are really required, but you should at least supply C. + +In the Future this may do a lot more stuff. + +=cut + +sub new { + my ($class, $hashref) = @_; + + my $self = bless { + table => '', + select => '*', + hashref => {}, + addl_from => '', + extra_sql => '', + order_by => '', + %$hashref, + }; + # load FS::$table? validate anything? + $self; +} + +=item clone + +Returns another object that's a copy of this one. + +=cut + +sub clone { + my $self = shift; + $self->new( dclone($self) ); +} + +=item and_where EXPR + +Adds a constraint to the WHERE clause of the query. All other constraints in +the WHERE clause should be joined with AND already; if not, they should be +grouped with parentheses. + +=cut + +sub and_where { + my $self = shift; + my $where = shift; + + if ($self->{extra_sql} =~ /^\s*(?:WHERE|AND)\s+(.*)/is) { + $where = "($where) AND $1"; + } + if (keys %{ $self->{hashref} }) { + $where = " AND $where"; + } else { + $where = " WHERE $where"; + } + $self->{extra_sql} = $where; + + return $self; +} + +=item qsearch + +Runs the query and returns all results. + +=cut + +sub qsearch { + my $self = shift; + FS::Record::qsearch({ %$self }); +} + +=item qsearchs + +Runs the query and returns only one result. + +=cut + +sub qsearchs { + my $self = shift; + FS::Record::qsearchs({ %$self }); +} + +1; diff --git a/httemplate/elements/freeside.css b/httemplate/elements/freeside.css index ece57a053..3dc344cbf 100644 --- a/httemplate/elements/freeside.css +++ b/httemplate/elements/freeside.css @@ -254,6 +254,24 @@ table.grid { border-radius: 2px; } +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; +} + th, td { vertical-align: top; } 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' : ''; + diff --git a/httemplate/search/elements/cust_pay_or_refund.html b/httemplate/search/elements/cust_pay_or_refund.html index 0e3fce303..ce513ab15 100755 --- a/httemplate/search/elements/cust_pay_or_refund.html +++ b/httemplate/search/elements/cust_pay_or_refund.html @@ -39,7 +39,7 @@ Examples: ) -<& search.html, +<& grouped-search.html, 'title' => $title, # XXX: translate 'name_singular' => $name_singular, 'query' => $sql_query, @@ -54,6 +54,16 @@ Examples: 'link_onclicks' => \@link_onclicks, 'color' => \@color, 'style' => \@style, + + 'group_column' => 'payby', + 'group_label' => 'payby_name', + 'subtotal' => { 'paid' => 'sum(paid)' }, + 'subtotal_row' => [ 'Subtotal', + sub { sprintf($money, $_[0]->paid) }, + ], + 'total_row' => [ 'Total', + sub { sprintf("$money", $_[0]->paid) }, + ], &> <%init> @@ -61,6 +71,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,7 +178,7 @@ 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; @@ -174,7 +187,7 @@ 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, ''; } @@ -252,14 +265,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 +330,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' => [ ... ], + +&> + +<%init> +my $type = 'html'; +if ($cgi->param('type') =~ /^(html|html-print|xls)$/) { + $type = $1; +} + +<& "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..fe47fb7d1 --- /dev/null +++ b/httemplate/search/elements/grouped-search/core @@ -0,0 +1,153 @@ +<%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 ], +} + + +<%shared> +my $conf = FS::Conf->new; + +<%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; +} + +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, +}; + diff --git a/httemplate/search/elements/grouped-search/html b/httemplate/search/elements/grouped-search/html new file mode 100644 index 000000000..aff17cea0 --- /dev/null +++ b/httemplate/search/elements/grouped-search/html @@ -0,0 +1,131 @@ +<%shared> +my $conf = FS::Conf->new; + +<%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('

No matching ' . $opt{'name'} . ' found.
'); + $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"; +} + + + +<& /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 +&> + +
+%# download links +

<% emt('Download full results') %>
+% $cgi->param('type', 'xls'); +<% emt('as Excel spreadsheet') %>
+% $cgi->param('type', 'html-print'); +<% emt('as printable copy') %>
+

+ +<% $pager %> + + +
+ + <& /search/elements/search-html.html:header_row, + 'header' => $opt{'header'}, + 'header2' => $opt{'header2'}, + 'sort_fields' => ($opt{'sort_fields'} || $opt{'fields'}), + &> + + + <& /search/elements/search-html.html:data_rows, + 'rows' => \@rows, + 'opt' => \%opt, + &> + + + <& /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 &> +% } + +
+ +<% $pager %> + + +<& /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..c4c3b1ef5 --- /dev/null +++ b/httemplate/search/elements/grouped-search/html-print @@ -0,0 +1,79 @@ +<%doc> + +The "printable" view (all groups on one page). + +<%init> +my %opt = @_; + +my $group_info = $m->comp('core', %opt); +my $ncols = scalar(@{ $opt{header} }); + +my $total_footer = $group_info->{total_footer} || []; + +<& /elements/header-popup.html, $opt{title} &> + + +<& /elements/table-grid.html &> + +<& /search/elements/search-html.html:header_row, + 'header' => $opt{'header'}, + 'header2' => $opt{'header2'}, + 'sort_fields' => ($opt{'sort_fields'} || $opt{'fields'}), +&> + + +% 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 + + + + <% $label %> + + +<& /search/elements/search-html.html:data_rows, + rows => \@rows, + opt => \%opt, +&> + + +<& /search/elements/search-html.html:footer_row, + row => $footer, + opt => \%opt, +&> + +% } # for $curr_group + +% if ( scalar @$total_footer ) { + +<& /search/elements/search-html.html:footer_row, + row => $total_footer, + opt => \%opt, +&> + +% } + +<& /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. + + +<%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); + 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 + +<%init> +my @values = grep /^(\d+)$/, $cgi->param( $param ); +if (@values) { + return "COALESCE($field, 0) IN (" . join(',', @values) . ')'; +} else { + return; +} + 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: - - <% ucfirst(PL($name_singular)) %> of type: - - - - + <& /elements/tr-select.html, + label => ucfirst(PL($name_singular)) . ' of type:', + field => 'payby', + options => [ keys(\%payby) ], + labels => \%payby, + multiple => 1, + size => 16 + &> <% mt('Check #:') |h %> @@ -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)') ], &> @@ -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', +); + diff --git a/httemplate/search/elements/search-html.html b/httemplate/search/elements/search-html.html index 10cc95539..d8ab30a32 100644 --- a/httemplate/search/elements/search-html.html +++ b/httemplate/search/elements/search-html.html @@ -181,300 +181,13 @@ % } - <% include('/elements/table-grid.html') %> - - -% 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($label); -% } -% -% $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"' -% } -% } - - <% $style %> - - > - <% $label %> - -% } - - -% if ( $opt{header2} ) { - -% foreach my $header ( @{ $opt{header2} } ) { -% my $label = ref($header) ? $header->{label} : $header; - - <% $label %> - -% } - -% } - -% 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(); -% } -% } - > - - -% 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 = $_; -% -% ''. -% -% join('', map { -% -% my $rowref = $_; -% -% ''. -% -% join('', map { -% -% my $e = $_; -% -% ''; -% -% } @$rowref ). -% -% ''; -% } @$tableref ). -% -% '
{$_}), -% qw( align bgcolor colspan rowspan -% style valign width ) -% ). -% '>'. -% -% ( $e->{'link'} -% ? '' -% : '' -% ). -% ( $e->{'onclick'} # don't use with 'link' -% ? '' -% : '' -% ). -% ( $e->{'size'} -% ? '' -% : '' -% ). -% ( $e->{'data_style'} -% ? '<'. uc($e->{'data_style'}). '>' -% : '' -% ). -% $e->{'data'}. -% ( $e->{'data_style'} -% ? '{'data_style'}). '>' -% : '' -% ). -% ( $e->{'size'} ? '' : '' ). -% ( $e->{'link'} || $e->{'onclick'} -% ? '' -% : '' ). -% '
'; -% -% } 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 =~ /^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(); -% } -% elsif ( $onclick ) { -% $a = qq(); -% } -% elsif ( $tooltip ) { -% $a = qq(); -% } -% $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 = ''; -% } -% -% 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; - - - -% } -% -% } else { -% -% foreach ( @$row ) { - -% } -% -% } - - - -% } - -% if ( $opt{'footer'} ) { - - - -% foreach my $footer ( @{ $opt{'footer'} } ) { -% $footer = &{$footer}() if ref($footer) eq 'CODE'; - -% } - - -% } - -
<% $cstyle %>><% $a %><% $font %><% $s %><% $field %><% $es %><% $font ? '' : '' %><% $a ? '' : '' %><% $_ %>
<% $footer %>
+ <& SELF:data_table, + rows => $rows, + null_link => $null_link, + link_agentnums => \@link_agentnums, + self_url => $self_url, + %opt + &> <% $pager %> @@ -521,14 +234,358 @@ 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); + +<%method data_table> +% my %opt = @_; +% my $rows = delete $opt{rows}; +% my $self_url = delete $opt{self_url}; +<& /elements/table-grid.html &> + + +<& SELF:header_row, + 'header' => $opt{'header'}, + 'header2' => $opt{'header2'}, + 'sort_fields' => ($opt{'sort_fields'} || $opt{'fields'}), +&> + + + +<& SELF:data_rows, rows => $rows, opt => \%opt &> + + +% if ( $opt{'footer'} ) { + +<& SELF:footer_row, row => $opt{'footer'}, opt => \%opt &> + +% } + + +<%method header_row> +<%args> +@sort_fields +@header +@header2 => () + + +% 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($label); +% } +% +% $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"' +% } +% } + + <% $style %> + + > + <% $label %> + +% } + + +% if ( @header2 ) { + +% foreach my $header ( @header2 ) { +% my $label = ref($header) ? $header->{label} : $header; + + <% $label %> + +% } + +% } + +<%method data_rows> +<%args> +$rows => [] +%opt + +% 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 +% 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(); +% } +% } + > + +% 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 = $_; +% +% ''. +% +% join('', map { +% +% my $rowref = $_; +% +% ''. +% +% join('', map { +% +% my $e = $_; +% +% ''; +% +% } @$rowref ). +% +% ''; +% } @$tableref ). +% +% '
{$_}), +% qw( align bgcolor colspan rowspan +% style valign width ) +% ). +% '>'. +% +% ( $e->{'link'} +% ? '' +% : '' +% ). +% ( $e->{'onclick'} # don't use with 'link' +% ? '' +% : '' +% ). +% ( $e->{'size'} +% ? '' +% : '' +% ). +% ( $e->{'data_style'} +% ? '<'. uc($e->{'data_style'}). '>' +% : '' +% ). +% $e->{'data'}. +% ( $e->{'data_style'} +% ? '{'data_style'}). '>' +% : '' +% ). +% ( $e->{'size'} ? '' : '' ). +% ( $e->{'link'} || $e->{'onclick'} +% ? '' +% : '' ). +% '
'; +% +% } 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 =~ /^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! title="<% $tooltip |h %>"!; +%# 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(); +% } +% elsif ( $onclick ) { +% $a = qq(); +% } +% elsif ( $tooltip ) { +% $a = qq(); +% } + +% } +% +% } +% +% 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 = ''; +% } +% +% 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; + + + +% } +% +% } else { # not $opt{'fields'} +% +% foreach ( @$row ) { + +% } +% +% } + + + +% $i++; +% +% } # foreach $row + +<%method footer_row> +<%args> +$row +%opt + +%# 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} }; + + +% foreach my $footer ( @$row ) { +% $footer = &{$footer}() if ref($footer) eq 'CODE'; +% my $align = shift @aligns; +% my $style = ''; +% $style .= "text-align: $align;" if $align; + +% } + + + 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); + + +<%method worksheet> +<%args> +$workbook +$title +%opt +$header +$rows + +<%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 =~ //i ) { # the only one in common use $value =~ s[][]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]); +} - + + diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html index 64f3a665f..80bc89d3e 100644 --- a/httemplate/search/elements/search.html +++ b/httemplate/search/elements/search.html @@ -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" @@ -200,6 +201,7 @@ Example: confmax => $confmax, maxrecords => $maxrecords, offset => $offset, + totals => $totals, opt => \%opt ) %> @@ -220,16 +222,6 @@ if ( !$curuser->access_right('Download report data') ) { $type = 'html'; } -my %align = ( - 'l' => 'left', - 'r' => 'right', - 'c' => 'center', - ' ' => '', - '.' => '', -); -$opt{align} = [ map $align{$_}, split(//, $opt{align}) ], - unless !$opt{align} || ref($opt{align}); - if($type =~ /csv|xls/) { my $h = $opt{'header'}; my @del; @@ -253,6 +245,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 +262,16 @@ 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; - - $opt{'count_query'} .= - ( $opt{'count_query'} =~ /WHERE/i ? ' AND ' : ' WHERE ' ). $agentnums_sql; + if ( $query->{'extra_sql'} =~ /\bWHERE\b/i or keys %{$query->{hashref}} ) { + $query->{'extra_sql'} .= " AND $agentnums_sql"; + $count_query .= " AND $agentnums_sql"; + } else { + $query->{'extra_sql'} .= " WHERE $agentnums_sql"; + $count_query .= " WHERE $agentnums_sql"; + } if ( $opt{'agent_pos'} || $opt{'agent_pos'} eq '0' and scalar($curuser->agentnums) > 1 ) { @@ -304,13 +302,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 +355,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,61 +387,56 @@ $order_by = $cgi->param('order_by') if $cgi->param('order_by'); my $header = [ map { ref($_) ? $_->{'label'} : $_ } @{$opt{header}} ]; my $rows; -if ( ref($opt{query}) ) { - - my @query; - if (ref($opt{query}) eq 'HASH') { - @query = ( $opt{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"; - } else { - warn "unparsable query order_by: ". $opt{query}->{'order_by'}; - die "unparsable query order_by: ". $opt{query}->{'order_by'}; - } + +my @query; +if (ref($query) eq 'HASH') { + @query = $query; + + if ( $order_by ) { + if ( $query->{'order_by'} ) { + if ( $query->{'order_by'} =~ /^(\s*ORDER\s+BY\s+)?(\S.*)$/is ) { + $query->{'order_by'} = "ORDER BY $order_by, $2"; } else { - $opt{query}->{'order_by'} = "ORDER BY $order_by"; + warn "unparsable query order_by: ". $query->{'order_by'}; + die "unparsable query order_by: ". $query->{'order_by'}; } + } else { + $query->{'order_by'} = "ORDER BY $order_by"; } - - $opt{query}->{'order_by'} .= " $limit"; - - } elsif (ref($opt{query}) eq 'ARRAY') { - @query = @{ $opt{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; - } + $query->{'order_by'} .= " $limit"; - #eval "use FS::$opt{'query'};"; - my @param = qw( select table addl_from hashref extra_sql order_by debug ); - $rows = [ qsearch( [ map { my $query = $_; - ({ map { $_ => $query->{$_} } @param }); - } - @query - ], - #'order_by' => $opt{order_by}. " ". $limit, - ) - ]; +} elsif (ref($query) eq 'ARRAY') { + # do we still use this? it was for the old 477 report. + @query = @{ $query }; } else { - my $sth = dbh->prepare("$opt{'query'} $limit") - or die "Error preparing $opt{'query'}: ". dbh->errstr; - $sth->execute - or die "Error executing $opt{'query'}: ". $sth->errstr; - - #can get # of rows without fetching them all? - $rows = $sth->fetchall_arrayref; + die "invalid query reference"; +} - $header ||= $sth->{NAME}; +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 debug ); +$rows = [ qsearch( [ map { my $query = $_; + ({ map { $_ => $query->{$_} } @param }); + } + @query + ], + #'order_by' => $opt{order_by}. " ". $limit, + ) + ]; + +# 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'};
<% $cstyle %>><% $a %><% $font %><% $s %><% $field %><% $es %><% $font ? '' : '' %><% $a ? '' : '' %><% $_ %>
<% $footer %>