diff options
Diffstat (limited to 'httemplate/search')
101 files changed, 13835 insertions, 0 deletions
diff --git a/httemplate/search/477.html b/httemplate/search/477.html new file mode 100755 index 000000000..bd7fb2d8b --- /dev/null +++ b/httemplate/search/477.html @@ -0,0 +1,92 @@ +% unless ( $type eq 'xml' ) { +<% include( '/elements/header.html', 'FCC Form 477 Results') %> +%}else{ +<?xml version="1.0" encoding="ISO-8859-1"?> +<Form_477_submission xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://specialreports.fcc.gov/wcb/Form477/XMLSchema-instance/form_477_upload_Schema.xsd" > +%} +% if ( $type eq 'html' || $type eq 'html-print' ) { +<TABLE WIDTH="100%"> + <TR><TD></TD> +%}elsif ( $type eq 'xml' ) { +%} +% unless ( $type eq 'html-print' || $type eq 'xml' ) { + + <TD ALIGN="right"> + + Download full results<BR> +% $cgi->param('_type', 'xml'); + as <A HREF="<% $cgi->self_url %>">XML file</A><BR> + +% $cgi->param('_type', 'html-print'); + as <A HREF="<% $cgi->self_url %>">printable copy</A> + + </TD> +% $cgi->param('_type', $type ); +% } +% if ( $type eq 'html' || $type eq 'html-print' ) { + </TR> +</TABLE> +%}elsif ( $type eq 'xml' ) { +%} +% foreach my $part ( @parts ) { +% if ( $part{$part} ) { +% +% if ( $part eq 'V' ) { +% next unless ( $part{'IIA'} || $part{'IIB'} ); +% } +% +% if ( $part eq 'VI' ) { +% next unless $part{'IA'}; +% } +% +% my @reports = (); +% if ( $part eq 'IA' ) { +% for ( my $tech = 0; $tech < scalar(@technology_option); $tech++ ) { +% next unless $technology_option[$tech]; +% my $url = &{$url_mangler}($cgi->self_url, $part); +% if ( $type eq 'xml' ) { +<<% 'Part_IA_'. chr(65 + $tech) %>> +% } +<% include( "477part${part}_summary.html", 'tech_code' => $tech, 'url' => $url ) %> +<% include( "477part${part}_detail.html", 'tech_code' => $tech, 'url' => $url ) %> +% if ( $type eq 'xml' ) { +</<% 'Part_IA_'. chr(65 + $tech) %>> +% } +% } +% } else { +% if ( $type eq 'xml' ) { +<<% 'Part_'. uc($part) %>> +% } +% my $url = &{$url_mangler}($cgi->self_url, $part); +<% include( "477part${part}.html", 'url' => $url ) %> +% if ( $type eq 'xml' ) { +</<% 'Part_'. uc($part) %>> +% } +% } +% } +% } +% +% if ( $type eq 'html' || $type eq 'html-print' ) { +<% include( '/elements/footer.html') %> +%}elsif ( $type eq 'xml' ) { +</Form_477_submission> +%} +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('List packages'); + +my %part = map { $_ => 1 } grep { /^\w+$/ } $cgi->param('part'); +my $type = $cgi->param('_type') || 'html'; +my $xlsname = '477report'; +my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi); +my $url_mangler = sub { + my ($url, $part) = (shift, shift); + $url =~ s/477\./477part$part./; + $url; +}; +my @parts = qw( IA IIA IIB IV V VI ); + +</%init> diff --git a/httemplate/search/477partIA_detail.html b/httemplate/search/477partIA_detail.html new file mode 100755 index 000000000..546d56c7f --- /dev/null +++ b/httemplate/search/477partIA_detail.html @@ -0,0 +1,125 @@ +<% include( 'elements/search.html', + 'html_init' => $html_init, + 'name' => 'lines', + 'query' => $query, + 'count_query' => $count_query, + 'really_disable_download' => 1, + 'disable_download' => 1, + 'nohtmlheader' => 1, + 'disable_total' => 1, + 'header' => [ '', @column_option_name ], + 'xml_elements' => [ @xml_elements ], + 'fields' => [ @fields ], + ) +%> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('List packages'); + +my %opt = @_; +my %search_hash = (); + +for ( qw(agentnum magic classnum) ) { + $search_hash{$_} = $cgi->param($_) if $cgi->param($_); +} + +my @column_option = grep { /^\d+/ } $cgi->param('part1_column_option') + if $cgi->param('part1_column_option'); + +my @row_option = grep { /^\d+/ } $cgi->param('part1_row_option') + if $cgi->param('part1_row_option'); + +my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi); + +my @column_option_name = scalar(@column_option) + ? ( map { my $part_pkg_report_option = + qsearchs({ 'table' => 'part_pkg_report_option', + 'hashref' => { num => $_ }, + }); + $part_pkg_report_option ? $part_pkg_report_option->name + : 'no such report option'; + } @column_option + ) + : ( 'all packages' ); + +my $where = join(' OR ', map { "num = $_" } @row_option ); +my %row_option_name = $where ? + ( map { $_->num => $_->name } + qsearch({ 'table' => 'part_pkg_report_option', + 'hashref' => {}, + 'extra_sql' => "WHERE $where", + }) + ) : + (); + +my $tech_code = $opt{tech_code}; +my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown'; +my $html_init = "<H2>Part IA $technology breakdown by speeds</H2>"; +my $xml_prefix = 'PartIA_'. chr(65 + $tech_code); + +my $query = 'SELECT '. join(' UNION ALL SELECT ',@row_option); +my $count_query = 'SELECT '. scalar(@row_option); + +my $value = sub { + my ($rowref, $column) = (shift, shift); + my $row = $rowref->[0]; + + if ($column eq 'name') { + return $row_option_name{$row} || 'no such report option'; + } elsif ( $column =~ /^(\d+)$/ ) { + my @report_option = ( $row || '', + $column_option[$1 - 2] || '', + $technology_option[$tech_code] || '', + ); + + my ( $count, $residential ) = FS::cust_pkg->fcc_477_count( + { %search_hash, 'report_option' => join(',', @report_option) } + ); + + my $percentage = sprintf('%.2f', $count ? 100 * $residential / $count : 0); + my $return = $count; + $return .= "<BR>$percentage% residential" + unless $cgi->param('_type') eq 'xml'; + return $return; + } else { + return '<FONT SIZE="+1" COLOR="#ff0000">Bad call to column_value</FONT>'; + } +}; + +my @fields = ( + sub { &{$value}(shift, 'name');}, + sub { &{$value}(shift, 2);}, + sub { &{$value}(shift, 3);}, + sub { &{$value}(shift, 4);}, + sub { &{$value}(shift, 5);}, + sub { &{$value}(shift, 6);}, + sub { &{$value}(shift, 7);}, + sub { &{$value}(shift, 8);}, + sub { &{$value}(shift, 9);}, + ); +shift @fields if $cgi->param('_type') eq 'xml'; + +my $xml_element = sub { + my ($rowref, $column) = (shift, shift); + my $row = $rowref->[0]; + + $row++; + $xml_prefix. $column. $row; + +}; + +my @xml_elements = ( + sub { &{$xml_element}(shift, 'f') }, + sub { &{$xml_element}(shift, 'g') }, + sub { &{$xml_element}(shift, 'h') }, + sub { &{$xml_element}(shift, 'i') }, + sub { &{$xml_element}(shift, 'j') }, + sub { &{$xml_element}(shift, 'k') }, + sub { &{$xml_element}(shift, 'l') }, + sub { &{$xml_element}(shift, 'm') }, +); + +</%init> diff --git a/httemplate/search/477partIA_summary.html b/httemplate/search/477partIA_summary.html new file mode 100755 index 000000000..269f2caf2 --- /dev/null +++ b/httemplate/search/477partIA_summary.html @@ -0,0 +1,80 @@ +<% include( 'elements/search.html', + 'html_init' => $html_init, + 'name' => 'lines', + 'query' => 'SELECT 1', + 'count_query' => 'SELECT 1', + 'really_disable_download' => 1, + 'disable_download' => 1, + 'nohtmlheader' => 1, + 'disable_total' => 1, + 'header' => [ + 'Total Connections', + '% owned loop', + '% billed to end users', + '% residential', + '% residential > 200kbps', + ], + 'xml_elements' => [ + $xml_prefix. 'a1', + $xml_prefix. 'b1', + $xml_prefix. 'c1', + $xml_prefix. 'd1', + $xml_prefix. 'e1', + ], + 'fields' => [ + sub { $total_count }, + sub { '100.00' }, + sub { '100.00' }, + sub { $total_percentage }, + sub { $total_percentage }, + ], + ) +%> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('List packages'); + +my %opt = @_; +my %search_hash = (); + +for ( qw(agentnum magic classnum) ) { + $search_hash{$_} = $cgi->param($_) if $cgi->param($_); +} + +my @column_option = grep { /^\d+$/ } $cgi->param('part1_column_option') + if $cgi->param('part1_column_option'); + +my @row_option = grep { /^\d+$/ } $cgi->param('part1_row_option') + if $cgi->param('part1_row_option'); + +my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi); + +my $total_count = 0; +my $total_residential = 0; +my $tech_code = $opt{tech_code}; +my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown'; +my $html_init = "<H2>Part IA $technology totals</H2>"; +my $xml_prefix = 'PartIA_'. chr(65 + $tech_code); + +foreach my $row ( @row_option ) { + foreach my $column ( @column_option ) { + + my @report_option = ( $row || '-1', $column || '-1', $technology_option[$tech_code] ); + + my ( $count, $residential ) = FS::cust_pkg->fcc_477_count( + { %search_hash, 'report_option' => join(',', @report_option) } + ); + + $total_count += $count; + $total_residential += $residential; + } +} + +my $total_percentage = + sprintf("%.2f", $total_count ? 100*$total_residential/$total_count : 0); + + +</%init> diff --git a/httemplate/search/477partIIA.html b/httemplate/search/477partIIA.html new file mode 100755 index 000000000..64f773a21 --- /dev/null +++ b/httemplate/search/477partIIA.html @@ -0,0 +1,113 @@ +<% include( 'elements/search.html', + 'html_init' => $html_init, + 'name' => 'lines', + 'query' => $query, + 'count_query' => 'SELECT 11', + 'really_disable_download' => 1, + 'disable_download' => 1, + 'nohtmlheader' => 1, + 'disable_total' => 1, + 'header' => [ @headers ], + 'xml_elements' => [ @xml_elements ], + 'fields' => [ @fields ], + ) +%> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('List packages'); + +my $html_init = '<H2>Part IIA</H2>'; +my %search_hash = (); + +for ( qw(agentnum magic classnum) ) { + $search_hash{$_} = $cgi->param($_) if $cgi->param($_); +} + +my @row_option = grep { /^\d+$/ } $cgi->param('part2a_row_option') + if $cgi->param('part2a_row_option'); + +# fudge in two rows of LD carrier +unshift @row_option, $row_option[0]; + +# fudge in the first pair of rows +unshift @row_option, ''; +unshift @row_option, ''; + +my $query = 'SELECT '. join(' UNION SELECT ', 1..11); + +my $total_count = 0; +my $column_value = sub { + my $row = shift; + + my @report_option = ( $row_option[$row - 1] || '' ); + + my $sql_query = FS::cust_pkg->search( + { %search_hash, 'report_option' => join(',', @report_option) } + ); + + my $count_sql = delete($sql_query->{'count_query'}); + if ( $row == 2 || $row == 4 ) { + $count_sql =~ s/COUNT\(\*\) FROM/COALESCE( sum(CASE WHEN cust_main.company IS NULL OR cust_main.company = '' THEN fcc_ds0s END), 0 ) FROM/ + or die "couldn't parse count_sql"; + } else { + $count_sql =~ s/COUNT\(\*\) FROM/COALESCE( sum(fcc_ds0s), 0 ) FROM/ + or die "couldn't parse count_sql"; + } + + my $count_sth = dbh->prepare($count_sql) + or die "Error preparing $count_sql: ". dbh->errstr; + $count_sth->execute + or die "Error executing $count_sql: ". $count_sth->errstr; + my $count_arrayref = $count_sth->fetchrow_arrayref; + my $count = $count_arrayref->[0]; + + $total_count = $count if $row == 1; + $count = sprintf('%.2f', $total_count ? 100*$count/$total_count : 0) + if $row != 1; + + return "$count"; +}; + +my @headers = ( + '', + 'End user lines', + 'UNE-P replacement', + 'UNE (unswitched)', + 'UNE-P', +); + +my @xml_elements = ( + sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}a" }, + sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}b" }, + sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}c" }, + sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}d" }, +); + + +my @rows = ( + 'lines', + '% residential', + '% LD carrier', + '% residential and LD carrier', + '% own loops', + '% obtained unswitched UNE loops', + '% UNE-P', + '% UNE-P replacement', + '% FTTP', + '% coax', + '% wireless', +); + +my @fields = ( + sub { my $row = shift; $rows[$row->[0] - 1]; }, + sub { my $row = shift; &{$column_value}($row->[0]); }, + sub { 0; }, + sub { 0; }, + sub { 0; }, +); + +shift @fields if $cgi->param('_type') eq 'xml'; +</%init> diff --git a/httemplate/search/477partIIB.html b/httemplate/search/477partIIB.html new file mode 100755 index 000000000..278dfdc8b --- /dev/null +++ b/httemplate/search/477partIIB.html @@ -0,0 +1,102 @@ +<% include( 'elements/search.html', + 'html_init' => $html_init, + 'name' => 'lines', + 'query' => $query, + 'count_query' => 'SELECT 11', + 'really_disable_download' => 1, + 'disable_download' => 1, + 'nohtmlheader' => 1, + 'disable_total' => 1, + 'header' => [ @headers ], + 'xml_elements' => [ @xml_elements ], + 'fields' => [ @fields ], + ) +%> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('List packages'); + +my $html_init = '<H2>Part IIB</H2>'; +my %search_hash = (); + +for ( qw(agentnum magic classnum) ) { + $search_hash{$_} = $cgi->param($_) if $cgi->param($_); +} + +my @row_option = grep { /^\d+$/ } $cgi->param('part2b_row_option') + if $cgi->param('part2b_row_option'); + +# fudge in 2nd row +unshift @row_option, $row_option[0]; + +my $query = 'SELECT '. join(' UNION SELECT ', 1..8); + +my $total_count = 0; +my $column_value = sub { + my $row = shift; + + my @report_option = ( $row_option[$row - 1] || '' ); + + my $sql_query = FS::cust_pkg->search( + { %search_hash, 'report_option' => join(',', @report_option) } + ); + + my $count_sql = delete($sql_query->{'count_query'}); + if ( $row == 2 ) { + $count_sql =~ s/COUNT\(\*\) FROM/COALESCE( sum(CASE WHEN cust_main.company IS NULL OR cust_main.company = '' THEN fcc_ds0s END), 0 ) FROM/ + or die "couldn't parse count_sql"; + } else { + $count_sql =~ s/COUNT\(\*\) FROM/COALESCE( sum(fcc_ds0s), 0 ) FROM/ + or die "couldn't parse count_sql"; + } + + my $count_sth = dbh->prepare($count_sql) + or die "Error preparing $count_sql: ". dbh->errstr; + $count_sth->execute + or die "Error executing $count_sql: ". $count_sth->errstr; + my $count_arrayref = $count_sth->fetchrow_arrayref; + my $count = $count_arrayref->[0]; + + $total_count = $count if $row == 1; + $count = sprintf('%.2f', $total_count ? 100*$count/$total_count : 0) + if $row != 1; + + return "$count"; +}; + +my @headers = ( + '', + 'with broadband', + 'without broadband', + 'wholesale', +); + +my @xml_elements = ( + sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}a" }, + sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}b" }, + sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}c" }, +); + +my @rows = ( + 'total number', + '% residential', + '% nomadic', + '% copper', + '% FTTP', + '% coax', + '% wireless', + '% other broadband', +); + +my @fields = ( + sub { my $row = shift; $rows[$row->[0] - 1]; }, + sub { 0; }, + sub { my $row = shift; &{$column_value}($row->[0]); }, + sub { 0; }, +); + +shift @fields if $cgi->param('_type') eq 'xml'; +</%init> diff --git a/httemplate/search/477partIV.html b/httemplate/search/477partIV.html new file mode 100755 index 000000000..269a925dc --- /dev/null +++ b/httemplate/search/477partIV.html @@ -0,0 +1,17 @@ +%if ( $cgi->param('_type') eq 'html' || $cgi->param('_type') eq 'html-print' ) { +<H2>Part IV</H2> +%} elsif ( $cgi->param('_type') eq 'xml' ) { +<notes> +%} +<% $cgi->param('notes') |h %> +%if ( $cgi->param('_type') eq 'xml' ) { +</notes> +%} +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('List packages'); + +</%init> diff --git a/httemplate/search/477partV.html b/httemplate/search/477partV.html new file mode 100755 index 000000000..c6ceac4db --- /dev/null +++ b/httemplate/search/477partV.html @@ -0,0 +1,53 @@ +<% include( 'elements/search.html', + 'html_init' => $html_init, + 'name' => 'zip code', + 'query' => [ @sql_query ], + 'count_query' => $count_query, + 'nohtmlheader' => 1, + 'disable_total' => 1, + 'header' => [ 'zip code' ], + 'xml_elements' => [ 'zip codes' ], + 'no_field_elements' => 1, + 'fields' => [ 'zip' ], + 'url' => $opt{url} || $cgi->self_url, + + ) +%> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('List packages'); + +my %opt = @_; +my $html_init = '<H2>Part V</H2>'; +my %search_hash = (); +my @sql_query = (); +my @count_query = (); + +for ( qw(agentnum magic classnum) ) { + $search_hash{$_} = $cgi->param($_) if $cgi->param($_); +} + +my $sql_query = FS::cust_pkg->search( { %search_hash, 'fcc_line' > 1 }); +$sql_query->{select} = 'DISTINCT zip'; +$sql_query->{extra_sql} =~ s/ORDER BY [.\w]+//; +push @sql_query, $sql_query; +push @count_query, delete($sql_query->{'count_query'}); +$count_query[0] =~ s/count\(\*\)/count(DISTINCT zip)/; +$count_query[0] =~ s/ORDER BY [.\w]+//; + +$search_hash{classnum} = $cgi->param('part2a_classnum') + if $cgi->param('part2a_classnum'); + +$sql_query = FS::cust_pkg->search( { %search_hash } ); +$sql_query->{select} = 'DISTINCT zip'; +$sql_query->{extra_sql} =~ s/ORDER BY [.\w]+//; +push @sql_query, $sql_query; +push @count_query, delete($sql_query->{'count_query'}); +$count_query[1] =~ s/count\(\*\)/count(DISTINCT zip)/; +$count_query[1] =~ s/ORDER BY [.\w]+//; +my $count_query = join(' UNION ', @count_query); + +</%init> diff --git a/httemplate/search/477partVI.html b/httemplate/search/477partVI.html new file mode 100755 index 000000000..dbd17032c --- /dev/null +++ b/httemplate/search/477partVI.html @@ -0,0 +1,130 @@ +<% include( 'elements/search.html', + 'html_init' => $html_init, + 'name' => 'regions', + 'query' => [ @sql_query ], + 'count_query' => $count_query, + 'order_by' => 'ORDER BY censustract', + 'avoid_quote' => 1, + 'no_csv_header' => 1, + 'nohtmlheader' => 1, + 'header' => [ + 'County code', + 'Census tract code', + 'Upload rate', + 'Download rate', + 'Technology code', + 'Technology code other', + 'Quantity', + 'Percentage residential', + ], + 'xml_elements' => [ + 'county_fips', + 'census_tract', + 'upload_rate_code', + 'download_rate_code', + 'technology_code', + 'technology_code_other', + 'value', + 'percentage', + ], + 'fields' => [ + sub { my $row = shift; substr($row->censustract, 2, 3) }, + sub { my $row = shift; substr($row->censustract, 5) }, + 'upload', + 'download', + 'technology_code', + sub { '' }, # doesn't really work + 'quantity', + sub { my $row = shift; sprintf "%.2f", $row->residential }, + ], + 'links' => [ + [ $link, $link_suffix ], + [ $link, $link_suffix ], + [ $link, $link_suffix ], + [ $link, $link_suffix ], + [ $link, $link_suffix ], + [ $link, $link_suffix ], + [ $link, $link_suffix ], + [ $link, $link_suffix ], + ], + 'url' => $opt{url} || $cgi->self_url, + 'xml_row_element' => 'Datarow', + ) +%> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('List packages'); + +my %opt = @_; + +my $html_init = '<H2>Part VI</H2>'; + +my %search_hash = (); +my @sql_query = (); + +for ( qw(agentnum magic classnum) ) { + $search_hash{$_} = $cgi->param($_) if $cgi->param($_); +} + +my @column_option = grep { /^\d+$/ } $cgi->param('part1_column_option') + if $cgi->param('part1_column_option'); + +my @row_option = grep { /^\d+$/ } $cgi->param('part1_row_option') + if $cgi->param('part1_row_option'); + +my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi); + +my $rowcount = 1; +foreach my $row ( @row_option ) { + my $columncount = 2; + foreach my $column ( @column_option ) { + my $tech_code = 0; + foreach my $technology ( @technology_option ) { + $tech_code++; + next unless $technology; + my @report_option = (); + push @report_option, $row if $row; + push @report_option, $column if $column; + push @report_option, $technology; + my $report_option = join(',', @report_option) if @report_option; + + my $sql_query = FS::cust_pkg->search( + { %search_hash, + ($report_option ? ( 'report_option' => $report_option ) : () ), + } + ); + my $extracolumns = "$rowcount AS upload, $columncount AS download, $tech_code as technology_code"; + my $percent = "CASE WHEN count(*) > 0 THEN 100-100*cast(count(cust_main.company) as numeric)/cast(count(*) as numeric) ELSE cast(0 as numeric) END AS residential"; + $sql_query->{select} = "count(*) AS quantity, $extracolumns, censustract, $percent"; + $sql_query->{extra_sql} =~ /^(.*)(ORDER BY pkgnum)(.*)$/s + or die "couldn't parse extra_sql"; + $sql_query->{extra_sql} = "$1 GROUP BY censustract $3"; + push @sql_query, $sql_query; + } + $columncount++; + } + $rowcount++; +} + +my $count_query = 'SELECT count(*) FROM ( ('. + join( ') UNION ALL (', + map { my $extra = $_->{extra_sql}; my $addl = $_->{addl_from}; + "SELECT censustract from cust_pkg $addl $extra"; + } + @sql_query + ). ') ) AS foo'; + +my $link = 'cust_pkg.cgi?'. + join(';', map{ "$_=". $search_hash{$_} } keys %search_hash). ';'; +my $link_suffix = sub { my $row = shift; + my $result = 'censustract='. $row->censustract. ';'; + $result .= 'report_option='. @row_option[$row->upload - 1] + if @row_option[$row->upload - 1]; + $result .= 'report_option='. @column_option[$row->download - 1] + if @column_option[$row->download - 1]; + $result; + }; +</%init> diff --git a/httemplate/search/agent_inventory.html b/httemplate/search/agent_inventory.html new file mode 100644 index 000000000..ac65371ca --- /dev/null +++ b/httemplate/search/agent_inventory.html @@ -0,0 +1,40 @@ +<% include('elements/search.html', + 'title' => 'Inventory summary per agent', + 'name_singular' => 'agent', + 'query' => { 'table' => 'agent', + 'hashref' => { 'disabled' => '' }, + 'extra_sql' => "AND $agentnums_sql", + }, + 'count_query' => "SELECT COUNT(*) FROM agent". + " WHERE disabled = '' OR disabled IS NULL". + " AND $agentnums_sql", + 'header' => \@header, + 'fields' => \@fields, + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); +#XXX List inventory + +my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql; + +my @header = ('Agent'); +my @fields = ('agent'); + + #{ 'disabled' => '' } +foreach my $inventory_class ( qsearch('inventory_class', {}) ) { + push @header, $inventory_class->classname; + push @fields, sub { + my $agent = shift; + my $sub = FS::inventory_class->countcell_factory( + 'p' => $p, 'agentnum' => $agent->agentnum, + ); + &{$sub}($inventory_class); + }; +} + +#XXX show global inventory too + +</%init> diff --git a/httemplate/search/cdr.html b/httemplate/search/cdr.html new file mode 100644 index 000000000..6b38d3ba7 --- /dev/null +++ b/httemplate/search/cdr.html @@ -0,0 +1,289 @@ +<% include( 'elements/search.html', + 'title' => $title, + 'name' => 'call detail records', + + 'query' => { 'table' => 'cdr', + 'hashref' => $hashref, + 'extra_sql' => $qsearch, + 'order_by' => 'ORDER BY calldate', + }, + 'count_query' => $count_query, + 'header' => [ + '', # checkbox column + @header, + ], + 'fields' => [ + sub { + return '' unless $edit_data; + $areboxes = 1; + my $cdr = shift; + my $acctid = $cdr->acctid; + qq!<INPUT NAME="acctid$acctid" TYPE="checkbox" VALUE="1">!; + }, + @fields, #XXX fill in some pretty-print + #processing, etc. + ], + 'links' => \@links, + + 'html_form' => qq!<FORM NAME="cdrForm" ACTION="$p/misc/cdr.cgi" METHOD="POST">!, + #false laziness w/queue.html + 'html_foot' => sub { + if ( $areboxes ) { + '<BR><INPUT TYPE="button" VALUE="select all" onClick="setAll(true)">'. + '<INPUT TYPE="button" VALUE="unselect all" onClick="setAll(false)">'. + qq!<BR><INPUT TYPE="submit" NAME="action" VALUE="reprocess selected" onClick="return confirm('Are you sure you want to reprocess the selected CDRs?')">!. + qq!<INPUT TYPE="submit" NAME="action" VALUE="delete selected" onClick="return confirm('Are you sure you want to delete the selected CDRs?')"><BR>!. + '<SCRIPT TYPE="text/javascript">'. + ' function setAll(setTo) { '. + ' theForm = document.cdrForm;'. + ' for (i=0,n=theForm.elements.length;i<n;i++)'. + ' if (theForm.elements[i].name.indexOf("acctid") != -1)'. + ' theForm.elements[i].checked = setTo;'. + ' }'. + '</SCRIPT>'; + } else { + ''; + } + }, + + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List rating data'); + +my $edit_data = $FS::CurrentUser::CurrentUser->access_right('Edit rating data'); + +my $areboxes = 0; + +my $title = 'Call Detail Records'; +my $hashref = {}; + +#process params for CDR search, populate $hashref... +# and fixup $count_query + +my @search = (); + +### +# dates +### + +my $str2time_sql = str2time_sql; +my $closing = str2time_sql_closing; + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +push @search, "$str2time_sql calldate $closing >= $beginning ", + "$str2time_sql calldate $closing <= $ending"; + +### +# duration / billsec +### + +push @search, FS::UI::Web::parse_lt_gt($cgi, 'duration'); +push @search, FS::UI::Web::parse_lt_gt($cgi, 'billsec'); + +#above here things just push @search +#below here things also have to define $hashref->{} or push @qsearch +my @qsearch = @search; + +### +# freesidestatus +### + +if ( $cgi->param('freesidestatus') eq 'NULL' ) { + + $title = "Unprocessed $title"; + $hashref->{'freesidestatus'} = ''; # Record.pm will take care of it + push @search, "( freesidestatus IS NULL OR freesidestatus = '' )"; + +} elsif ( $cgi->param('freesidestatus') =~ /^([\w ]+)$/ ) { + + $title = "Processed $title"; + $hashref->{'freesidestatus'} = $1; + push @search, "freesidestatus = '$1'"; + +} + +### +# termpartNstatus +### + +foreach my $param ( grep /^termpart\d+status$/, $cgi->param ) { + + my $status = $cgi->param($param); + + $param =~ /^termpart(\d+)status$/ or die 'guru meditation 54something'; + my $termpart = $1; + + my $search = ''; + if ( $status eq 'NULL' ) { + + #false lazienss w/cdr_termination.pm (i should be a part_termination method) + my $where_term = + "( cdr.acctid = cdr_termination.acctid AND termpart = $termpart ) "; + #my $join_term = "LEFT JOIN cdr_termination ON ( $where_term )"; + $search = + "NOT EXISTS ( SELECT 1 FROM cdr_termination WHERE $where_term )"; + + } elsif ( $cgi->param('freesidestatus') =~ /^([\w ]+)$/ ) { + + #false lazienss w/cdr_termination.pm (i should be a part_termination method) + my $where_term = + "( cdr.acctid = cdr_termination.acctid AND termpart = $termpart AND status = '$1' ) "; + #my $join_term = "LEFT JOIN cdr_termination ON ( $where_term )"; + $search = + "EXISTS ( SELECT 1 FROM cdr_termination WHERE $where_term )"; + + } + + if ( $search ) { + push @search, $search; + push @qsearch, $search; + } + +} + +### +# src/dest/charged_party +### + +if ( $cgi->param('src') =~ /^\s*([\d\-\+\ ]+)\s*$/ ) { + ( my $src = $1 ) =~ s/\D//g; + $hashref->{'src'} = $src; + push @search, "src = '$src'"; +} + +if ( $cgi->param('dst') =~ /^\s*([\d\-\+ ]+)\s*$/ ) { + ( my $dst = $1 ) =~ s/\D//g; + $hashref->{'dst'} = $dst; + push @search, "dst = '$dst'"; +} + +if ( $cgi->param('dcontext') =~ /^\s*(.+)\s*$/ ) { + my $dcontext = $1; + $hashref->{'dcontext'} = $dcontext; + push @search, "dcontext = '$dcontext'"; +} + +if ( $cgi->param('charged_party') =~ /^\s*([\d\-\+\ ]+)\s*$/ ) { + ( my $charged_party = $1 ) =~ s/\D//g; + #$hashref->{'charged_party'} = $charged_party; + #push @search, "charged_party = '$charged_party'"; + #XXX countrycode + + my $search = " ( charged_party = '$charged_party' + OR charged_party = '1$charged_party' ) "; + + push @search, $search; + push @qsearch, $search; +} + +### +# cdrbatchnum (or legacy cdrbatch) +### + +if ( $cgi->param('cdrbatch') ) { + + my $cdr_batch = + qsearchs('cdr_batch', { 'cdrbatch' => scalar($cgi->param('cdrbatch')) } ); + if ( $cdr_batch ) { + $hashref->{cdrbatchnum} = $cdr_batch->cdrbatchnum; + push @search, 'cdrbatchnum = '. $cdr_batch->cdrbatchnum; + } else { + die "unknown cdrbatch ". $cgi->param('cdrbatch'); + } + +} elsif ( $cgi->param('cdrbatchnum') ne '__ALL__' ) { + + if ( $cgi->param('cdrbatchnum') eq '' ) { + my $search = "( cdrbatchnum IS NULL )"; + push @qsearch, $search; + push @search, $search; + } elsif ( $cgi->param('cdrbatchnum') =~ /^(\d+)$/ ) { + $hashref->{cdrbatchnum} = $1; + push @search, "cdrbatchnum = $1"; + } + +} + +### +# acctid +### + +if ( $cgi->param('acctid') =~ /\d/ ) { + my $acctid = $cgi->param('acctid'); + $acctid =~ s/\r\n/\n/g; #browsers? + my @acctid = map { /^\s*(\d+)\s*$/ or die "guru meditation #4"; $1; } + grep { /^\s*(\d+)\s*$/ } + split(/\n/, $acctid); + if ( @acctid ) { + my $search = 'acctid IN ( '. join(',', @acctid). ' )'; + push @qsearch, $search; + push @search, $search; + } +} + +### +# finish it up +### + +my $search = join(' AND ', @search); +$search = "WHERE $search" if $search; + +my $count_query = "SELECT COUNT(*) FROM cdr $search"; + +my $qsearch = join(' AND ', @qsearch); +$qsearch = ( scalar(keys %$hashref) ? ' AND ' : ' WHERE ' ) . $qsearch + if $qsearch; + +### +# display fields +### + +my %header = %{ FS::cdr->table_info->{'fields'} }; + +my @first = qw( acctid calldate clid charged_party src dst dcontext ); +my %first = map { $_=>1 } @first; + +my @fields = ( @first, grep !$first{$_}, fields('cdr') ); + +if ( $cgi->param('show') ) { + @fields = grep $cgi->param("show_$_"), @fields; +} + +my @header = map { + if ( exists($header{$_}) ) { + $header{$_}; + } else { + my $header = $_; + $header =~ s/\_/ /g; #//wtf + ucfirst($header); + } + } @fields; + +my $date_sub_factory = sub { + my $column = shift; + sub { + #my $cdr = shift; + my $date = shift->$column(); + $date ? time2str( '%Y-%m-%d %T', $date ) : ''; #config time2str format? + }; +}; + +my %fields = ( + #any other formatters? + map { $_ => &{ $date_sub_factory }($_) } qw( startdate answerdate enddate ) +); + +my %links = ( + 'svcnum' => + sub { $_[0]->svcnum ? [ $p.'view/svc_phone.cgi?', 'svcnum' ] : ''; }, +); + +@fields = map { exists($fields{$_}) ? $fields{$_} : $_ } @fields; + + #checkbox column +my @links = ( '', map { exists($links{$_}) ? $links{$_} : '' } @fields ); + +</%init> diff --git a/httemplate/search/cust_bill.html b/httemplate/search/cust_bill.html new file mode 100755 index 000000000..1e9ee8dcb --- /dev/null +++ b/httemplate/search/cust_bill.html @@ -0,0 +1,250 @@ +<% include( 'elements/search.html', + 'title' => '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' => [ 'Invoice #', + 'Balance', + 'Net Amount', + 'Gross Amount', + '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, + ], + '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(), + ], + + + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List invoices'); + +my $join_cust_main = 'LEFT JOIN cust_main USING ( custnum )'; +#here is the agent virtualization +my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql; + +my( $count_query, $sql_query ); +my $count_addl = ''; +#my $distinct = ''; +my %search; + +if ( $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/ ) { + + my $invnum_or_invid = "( invnum = $2 OR agent_invid = $2 )"; + my $where = "WHERE $invnum_or_invid AND $agentnums_sql"; + + $count_query = "SELECT COUNT(*) FROM cust_bill $join_cust_main $where"; + + $sql_query = { + #'select' => '*', + 'table' => 'cust_bill', + 'addl_from' => $join_cust_main, + 'hashref' => {}, + 'extra_sql' => $where, + }; + +} else { + + #some false laziness w/cust_bill::re_X + my $orderby = 'ORDER BY cust_bill._date'; + + if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + $search{'agentnum'} = $1; + } + + # begin/end/beginning/ending + my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, ''); + $search{'_date'} = [ $beginning, $ending ] + unless $beginning == 0 && $ending == 4294967295; + + if ( $cgi->param('invnum_min') =~ /^\s*(\d+)\s*$/ ) { + $search{'invnum_min'} = $1; + } + if ( $cgi->param('invnum_max') =~ /^\s*(\d+)\s*$/ ) { + $search{'invnum_max'} = $1; + } + + #amounts + $search{$_} = [ FS::UI::Web::parse_lt_gt($cgi, $_) ] + foreach qw( charged owed ); + + $search{'open'} = 1 if $cgi->param('open'); + $search{'net'} = 1 if $cgi->param('net' ); + + my($query) = $cgi->keywords; + if ( $query =~ /^(OPEN(\d*)_)?(invnum|date|custnum)$/ ) { + $search{'open'} = 1 if $1; + ($search{'days'}, my $field) = ($2, $3); + $field = "_date" if $field eq 'date'; + $orderby = "ORDER BY cust_bill.$field"; + } + + if ( $cgi->param('newest_percust') ) { + $search{'newest_percust'} = 1; + $count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'"; + } + + my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where( \%search ); + + unless ( $count_query ) { + $count_query = 'SELECT COUNT(*), '. join(', ', + map "SUM($_)", + ( 'charged', + FS::cust_bill->net_sql, + FS::cust_bill->owed_sql, + ) + ); + $count_addl = [ '$%.2f invoiced (gross)', + '$%.2f invoiced (net)', + '$%.2f outstanding balance', + ]; + } + $count_query .= " FROM cust_bill $join_cust_main $extra_sql"; + + $sql_query = { + 'table' => 'cust_bill', + 'addl_from' => $join_cust_main, + 'hashref' => {}, + #'select' => "$distinct ". join(', ', + 'select' => join(', ', + 'cust_bill.*', + #( map "cust_main.$_", qw(custnum last first company) ), + 'cust_main.custnum as cust_main_custnum', + FS::UI::Web::cust_sql_fields(), + FS::cust_bill->owed_sql. ' AS owed', + FS::cust_bill->net_sql. ' AS net', + ), + 'extra_sql' => $extra_sql, + 'order_by' => $orderby, + }; + +} + +my $link = [ "${p}view/cust_bill.cgi?", 'invnum', ]; +my $clink = sub { + my $cust_bill = shift; + $cust_bill->cust_main_custnum + ? [ "${p}view/cust_main.cgi?", 'custnum' ] + : ''; +}; + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my $html_init = join("\n", map { + ( my $action = $_ ) =~ s/_$//; + include('/elements/progress-init.html', + $_.'form', + [ keys %search ], + "../misc/${_}invoices.cgi", + { 'message' => "Invoices re-${action}ed" }, #would be nice to show the number of them, but... + $_, #key + ), + qq!<FORM NAME="${_}form">!, + ( map { my $f = $_; + my @values = ref($search{$f}) ? @{ $search{$f} } : $search{$f}; + map qq!<INPUT TYPE="hidden" NAME="$f" VALUE="$_">!, @values; + } + keys %search + ), + qq!</FORM>! +} qw( print_ email_ fax_ ftp_ spool_ ) ). + +'<SCRIPT TYPE="text/javascript"> + +function confirm_print_process() { + if ( ! confirm("Are you sure you want to reprint these invoices?") ) { + return; + } + print_process(); +} +function confirm_email_process() { + if ( ! confirm("Are you sure you want to re-email these invoices?") ) { + return; + } + email_process(); +} +function confirm_fax_process() { + if ( ! confirm("Are you sure you want to re-fax these invoices?") ) { + return; + } + fax_process(); +} +function confirm_ftp_process() { + if ( ! confirm("Are you sure you want to re-FTP these invoices?") ) { + return; + } + ftp_process(); +} +function confirm_spool_process() { + if ( ! confirm("Are you sure you want to re-spool these invoices?") ) { + return; + } + spool_process(); +} + +</SCRIPT>'; + +my $menubar = []; + +if ( $FS::CurrentUser::CurrentUser->access_right('Resend invoices') ) { + + push @$menubar, 'Print these invoices' => + "javascript:confirm_print_process()", + 'Email these invoices' => + "javascript:confirm_email_process()"; + + push @$menubar, 'Fax these invoices' => + "javascript:confirm_fax_process()" + if $conf->exists('hylafax'); + + push @$menubar, 'FTP these invoices' => + "javascript:confirm_ftp_process()" + if $conf->exists('cust_bill-ftpformat'); + + push @$menubar, 'Spool these invoices' => + "javascript:confirm_spool_process()" + if $conf->exists('cust_bill-spoolformat'); + +} + +</%init> diff --git a/httemplate/search/cust_bill_event.cgi b/httemplate/search/cust_bill_event.cgi new file mode 100644 index 000000000..16c9acdc7 --- /dev/null +++ b/httemplate/search/cust_bill_event.cgi @@ -0,0 +1,166 @@ +<% include( 'elements/search.html', + 'title' => $title, + 'html_init' => $html_init, + 'menubar' => $menubar, + 'name' => 'billing events', + 'query' => $sql_query, + 'count_query' => $count_sql, + 'header' => [ 'Event', + 'Date', + 'Status', + #'Inv #', 'Inv Date', 'Cust #', + 'Invoice', + FS::UI::Web::cust_header(), + ], + 'fields' => [ + 'event', + sub { time2str("%b %d %Y %T", $_[0]->_date) }, + sub { + #my $cust_bill_event = shift; + my $status = $_[0]->status; + $status .= ': '.$_[0]->statustext + if $_[0]->statustext; + $status; + }, + sub { + #my $cust_bill_event = shift; + 'Invoice #'. $_[0]->invnum. + ' ('. + time2str("%D", $_[0]->cust_bill_date). + ')'; + }, + \&FS::UI::Web::cust_fields, + ], + 'align' => 'lrlr'.FS::UI::Web::cust_aligns(), + 'links' => [ + '', + '', + '', + sub { + my $part_bill_event = shift; + my $template = $part_bill_event->templatename; + $template .= '-' if $template; + [ "${p}view/cust_bill.cgi?$template", 'invnum']; + }, + ( map { $_ ne 'Cust. Status' ? $link_cust : '' } + FS::UI::Web::cust_header() + ), + ], + 'color' => [ + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('Billing event reports') + or $curuser->access_right('View customer billing events') + && $cgi->param('invnum') =~ /^(\d+)$/; + +my $title = $cgi->param('failed') + ? 'Failed invoice events' + : 'Invoice events'; + +my %search = (); + +if ( $cgi->param('agentnum') && $cgi->param('agentnum') =~ /^(\d+)$/ ) { + $search{agentnum} = $1; +} + +($search{beginning}, $search{ending}) + = FS::UI::Web::parse_beginning_ending($cgi); + +if ( $cgi->param('failed') ) { + $search{failed} = '1'; +} + +if ( $cgi->param('part_bill_event.payby') =~ /^(\w+)$/ ) { + $search{payby} = $1; +} + +if ( $cgi->param('invnum') =~ /^(\d+)$/ ) { + $search{invnum} = $1; +} + +my $where = 'WHERE '. FS::cust_bill_event->search_sql_where( \%search ); + +my $join = 'LEFT JOIN part_bill_event USING ( eventpart ) '. + 'LEFT JOIN cust_bill USING ( invnum ) '. + 'LEFT JOIN cust_main USING ( custnum ) '; + +my $sql_query = { + 'table' => 'cust_bill_event', + 'select' => join(', ', + 'cust_bill_event.*', + 'part_bill_event.event', + 'cust_bill.custnum', + 'cust_bill._date AS cust_bill_date', + 'cust_main.custnum AS cust_main_custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'hashref' => {}, + 'extra_sql' => "$where ORDER BY _date ASC", + 'addl_from' => $join, +}; + +my $count_sql = "SELECT COUNT(*) FROM cust_bill_event $join $where"; + +my $conf = new FS::Conf; + +my $html_init = ' + <FONT SIZE="+1">Invoice events are the deprecated, old-style actions taken o +n open invoices. See Reports->Billing events->Billing events for current event reports.</FONT><BR><BR>'; + +$html_init .= join("\n", map { + ( my $action = $_ ) =~ s/_$//; + include('/elements/progress-init.html', + $_.'form', + [ keys(%search) ], + "../misc/${_}invoice_events.cgi", + { 'message' => "Invoices re-${action}ed" }, #would be nice to show the number of them, but... + $_, #key + ), + qq!<FORM NAME="${_}form">!, + qq!<INPUT TYPE="hidden" NAME="action" VALUE="$_">!, #not used though + (map {qq!<INPUT TYPE="hidden" NAME="$_" VALUE="$search{$_}">!} keys(%search)), + qq!</FORM>! +} qw( print_ email_ fax_ ) ); + +my $menubar = []; + +if ( $curuser->access_right('Resend invoices') ) { + + push @$menubar, 'Re-print these events' => + "javascript:print_process()", + 'Re-email these events' => + "javascript:email_process()", + ; + + push @$menubar, 'Re-fax these events' => + "javascript:fax_process()" + if $conf->exists('hylafax'); + +} + +my $link_cust = sub { + my $cust_bill_event = shift; + $cust_bill_event->cust_main_custnum + ? [ "${p}view/cust_main.cgi?", 'custnum' ] + : ''; +}; + +</%init> diff --git a/httemplate/search/cust_bill_event.html b/httemplate/search/cust_bill_event.html new file mode 100755 index 000000000..0f84a5581 --- /dev/null +++ b/httemplate/search/cust_bill_event.html @@ -0,0 +1,67 @@ +<% include( + '/elements/header.html', + ( $cgi->param('failed') ? 'Failed invoice events' : 'Invoice events' ), + ) +%> + + <FONT SIZE="+1">Invoice events are the deprecated, old-style actions taken + on open invoices. See Reports->Billing events->Billing events for current event reports.</FONT><BR><BR> + + <FORM ACTION="cust_bill_event.cgi" METHOD="GET"> + <INPUT TYPE="hidden" NAME="failed" VALUE="<% $cgi->param('failed') ? 1 : 0 %>"> + <TABLE> + + <% include( '/elements/tr-select-agent.html', 'disable_empty'=>0 ) %> + + <!--<TR> + <TD ALIGN="right">Customer type</TD> + <TD><SELECT MULTIPLE NAME="perhaps_payby"> + <OPTION SELECTED VALUE="CARD">Credit card (automatic) + <OPTION SELECTED VALUE="CHEK">E-check (automatic) + <OPTION SELECTED VALUE="LECB">Phone bill billing + <OPTION SELECTED VALUE="BILL">Billing + <OPTION SELECTED VALUE="DCRD">Credit card (on-demand) + <OPTION SELECTED VALUE="DCHK">E-check (on-demand) + </TD> + </TR> + --> + <% include( '/elements/tr-input-beginning_ending.html' ) %> + <!-- + <TR> + <TD ALIGN="right">Events: </TD> + <TD> + <SELECT NAME="eventpart"> + <OPTION SELECTED VALUE=""><% $cgi->param('failed') ? '(all failed events)' : '(all events)' %> +% #foreach my $part_bill_event ( qsearch( 'part_bill_event', {} ) ) { +% #} + + </SELECT> + </TD> + </TR> + --> + <TR> + <TD ALIGN="right">Events for payment type: </TD> + <TD> + <SELECT NAME="part_bill_event.payby"> + <OPTION SELECTED VALUE="">(all) + <OPTION VALUE="CARD">Credit card (automatic) + <OPTION VALUE="BILL">Billing + <OPTION VALUE="CHEK">Electronic check (automatic) + <OPTION VALUE="DCRD">Credit card (on-demand) + <OPTION VALUE="DCHK">Electronic check (on-demand) + <OPTION VALUE="LECB">Phone bill billing + <OPTION VALUE="COMP">Complimentary + </SELECT> + </TD> + </TR> + </TABLE> + <BR><INPUT TYPE="submit" VALUE="Get Report"> + </FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Billing event reports'); + +</%init> diff --git a/httemplate/search/cust_bill_pay.html b/httemplate/search/cust_bill_pay.html new file mode 100644 index 000000000..4272d8669 --- /dev/null +++ b/httemplate/search/cust_bill_pay.html @@ -0,0 +1,141 @@ +<% include( 'elements/search.html', + 'title' => $title, + 'name' => 'net payments', + 'query' => $sql_query, + 'count_query' => $count_query, + 'count_addl' => [ '$%.2f total paid (net)', ], + 'header' => [ 'Net applied', + 'Invoice', + 'Invoice amount', + 'Invoice date', + 'Payment', + 'Payment amount', + 'Payment date', + 'By', + FS::UI::Web::cust_header(), + ], + 'fields' => [ + sub { $money_char.sprintf('%.2f', shift->amount ) }, + 'invnum', + sub { $money_char.sprintf('%.2f', shift->cust_bill_charged)}, + sub { time2str('%b %d %Y', shift->cust_bill_date ) }, + sub { shift->cust_pay->payby_payinfo_pretty }, + sub { $money_char.sprintf('%.2f', shift->cust_pay_paid)}, + sub { time2str('%b %d %Y', shift->cust_pay_date ) }, + sub { shift->cust_pay_otaker }, + \&FS::UI::Web::cust_fields, + ], + 'align' => 'rrrrlrrl'.FS::UI::Web::cust_aligns(), + 'links' => [ + '', + $cust_bill_link, + $cust_bill_link, + $cust_bill_link, + $cust_pay_link, + $cust_pay_link, + $cust_pay_link, + '', + ( map { $_ ne 'Cust. Status' ? $cust_link : '' } + FS::UI::Web::cust_header() + ), + ], + 'color' => [ + '', + '', + '', + '', + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my $title = 'Net Payment Search Results'; + +my @search = (); + +if ( $cgi->param('agentnum') && $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @search, "agentnum = $1"; + my $agent = qsearchs('agent', { 'agentnum' => $1 } ); + die "unknown agentnum $1" unless $agent; + $title = $agent->agent. " $title"; +} + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +push @search, "cust_bill._date >= $beginning ", + "cust_bill._date <= $ending"; + +#here is the agent virtualization +push @search, $FS::CurrentUser::CurrentUser->agentnums_sql; + +my $where = 'WHERE '. join(' AND ', @search); +# +my $count_query = 'SELECT COUNT(*), SUM(amount) + FROM cust_bill_pay + LEFT JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_main USING ( custnum ) '. + $where; + +my $sql_query = { + 'table' => 'cust_bill_pay', + 'select' => join(', ', + 'cust_bill_pay.*', + 'cust_bill._date AS cust_bill_date', + 'cust_bill.charged AS cust_bill_charged', + 'cust_pay.paid AS cust_pay_paid', + 'cust_pay._date AS cust_pay_date', + 'cust_pay.otaker AS cust_pay_otaker', + 'cust_pay.custnum AS custnum', + 'cust_main.custnum AS cust_main_custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'hashref' => {}, + 'extra_sql' => $where, + 'addl_from' => 'LEFT JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_pay USING ( paynum ) + LEFT JOIN cust_main ON ( cust_bill.custnum = cust_main.custnum )', +}; + +my $cust_bill_link = sub { + my $cust_bill_pay = shift; + $cust_bill_pay->invnum + ? [ "${p}view/cust_bill.cgi?", 'invnum' ] + : ''; +}; + +my $cust_pay_link = sub { + my $cust_bill_pay = shift; + $cust_bill_pay->paynum + ? [ "${p}view/cust_pay.html?paynum=", 'paynum' ] + : ''; +}; + +my $cust_link = sub { + my $cust_credit_bill = shift; + $cust_credit_bill->cust_main_custnum + ? [ "${p}view/cust_main.cgi?", 'cust_main_custnum' ] + : ''; +}; + +</%init> diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi new file mode 100644 index 000000000..77901de87 --- /dev/null +++ b/httemplate/search/cust_bill_pkg.cgi @@ -0,0 +1,595 @@ +<% include( 'elements/search.html', + 'title' => 'Line items', + 'name' => 'line items', + 'query' => $query, + 'count_query' => $count_query, + 'count_addl' => [ $money_char. '%.2f total', + $unearned ? ( $money_char. '%.2f unearned revenue' ) : (), + ], + 'header' => [ + #'#', + 'Description', + ( $unearned + ? ( 'Unearned', 'Owed', 'Payment date' ) + : ( 'Setup charge' ) + ), + ( $use_usage eq 'usage' + ? 'Usage charge' + : 'Recurring charge' + ), + ( $unearned + ? ( 'Charge start', 'Charge end' ) + : () + ), + 'Invoice', + 'Date', + FS::UI::Web::cust_header(), + ], + 'fields' => [ + #'billpkgnum', + sub { $_[0]->pkgnum > 0 + ? $_[0]->get('pkg') # possibly use override.pkg + : $_[0]->get('itemdesc') # but i think this correct + }, + #strikethrough or "N/A ($amount)" or something these when + # they're not applicable to pkg_tax search + sub { my $cust_bill_pkg = shift; + if ( $unearned ) { + my $period = + $cust_bill_pkg->edate - $cust_bill_pkg->sdate; + my $elapsed = $unearned - $cust_bill_pkg->sdate; + $elapsed = 0 if $elapsed < 0; + + my $remaining = 1 - $elapsed/$period; + + sprintf($money_char. '%.2f', + $remaining * $cust_bill_pkg->recur ); + + } else { + sprintf($money_char.'%.2f', $cust_bill_pkg->setup ); + } + }, + ( $unearned + ? ( $owed_sub, $payment_date_sub, ) + : () + ), + sub { my $row = shift; + my $value = 0; + if ( $use_usage eq 'recurring' ) { + $value = $row->recur - $row->usage; + } elsif ( $use_usage eq 'usage' ) { + $value = $row->usage; + } else { + $value = $row->recur; + } + sprintf($money_char.'%.2f', $value ); + }, + ( $unearned + ? ( sub { time2str('%b %d %Y', shift->sdate ) }, + sub { time2str('%b %d %Y', shift->edate ) }, + ) + : () + ), + 'invnum', + sub { time2str('%b %d %Y', shift->_date ) }, + \&FS::UI::Web::cust_fields, + ], + 'links' => [ + #'', + '', + '', + ( $unearned ? ( '', '' ) : () ), + '', + ( $unearned ? ( '', '' ) : () ), + $ilink, + $ilink, + ( map { $_ ne 'Cust. Status' ? $clink : '' } + FS::UI::Web::cust_header() + ), + ], + #'align' => 'rlrrrc'.FS::UI::Web::cust_aligns(), + 'align' => 'lr'. + ( $unearned ? 'rc' : '' ). + 'r'. + ( $unearned ? 'cc' : '' ). + 'rc'. + FS::UI::Web::cust_aligns(), + 'color' => [ + #'', + '', + '', + ( $unearned ? ( '', '' ) : () ), + '', + ( $unearned ? ( '', '' ) : () ), + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + #'', + '', + '', + ( $unearned ? ( '', '' ) : () ), + '', + ( $unearned ? ( '', '' ) : () ), + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +#LOTS of false laziness below w/cust_credit_bill_pkg.cgi + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; + +my $unearned = ''; + +#here is the agent virtualization +my $agentnums_sql = + $FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' ); + +my @where = ( $agentnums_sql ); + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +push @where, "_date >= $beginning", + "_date <= $ending"; + +push @where , " payby != 'COMP' " + unless $cgi->param('include_comp_cust'); + +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @where, "cust_main.agentnum = $1"; +} + +#classnum +# not specified: all classes +# 0: empty class +# N: classnum +my $use_override = $cgi->param('use_override'); +if ( $cgi->param('classnum') =~ /^(\d+)$/ ) { + my $comparison = ''; + if ( $1 == 0 ) { + $comparison = "IS NULL"; + } else { + $comparison = "= $1"; + } + + if ( $use_override ) { + push @where, "( + part_pkg.classnum $comparison AND pkgpart_override IS NULL OR + override.classnum $comparison AND pkgpart_override IS NOT NULL + )"; + } else { + push @where, "part_pkg.classnum $comparison"; + } +} + +if ( $cgi->param('taxclass') + && ! $cgi->param('istax') #no part_pkg.taxclass in this case + #(should we save a taxclass or a link to taxnum + # in cust_bill_pkg or something like + # cust_bill_pkg_tax_location?) + ) +{ + + #override taxclass when use_override is specified? probably + #if ( $use_override ) { + # + # push @where, + # ' ( '. join(' OR ', + # map { + # ' ( part_pkg.taxclass = '. dbh->quote($_). + # ' AND pkgpart_override IS NULL '. + # ' OR '. + # ' override.taxclass = '. dbh->quote($_). + # ' AND pkgpart_override IS NOT NULL '. + # ' ) ' + # } + # $cgi->param('taxclass') + # ). + # ' ) '; + # + #} else { + + push @where, + ' ( '. join(' OR ', + map ' part_pkg.taxclass = '.dbh->quote($_), + $cgi->param('taxclass') + ). + ' ) '; + + #} + +} + +my @loc_param = qw( city county state country ); + +if ( $cgi->param('out') ) { + + my ( $loc_sql, @param ) = FS::cust_pkg->location_sql( 'ornull' => 1 ); + while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution + $loc_sql =~ s/\?/'cust_main_county.'.shift(@param)/e; + } + + $loc_sql =~ s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g + if $cgi->param('istax'); + + push @where, " + 0 = ( + SELECT COUNT(*) FROM cust_main_county + WHERE cust_main_county.tax > 0 + AND $loc_sql + ) + "; + + #not linked to by anything, but useful for debugging "out of taxable region" + if ( grep $cgi->param($_), @loc_param ) { + + my %ph = map { $_ => dbh->quote( scalar($cgi->param($_)) ) } @loc_param; + + my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; + while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution + $loc_sql =~ s/\?/$ph{shift(@param)}/e; + } + + push @where, $loc_sql; + + } + +} elsif ( $cgi->param('country') ) { + + my @counties = $cgi->param('county'); + + if ( scalar(@counties) > 1 ) { + + #hacky, could be more efficient. care if it is ever used for more than the + # tax-report_groups filtering kludge + + my $locs_sql = + ' ( '. join(' OR ', map { + + my %ph = ( 'county' => dbh->quote($_), + map { $_ => dbh->quote( $cgi->param($_) ) } + qw( city state country ) + ); + + my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; + while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution + $loc_sql =~ s/\?/$ph{shift(@param)}/e; + } + + $loc_sql; + + } @counties + + ). ' ) '; + + push @where, $locs_sql; + + } else { + + my %ph = map { $_ => dbh->quote( scalar($cgi->param($_)) ) } @loc_param; + + my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; + while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution + $loc_sql =~ s/\?/$ph{shift(@param)}/e; + } + + push @where, $loc_sql; + + } + + if ( $cgi->param('istax') ) { + if ( $cgi->param('taxname') ) { + push @where, 'itemdesc = '. dbh->quote( $cgi->param('taxname') ); + #} elsif ( $cgi->param('taxnameNULL') { + } else { + push @where, "( itemdesc IS NULL OR itemdesc = '' OR itemdesc = 'Tax' )"; + } + } elsif ( $cgi->param('nottax') ) { + #what can we usefully do with "taxname" ???? look up a class??? + } else { + #warn "neither nottax nor istax parameters specified"; + } + + if ( $cgi->param('taxclassNULL') ) { + + my %hash = ( 'country' => scalar($cgi->param('country')) ); + foreach (qw( state county )) { + $hash{$_} = scalar($cgi->param($_)) if $cgi->param($_); + } + my $cust_main_county = qsearchs('cust_main_county', \%hash); + die "unknown base region for empty taxclass" unless $cust_main_county; + + my $same_sql = $cust_main_county->sql_taxclass_sameregion; + push @where, $same_sql if $same_sql; + + } + +} elsif ( scalar( grep( /locationtaxid/, $cgi->param ) ) ) { + + # this should really be shoved out to FS::cust_pkg->location_sql or something + # along with the code in report_newtax.cgi + + my %pn = ( + 'county' => 'tax_rate_location.county', + 'state' => 'tax_rate_location.state', + 'city' => 'tax_rate_location.city', + 'locationtaxid' => 'cust_bill_pkg_tax_rate_location.locationtaxid', + ); + + my %ph = map { ( $pn{$_} => dbh->quote( $cgi->param($_) || '' ) ) } + qw( city county state locationtaxid ); + + push @where, + join( ' AND ', map { "( $_ = $ph{$_} OR $ph{$_} = '' AND $_ IS NULL)" } + keys %ph + ); + +} elsif ( $cgi->param('unearned_now') =~ /^(\d+)$/ ) { + + $unearned = $1; + + push @where, "cust_bill_pkg.sdate < $unearned", + "cust_bill_pkg.edate > $unearned", + "cust_bill_pkg.recur != 0", + "part_pkg.freq != '0'", + "part_pkg.freq != '1'", + "part_pkg.freq NOT LIKE '%h'", + "part_pkg.freq NOT LIKE '%d'", + "part_pkg.freq NOT LIKE '%w'"; + +} + +if ( $cgi->param('itemdesc') ) { + if ( $cgi->param('itemdesc') eq 'Tax' ) { + push @where, "(itemdesc='Tax' OR itemdesc is null)"; + } else { + push @where, 'itemdesc='. dbh->quote($cgi->param('itemdesc')); + } +} + +if ( $cgi->param('report_group') =~ /^(=|!=) (.*)$/ && $cgi->param('istax') ) { + my ( $group_op, $group_value ) = ( $1, $2 ); + if ( $group_op eq '=' ) { + #push @where, 'itemdesc LIKE '. dbh->quote($group_value.'%'); + push @where, 'itemdesc = '. dbh->quote($group_value); + } elsif ( $group_op eq '!=' ) { + push @where, '( itemdesc != '. dbh->quote($group_value) .' OR itemdesc IS NULL )'; + } else { + die "guru meditation #00de: group_op $group_op\n"; + } + +} + +push @where, 'cust_bill_pkg.pkgnum != 0' if $cgi->param('nottax'); +push @where, 'cust_bill_pkg.pkgnum = 0' if $cgi->param('istax'); + +if ( $cgi->param('cust_tax') ) { + #false laziness -ish w/report_tax.cgi + my $cust_exempt; + if ( $cgi->param('taxname') ) { + my $q_taxname = dbh->quote($cgi->param('taxname')); + $cust_exempt = + "( tax = 'Y' + OR EXISTS ( SELECT 1 FROM cust_main_exemption + WHERE cust_main_exemption.custnum = cust_main.custnum + AND cust_main_exemption.taxname = $q_taxname ) + ) + "; + } else { + $cust_exempt = " tax = 'Y' "; + } + + push @where, $cust_exempt; +} + +my $use_usage = $cgi->param('use_usage'); + +my $count_query; +if ( $cgi->param('pkg_tax') ) { + + $count_query = + "SELECT COUNT(*), + SUM( + ( CASE WHEN part_pkg.setuptax = 'Y' + THEN cust_bill_pkg.setup + ELSE 0 + END + ) + + + ( CASE WHEN part_pkg.recurtax = 'Y' + THEN cust_bill_pkg.recur + ELSE 0 + END + ) + ) + "; + + push @where, "( ( part_pkg.setuptax = 'Y' AND cust_bill_pkg.setup > 0 ) + OR ( part_pkg.recurtax = 'Y' AND cust_bill_pkg.recur > 0 ) )", + "( tax != 'Y' OR tax IS NULL )"; + +} elsif ( $cgi->param('taxable') ) { + + my $setup_taxable = "( + CASE WHEN part_pkg.setuptax = 'Y' + THEN 0 + ELSE cust_bill_pkg.setup + END + )"; + + my $recur_taxable = "( + CASE WHEN part_pkg.recurtax = 'Y' + THEN 0 + ELSE cust_bill_pkg.recur + END + )"; + + my $exempt = "( + SELECT COALESCE( SUM(amount), 0 ) FROM cust_tax_exempt_pkg + WHERE cust_tax_exempt_pkg.billpkgnum = cust_bill_pkg.billpkgnum + )"; + + $count_query = + "SELECT COUNT(*), SUM( $setup_taxable + $recur_taxable - $exempt )"; + + push @where, + #not tax-exempt package (setup or recur) + "( + ( ( part_pkg.setuptax != 'Y' OR part_pkg.setuptax IS NULL ) + AND cust_bill_pkg.setup > 0 ) + OR + ( ( part_pkg.recurtax != 'Y' OR part_pkg.recurtax IS NULL ) + AND cust_bill_pkg.recur > 0 ) + )", + #not a tax_exempt customer + "( tax != 'Y' OR tax IS NULL )"; + #not covered in full by a monthly tax exemption (texas tax) + "0 < ( $setup_taxable + $recur_taxable - $exempt )", + +} else { + + $count_query = "SELECT COUNT(*), "; + + if ( $use_usage eq 'recurring' ) { + $count_query .= "SUM(setup + recur - usage)"; + } elsif ( $use_usage eq 'usage' ) { + $count_query .= "SUM(usage)"; + } elsif ( $unearned ) { + $count_query .= "SUM(cust_bill_pkg.recur)"; + } else { + $count_query .= "SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)"; + } + + if ( $unearned ) { + + #false laziness w/report_prepaid_income.cgi + + my $float = 'REAL'; #'DOUBLE PRECISION'; + + my $period = "CAST(cust_bill_pkg.edate - cust_bill_pkg.sdate AS $float)"; + my $elapsed = "(CASE WHEN cust_bill_pkg.sdate > $unearned + THEN 0 + ELSE ($unearned - cust_bill_pkg.sdate) + END)"; + #my $elapsed = "CAST($unearned - cust_bill_pkg.sdate AS $float)"; + + my $remaining = "(1 - $elapsed/$period)"; + + $count_query .= ", SUM($remaining * cust_bill_pkg.recur)"; + + } + +} + +my $join_cust = ' JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_main USING ( custnum ) '; + + +my $join_pkg; +if ( $cgi->param('nottax') ) { + + $join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum ) + LEFT JOIN part_pkg USING ( pkgpart ) + LEFT JOIN part_pkg AS override + ON pkgpart_override = override.pkgpart '; + $join_pkg .= ' LEFT JOIN cust_location USING ( locationnum ) ' + if $conf->exists('tax-pkg_address'); + +} elsif ( $cgi->param('istax') ) { + + #false laziness w/report_tax.cgi $taxfromwhere + if ( $conf->exists('tax-pkg_address') ) { + $join_pkg .= ' LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum ) + LEFT JOIN cust_location USING ( locationnum ) '; + + #quelle kludge, somewhat false laziness w/report_tax.cgi + s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g for @where; + } elsif ( scalar( grep( /locationtaxid/, $cgi->param ) ) || + $cgi->param('iscredit') eq 'rate') { + $join_pkg .= + ' LEFT JOIN cust_bill_pkg_tax_rate_location USING ( billpkgnum ) '. + ' LEFT JOIN tax_rate_location USING ( taxratelocationnum ) '; + } + + if ( $cgi->param('iscredit') ) { + $join_pkg .= ' JOIN cust_credit_bill_pkg USING ( billpkgnum'; + if ( $conf->exists('tax-pkg_address') ) { + $join_pkg .= ', billpkgtaxlocationnum )'; + push @where, "billpkgtaxratelocationnum IS NULL"; + } elsif ( $cgi->param('iscredit') eq 'rate' ) { + $join_pkg .= ', billpkgtaxratelocationnum )'; + } else { + $join_pkg .= ' )'; + push @where, "billpkgtaxratelocationnum IS NULL"; + } + } + +} else { + + #die? + warn "neiether nottax nor istax parameters specified"; + #same as before? + $join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum ) + LEFT JOIN part_pkg USING ( pkgpart ) '; + +} + +my $where = ' WHERE '. join(' AND ', @where); + +if ($use_usage) { + $count_query .= + " FROM (SELECT cust_bill_pkg.setup, cust_bill_pkg.recur, + ( SELECT COALESCE( SUM(amount), 0 ) FROM cust_bill_pkg_detail + WHERE cust_bill_pkg.billpkgnum = cust_bill_pkg_detail.billpkgnum + ) AS usage FROM cust_bill_pkg $join_cust $join_pkg $where + ) AS countquery"; +} else { + $count_query .= " FROM cust_bill_pkg $join_cust $join_pkg $where"; +} + +my @select = ( 'cust_bill_pkg.*', + 'cust_bill._date', ); + +push @select, 'part_pkg.pkg', + 'part_pkg.freq', + unless $cgi->param('istax'); + +push @select, 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(); + +my $query = { + 'table' => 'cust_bill_pkg', + 'addl_from' => "$join_cust $join_pkg", + 'hashref' => {}, + 'select' => join(', ', @select ), + 'extra_sql' => $where, + 'order_by' => 'ORDER BY _date, billpkgnum', +}; + +my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ]; +my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ]; + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my $owed_sub = sub { + $money_char. shift->owed_recur; #_recur :/ +}; + +my $payment_date_sub = sub { + #my $cust_bill_pkg = shift; + my @cust_pay = sort { $a->_date <=> $b->_date } + map $_->cust_bill_pay->cust_pay, + shift->cust_bill_pay_pkg('recur') #recur :/ + or return ''; + time2str('%b %d %Y', $cust_pay[-1]->_date ); +}; + +</%init> diff --git a/httemplate/search/cust_bill_pkg_discount.html b/httemplate/search/cust_bill_pkg_discount.html new file mode 100644 index 000000000..088b29115 --- /dev/null +++ b/httemplate/search/cust_bill_pkg_discount.html @@ -0,0 +1,151 @@ +<% include( 'elements/search.html', + 'title' => 'Discounts', + 'name' => 'discounts', + 'query' => $query, + 'count_query' => $count_query, + 'count_addl' => [ $money_char. '%.2f total', ], + 'header' => [ + #'#', + 'Discount', + 'Amount', + 'Months', + 'Package', + 'Invoice', + 'Date', + FS::UI::Web::cust_header(), + ], + 'fields' => [ + #'billpkgdiscountnum', + sub { $_[0]->cust_pkg_discount->discount->description }, + sub { sprintf($money_char.'%.2f', shift->amount ) }, + sub { my $m = shift->months; + $m =~ /\./ ? sprintf('%.2f', $m) : $m; + }, + 'pkg',#sub { $_[0]->cust_bill_pkg->cust_pkg->part_pkg->pkg }, + 'invnum', + sub { time2str('%b %d %Y', shift->_date ) }, + \&FS::UI::Web::cust_fields, + ], + 'links' => [ + #'', + '', #link to customer discount??? + '', + '', + '', + $ilink, + $ilink, + ( map { $_ ne 'Cust. Status' ? $clink : '' } + FS::UI::Web::cust_header() + ), + ], + #'align' => 'rlrrrc'.FS::UI::Web::cust_aligns(), + 'align' => 'lrrlrr'.FS::UI::Web::cust_aligns(), + 'color' => [ + #'', + '', + '', + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + #'', + '', + '', + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +#a little false laziness below w/cust_bill_pkg.cgi + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; + +#here is the agent virtualization +my $agentnums_sql = + $FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' ); + +my @where = ( $agentnums_sql ); + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +push @where, "_date >= $beginning", + "_date <= $ending"; + +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @where, "cust_main.agentnum = $1"; +} + +# #classnum +# # not specified: all classes +# # 0: empty class +# # N: classnum +# my $use_override = $cgi->param('use_override'); +# if ( $cgi->param('classnum') =~ /^(\d+)$/ ) { +# my $comparison = ''; +# if ( $1 == 0 ) { +# $comparison = "IS NULL"; +# } else { +# $comparison = "= $1"; +# } +# +# if ( $use_override ) { +# push @where, "( +# part_pkg.classnum $comparison AND pkgpart_override IS NULL OR +# override.classnum $comparison AND pkgpart_override IS NOT NULL +# )"; +# } else { +# push @where, "part_pkg.classnum $comparison"; +# } +# } + +my $count_query = "SELECT COUNT(*), SUM(amount)"; + +my $join_cust = ' JOIN cust_bill_pkg USING ( billpkgnum ) + JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_main USING ( custnum ) '; + +my $join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum ) + LEFT JOIN part_pkg USING ( pkgpart ) '; + #LEFT JOIN part_pkg AS override + # ON pkgpart_override = override.pkgpart '; + +my $where = ' WHERE '. join(' AND ', @where); + +$count_query .= " FROM cust_bill_pkg_discount $join_cust $join_pkg $where"; + +my @select = ( + 'cust_bill_pkg_discount.*', + #'cust_bill_pkg.*', + 'cust_bill.invnum', + 'cust_bill._date', + ); +push @select, 'part_pkg.pkg'; +push @select, 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(); + +my $query = { + 'table' => 'cust_bill_pkg_discount', + 'addl_from' => "$join_cust $join_pkg", + 'hashref' => {}, + 'select' => join(', ', @select ), + 'extra_sql' => $where, + 'order_by' => 'ORDER BY _date, billpkgdiscountnum', +}; + +my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ]; +my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ]; + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +</%init> diff --git a/httemplate/search/cust_credit.html b/httemplate/search/cust_credit.html new file mode 100755 index 000000000..9a14dceca --- /dev/null +++ b/httemplate/search/cust_credit.html @@ -0,0 +1,104 @@ +<% include( 'elements/search.html', + 'title' => $title, + 'name' => 'credits', + 'query' => $sql_query, + 'count_query' => $count_query, + 'count_addl' => [ '$%.2f total credited (gross)', ], + #'redirect' => $link, + 'header' => [ 'Amount', + 'Date', + 'By', + 'Reason', + FS::UI::Web::cust_header(), + ], + 'fields' => [ + #'crednum', + sub { sprintf('$%.2f', shift->amount ) }, + sub { time2str('%b %d %Y', shift->_date ) }, + 'otaker', + 'reason', + \&FS::UI::Web::cust_fields, + ], + #'align' => 'rrrllll', + 'align' => 'rrll'.FS::UI::Web::cust_aligns(), + 'links' => [ + '', + '', + '', + '', + ( map { $_ ne 'Cust. Status' ? $clink : '' } + FS::UI::Web::cust_header() + ), + ], + 'color' => [ + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $title = 'Credit Search Results'; +#my( $count_query, $sql_query ); + +my @search = (); + +if ( $cgi->param('otaker') && $cgi->param('otaker') =~ /^([\w\.\-]+)$/ ) { + push @search, "cust_credit.otaker = '$1'"; +} + +if ( $cgi->param('agentnum') && $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @search, "agentnum = $1"; + my $agent = qsearchs('agent', { 'agentnum' => $1 } ); + die "unknown agentnum $1" unless $agent; + $title = $agent->agent. " $title"; +} + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +push @search, "_date >= $beginning ", + "_date <= $ending"; + +push @search, FS::UI::Web::parse_lt_gt($cgi, 'amount' ); + +#here is the agent virtualization +push @search, $FS::CurrentUser::CurrentUser->agentnums_sql; + +my $where = 'WHERE '. join(' AND ', @search); + +my $count_query = 'SELECT COUNT(*), SUM(amount) '. + 'FROM cust_credit LEFT JOIN cust_main USING ( custnum ) '. + $where; + +my $sql_query = { + 'table' => 'cust_credit', + 'select' => join(', ', + 'cust_credit.*', + 'cust_main.custnum as cust_main_custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'hashref' => {}, + 'extra_sql' => $where, + 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )', +}; + + my $clink = sub { + my $cust_bill = shift; + $cust_bill->cust_main_custnum + ? [ "${p}view/cust_main.cgi?", 'custnum' ] + : ''; + }; + +</%init> diff --git a/httemplate/search/cust_credit_bill.html b/httemplate/search/cust_credit_bill.html new file mode 100644 index 000000000..818e603a1 --- /dev/null +++ b/httemplate/search/cust_credit_bill.html @@ -0,0 +1,135 @@ +<% include( 'elements/search.html', + 'title' => $title, + 'name' => 'net credits', + 'query' => $sql_query, + 'count_query' => $count_query, + 'count_addl' => [ '$%.2f total credited (net)', ], + 'header' => [ 'Net applied', + 'to Invoice', + 'Credit', + 'By', + 'Reason', + FS::UI::Web::cust_header(), + ], + 'fields' => [ + sub { $money_char. sprintf('%.2f', shift->amount ) }, + sub { my $ccb = shift; + '#'.$ccb->invnum. ' '. + time2str('%b %d %Y', $ccb->cust_bill_date ). + " ($money_char". + sprintf('%.2f', $ccb->cust_bill_amount). + ")" + }, + sub { my $ccb = shift; + time2str('%b %d %Y', $ccb->_date ). + " ($money_char". + sprintf('%.2f', $ccb->cust_credit_amount ). + ")" + }, + sub { shift->cust_credit->otaker }, + sub { shift->cust_credit->reason }, + \&FS::UI::Web::cust_fields, + ], + 'align' => 'rrrll'.FS::UI::Web::cust_aligns(), + 'links' => [ + '', + $cust_bill_link, + '', + '', + '', + ( map { $_ ne 'Cust. Status' ? $cust_link : '' } + FS::UI::Web::cust_header() + ), + ], + 'color' => [ + '', + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my $title = 'Net Credit Search Results'; + +my @search = (); + +if ( $cgi->param('agentnum') && $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @search, "agentnum = $1"; + my $agent = qsearchs('agent', { 'agentnum' => $1 } ); + die "unknown agentnum $1" unless $agent; + $title = $agent->agent. " $title"; +} + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +push @search, "cust_bill._date >= $beginning ", + "cust_bill._date <= $ending"; + +#here is the agent virtualization +push @search, $FS::CurrentUser::CurrentUser->agentnums_sql; + +my $where = 'WHERE '. join(' AND ', @search); +# +my $count_query = 'SELECT COUNT(*), SUM(amount) + FROM cust_credit_bill + LEFT JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_main USING ( custnum ) '. + $where; + +my $sql_query = { + 'table' => 'cust_credit_bill', + 'select' => join(', ', + 'cust_credit_bill.*', + 'cust_credit.amount AS cust_credit_amount', + 'cust_bill._date AS cust_bill_date', + 'cust_bill.charged AS cust_bill_charged', + 'cust_credit.custnum AS custnum', + 'cust_main.custnum AS cust_main_custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'hashref' => {}, + 'extra_sql' => $where, + 'addl_from' => 'LEFT JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_credit USING ( crednum ) + LEFT JOIN cust_main ON ( cust_bill.custnum = cust_main.custnum )', +}; + +my $cust_bill_link = sub { + my $cust_credit_bill = shift; + $cust_credit_bill->invnum + ? [ "${p}view/cust_bill.cgi?", 'invnum' ] + : ''; +}; + +#my $cust_credit_link = sub { +# my $cust_credit_bill = shift; +# $cust_credit_bill->crednum +# ? [ "${p}view/cust_credit.cgi?", 'crednum' ] +# : ''; +#}; + +my $cust_link = sub { + my $cust_credit_bill = shift; + $cust_credit_bill->cust_main_custnum + ? [ "${p}view/cust_main.cgi?", 'cust_main_custnum' ] + : ''; +}; + +</%init> diff --git a/httemplate/search/cust_credit_bill_pkg.html b/httemplate/search/cust_credit_bill_pkg.html new file mode 100644 index 000000000..52e0ac6fe --- /dev/null +++ b/httemplate/search/cust_credit_bill_pkg.html @@ -0,0 +1,520 @@ +<% include( 'elements/search.html', + 'title' => 'Tax credits', #well, actually application of + 'name' => 'tax credits', # credit to line item + 'query' => $query, + 'count_query' => $count_query, + 'count_addl' => [ $money_char. '%.2f total', ], + 'header' => [ + #'#', + + 'Amount', + + #credit + 'Date', + 'By', + 'Reason', + + # line item + 'Description', + + #invoice + 'Invoice', + 'Date', + FS::UI::Web::cust_header(), + ], + 'fields' => [ + #'creditbillpkgnum', + sub { sprintf($money_char.'%.2f', shift->amount ) }, + + sub { time2str('%b %d %Y', shift->get('cust_credit_date') ) }, + 'otaker', + sub { shift->cust_credit_bill->cust_credit->reason }, + + sub { $_[0]->pkgnum > 0 + ? $_[0]->get('pkg') # possibly use override.pkg + : $_[0]->get('itemdesc') # but i think this correct + }, + 'invnum', + sub { time2str('%b %d %Y', shift->_date ) }, + \&FS::UI::Web::cust_fields, + ], + 'links' => [ + '', + '', + '', + '', + '', + $ilink, + $ilink, + ( map { $_ ne 'Cust. Status' ? $clink : '' } + FS::UI::Web::cust_header() + ), + ], + 'align' => 'rrlllrr'.FS::UI::Web::cust_aligns(), + 'color' => [ + '', + '', + '', + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +#LOTS of false laziness below w/cust_bill_pkg.cgi + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; + +#here is the agent virtualization +my $agentnums_sql = + $FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' ); + +my @where = ( $agentnums_sql ); + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +push @where, "cust_bill._date >= $beginning", + "cust_bill._date <= $ending"; + +push @where , " payby != 'COMP' " + unless $cgi->param('include_comp_cust'); + +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @where, "cust_main.agentnum = $1"; +} + +#classnum +# not specified: all classes +# 0: empty class +# N: classnum +my $use_override = $cgi->param('use_override'); +if ( $cgi->param('classnum') =~ /^(\d+)$/ ) { + my $comparison = ''; + if ( $1 == 0 ) { + $comparison = "IS NULL"; + } else { + $comparison = "= $1"; + } + + if ( $use_override ) { + push @where, "( + part_pkg.classnum $comparison AND pkgpart_override IS NULL OR + override.classnum $comparison AND pkgpart_override IS NOT NULL + )"; + } else { + push @where, "part_pkg.classnum $comparison"; + } +} + +if ( $cgi->param('taxclass') + && ! $cgi->param('istax') #no part_pkg.taxclass in this case + #(should we save a taxclass or a link to taxnum + # in cust_bill_pkg or something like + # cust_bill_pkg_tax_location?) + ) +{ + + #override taxclass when use_override is specified? probably + #if ( $use_override ) { + # + # push @where, + # ' ( '. join(' OR ', + # map { + # ' ( part_pkg.taxclass = '. dbh->quote($_). + # ' AND pkgpart_override IS NULL '. + # ' OR '. + # ' override.taxclass = '. dbh->quote($_). + # ' AND pkgpart_override IS NOT NULL '. + # ' ) ' + # } + # $cgi->param('taxclass') + # ). + # ' ) '; + # + #} else { + + push @where, + ' ( '. join(' OR ', + map ' part_pkg.taxclass = '.dbh->quote($_), + $cgi->param('taxclass') + ). + ' ) '; + + #} + +} + +my @loc_param = qw( city county state country ); + +if ( $cgi->param('out') ) { + + my ( $loc_sql, @param ) = FS::cust_pkg->location_sql( 'ornull' => 1 ); + while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution + $loc_sql =~ s/\?/'cust_main_county.'.shift(@param)/e; + } + + $loc_sql =~ s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g + if $cgi->param('istax'); + + push @where, " + 0 = ( + SELECT COUNT(*) FROM cust_main_county + WHERE cust_main_county.tax > 0 + AND $loc_sql + ) + "; + + #not linked to by anything, but useful for debugging "out of taxable region" + if ( grep $cgi->param($_), @loc_param ) { + + my %ph = map { $_ => dbh->quote( scalar($cgi->param($_)) ) } @loc_param; + + my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; + while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution + $loc_sql =~ s/\?/$ph{shift(@param)}/e; + } + + push @where, $loc_sql; + + } + +} elsif ( $cgi->param('country') ) { + + my @counties = $cgi->param('county'); + + if ( scalar(@counties) > 1 ) { + + #hacky, could be more efficient. care if it is ever used for more than the + # tax-report_groups filtering kludge + + my $locs_sql = + ' ( '. join(' OR ', map { + + my %ph = ( 'county' => dbh->quote($_), + map { $_ => dbh->quote( $cgi->param($_) ) } + qw( city state country ) + ); + + my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; + while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution + $loc_sql =~ s/\?/$ph{shift(@param)}/e; + } + + $loc_sql; + + } @counties + + ). ' ) '; + + push @where, $locs_sql; + + } else { + + my %ph = map { $_ => dbh->quote( scalar($cgi->param($_)) ) } @loc_param; + + my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; + while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution + $loc_sql =~ s/\?/$ph{shift(@param)}/e; + } + + push @where, $loc_sql; + + } + + my($title, $name); + if ( $cgi->param('istax') ) { + $title = 'Tax credits'; + $name = 'tax credits'; + if ( $cgi->param('taxname') ) { + push @where, 'itemdesc = '. dbh->quote( $cgi->param('taxname') ); + #} elsif ( $cgi->param('taxnameNULL') { + } else { + push @where, "( itemdesc IS NULL OR itemdesc = '' OR itemdesc = 'Tax' )"; + } + } elsif ( $cgi->param('nottax') ) { + $title = 'Credit applications to line items'; + $name = 'applications'; + #what can we usefully do with "taxname" ???? look up a class??? + } else { + $title = 'Credit applications to line items'; + $name = 'applications'; + #warn "neither nottax nor istax parameters specified"; + } + + if ( $cgi->param('taxclassNULL') ) { + + my %hash = ( 'country' => scalar($cgi->param('country')) ); + foreach (qw( state county )) { + $hash{$_} = scalar($cgi->param($_)) if $cgi->param($_); + } + my $cust_main_county = qsearchs('cust_main_county', \%hash); + die "unknown base region for empty taxclass" unless $cust_main_county; + + my $same_sql = $cust_main_county->sql_taxclass_sameregion; + push @where, $same_sql if $same_sql; + + } + +} elsif ( scalar( grep( /locationtaxid/, $cgi->param ) ) ) { + + # this should really be shoved out to FS::cust_pkg->location_sql or something + # along with the code in report_newtax.cgi + + my %pn = ( + 'county' => 'tax_rate_location.county', + 'state' => 'tax_rate_location.state', + 'city' => 'tax_rate_location.city', + 'locationtaxid' => 'cust_bill_pkg_tax_rate_location.locationtaxid', + ); + + my %ph = map { ( $pn{$_} => dbh->quote( $cgi->param($_) || '' ) ) } + qw( city county state locationtaxid ); + + push @where, + join( ' AND ', map { "( $_ = $ph{$_} OR $ph{$_} = '' AND $_ IS NULL)" } + keys %ph + ); + +} + +if ( $cgi->param('itemdesc') ) { + if ( $cgi->param('itemdesc') eq 'Tax' ) { + push @where, "(itemdesc='Tax' OR itemdesc is null)"; + } else { + push @where, 'itemdesc='. dbh->quote($cgi->param('itemdesc')); + } +} + +if ( $cgi->param('report_group') =~ /^(=|!=) (.*)$/ && $cgi->param('istax') ) { + my ( $group_op, $group_value ) = ( $1, $2 ); + if ( $group_op eq '=' ) { + #push @where, 'itemdesc LIKE '. dbh->quote($group_value.'%'); + push @where, 'itemdesc = '. dbh->quote($group_value); + } elsif ( $group_op eq '!=' ) { + push @where, '( itemdesc != '. dbh->quote($group_value) .' OR itemdesc IS NULL )'; + } else { + die "guru meditation #00de: group_op $group_op\n"; + } + +} + +push @where, 'cust_bill_pkg.pkgnum != 0' if $cgi->param('nottax'); +push @where, 'cust_bill_pkg.pkgnum = 0' if $cgi->param('istax'); + +if ( $cgi->param('cust_tax') ) { + #false laziness -ish w/report_tax.cgi + my $cust_exempt; + if ( $cgi->param('taxname') ) { + my $q_taxname = dbh->quote($cgi->param('taxname')); + $cust_exempt = + "( tax = 'Y' + OR EXISTS ( SELECT 1 FROM cust_main_exemption + WHERE cust_main_exemption.custnum = cust_main.custnum + AND cust_main_exemption.taxname = $q_taxname ) + ) + "; + } else { + $cust_exempt = " tax = 'Y' "; + } + + push @where, $cust_exempt; +} + +my $use_usage = $cgi->param('use_usage'); + +my $count_query; +if ( $cgi->param('pkg_tax') ) { #does this mean anything here? + + $count_query = + "SELECT COUNT(*), + SUM( + ( CASE WHEN part_pkg.setuptax = 'Y' + THEN cust_bill_pkg.setup + ELSE 0 + END + ) + + + ( CASE WHEN part_pkg.recurtax = 'Y' + THEN cust_bill_pkg.recur + ELSE 0 + END + ) + ) + "; + + push @where, "( ( part_pkg.setuptax = 'Y' AND cust_bill_pkg.setup > 0 ) + OR ( part_pkg.recurtax = 'Y' AND cust_bill_pkg.recur > 0 ) )", + "( tax != 'Y' OR tax IS NULL )"; + +} elsif ( $cgi->param('taxable') ) { #again, meaningful? + + my $setup_taxable = "( + CASE WHEN part_pkg.setuptax = 'Y' + THEN 0 + ELSE cust_bill_pkg.setup + END + )"; + + my $recur_taxable = "( + CASE WHEN part_pkg.recurtax = 'Y' + THEN 0 + ELSE cust_bill_pkg.recur + END + )"; + + my $exempt = "( + SELECT COALESCE( SUM(amount), 0 ) FROM cust_tax_exempt_pkg + WHERE cust_tax_exempt_pkg.billpkgnum = cust_bill_pkg.billpkgnum + )"; + + $count_query = + "SELECT COUNT(*), SUM( $setup_taxable + $recur_taxable - $exempt )"; + + push @where, + #not tax-exempt package (setup or recur) + "( + ( ( part_pkg.setuptax != 'Y' OR part_pkg.setuptax IS NULL ) + AND cust_bill_pkg.setup > 0 ) + OR + ( ( part_pkg.recurtax != 'Y' OR part_pkg.recurtax IS NULL ) + AND cust_bill_pkg.recur > 0 ) + )", + #not a tax_exempt customer + "( tax != 'Y' OR tax IS NULL )"; + #not covered in full by a monthly tax exemption (texas tax) + "0 < ( $setup_taxable + $recur_taxable - $exempt )", + +} else { + + $count_query = "SELECT COUNT(*), "; + + if ( $use_usage eq 'recurring' ) { #mean anything? + $count_query .= "SUM(setup + recur - usage)"; + } elsif ( $use_usage eq 'usage' ) { #mean anything? + $count_query .= "SUM(usage)"; + } else { + $count_query .= "SUM(cust_credit_bill_pkg.amount)"; + } + +} + +my $join_cust = + ' JOIN cust_bill ON ( cust_bill_pkg.invnum = cust_bill.invnum ) + LEFT JOIN cust_main ON ( cust_bill.custnum = cust_main.custnum ) '; + + +my $join_pkg; + +my $join_cust_bill_pkg = 'LEFT JOIN cust_bill_pkg USING ( billpkgnum '; + +if ( $cgi->param('nottax') ) { + + $join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum ) + LEFT JOIN part_pkg USING ( pkgpart ) + LEFT JOIN part_pkg AS override + ON pkgpart_override = override.pkgpart '; + $join_pkg .= ' LEFT JOIN cust_location USING ( locationnum ) ' + if $conf->exists('tax-pkg_address'); + +} elsif ( $cgi->param('istax') ) { + + #false laziness w/report_tax.cgi $taxfromwhere + if ( $conf->exists('tax-pkg_address') ) { + $join_pkg .= ' LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum ) + LEFT JOIN cust_location USING ( locationnum ) '; + + #quelle kludge, somewhat false laziness w/report_tax.cgi + s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g for @where; + } elsif ( scalar( grep( /locationtaxid/, $cgi->param ) ) || + $cgi->param('iscredit') eq 'rate') { + $join_pkg .= + ' LEFT JOIN cust_bill_pkg_tax_rate_location USING ( billpkgnum ) '. + ' LEFT JOIN tax_rate_location USING ( taxratelocationnum ) '; + } + + if ( $conf->exists('tax-pkg_address') ) { + $join_cust_bill_pkg .= ', billpkgtaxlocationnum )'; + push @where, "billpkgtaxratelocationnum IS NULL"; + #} elsif ( $cgi->param('iscredit') eq 'rate' ) { + # $join_pkg .= ', billpkgtaxratelocationnum )'; + } else { + $join_cust_bill_pkg .= ' )'; + push @where, "billpkgtaxratelocationnum IS NULL"; + } + +} else { + + #die? + warn "neiether nottax nor istax parameters specified"; + #same as before? + $join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum ) + LEFT JOIN part_pkg USING ( pkgpart ) '; + +} + +my $where = ' WHERE '. join(' AND ', @where); + +my $join_credit = ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) + LEFT JOIN cust_credit USING ( crednum ) '; + +#if ($use_usage) { +# $count_query .= +# " FROM (SELECT cust_bill_pkg.setup, cust_bill_pkg.recur, +# ( SELECT COALESCE( SUM(amount), 0 ) FROM cust_bill_pkg_detail +# WHERE cust_bill_pkg.billpkgnum = cust_bill_pkg_detail.billpkgnum +# ) AS usage FROM cust_bill_pkg $join_cust $join_pkg $where +# ) AS countquery"; +#} else { + $count_query .= " FROM cust_credit_bill_pkg + $join_pkg + $join_cust_bill_pkg + $join_credit + $join_cust + $where"; +#} + +my @select = ( 'cust_credit_bill_pkg.*', + 'cust_bill_pkg.*', + 'cust_credit.otaker', + 'cust_credit._date AS cust_credit_date', + 'cust_bill._date', + ); +push @select, 'part_pkg.pkg' unless $cgi->param('istax'); +push @select, 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(); + +my $query = { + 'table' => 'cust_credit_bill_pkg', + 'addl_from' => "$join_pkg + $join_cust_bill_pkg + $join_credit + $join_cust", + 'hashref' => {}, + 'select' => join(', ', @select ), + 'extra_sql' => $where, + 'order_by' => 'ORDER BY creditbillpkgnum', #cust_bill. or cust_credit._date? +}; + +my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ]; +my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ]; + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +</%init> diff --git a/httemplate/search/cust_credit_refund.html b/httemplate/search/cust_credit_refund.html new file mode 100644 index 000000000..d9abe2e00 --- /dev/null +++ b/httemplate/search/cust_credit_refund.html @@ -0,0 +1,130 @@ +<% include( 'elements/search.html', + 'title' => $title, + 'name' => 'net refunds', + 'query' => $sql_query, + 'count_query' => $count_query, + 'count_addl' => [ '$%.2f total refunded (net)', ], + 'header' => [ 'Net applied', + 'to Credit', + 'Refund', + 'By', + FS::UI::Web::cust_header(), + ], + 'fields' => [ + sub { $money_char. sprintf('%.2f', shift->amount ) }, + sub { my $ccr = shift; + '#'.$ccr->crednum. ' '. + time2str('%b %d %Y', $ccr->cust_credit_date ). + " ($money_char". + sprintf('%.2f', $ccr->cust_credit_amount). + ")" + }, + sub { my $ccr = shift; + time2str('%b %d %Y', $ccr->_date ). + " ($money_char". + sprintf('%.2f', $ccr->cust_refund_refund ). + ")" + }, + sub { shift->cust_refund->otaker }, + \&FS::UI::Web::cust_fields, + ], + 'align' => 'rrrl'.FS::UI::Web::cust_aligns(), + 'links' => [ + '', + '', + '', + '', + ( map { $_ ne 'Cust. Status' ? $cust_link : '' } + FS::UI::Web::cust_header() + ), + ], + 'color' => [ + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my $title = 'Net Refund Search Results'; + +my @search = (); + +if ( $cgi->param('agentnum') && $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @search, "agentnum = $1"; + my $agent = qsearchs('agent', { 'agentnum' => $1 } ); + die "unknown agentnum $1" unless $agent; + $title = $agent->agent. " $title"; +} + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +push @search, "cust_credit._date >= $beginning ", + "cust_credit._date <= $ending"; + +#here is the agent virtualization +push @search, $FS::CurrentUser::CurrentUser->agentnums_sql; + +my $where = 'WHERE '. join(' AND ', @search); +# +my $count_query = 'SELECT COUNT(*), SUM(cust_credit_refund.amount) + FROM cust_credit_refund + LEFT JOIN cust_credit USING ( crednum ) + LEFT JOIN cust_main USING ( custnum ) '. + $where; + +my $sql_query = { + 'table' => 'cust_credit_refund', + 'select' => join(', ', + 'cust_credit_refund.*', + 'cust_refund.refund AS cust_refund_refund', + 'cust_credit._date AS cust_credit_date', + 'cust_credit.amount AS cust_credit_amnount', + 'cust_refund.custnum AS custnum', + 'cust_main.custnum AS cust_main_custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'hashref' => {}, + 'extra_sql' => $where, + 'addl_from' => 'LEFT JOIN cust_credit USING ( crednum ) + LEFT JOIN cust_refund USING ( refundnum ) + LEFT JOIN cust_main ON ( cust_credit.custnum = cust_main.custnum )', +}; + +#my $cust_credit_link = sub { +# my $cust_credit_refund = shift; +# $cust_credit_refund->crednum +# ? [ "${p}view/cust_credit.cgi?", 'credum' ] +# : ''; +#}; + +#my $cust_refund_link = sub { +# my $cust_credit_refund = shift; +# $cust_credit_refund->refundnum +# ? [ "${p}view/cust_refund.cgi?", 'refundnum' ] +# : ''; +#}; + +my $cust_link = sub { + my $cust_credit_refund = shift; + $cust_credit_refund->cust_main_custnum + ? [ "${p}view/cust_main.cgi?", 'cust_main_custnum' ] + : ''; +}; + +</%init> diff --git a/httemplate/search/cust_event.html b/httemplate/search/cust_event.html new file mode 100644 index 000000000..a0429e44f --- /dev/null +++ b/httemplate/search/cust_event.html @@ -0,0 +1,271 @@ +<% include( 'elements/search.html', + 'title' => $title, + 'html_init' => $html_init, + 'menubar' => $menubar, + 'name' => 'billing events', + 'query' => $sql_query, + 'count_query' => $count_sql, + 'header' => [ 'Event', + 'Date', + 'Status', + 'Trigger', + #'Inv #', 'Inv Date', 'Cust #', + #'Invoice', + + FS::UI::Web::cust_header(), #'cust_main_custnum', + ], + 'fields' => [ + 'event', + sub { time2str("%b %d %Y %T", $_[0]->_date) }, + $status_sub, + $trigger_sub, + #sub { + # #my $cust_event = shift; + # 'Invoice #'. $_[0]->invnum. + # ' ('. + # time2str("%D", $_[0]->cust_bill_date). + # ')'; + # }, + \&FS::UI::Web::cust_fields, + ], + 'align' => 'lrll'.FS::UI::Web::cust_aligns(), + 'links' => [ + '', + '', + '', + $trigger_link, + #sub { + # my $part_event = shift; + # #XXX + # my $template = $part_event->templatename; + # $template .= '-' if $template; + # [ "${p}view/cust_bill.cgi?$template", 'invnum']; + #}, + + ( map { $_ ne 'Cust. Status' ? $link_cust : '' } + FS::UI::Web::cust_header() + ), + ], + 'color' => [ + '', + '', + '', + '', + #'', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + #'', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%once> + +my $status_sub = sub { + my $cust_event = shift; + + my $status = $cust_event->status; + $status .= ': '.$cust_event->statustext + if $cust_event->statustext; + + my $part_event = $cust_event->part_event; + + if ( $part_event->eventtable eq 'cust_bill' + && ( $part_event->templatename || $part_event->option('notice_name') ) + ) + { + my $link = 'invnum='. $cust_event->tablenum; + $link .= ';template='. uri_escape($part_event->templatename) + if $part_event->templatename; + $link .= ';notice_name='. uri_escape($part_event->option('notice_name')) + if $part_event->option('notice_name'); + + my $conf = new FS::Conf; + my $cust_bill = $cust_event->cust_X; + + $status .= qq{ + ( <A HREF="${p}view/cust_bill.cgi?$link">view</A> + | <A HREF="${p}view/cust_bill-pdf.cgi?$link">view typeset</A> + | <A HREF="${p}misc/send-invoice.cgi?method=print;$link">re-print</A> + }; + + if ( grep { $_ ne 'POST' } $cust_bill->cust_main->invoicing_list ) { + $status .= qq{ + | <A HREF="${p}misc/send-invoice.cgi?method=email;$link">re-email</A> + }; + } + + if ( $conf->exists('hylafax') && length($cust_bill->cust_main->fax) ) { + $status .= qq{ + | <A HREF="${p}misc/send-invoice.cgi?method=fax;$link">re-fax</A> + } + } + + $status .= ' ) '; + + } + + $status; +}; + +my $trigger_sub = sub { + my $cust_event = shift; + my $eventtable = $cust_event->eventtable; + my $label = FS::part_event->eventtable_labels->{$eventtable}; + #if ( $eventtable eq 'cust_pkg' || $eventtable eq 'cust_bill' ) { + "$label #". $cust_event->tablenum; + #} else { + # $label; + #} +}; + +my $trigger_link = sub { + my $cust_event = shift; + my $eventtable = $cust_event->eventtable; + if ( $eventtable eq 'cust_pkg' ) { + my $custnum = $cust_event->cust_main_custnum; + my $show = $FS::CurrentUser::CurrentUser->default_customer_view =~ /^(jumbo|packages)$/ + ? '' + : ';show=packages'; + my $pkgnum = $cust_event->tablenum; + my $frag = "cust_pkg$pkgnum"; #hack for IE ignoring real #fragment + [ "${p}view/cust_main.cgi?custnum=$custnum$show;fragment=$frag#cust_pkg", 'tablenum' ]; + } else { + [ "${p}view/$eventtable.cgi?", 'tablenum' ]; + } +}; + +</%once> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('Billing event reports') + or $curuser->access_right('View customer billing events') + && ( $cgi->param('custnum') =~ /^(\d+)$/ + || $cgi->param('invnum') =~ /^(\d+)$/ + || $cgi->param('pkgnum') =~ /^(\d+)$/ + ); + +my $title = $cgi->param('failed') ? 'Failed billing events' : 'Billing events'; + +my %search = (); + +my @scalars = qw( agentnum status custnum invnum pkgnum failed ); +for my $param (@scalars) { + $search{$param} = scalar( $cgi->param($param) ) + if $cgi->param($param); +} + +#lists +my @lists = qw( payby eventpart ); +foreach my $param (@lists) { + $search{$param} = [ $cgi->param($param) ]; +} + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +$search{'beginning'} = $beginning; +$search{'ending'} = $ending; + +my $where = ' WHERE '. FS::cust_event->search_sql_where( \%search ); + +my $join = FS::cust_event->join_sql(); + +my $sql_query = { + 'table' => 'cust_event', + 'select' => join(', ', + 'cust_event.*', + 'part_event.*', + #'cust_bill.custnum', + #'cust_bill._date AS cust_bill_date', + 'cust_main.custnum AS cust_main_custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'hashref' => {}, + 'extra_sql' => "$where ORDER BY _date ASC", + 'addl_from' => $join, +}; + +my $count_sql = "SELECT COUNT(*) FROM cust_event $join $where"; + +my $conf = new FS::Conf; + +my @params = ( @scalars, qw( beginning ending ) ); + +my $html_init = join("\n", map { + ( my $action = $_ ) =~ s/_$//; + include('/elements/progress-init.html', + $_.'form', + [ 'action', @params ], + "../misc/${_}events.cgi", + { 'message' => "Invoices re-${action}ed" }, #would be nice to show the number of them, but... + $_, #key + ), + qq!<FORM NAME="${_}form">!, + qq!<INPUT TYPE="hidden" NAME="action" VALUE="$_">!, #not used though + ( map { my $value = encode_entities( $search{$_} ); + qq(<INPUT TYPE="hidden" NAME="$_" VALUE="$value">); + } + @params #keys %search + ), + ( map { my $value = encode_entities( join(',', @{ $search{$_} } ) ); + qq(<INPUT TYPE="hidden" NAME="$_" VALUE="$value">); + } + @lists + ), + qq!</FORM>! +} qw( print_ email_ fax_ ) ). + +'<SCRIPT TYPE="text/javascript"> + +function confirm_print_process() { + if ( ! confirm("Are you sure you want to reprint these invoices?") ) { + return; + } + print_process(); +} +function confirm_email_process() { + if ( ! confirm("Are you sure you want to re-email these invoices?") ) { + return; + } + email_process(); +} +function confirm_fax_process() { + if ( ! confirm("Are you sure you want to re-fax these invoices?") ) { + return; + } + fax_process(); +} + +</SCRIPT>'; + +my $menubar = []; + +if ( $curuser->access_right('Resend invoices') ) { + + push @$menubar, 'Re-print these events' => + "javascript:confirm_print_process()", + 'Re-email these events' => + "javascript:confirm_email_process()", + ; + + push @$menubar, 'Re-fax these events' => + "javascript:confirm_fax_process()" + if $conf->exists('hylafax'); + +} + +my $link_cust = sub { + my $cust_event = shift; + $cust_event->cust_main_custnum + ? [ "${p}view/cust_main.cgi?", 'cust_main_custnum' ] + : ''; +}; + +</%init> diff --git a/httemplate/search/cust_main-otaker.cgi b/httemplate/search/cust_main-otaker.cgi new file mode 100755 index 000000000..0c252e44b --- /dev/null +++ b/httemplate/search/cust_main-otaker.cgi @@ -0,0 +1,31 @@ +<% include('/elements/header.html', 'Customer Search' ) %> + +<FORM ACTION="cust_main.cgi" METHOD="GET"> + +Search for <B>Order taker</B>: + <INPUT TYPE="hidden" NAME="otaker_on" VALUE="TRUE"> +% my $sth = dbh->prepare("SELECT DISTINCT otaker FROM cust_main") +% or die dbh->errstr; +% $sth->execute() or die $sth->errstr; +% #my @otakers = map { $_->[0] } @{$sth->fetchall_arrayref}; +% + +<SELECT NAME="otaker"> +% my $otaker; while ( $otaker = $sth->fetchrow_arrayref ) { + + <OPTION><% $otaker->[0] %> +% } + +</SELECT> + +<P><INPUT TYPE="submit" VALUE="Search"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +</%init> diff --git a/httemplate/search/cust_main-zip.html b/httemplate/search/cust_main-zip.html new file mode 100644 index 000000000..e87b21474 --- /dev/null +++ b/httemplate/search/cust_main-zip.html @@ -0,0 +1,110 @@ +<% include( 'elements/search.html', + 'title' => 'Zip code Search Results', + 'name' => 'zip codes', + 'query' => $sql_query, + 'count_query' => $count_sql, + 'header' => [ 'Zip code', 'Customers', ], + #'fields' => [ 'zip', 'num_cust', ], + 'links' => [ '', sub { 'somewhere'; } ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List zip codes'); + +# XXX link to customers + +my @where = (); + +# select status + +if ( $cgi->param('status') =~ /^(prospect|uncancel|active|susp|cancel)$/ ) { + my $method = $1.'_sql'; + push @where, FS::cust_main->$method(); +} + +# select agent +# XXX this needs to be virtualized by agent too (like lots of stuff) + +my $agentnum = ''; +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + $agentnum = $1; + push @where, "cust_main.agentnum = $agentnum"; +} + +# select svcdb + +if ( $cgi->param('svcdb') =~ /^(\w+)$/ ) { + my $svcdb = $1; + push @where, "EXISTS( SELECT 1 FROM $svcdb LEFT JOIN cust_svc USING ( svcnum ) + LEFT JOIN cust_pkg USING ( pkgnum ) + WHERE cust_pkg.custnum = cust_main.custnum + )"; +} + +my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where) : ''; + +# bill zip vs ship zip + +sub fieldorempty { + my $field = shift; + "CASE WHEN $field IS NULL THEN '' ELSE $field END"; +} + +sub strip_plus4 { + my $field = shift; + "CASE WHEN $field is NULL + THEN '' + ELSE CASE WHEN $field LIKE '_____-____' + THEN SUBSTRING($field FROM 1 FOR 5) + ELSE $field + END + END"; +} + +my( $zip, $czip); +if ( $cgi->param('column') eq 'ship_zip' ) { + + my $casewhen_noship = + "CASE WHEN ( ship_last IS NULL OR ship_last = '' ) THEN "; + + $czip = "$casewhen_noship zip ELSE ship_zip END"; + + if ( $cgi->param('ignore_plus4') ) { + $zip = $casewhen_noship. strip_plus4('zip'). + " ELSE ". strip_plus4('ship_zip'). ' END'; + + } else { + $zip = $casewhen_noship. fieldorempty('zip'). + " ELSE ". fieldorempty('ship_zip'). ' END'; + } + +} else { + + $czip = 'zip'; + + if ( $cgi->param('ignore_plus4') ) { + $zip = strip_plus4('zip'); + } else { + $zip = fieldorempty('zip'); + } + +} + +# construct the queries and send 'em off + +my $sql_query = + "SELECT $zip AS zipcode, + COUNT(*) AS num_cust + FROM cust_main + $where + GROUP BY zipcode + ORDER BY num_cust DESC + "; + +my $count_sql = "select count(distinct $czip) from cust_main $where"; + +# XXX should link... + +</%init> diff --git a/httemplate/search/cust_main.cgi b/httemplate/search/cust_main.cgi new file mode 100755 index 000000000..e65dc7117 --- /dev/null +++ b/httemplate/search/cust_main.cgi @@ -0,0 +1,741 @@ +%my $curuser = $FS::CurrentUser::CurrentUser; +% +%die "access denied" +% unless $curuser->access_right('List customers'); +% +%my $conf = new FS::Conf; +%my $maxrecords = $conf->config('maxsearchrecordsperpage'); +% +%#my $cache; +% +%#my $monsterjoin = <<END; +%#cust_main left outer join ( +%# ( cust_pkg left outer join part_pkg using(pkgpart) +%# ) left outer join ( +%# ( +%# ( +%# ( cust_svc left outer join part_svc using (svcpart) +%# ) left outer join svc_acct using (svcnum) +%# ) left outer join svc_domain using(svcnum) +%# ) left outer join svc_forward using(svcnum) +%# ) using (pkgnum) +%#) using (custnum) +%#END +% +%#my $monsterjoin = <<END; +%#cust_main left outer join ( +%# ( cust_pkg left outer join part_pkg using(pkgpart) +%# ) left outer join ( +%# ( +%# ( +%# ( cust_svc left outer join part_svc using (svcpart) +%# ) left outer join ( +%# svc_acct left outer join ( +%# select svcnum, domain, catchall from svc_domain +%# ) as svc_acct_domsvc ( +%# svc_acct_svcnum, svc_acct_domain, svc_acct_catchall +%# ) on svc_acct.domsvc = svc_acct_domsvc.svc_acct_svcnum +%# ) using (svcnum) +%# ) left outer join svc_domain using(svcnum) +%# ) left outer join svc_forward using(svcnum) +%# ) using (pkgnum) +%#) using (custnum) +%#END +% +%my $limit = ''; +%$limit .= "LIMIT $maxrecords" if $maxrecords; +% +%my $offset = $cgi->param('offset') || 0; +%$limit .= " OFFSET $offset" if $offset; +% +%my $total = 0; +% +%my(@cust_main, $sortby, $orderby); +%my @select = (); +%my @addl_headers = (); +%my @addl_cols = (); +%if ( $cgi->param('browse') +% || $cgi->param('otaker_on') +% || $cgi->param('agentnum_on') +%) { +% +% my %search = (); +% +% if ( $cgi->param('browse') ) { +% my $query = $cgi->param('browse'); +% if ( $query eq 'custnum' ) { +% if ( $conf->exists('cust_main-default_agent_custid') ) { +% $sortby=\*display_custnum_sort; +% $orderby = "ORDER BY CASE WHEN agent_custid IS NOT NULL AND agent_custid != '' THEN CAST(agent_custid AS BIGINT) ELSE custnum END"; +% } else { +% $sortby=\*custnum_sort; +% $orderby = "ORDER BY custnum"; +% } +% } elsif ( $query eq 'last' ) { +% $sortby=\*last_sort; +% $orderby = "ORDER BY LOWER(last || ' ' || first)"; +% } elsif ( $query eq 'company' ) { +% $sortby=\*company_sort; +% $orderby = "ORDER BY LOWER(company || ' ' || last || ' ' || first )"; +% } elsif ( $query eq 'tickets' ) { +% $sortby = \*tickets_sort; +% $orderby = "ORDER BY tickets DESC"; +% push @select, FS::TicketSystem->sql_num_customer_tickets. " as tickets"; +% push @addl_headers, 'Tickets'; +% push @addl_cols, 'tickets'; +% } else { +% die "unknown browse field $query"; +% } +% } else { +% $sortby = \*last_sort; #?? +% $orderby = "ORDER BY LOWER(last || ' ' || first)"; #?? +% } +% +% if ( $cgi->param('otaker_on') ) { +% die "access denied" +% unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); +% $cgi->param('otaker') =~ /^(\w{1,32})$/ or errorpage("Illegal otaker"); +% $search{otaker} = $1; +% } elsif ( $cgi->param('agentnum_on') ) { +% $cgi->param('agentnum') =~ /^(\d+)$/ or errorpage("Illegal agentnum"); +% $search{agentnum} = $1; +%# } else { +%# die "unknown query..."; +% } +% +% my @qual = (); +% +% my $ncancelled = ''; +% +% if ( $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me +% || ( $conf->exists('hidecancelledcustomers') +% && ! $cgi->param('showcancelledcustomers') ) +% ) { +% #grep { $_->ncancelled_pkgs || ! $_->all_pkgs } +% push @qual, FS::cust_main->uncancel_sql; +% +% } +% +% push @qual, FS::cust_main->cancel_sql if $cgi->param('cancelled'); +% push @qual, FS::cust_main->prospect_sql if $cgi->param('prospect'); +% push @qual, FS::cust_main->active_sql if $cgi->param('active'); +% push @qual, FS::cust_main->inactive_sql if $cgi->param('inactive'); +% push @qual, FS::cust_main->susp_sql if $cgi->param('suspended'); +% +% #EWWWWWW +% my $qual = join(' AND ', +% map { "$_ = ". dbh->quote($search{$_}) } keys %search ); +% +% my $addl_qual = join(' AND ', @qual); +% +% #here is the agent virtualization +% $addl_qual .= ( $addl_qual ? ' AND ' : '' ). +% $FS::CurrentUser::CurrentUser->agentnums_sql; +% +% if ( $addl_qual ) { +% $qual .= ' AND ' if $qual; +% $qual .= $addl_qual; +% } +% +% $qual = " WHERE $qual" if $qual; +% my $statement = "SELECT COUNT(*) FROM cust_main $qual"; +% my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement"; +% $sth->execute or die "Error executing \"$statement\": ". $sth->errstr; +% +% $total = $sth->fetchrow_arrayref->[0]; +% +% if ( $addl_qual ) { +% if ( %search ) { +% $addl_qual = " AND $addl_qual"; +% } else { +% $addl_qual = " WHERE $addl_qual"; +% } +% } +% +% my $select; +% if ( @select ) { +% $select = 'cust_main.*, '. join (', ', @select); +% } else { +% $select = '*'; +% } +% +% @cust_main = qsearch('cust_main', \%search, $select, +% "$addl_qual $orderby $limit" ); +% +%# foreach my $cust_main ( @just_cust_main ) { +%# +%# my @one_cust_main; +%# $FS::Record::DEBUG=1; +%# ( $cache, @one_cust_main ) = jsearch( +%# "$monsterjoin", +%# { 'custnum' => $cust_main->custnum }, +%# '', +%# '', +%# 'cust_main', +%# 'custnum', +%# ); +%# push @cust_main, @one_cust_main; +%# } +% +%} else { +% @cust_main=(); +% $sortby = \*last_sort; +% +% push @cust_main, @{&custnumsearch} +% if $cgi->param('custnum_on') && $cgi->param('custnum_text'); +% push @cust_main, @{&cardsearch} +% if $cgi->param('card_on') && $cgi->param('card'); +% push @cust_main, @{&lastsearch} +% if $cgi->param('last_on') && $cgi->param('last_text'); +% push @cust_main, @{&companysearch} +% if $cgi->param('company_on') && $cgi->param('company_text'); +% push @cust_main, @{&address2search} +% if $cgi->param('address2_on') && $cgi->param('address2_text'); +% push @cust_main, @{&phonesearch} +% if $cgi->param('phone_on') && $cgi->param('phone_text'); +% push @cust_main, @{&referralsearch} +% if $cgi->param('referral_custnum'); +% +% if ( $cgi->param('company_on') && $cgi->param('company_text') ) { +% $sortby = \*company_sort; +% push @cust_main, @{&companysearch}; +% } +% +% if ( $cgi->param('search_cust') ) { +% $sortby = \*company_sort; +% $orderby = "ORDER BY LOWER(company || ' ' || last || ' ' || first )"; +% push @cust_main, smart_search( 'search' => $cgi->param('search_cust') ); +% } +% +% @cust_main = grep { $_->ncancelled_pkgs || ! $_->all_pkgs } @cust_main +% if ! $cgi->param('cancelled') +% && ( +% $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me +% || ( $conf->exists('hidecancelledcustomers') +% && ! $cgi->param('showcancelledcustomers') ) +% ); +% +% my %saw = (); +% @cust_main = grep { !$saw{$_->custnum}++ } @cust_main; +%} +% +%my %all_pkgs; +%if ( $conf->exists('hidecancelledpackages' ) ) { +% %all_pkgs = map { $_->custnum => [ $_->ncancelled_pkgs ] } @cust_main; +%} else { +% %all_pkgs = map { $_->custnum => [ $_->all_pkgs ] } @cust_main; +%} +%#%all_pkgs = (); +% +%if ( scalar(@cust_main) == 1 && ! $cgi->param('referral_custnum') ) { +% if ( $cgi->param('quickpay') eq 'yes' ) { +% print $cgi->redirect(popurl(2). "edit/cust_pay.cgi?quickpay=yes;custnum=". $cust_main[0]->custnum); +% } else { +% print $cgi->redirect(popurl(2). "view/cust_main.cgi?". $cust_main[0]->custnum); +% } +% #exit; +%} elsif ( scalar(@cust_main) == 0 ) { +% + +<!-- mason kludge --> +% +% errorpage("No matching customers found!"); +%} else { +% + +<% include('/elements/header.html', "Customer Search Results", '' ) %> +% $total ||= scalar(@cust_main); + + + <% $total %> matching customers found + +% my $pager = include( '/elements/pager.html', +% 'offset' => $offset, +% 'num_rows' => scalar(@cust_main), +% 'total' => $total, +% 'maxrecords' => $maxrecords, +% ); +% +% unless ( $cgi->param('cancelled') ) { +% if ( $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me +% || ( $conf->exists('hidecancelledcustomers') +% && ! $cgi->param('showcancelledcustomers') +% ) +% ) { +% $cgi->param('showcancelledcustomers', 1); +% $cgi->param('offset', 0); +% print qq!( <a href="!. $cgi->self_url. qq!">show!; +% } else { +% $cgi->param('showcancelledcustomers', 0); +% $cgi->param('offset', 0); +% print qq!( <a href="!. $cgi->self_url. qq!">hide!; +% } +% print ' cancelled customers</a> )'; +% } +% +% if ( $cgi->param('referral_custnum') ) { +% $cgi->param('referral_custnum') =~ /^(\d+)$/ +% or errorpage("Illegal referral_custnum"); +% my $referral_custnum = $1; +% my $cust_main = qsearchs('cust_main', { custnum => $referral_custnum } ); +% print '<FORM METHOD="GET">'. +% qq!<INPUT TYPE="hidden" NAME="referral_custnum" VALUE="$referral_custnum">!. +% 'referrals of <A HREF="'. popurl(2). +% "view/cust_main.cgi?$referral_custnum\">$referral_custnum: ". +% ( $cust_main->company +% || $cust_main->last. ', '. $cust_main->first ). +% '</A>'; +% print "\n",<<END; +% <SCRIPT> +% function changed(what) { +% what.form.submit(); +% } +% </SCRIPT> +%END +% print ' <SELECT NAME="referral_depth" SIZE="1" onChange="changed(this)">'; +% my $max = 8; #config file +% $cgi->param('referral_depth') =~ /^(\d*)$/ +% or errorpage("Illegal referral_depth"); +% my $referral_depth = $1; +% +% foreach my $depth ( 1 .. $max ) { +% print '<OPTION', +% ' SELECTED'x($depth == $referral_depth), +% ">$depth"; +% } +% print "</SELECT> levels deep". +% '<NOSCRIPT> <INPUT TYPE="submit" VALUE="change"></NOSCRIPT>'. +% '</FORM>'; +% } +% +% my @custom_priorities = (); +% if ( $conf->config('ticket_system-custom_priority_field') +% && @{[ $conf->config('ticket_system-custom_priority_field-values') ]} ) { +% @custom_priorities = +% $conf->config('ticket_system-custom_priority_field-values'); +% } +% +% print "<BR><BR>". $pager. include('/elements/table-grid.html'). <<END; +% <TR> +% <TH CLASS="grid" BGCOLOR="#cccccc">#</TH> +% <TH CLASS="grid" BGCOLOR="#cccccc">Status</TH> +% <TH CLASS="grid" BGCOLOR="#cccccc">(bill) name</TH> +% <TH CLASS="grid" BGCOLOR="#cccccc">company</TH> +%END +% +%if ( defined dbdef->table('cust_main')->column('ship_last') ) { +% print <<END; +% <TH CLASS="grid" BGCOLOR="#cccccc">(service) name</TH> +% <TH CLASS="grid" BGCOLOR="#cccccc">company</TH> +%END +%} +% +%foreach my $addl_header ( @addl_headers ) { +% print '<TH CLASS="grid" BGCOLOR="#cccccc">'. "$addl_header</TH>"; +%} +% +%print <<END; +% <TH CLASS="grid" BGCOLOR="#cccccc">Packages</TH> +% <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=2>Services</TH> +% </TR> +%END +% +% my $bgcolor1 = '#eeeeee'; +% my $bgcolor2 = '#ffffff'; +% my $bgcolor; +% +% my(%saw,$cust_main); +% foreach $cust_main ( +% sort $sortby grep(!$saw{$_->custnum}++, @cust_main) +% ) { +% +% if ( $bgcolor eq $bgcolor1 ) { +% $bgcolor = $bgcolor2; +% } else { +% $bgcolor = $bgcolor1; +% } +% +% my($custnum,$last,$first,$company)=( +% $cust_main->custnum, +% $cust_main->getfield('last'), +% $cust_main->getfield('first'), +% $cust_main->company, +% ); +% +% my(@lol_cust_svc); +% my($rowspan)=0;#scalar( @{$all_pkgs{$custnum}} ); +% foreach ( @{$all_pkgs{$custnum}} ) { +% #my(@cust_svc) = qsearch( 'cust_svc', { 'pkgnum' => $_->pkgnum } ); +% my @cust_svc = $_->cust_svc; +% push @lol_cust_svc, \@cust_svc; +% $rowspan += scalar(@cust_svc) || 1; +% } +% +% #my($rowspan) = scalar(@{$all_pkgs{$custnum}}); +% my $view; +% if ( defined $cgi->param('quickpay') && $cgi->param('quickpay') eq 'yes' ) { +% $view = $p. 'edit/cust_pay.cgi?quickpay=yes;custnum='. $custnum; +% } else { +% $view = $p. 'view/cust_main.cgi?'. $custnum; +% } +% my $pcompany = $company +% ? qq!<A HREF="$view"><FONT SIZE=-1>$company</FONT></A>! +% : '<FONT SIZE=-1> </FONT>'; +% +% my $status = $cust_main->status; +% my $statuscol = $cust_main->statuscolor; + + <TR> + <TD CLASS="grid" ALIGN="right" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %>><A HREF="<% $view %>"><FONT SIZE=-1><% $cust_main->display_custnum %></FONT></A></TD> + <TD CLASS="grid" ALIGN="center" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %>><FONT SIZE="-1" COLOR="#<% $statuscol %>"><B><% ucfirst($status) %></B></FONT></TD> + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %>><A HREF="<% $view %>"><FONT SIZE=-1><% "$last, $first" %></FONT></A></TD> + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %>><% $pcompany %></TD> +% +% if ( defined dbdef->table('cust_main')->column('ship_last') ) { +% my($ship_last,$ship_first,$ship_company)=( +% $cust_main->ship_last || $cust_main->getfield('last'), +% $cust_main->ship_last ? $cust_main->ship_first : $cust_main->first, +% $cust_main->ship_last ? $cust_main->ship_company : $cust_main->company, +% ); +% my $pship_company = $ship_company +% ? qq!<A HREF="$view"><FONT SIZE=-1>$ship_company</FONT></A>! +% : '<FONT SIZE=-1> </FONT>'; +% + + + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %>><A HREF="<% $view %>"><FONT SIZE=-1><% "$ship_last, $ship_first" %></FONT></A></TD> + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %>><% $pship_company %></A></TD> +% } +% +% foreach my $addl_col ( @addl_cols ) { +% if ( $addl_col eq 'tickets' ) { +% if ( @custom_priorities ) { + + + <TD CLASS="inv" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %> ALIGN=right><FONT SIZE=-1> + + <TABLE CLASS="inv" CELLSPACING=0 CELLPADDING=0> +% foreach my $priority ( @custom_priorities, '' ) { +% +% my $num = +% FS::TicketSystem->num_customer_tickets($custnum,$priority); +% my $ahref = ''; +% $ahref= '<A HREF="'. +% FS::TicketSystem->href_customer_tickets($custnum,$priority). +% '">' +% if $num; +% + + + <TR> + <TD ALIGN=right> + <FONT SIZE=-1><% $ahref.$num %></A></FONT> + </TD> + <TD ALIGN=left> + <FONT SIZE=-1><% $ahref %><% $priority || '<i>(none)</i>' %></A></FONT> + </TD> + </TR> +% } + + + <TR> + <TH ALIGN=right STYLE="border-top: dashed 1px black"> + <FONT SIZE=-1> +% } else { + + + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %> ALIGN=right><FONT SIZE=-1> +% } +% +% my $ahref = ''; +% $ahref = '<A HREF="'. +% FS::TicketSystem->href_customer_tickets($custnum). +% '">' +% if $cust_main->get($addl_col); +% + + + <% $ahref %><% $cust_main->get($addl_col) %></A> +% if ( @custom_priorities ) { + + + </FONT></TH> + <TH ALIGN=left STYLE="border-top: dashed 1px black"> + <FONT SIZE=-1><% ${ahref} %>Total</A><FONT> + </TH> + </TR> + </TABLE> +% } + + + </FONT></TD> +% } else { + + + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %> ALIGN=right><FONT SIZE=-1> + <% $cust_main->get($addl_col) %> + </FONT></TD> +% +% } +% } +% +% my($n1)=''; +% foreach ( @{$all_pkgs{$custnum}} ) { +% my $pkgnum = $_->pkgnum; +%# my $part_pkg = qsearchs( 'part_pkg', { pkgpart => $_->pkgpart } ); +% my $part_pkg = $_->part_pkg; +% +% my $pkg_comment = $part_pkg->pkg_comment(nopkgpart => 1); +% my $show = $curuser->default_customer_view =~ /^(jumbo|packages)$/ +% ? '' +% : ';show=packages'; +% my $frag = "cust_pkg$pkgnum"; #hack for IE ignoring real #fragment +% my $pkgview = "${p}view/cust_main.cgi?custnum=$custnum$show;fragment=$frag#$frag"; +% my @cust_svc = @{shift @lol_cust_svc}; +% #my(@cust_svc) = qsearch( 'cust_svc', { 'pkgnum' => $_->pkgnum } ); +% my $rowspan = scalar(@cust_svc) || 1; +% +% print $n1, qq!<TD CLASS="grid" BGCOLOR="$bgcolor" ROWSPAN=$rowspan><A HREF="$pkgview"><FONT SIZE=-1>$pkg_comment</FONT></A></TD>!; +% +% my($n2)=''; +% foreach my $cust_svc ( @cust_svc ) { +% my($label, $value, $svcdb) = $cust_svc->label; +% my($svcnum) = $cust_svc->svcnum; +% my($sview) = $p.'view'; +% print $n2, +% qq!<TD CLASS="grid" BGCOLOR="$bgcolor" >!. FS::UI::Web::svc_link($m, $cust_svc->part_svc, $cust_svc) . qq!</TD> !. +% qq!<TD CLASS="grid" BGCOLOR="$bgcolor" >!. FS::UI::Web::svc_label_link($m, $cust_svc->part_svc, $cust_svc) . qq!</TD> !; +% $n2="</TR><TR>"; +% } +% +% unless ( @cust_svc ) { +% print qq!<TD CLASS="grid" BGCOLOR="$bgcolor" COLSPAN=2> </TD>!; +% } +% +% #print qq!</TR><TR>\n!; +% $n1="</TR><TR>"; +% } +% +% unless ( @{$all_pkgs{$custnum}} ) { +% print qq!<TD CLASS="grid" BGCOLOR="$bgcolor" COLSPAN=3> </TD>!; +% } +% +% print "</TR>"; +% } +% +% + + + </TABLE><% $pager %> + + <% include('/elements/footer.html') %> +% } +% +%#undef $cache; #does this help? +% +%# +% +%sub last_sort { +% lc($a->getfield('last')) cmp lc($b->getfield('last')) +% || lc($a->first) cmp lc($b->first); +%} +% +%sub company_sort { +% return -1 if $a->company && ! $b->company; +% return 1 if ! $a->company && $b->company; +% lc($a->company) cmp lc($b->company) +% || lc($a->getfield('last')) cmp lc($b->getfield('last')) +% || lc($a->first) cmp lc($b->first);; +%} +% +%sub display_custnum_sort { +% $a->display_custnum <=> $b->display_custnum; +%} +% +%sub custnum_sort { +% $a->getfield('custnum') <=> $b->getfield('custnum'); +%} +% +%sub tickets_sort { +% $b->getfield('tickets') <=> $a->getfield('tickets'); +%} +% +%sub custnumsearch { +% +% my $custnum = $cgi->param('custnum_text'); +% $custnum =~ s/\D//g; +% $custnum =~ /^(\d{1,23})$/ or errorpage("Illegal customer number"); +% $custnum = $1; +% +% [ qsearchs('cust_main', { 'custnum' => $custnum } ) ]; +%} +% +%sub cardsearch { +% +% my($card)=$cgi->param('card'); +% $card =~ s/\D//g; +% $card =~ /^(\d{13,16})$/ or errorpage("Illegal card number"); +% my($payinfo)=$1; +% +% [ qsearch('cust_main',{'payinfo'=>$payinfo, 'payby'=>'CARD'}), +% qsearch('cust_main',{'payinfo'=>$payinfo, 'payby'=>'DCRD'}) +% ]; +%} +% +%sub referralsearch { +% $cgi->param('referral_custnum') =~ /^(\d+)$/ +% or errorpage("Illegal referral_custnum"); +% my $cust_main = qsearchs('cust_main', { 'custnum' => $1 } ) +% or errorpage("Customer $1 not found"); +% my $depth; +% if ( $cgi->param('referral_depth') ) { +% $cgi->param('referral_depth') =~ /^(\d+)$/ +% or errorpage("Illegal referral_depth"); +% $depth = $1; +% } else { +% $depth = 1; +% } +% [ $cust_main->referral_cust_main($depth) ]; +%} +% +%sub lastsearch { +% my(%last_type); +% my @cust_main; +% foreach ( $cgi->param('last_type') ) { +% $last_type{$_}++; +% } +% +% $cgi->param('last_text') =~ /^([\w \,\.\-\']*)$/ +% or errorpage("Illegal last name"); +% my($last)=$1; +% +% if ( $last_type{'Exact'} || $last_type{'Fuzzy'} ) { +% push @cust_main, qsearch( 'cust_main', +% { 'last' => { 'op' => 'ILIKE', +% 'value' => $last } } ); +% +% push @cust_main, qsearch( 'cust_main', +% { 'ship_last' => { 'op' => 'ILIKE', +% 'value' => $last } } ) +% if defined dbdef->table('cust_main')->column('ship_last'); +% } +% +% if ( $last_type{'Substring'} || $last_type{'All'} ) { +% +% push @cust_main, qsearch( 'cust_main', +% { 'last' => { 'op' => 'ILIKE', +% 'value' => "%$last%" } } ); +% +% push @cust_main, qsearch( 'cust_main', +% { 'ship_last' => { 'op' => 'ILIKE', +% 'value' => "%$last%" } } ) +% if defined dbdef->table('cust_main')->column('ship_last'); +% +% } +% +% if ( $last_type{'Fuzzy'} || $last_type{'All'} ) { +% push @cust_main, FS::cust_main->fuzzy_search( { 'last' => $last } ); +% } +% +% #if ($last_type{'Sound-alike'}) { +% #} +% +% \@cust_main; +%} +% +%sub companysearch { +% +% my(%company_type); +% my @cust_main; +% foreach ( $cgi->param('company_type') ) { +% $company_type{$_}++ +% }; +% +% $cgi->param('company_text') =~ +% /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/ +% or errorpage("Illegal company"); +% my $company = $1; +% +% if ( $company_type{'Exact'} || $company_type{'Fuzzy'} ) { +% push @cust_main, qsearch( 'cust_main', +% { 'company' => { 'op' => 'ILIKE', +% 'value' => $company } } ); +% +% push @cust_main, qsearch( 'cust_main', +% { 'ship_company' => { 'op' => 'ILIKE', +% 'value' => $company } } ) +% if defined dbdef->table('cust_main')->column('ship_last'); +% } +% +% if ( $company_type{'Substring'} || $company_type{'All'} ) { +% +% push @cust_main, qsearch( 'cust_main', +% { 'company' => { 'op' => 'ILIKE', +% 'value' => "%$company%" } } ); +% +% push @cust_main, qsearch( 'cust_main', +% { 'ship_company' => { 'op' => 'ILIKE', +% 'value' => "%$company%" } }) +% if defined dbdef->table('cust_main')->column('ship_last'); +% +% } +% +% if ( $company_type{'Fuzzy'} || $company_type{'All'} ) { +% push @cust_main, FS::cust_main->fuzzy_search( { 'company' => $company } ); +% } +% +% if ($company_type{'Sound-alike'}) { +% } +% +% \@cust_main; +%} +% +%sub address2search { +% my @cust_main; +% +% $cgi->param('address2_text') =~ +% /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/ +% or errorpage("Illegal address2"); +% my $address2 = $1; +% +% push @cust_main, qsearch( 'cust_main', +% { 'address2' => { 'op' => 'ILIKE', +% 'value' => $address2 } } ); +% push @cust_main, qsearch( 'cust_main', +% { 'ship_address2' => { 'op' => 'ILIKE', +% 'value' => $address2 } } ); +% +% \@cust_main; +%} +% +%sub phonesearch { +% my @cust_main; +% +% my $phone = $cgi->param('phone_text'); +% +% #(no longer really) false laziness with Record::ut_phonen +% #only works with US/CA numbers... +% $phone =~ s/\D//g; +% if ( $phone =~ /^(\d{3})(\d{3})(\d{4})(\d*)$/ ) { +% $phone = "$1-$2-$3"; +% $phone .= " x$4" if $4; +% } elsif ( $phone =~ /^(\d{3})(\d{4})$/ ) { +% $phone = "$1-$2"; +% } elsif ( $phone =~ /^(\d{3,4})$/ ) { +% $phone = $1; +% } else { +% errorpage(gettext('illegal_phone'). ": $phone"); +% } +% +% my @fields = qw(daytime night fax); +% push @fields, qw(ship_daytime ship_night ship_fax) +% if defined dbdef->table('cust_main')->column('ship_last'); +% +% for my $field ( @fields ) { +% push @cust_main, qsearch ( 'cust_main', +% { $field => { 'op' => 'LIKE', +% 'value' => "%$phone%" } } ); +% } +% +% \@cust_main; +%} diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html new file mode 100755 index 000000000..270fc38cc --- /dev/null +++ b/httemplate/search/cust_main.html @@ -0,0 +1,111 @@ +<% include( 'elements/search.html', + 'title' => 'Customer Search Results', + 'menubar' => $menubar, + 'name' => 'customers', + 'query' => $sql_query, + 'count_query' => $count_query, + 'header' => [ FS::UI::Web::cust_header( + $cgi->param('cust_fields') + ), + @extra_headers, + ], + 'fields' => [ + \&FS::UI::Web::cust_fields, + @extra_fields, + ], + '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 customers') && + $FS::CurrentUser::CurrentUser->access_right('List packages') + ); + +my %search_hash = (); + +#$search_hash{'query'} = $cgi->keywords; + +#scalars +my @scalars = qw ( + agentnum status cancelled_pkgs cust_fields flattened_pkgs custbatch usernum + no_censustract paydate_year paydate_month invoice_terms +); + +for my $param ( @scalars ) { + $search_hash{$param} = scalar( $cgi->param($param) ) + if $cgi->param($param); +} + +#lists +for my $param (qw( classnum payby )) { + $search_hash{$param} = [ $cgi->param($param) ]; +} + +### +# parse dates +### + +foreach my $field (qw( signupdate )) { + + my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field); + + next if $beginning == 0 && $ending == 4294967295 && !defined($cgi->param('signuphour')); + #or $disable{$cgi->param('status')}->{$field}; + + $search_hash{$field} = [ $beginning, $ending, $cgi->param('signuphour') ]; + +} + +## +# amounts +## + +$search_hash{'current_balance'} = + [ FS::UI::Web::parse_lt_gt($cgi, 'current_balance') ]; + +### +# etc +### + +my $sql_query = FS::cust_main->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' ]; + +### +# email links +### + +my $menubar = []; + +if ( $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices') ) { + + my $uri = new URI; + $uri->query_form( \%search_hash ); + my $query = $uri->query; + + push @$menubar, 'Email a notice to these customers' => + "${p}misc/email-customers.html?$query", + +} + +</%init> diff --git a/httemplate/search/cust_pay.cgi b/httemplate/search/cust_pay.cgi new file mode 100755 index 000000000..65bd39e19 --- /dev/null +++ b/httemplate/search/cust_pay.cgi @@ -0,0 +1,7 @@ +<% include( 'elements/cust_pay_or_refund.html', + 'thing' => 'pay', + 'amount_field' => 'paid', + 'name_singular' => 'payment', + 'name_verb' => 'paid', + ) +%> diff --git a/httemplate/search/cust_pay_batch.cgi b/httemplate/search/cust_pay_batch.cgi new file mode 100755 index 000000000..7376e9dcb --- /dev/null +++ b/httemplate/search/cust_pay_batch.cgi @@ -0,0 +1,200 @@ +<% include('elements/search.html', + 'title' => 'Batch payment details', + 'name' => 'batch details', + 'query' => $sql_query, + 'count_query' => $count_query, + 'html_init' => $pay_batch ? $html_init : '', + 'header' => [ '#', + 'Inv #', + 'Customer', + 'Customer', + 'Card Name', + 'Card', + 'Exp', + 'Amount', + 'Status', + ], + 'fields' => [ sub { + shift->[0]; + }, + sub { + shift->[1]; + }, + sub { + shift->[2]; + }, + sub { + my $cpb = shift; + $cpb->[3] . ', ' . $cpb->[4]; + }, + sub { + shift->[5]; + }, + sub { + my $cardnum = shift->[6]; + 'x'x(length($cardnum)-4). substr($cardnum,(length($cardnum)-4)); + }, + sub { + shift->[7] =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; + my( $mon, $year ) = ( $2, $1 ); + $mon = "0$mon" if length($mon) == 1; + "$mon/$year"; + }, + sub { + shift->[8]; + }, + sub { + shift->[9]; + }, + ], + 'align' => 'lllllllrl', + 'links' => [ ['', sub{'#';}], + ["${p}view/cust_bill.cgi?", sub{shift->[1];},], + ["${p}view/cust_main.cgi?", sub{shift->[2];},], + ["${p}view/cust_main.cgi?", sub{shift->[2];},], + ], + ) +%> +<%init> + +my $conf = new FS::Conf; + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports') + || $FS::CurrentUser::CurrentUser->access_right('Process batches') + || ( $cgi->param('custnum') + && ( $conf->exists('batch-enable') + || $conf->config('batch-enable_payby') + ) + #&& $FS::CurrentUser::CurrentUser->access_right('View customer batched payments') + ); + +my( $count_query, $sql_query ); +my $hashref = {}; +my @search = (); +my $orderby = 'paybatchnum'; + +my( $pay_batch, $batchnum ) = ( '', ''); +if ( $cgi->param('batchnum') && $cgi->param('batchnum') =~ /^(\d+)$/ ) { + push @search, "batchnum = $1"; + $pay_batch = qsearchs('pay_batch', { 'batchnum' => $1 } ); + die "Batch $1 not found!" unless $pay_batch; + $batchnum = $pay_batch->batchnum; +} + +if ( $cgi->param('custnum') && $cgi->param('custnum') =~ /^(\d+)$/ ) { + push @search, "custnum = $1"; +} + +if ( $cgi->param('status') && $cgi->param('status') =~ /^(\w)$/ ) { + push @search, "pay_batch.status = '$1'"; +} + +if ( $cgi->param('payby') ) { + $cgi->param('payby') =~ /^(CARD|CHEK)$/ + or die "illegal payby " . $cgi->param('payby'); + + push @search, "cust_pay_batch.payby = '$1'"; +} + +if ( not $cgi->param('dcln') ) { + push @search, "cpb.status IS DISTINCT FROM 'Approved'"; +} + +my ($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +unless ($pay_batch){ + push @search, "pay_batch.upload >= $beginning" if ($beginning); + push @search, "pay_batch.upload <= $ending" if ($ending < 4294967295);#2^32-1 + $orderby = "pay_batch.download,paybatchnum"; +} + +push @search, $FS::CurrentUser::CurrentUser->agentnums_sql; +my $search = ' WHERE ' . join(' AND ', @search); + +$count_query = 'SELECT COUNT(*) FROM cust_pay_batch AS cpb ' . + 'LEFT JOIN cust_main USING ( custnum ) ' . + 'LEFT JOIN pay_batch USING ( batchnum )' . + $search; + +#grr +$sql_query = "SELECT paybatchnum,invnum,custnum,cpb.last,cpb.first," . + "cpb.payname,cpb.payinfo,cpb.exp,amount,cpb.status " . + "FROM cust_pay_batch AS cpb " . + 'LEFT JOIN cust_main USING ( custnum ) ' . + 'LEFT JOIN pay_batch USING ( batchnum ) ' . + "$search ORDER BY $orderby"; + +my $html_init = ''; +if ( $pay_batch ) { + my $fixed = $conf->config('batch-fixed_format-'. $pay_batch->payby); + if ( + $pay_batch->status eq 'O' + || ( $pay_batch->status eq 'I' + && $FS::CurrentUser::CurrentUser->access_right('Reprocess batches') + ) + || ( $pay_batch->status eq 'R' + && $FS::CurrentUser::CurrentUser->access_right('Redownload resolved batches') + ) + ) { + $html_init .= qq!<FORM ACTION="$p/misc/download-batch.cgi" METHOD="POST">!; + if ( $fixed ) { + $html_init .= qq!<INPUT TYPE="hidden" NAME="format" VALUE="$fixed">!; + } else { + $html_init .= qq!Download batch in format <SELECT NAME="format">!. + qq!<OPTION VALUE="">Default batch mode</OPTION>!. + qq!<OPTION VALUE="csv-td_canada_trust-merchant_pc_batch">CSV file for TD Canada Trust Merchant PC Batch</OPTION>!. + qq!<OPTION VALUE="csv-chase_canada-E-xactBatch">CSV file for Chase Canada E-xactBatch</OPTION>!. + qq!<OPTION VALUE="PAP">80 byte file for TD Canada Trust PAP Batch</OPTION>!. + qq!<OPTION VALUE="BoM">Bank of Montreal ECA batch</OPTION>!. + qq!<OPTION VALUE="ach-spiritone">Spiritone ACH batch</OPTION>!. + qq!<OPTION VALUE="paymentech">Chase Paymentech XML</OPTION>!. + qq!<OPTION VALUE="RBC">Royal Bank of Canada PDS</OPTION>!. + qq!</SELECT>!; + } + $html_init .= qq!<INPUT TYPE="hidden" NAME="batchnum" VALUE="$batchnum"><INPUT TYPE="submit" VALUE="Download"></FORM><BR>!; + } + + if ( + $pay_batch->status eq 'I' + || ( $pay_batch->status eq 'R' + && $FS::CurrentUser::CurrentUser->access_right('Reprocess batches') + ) + ) { + $html_init .= qq!<FORM ACTION="$p/misc/upload-batch.cgi" METHOD="POST" ENCTYPE="multipart/form-data">!. + qq!Upload results<BR>!. + qq!Filename <INPUT TYPE="file" NAME="batch_results"><BR>!; + if ( $fixed ) { + $html_init .= qq!<INPUT TYPE="hidden" NAME="format" VALUE="$fixed">!; + } else { + $html_init .= qq!Format <SELECT NAME="format">!. + qq!<OPTION VALUE="">Default batch mode</OPTION>!. + qq!<OPTION VALUE="csv-td_canada_trust-merchant_pc_batch">CSV results from TD Canada Trust Merchant PC Batch</OPTION>!. + qq!<OPTION VALUE="csv-chase_canada-E-xactBatch">CSV file for Chase Canada E-xactBatch</OPTION>!. + qq!<OPTION VALUE="PAP">264 byte results for TD Canada Trust PAP Batch</OPTION>!. + qq!<OPTION VALUE="BoM">Bank of Montreal ECA results</OPTION>!. + qq!<OPTION VALUE="ach-spiritone">Spiritone ACH batch</OPTION>!. + qq!<OPTION VALUE="paymentech">Chase Paymentech XML</OPTION>!. + qq!<OPTION VALUE="RBC">Royal Bank of Canada PDS</OPTION>!. + qq!</SELECT><BR>!; + } + $html_init .= qq!<INPUT TYPE="hidden" NAME="batchnum" VALUE="$batchnum">!; + $html_init .= '<INPUT TYPE="submit" VALUE="Upload"></FORM><BR>'; + } + +} + +if ($pay_batch) { + my $sth = dbh->prepare($count_query) or die dbh->errstr. "doing $count_query"; + $sth->execute or die "Error executing \"$count_query\": ". $sth->errstr; + my $cards = $sth->fetchrow_arrayref->[0]; + + my $st = "SELECT SUM(amount) from cust_pay_batch WHERE batchnum=". $batchnum; + $sth = dbh->prepare($st) or die dbh->errstr. "doing $st"; + $sth->execute or die "Error executing \"$st\": ". $sth->errstr; + my $total = $sth->fetchrow_arrayref->[0]; + + $html_init .= "$cards credit card payments batched<BR>\$" . + sprintf("%.2f", $total) ." total in batch<BR>"; +} + +</%init> diff --git a/httemplate/search/cust_pay_pending.html b/httemplate/search/cust_pay_pending.html new file mode 100755 index 000000000..f46e08ab1 --- /dev/null +++ b/httemplate/search/cust_pay_pending.html @@ -0,0 +1,57 @@ +<% include( 'elements/cust_pay_or_refund.html', + 'thing' => 'pay_pending', + 'amount_field' => 'paid', + 'name_singular' => 'pending payment', + 'name_verb' => 'pending', + 'disable_link' => 1, + 'disable_by' => 1, #add otaker to cust_pay_pending? + 'html_init' => include('/elements/init_overlib.html'), + 'addl_header' => [ 'Time', 'Payment Status', ], + 'addl_fields' => [ sub { time2str('%r', shift->_date ) }, + $status_sub, + ], + 'redirect_empty' => $redirect_empty, + ) +%> +<%init> + +my %statusaction = ( + 'new' => 'delete', + 'pending' => 'complete', + #'authorized' => '', + #'captured' => '', + #'declined' => '', + #wouldn't need to take action on a done state#'done' +); + +my $edit_pending = + $FS::CurrentUser::CurrentUser->access_right('Edit customer pending payments'); + +my $status_sub = sub { + my $pending = shift; + my $return = $pending->status; + my $action = $statusaction{$pending->status}; + return $return unless $action && $edit_pending; + my $link = include('/elements/popup_link.html', + 'action' => $p. 'edit/cust_pay_pending.html'. + '?paypendingnum='. $pending->paypendingnum. + ";action=$action", + 'label' => $action, + 'color' => '#ff0000', + 'width' => 655, + 'height' => ( $action eq 'delete' ? 480 : 575 ), + 'actionlabel' => ucfirst($action). ' pending payment', + ); + $return. qq! <FONT SIZE="-1">($link)</FONT>!; +}; + +my $redirect_empty = sub { + my $cgi = shift; + if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { + $p. "view/cust_main.cgi?$1"; + } else { + ''; + } +}; + +</%init> diff --git a/httemplate/search/cust_pay_void.html b/httemplate/search/cust_pay_void.html new file mode 100755 index 000000000..431bb2c6b --- /dev/null +++ b/httemplate/search/cust_pay_void.html @@ -0,0 +1,13 @@ +<% include( 'elements/cust_pay_or_refund.html', + 'thing' => 'pay_void', + 'amount_field' => 'paid', + 'name_singular' => 'voided payment', + 'name_verb' => 'voided', # 'paid', + 'disable_by' => 1, #showing original not voiding otaker + 'addl_header' => [ 'Void Date', ], # 'Void Reason' ], + 'addl_fields' => [ + sub { time2str('%b %d %Y', shift->void_date ) }, + #'reason', + ], + ) +%> diff --git a/httemplate/search/cust_pkg.cgi b/httemplate/search/cust_pkg.cgi new file mode 100755 index 000000000..adbec7a74 --- /dev/null +++ b/httemplate/search/cust_pkg.cgi @@ -0,0 +1,283 @@ +<% include( 'elements/search.html', + 'html_init' => $html_init, + 'title' => 'Package Search Results', + 'name' => 'packages', + 'query' => $sql_query, + 'count_query' => $count_query, + #'redirect' => $link, + 'header' => [ '#', + 'Quan.', + 'Package', + 'Class', + 'Status', + 'Setup', + 'Base Recur', + 'Freq.', + 'Setup', + 'Last bill', + 'Next bill', + 'Adjourn', + 'Susp.', + 'Expire', + 'Cancel', + 'Reason', + FS::UI::Web::cust_header( + $cgi->param('cust_fields') + ), + 'Services', + ], + 'fields' => [ + 'pkgnum', + 'quantity', + sub { #my $part_pkg = $part_pkg{shift->pkgpart}; + #$part_pkg->pkg; # ' - '. $part_pkg->comment; + $_[0]->pkg; # ' - '. $_[0]->comment; + }, + 'classname', + sub { ucfirst(shift->status); }, + sub { sprintf( $money_char.'%.2f', + shift->part_pkg->option('setup_fee'), + ); + }, + sub { my $c = shift; + sprintf( $money_char.'%.2f', + $c->part_pkg->base_recur($c) + ); + }, + sub { #shift->part_pkg->freq_pretty; + + #my $part_pkg = $part_pkg{shift->pkgpart}; + #$part_pkg->freq_pretty; + + FS::part_pkg::freq_pretty(shift); + }, + + #sub { time2str('%b %d %Y', shift->setup); }, + #sub { time2str('%b %d %Y', shift->last_bill); }, + #sub { time2str('%b %d %Y', shift->bill); }, + #sub { time2str('%b %d %Y', shift->susp); }, + #sub { time2str('%b %d %Y', shift->expire); }, + #sub { time2str('%b %d %Y', shift->get('cancel')); }, + ( map { time_or_blank($_) } + qw( setup last_bill bill adjourn susp expire cancel ) ), + + sub { my $self = shift; + my $return = ''; + foreach my $action ( qw ( cancel susp ) ) { + my $reason = $self->last_reason($action); + $return = $reason->reason if $reason; + last if $return; + } + $return; + }, + + \&FS::UI::Web::cust_fields, + #sub { '<table border=0 cellspacing=0 cellpadding=0 STYLE="border:none">'. + # join('', map { '<tr><td align="right" style="border:none">'. $_->[0]. + # ':</td><td style="border:none">'. $_->[1]. '</td></tr>' } + # shift->labels + # ). + # '</table>'; + # }, + sub { + [ map { + [ + { 'data' => $_->[0]. ':', + 'align'=> 'right', + }, + { 'data' => $_->[1], + 'align'=> 'left', + 'link' => $p. 'view/' . + $_->[2]. '.cgi?'. $_->[3], + }, + ]; + } shift->labels + ]; + }, + ], + 'color' => [ + '', + '', + '', + '', + sub { shift->statuscolor; }, + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + '', + ], + 'style' => [ '', '', '', '', 'b', '', '', '', '', '', '', '', '', '', '', '', + FS::UI::Web::cust_styles() ], + 'size' => [ '', '', '', '', '-1' ], + 'align' => 'rrlccrrlrrrrrrrl'. FS::UI::Web::cust_aligns(). 'r', + 'links' => [ + $link, + $link, + $link, + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + ( map { $_ ne 'Cust. Status' ? $clink : '' } + FS::UI::Web::cust_header( + $cgi->param('cust_fields') + ) + ), + '', + ], + ) +%> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('List packages'); + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +# my %part_pkg = map { $_->pkgpart => $_ } qsearch('part_pkg', {}); + +my %search_hash = (); + +#some false laziness w/misc/bulk_change_pkg.cgi + +$search_hash{'query'} = $cgi->keywords; + +for (qw( agentnum custnum magic status classnum custom cust_fields )) { + $search_hash{$_} = $cgi->param($_) if $cgi->param($_); +} + +$search_hash{'pkgpart'} = [ $cgi->param('pkgpart') ]; + +for my $param ( qw(censustract) ) { + $search_hash{$param} = $cgi->param($param) || '' + if ( grep { /$param/ } $cgi->param ); +} + +my @report_option = $cgi->param('report_option') + if $cgi->param('report_option'); +$search_hash{report_option} = join(',', @report_option) if @report_option; + +### +# parse dates +### + +#false laziness w/report_cust_pkg.html +my %disable = ( + 'all' => {}, + 'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'adjourn'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, }, + 'active' => { 'susp'=>1, 'cancel'=>1 }, + 'suspended' => { 'cancel' => 1 }, + 'cancelled' => {}, + '' => {}, +); + +foreach my $field (qw( setup last_bill bill adjourn susp expire cancel active )) { + + my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field); + + next if $beginning == 0 && $ending == 4294967295 + or $disable{$cgi->param('status')}->{$field}; + + $search_hash{$field} = [ $beginning, $ending ]; + +} + +my $sql_query = FS::cust_pkg->search(\%search_hash); +my $count_query = delete($sql_query->{'count_query'}); + +my $show = $curuser->default_customer_view =~ /^(jumbo|packages)$/ + ? '' + : ';show=packages'; + +my $link = sub { + my $self = shift; + my $frag = 'cust_pkg'. $self->pkgnum; #hack for IE ignoring real #fragment + [ "${p}view/cust_main.cgi?custnum=".$self->custnum. + "$show;fragment=$frag#cust_pkg", + 'pkgnum' + ]; +}; + +my $clink = sub { + my $cust_pkg = shift; + $cust_pkg->cust_main_custnum + ? [ "${p}view/cust_main.cgi?", 'custnum' ] + : ''; +}; + +#if ( scalar(@cust_pkg) == 1 ) { +# print $cgi->redirect("${p}view/cust_main.cgi?". $cust_pkg[0]->custnum. +# "#cust_pkg". $cust_pkg[0]->pkgnum ); + +# my @cust_svc = qsearch( 'cust_svc', { 'pkgnum' => $pkgnum } ); +# my $rowspan = scalar(@cust_svc) || 1; + +# my $n2 = ''; +# foreach my $cust_svc ( @cust_svc ) { +# my($label, $value, $svcdb) = $cust_svc->label; +# my $svcnum = $cust_svc->svcnum; +# my $sview = $p. "view"; +# print $n2,qq!<TD><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$label</FONT></A></TD>!, +# qq!<TD><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$value</FONT></A></TD>!; +# $n2="</TR><TR>"; +# } + +sub time_or_blank { + my $column = shift; + return sub { + my $record = shift; + my $value = $record->get($column); #mmm closures + $value ? time2str('%b %d %Y', $value ) : ''; + }; +} + +my $html_init = sub { + my $query = shift; + my $text = ''; + my $curuser = $FS::CurrentUser::CurrentUser; + + if ( $curuser->access_right('Bulk change customer packages') ) { + $text .= include('/elements/init_overlib.html'). + include( '/elements/popup_link.html', + 'label' => 'Change these packages', + 'action' => "${p}misc/bulk_change_pkg.cgi?$query", + 'actionlabel' => 'Change Packages', + 'width' => 569, + 'height' => 210, + ). '<BR>'; + + if ( $curuser->access_right('Edit customer package dates') ) { + $text .= include( '/elements/popup_link.html', + 'label' => 'Increment next bill date', + 'action' => "${p}misc/bulk_pkg_increment_bill.cgi?$query", + 'actionlabel' => 'Increment Bill Date', + 'width' => 569, + 'height' => 210, + ). '<BR>'; + } + } + return $text; +}; + +</%init> diff --git a/httemplate/search/cust_pkg_discount.html b/httemplate/search/cust_pkg_discount.html new file mode 100644 index 000000000..233345e1c --- /dev/null +++ b/httemplate/search/cust_pkg_discount.html @@ -0,0 +1,122 @@ +<% include( 'elements/search.html', + 'title' => 'Package discounts', + 'name' => 'discounts', + 'query' => $query, + 'count_query' => $count_query, + #'redirect' => $link, + 'header' => [ 'Status', + 'Discount', + 'Months used', + 'Employee', + 'Package', + FS::UI::Web::cust_header( + # $cgi->param('cust_fields') + ), + ], + 'fields' => [ + sub { ucfirst( shift->status ) }, + sub { shift->discount->description }, + sub { my $m = shift->months_used; + $m =~ /\./ ? sprintf('%.2f',$m) : $m; + }, + 'otaker', + 'pkg', + \&FS::UI::Web::cust_fields, + ], + 'links' => [ + '', + '', + '', + '', + '', + ( map { $_ ne 'Cust. Status' ? $clink : ''} + FS::UI::Web::cust_header() + ), + ], + 'align' => 'clrll'. FS::UI::Web::cust_aligns(), + 'color' => [ + '', + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +#my $conf = new FS::Conf; + +#here is the agent virtualization +my $agentnums_sql = + $FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' ); + +my @where = ( $agentnums_sql ); + +#status +if ( $cgi->param('status') eq 'active' ) { + push @where, " ( cust_pkg_discount.disabled IS NULL + OR cust_pkg_discount.disabled != 'Y' ) + AND ( months IS NULL OR months_used < months ) "; + #XXX also end date +} elsif ( $cgi->param('status') eq 'expired' ) { + push @where, " ( cust_pkg_discount.disabled IS NOT NULL + AND cust_pkg_discount.disabled = 'Y' ) + OR ( months IS NOT NULL AND months_used >= months ) + "; #XXX also end date +} + +#otaker +if ( $cgi->param('otaker') && $cgi->param('otaker') =~ /^([\w\.\-]+)$/ ) { + push @where, "cust_pkg_discount.otaker = '$1'"; +} + +#agent +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @where, "cust_main.agentnum = $1"; +} + +my $count_query = "SELECT COUNT(*), SUM(amount)"; + +my $join = ' LEFT JOIN discount USING ( discountnum ) + LEFT JOIN cust_pkg USING ( pkgnum ) + LEFT JOIN part_pkg USING ( pkgpart ) + LEFT JOIN cust_main USING ( custnum ) '; + +my $where = ' WHERE '. join(' AND ', @where); + +$count_query .= " FROM cust_pkg_discount $join $where"; + +my @select = ( + 'cust_pkg_discount.*', + 'part_pkg.pkg', + ); +push @select, 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(); + +my $query = { + 'table' => 'cust_pkg_discount', + 'addl_from' => $join, + 'hashref' => {}, + 'select' => join(', ', @select ), + 'extra_sql' => $where, + 'order_by' => 'ORDER BY pkgdiscountnum', +}; + +my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ]; + +my $conf = new FS::Conf; + +</%init> diff --git a/httemplate/search/cust_refund.html b/httemplate/search/cust_refund.html new file mode 100644 index 000000000..e31e088eb --- /dev/null +++ b/httemplate/search/cust_refund.html @@ -0,0 +1,7 @@ +<% include( 'elements/cust_pay_or_refund.html', + 'thing' => 'refund', + 'amount_field' => 'refund', + 'name_singular' => 'refund', + 'name_verb' => 'refunded', + ) +%> diff --git a/httemplate/search/cust_svc.html b/httemplate/search/cust_svc.html new file mode 100644 index 000000000..2c17561f2 --- /dev/null +++ b/httemplate/search/cust_svc.html @@ -0,0 +1,140 @@ +<% include( 'elements/search.html', + 'title' => 'Service search results', + 'name' => 'services', + 'query' => $sql_query, + 'count_query' => $count_query, + 'redirect' => $link, + 'header' => [ '#', + 'Service', + # package? + FS::UI::Web::cust_header(), + ], + 'fields' => [ 'svcnum', + sub { + #$_[0]->svc. ': '. $_[0]->label; + my($label, $value, $svcdb) = $_[0]->label; + "$label: $value"; + }, + # package? + \&FS::UI::Web::cust_fields, + ], + 'links' => [ $link, + $link, + # package? + ( map { $_ ne 'Cust. Status' ? $link_cust : '' } + FS::UI::Web::cust_header() + ), + ], + 'align' => 'rl'. FS::UI::Web::cust_aligns(), + 'color' => [ + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List services'); + +my $addl_from = ' LEFT JOIN part_svc USING ( svcpart ) '. + ' LEFT JOIN cust_pkg USING ( pkgnum ) '. + ' LEFT JOIN cust_main USING ( custnum ) '; + +my @extra_sql = (); +my $orderby = 'ORDER BY svcnum'; #has to be ordered by something + #for pagination to work +if ( length( $cgi->param('search_svc') ) ) { + + my $string = $cgi->param('search_svc'); + $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace + + # implement fuzzy searching in subclasses too at some point? + # service searching maybe shouldn't be fuzzy... + + push @extra_sql, + ' ( '. join(' OR ', + map { my $table = $_; + my $search_sql = "FS::$table"->search_sql($string); + " ( svcdb = '$table' + AND 0 < ( SELECT COUNT(*) FROM $table + WHERE $table.svcnum = cust_svc.svcnum + AND $search_sql + ) + ) "; + } + FS::part_svc->svc_tables + ). ' ) '; + +} elsif ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { + + $cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unknown svcdb"; + push @extra_sql, "svcdb = '$1'"; + + push @extra_sql, 'pkgnum IS NULL' + if $cgi->param('magic') eq 'unlinked'; + + if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { + my $sortby = $1; + $orderby = "ORDER BY $sortby"; + } + +} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) { + + push @extra_sql, "svcpart = $1"; + +} else { + errorpage("No search term specified"); +} + +#here is the agent virtualization +push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( + 'null_right' => 'View/link unlinked services' + ); + +my $extra_sql = ' WHERE '. join(' AND ', @extra_sql ); + +my $sql_query = { + 'select' => join(', ', + 'cust_svc.*', + 'part_svc.*', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'table' => 'cust_svc', + 'addl_from' => $addl_from, + 'hashref' => {}, + 'extra_sql' => "$extra_sql $orderby", +}; + +my $count_query = "SELECT COUNT(*) FROM cust_svc $addl_from $extra_sql"; + +my $link = sub { + my $cust_svc = shift; + my $url = svc_url( + 'm' => $m, + 'action' => 'view', + #'part_svc' => $cust_svc->part_svc, + 'svcdb' => $cust_svc->svcdb, #we have it from the joined search + #'svc' => $cust_svc, #redundant + 'query' => '', + ); + [ $url, 'svcnum' ]; +}; + +my $link_cust = sub { + my $cust_svc = shift; + if ( $cust_svc->custnum ) { + [ "${p}view/cust_main.cgi?", 'custnum' ]; + } else { + ''; + } +}; + +</%init> diff --git a/httemplate/search/cust_tax_adjustment.html b/httemplate/search/cust_tax_adjustment.html new file mode 100644 index 000000000..925476516 --- /dev/null +++ b/httemplate/search/cust_tax_adjustment.html @@ -0,0 +1,54 @@ +<% include( 'elements/search.html', + 'title' => $title, + 'name_singular' => 'tax adjustment', + 'query' => $query, + 'count_query' => $count_query, + 'header' => [ 'Tax', 'Amount', 'Comment', 'Invoice' ], + 'fields' => [ 'taxname', + sub { $money_char. shift->amount }, + 'comment', + sub { my $l = shift->cust_bill_pkg; + $l ? '#'.$l->invnum : ''; + }, + ], + 'links' => [ '', '', '', $ilink ], + ) +%> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Add customer tax adjustment'); + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my $count_query = 'SELECT COUNT(*) FROM cust_tax_adjustment'; + +my $hashref = {}; + +my $custnum = ''; +my $cust_main = ''; +if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { + $custnum = $1; + $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ); + $hashref->{'custnum'} = $custnum; + $count_query .= " WHERE custnum = $custnum "; +} + +my $title = 'Tax adjustments'; +$title .= ' for '. $cust_main->name if $cust_main; + +my $query = { 'table' => 'cust_tax_adjustment', + 'hashref' => $hashref, + }; + +my $ilink = [ $p.'view/cust_bill.cgi?', sub { my $l = shift->cust_bill_pkg; + $l ? $l->invnum : 'EXCEPTION'; + } + ]; + +#XXX would be nice to list customer fields on the report too, if we ever need +# to link to here without a custnum (i'm sure we will, eventually...) + +</%init> diff --git a/httemplate/search/cust_tax_exempt.cgi b/httemplate/search/cust_tax_exempt.cgi new file mode 100644 index 000000000..3704b208a --- /dev/null +++ b/httemplate/search/cust_tax_exempt.cgi @@ -0,0 +1,139 @@ +<% include( 'elements/search.html', + 'title' => 'Legacy tax exemptions', + 'name' => 'legacy tax exemptions', + 'query' => $query, + 'count_query' => $count_query, + 'count_addl' => [ $money_char. '%.2f total', ], + 'header' => [ + '#', + 'Month', + 'Inserted', + 'Amount', + FS::UI::Web::cust_header(), + ], + 'fields' => [ + 'exemptnum', + sub { $_[0]->month. '/'. $_[0]->year; }, + sub { my $h = $_[0]->h_search('insert'); + $h ? time2str('%L/%d/%Y', $h->history_date ) : '' + }, + sub { $money_char. $_[0]->amount; }, + + \&FS::UI::Web::cust_fields, + ], + 'links' => [ + '', + '', + '', + '', + + ( map { $_ ne 'Cust. Status' ? $clink : '' } + FS::UI::Web::cust_header() + ), + ], + 'align' => 'rrrr'.FS::UI::Web::cust_aligns(), + 'color' => [ + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +my $join_cust = " + LEFT JOIN cust_main USING ( custnum ) +"; + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('View customer tax exemptions'); + +my @where = (); + +#my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +#if ( $beginning || $ending ) { +# push @where, "_date >= $beginning", +# "_date <= $ending"; +# #"payby != 'COMP'; +#} + +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @where, "agentnum = $1"; +} + +if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { + push @where, "cust_main.custnum = $1"; +} + +#prospect active inactive suspended cancelled +if ( grep { $cgi->param('status') eq $_ } FS::cust_main->statuses() ) { + my $method = $cgi->param('status'). '_sql'; + #push @where, $class->$method(); + push @where, FS::cust_main->$method(); +} + +if ( $cgi->param('out') ) { + + push @where, " + 0 = ( + SELECT COUNT(*) FROM cust_main_county AS county_out + WHERE ( county_out.county = cust_main.county + OR ( county_out.county IS NULL AND cust_main.county = '' ) + OR ( county_out.county = '' AND cust_main.county IS NULL) + OR ( county_out.county IS NULL AND cust_main.county IS NULL) + ) + AND ( county_out.state = cust_main.state + OR ( county_out.state IS NULL AND cust_main.state = '' ) + OR ( county_out.state = '' AND cust_main.state IS NULL ) + OR ( county_out.state IS NULL AND cust_main.state IS NULL ) + ) + AND county_out.country = cust_main.country + AND county_out.tax > 0 + ) + "; + +} elsif ( $cgi->param('country' ) ) { + + my $county = dbh->quote( $cgi->param('county') ); + my $state = dbh->quote( $cgi->param('state') ); + my $country = dbh->quote( $cgi->param('country') ); + push @where, "( county = $county OR $county = '' )", + "( state = $state OR $state = '' )", + " country = $country"; + push @where, 'taxclass = '. dbh->quote( $cgi->param('taxclass') ) + if $cgi->param('taxclass'); + +} + +my $where = scalar(@where) ? 'WHERE '.join(' AND ', @where) : ''; + +my $count_query = "SELECT COUNT(*), SUM(amount)". + " FROM cust_tax_exempt $join_cust $where"; + +my $query = { + 'table' => 'cust_tax_exempt', + 'addl_from' => $join_cust, + 'hashref' => {}, + 'select' => join(', ', + 'cust_tax_exempt.*', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'extra_sql' => $where, +}; + +my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ]; + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +</%init> diff --git a/httemplate/search/cust_tax_exempt.html b/httemplate/search/cust_tax_exempt.html new file mode 100644 index 000000000..869854f06 --- /dev/null +++ b/httemplate/search/cust_tax_exempt.html @@ -0,0 +1,31 @@ +<% include('/elements/header.html', 'Legacy tax exemption report' ) %> + +<FORM ACTION="cust_tax_exempt.cgi" METHOD="GET"> + + <TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Search options</FONT></TH> + </TR> + + <% include( '/elements/tr-select-cust_main-status.html', + 'label' => 'Customer Status' + ) + %> + + </TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('View customer tax exemptions'); + +</%init> + diff --git a/httemplate/search/cust_tax_exempt_pkg.cgi b/httemplate/search/cust_tax_exempt_pkg.cgi new file mode 100644 index 000000000..3a5155ae8 --- /dev/null +++ b/httemplate/search/cust_tax_exempt_pkg.cgi @@ -0,0 +1,182 @@ +<% include( 'elements/search.html', + 'title' => 'Tax exemptions', + 'name' => 'tax exemptions', + 'query' => $query, + 'count_query' => $count_query, + 'count_addl' => [ $money_char. '%.2f total', ], + 'header' => [ + '#', + 'Month', + 'Amount', + 'Line item', + 'Invoice', + 'Date', + FS::UI::Web::cust_header(), + ], + 'fields' => [ + 'exemptpkgnum', + sub { $_[0]->month. '/'. $_[0]->year; }, + sub { $money_char. $_[0]->amount; }, + + sub { + $_[0]->billpkgnum. ': '. + ( $_[0]->pkgnum > 0 + ? $_[0]->get('pkg') + : $_[0]->get('itemdesc') + ). + ' ('. + ( $_[0]->setup > 0 + ? $money_char. $_[0]->setup. ' setup' + : '' + ). + ( $_[0]->setup > 0 && $_[0]->recur > 0 + ? ' / ' + : '' + ). + ( $_[0]->recur > 0 + ? $money_char. $_[0]->recur. ' recur' + : '' + ). + ')'; + }, + + 'invnum', + sub { time2str('%b %d %Y', shift->_date ) }, + + \&FS::UI::Web::cust_fields, + ], + 'links' => [ + '', + '', + '', + + '', + $ilink, + $ilink, + + ( map { $_ ne 'Cust. Status' ? $clink : '' } + FS::UI::Web::cust_header() + ), + ], + 'align' => 'rrrlrc'.FS::UI::Web::cust_aligns(), # 'rlrrrc', + 'color' => [ + '', + '', + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%once> + +my $join_cust = " + JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_main USING ( custnum ) +"; + +my $join_pkg = " + LEFT JOIN cust_pkg USING ( pkgnum ) + LEFT JOIN part_pkg USING ( pkgpart ) +"; + +my $join = " + JOIN cust_bill_pkg USING ( billpkgnum ) + $join_cust + $join_pkg +"; + +</%once> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('View customer tax exemptions'); + +my @where = (); + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +if ( $beginning || $ending ) { + push @where, "_date >= $beginning", + "_date <= $ending"; + #"payby != 'COMP'; +} + +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @where, "cust_main.agentnum = $1"; +} + +if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { + push @where, "cust_main.custnum = $1"; +} + +if ( $cgi->param('out') ) { + + push @where, " + 0 = ( + SELECT COUNT(*) FROM cust_main_county AS county_out + WHERE ( county_out.county = cust_main.county + OR ( county_out.county IS NULL AND cust_main.county = '' ) + OR ( county_out.county = '' AND cust_main.county IS NULL) + OR ( county_out.county IS NULL AND cust_main.county IS NULL) + ) + AND ( county_out.state = cust_main.state + OR ( county_out.state IS NULL AND cust_main.state = '' ) + OR ( county_out.state = '' AND cust_main.state IS NULL ) + OR ( county_out.state IS NULL AND cust_main.state IS NULL ) + ) + AND county_out.country = cust_main.country + AND county_out.tax > 0 + ) + "; + +} elsif ( $cgi->param('country' ) ) { + + my $county = dbh->quote( $cgi->param('county') ); + my $state = dbh->quote( $cgi->param('state') ); + my $country = dbh->quote( $cgi->param('country') ); + push @where, "( county = $county OR $county = '' )", + "( state = $state OR $state = '' )", + " country = $country"; + push @where, 'taxclass = '. dbh->quote( $cgi->param('taxclass') ) + if $cgi->param('taxclass'); + +} + +my $where = scalar(@where) ? 'WHERE '.join(' AND ', @where) : ''; + +my $count_query = "SELECT COUNT(*), SUM(amount)". + " FROM cust_tax_exempt_pkg $join $where"; + +my $query = { + 'table' => 'cust_tax_exempt_pkg', + 'addl_from' => $join, + 'hashref' => {}, + 'select' => join(', ', + 'cust_tax_exempt_pkg.*', + 'cust_bill_pkg.*', + 'cust_bill.*', + 'part_pkg.pkg', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'extra_sql' => $where, +}; + +my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ]; +my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ]; + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +</%init> diff --git a/httemplate/search/elements/cust_main_dayranges.html b/httemplate/search/elements/cust_main_dayranges.html new file mode 100644 index 000000000..c53e68016 --- /dev/null +++ b/httemplate/search/elements/cust_main_dayranges.html @@ -0,0 +1,268 @@ +<%doc> + +Example: + + include( 'elements/cust_main_dayranges.html', + 'title' => 'Accounts Receivable Aging Summary', + 'range_sub' => $mysub, + ) + + my $mysub = sub { + my( $start, $end ) = @_; + + "SQL EXPRESSION BASED ON $start AND $end"; + }; + +</%doc> +<% include( 'search.html', + 'name' => 'customers', + 'query' => $sql_query, + 'count_query' => $count_sql, + 'header' => [ + FS::UI::Web::cust_header(), + '0-30', + '30-60', + '60-90', + '90+', + 'Total', + @pay_head, + ], + 'footer' => [ + 'Total', + ( map '', + ( 1 .. + scalar(FS::UI::Web::cust_header()-1) + ), + ), + + sprintf( $money_char.'%.2f', + $row->{'rangecol_0_30'} ), + sprintf( $money_char.'%.2f', + $row->{'rangecol_30_60'} ), + sprintf( $money_char.'%.2f', + $row->{'rangecol_60_90'} ), + sprintf( $money_char.'%.2f', + $row->{'rangecol_90_0'} ), + sprintf( '<b>'. $money_char.'%.2f'. '</b>', + $row->{'rangecol_0_0'} ), + ('') x @pay_labels, + ], + 'fields' => [ + FS::UI::Web::cust_fields_subs(), + format_rangecol('0_30'), + format_rangecol('30_60'), + format_rangecol('60_90'), + format_rangecol('90_0'), + format_rangecol('0_0'), + @pay_labels, + ], + 'links' => [ + ( map { $_ ne 'Cust. Status' ? $clink : '' } + FS::UI::Web::cust_header() + ), + '', + '', + '', + '', + '', + @pay_links, + ], + #'align' => 'rlccrrrrr', + 'align' => FS::UI::Web::cust_aligns(). + 'rrrrr'. + ('c' x @pay_labels), + #'size' => [ '', '', '-1', '-1', '', '', '', '', '', ], + #'style' => [ '', '', 'b', 'b', '', '', '', '', 'b', ], + 'size' => [ ( map '', FS::UI::Web::cust_header() ), + #'-1', '', '', '', '', '', ], + '', '', '', '', '', '', + ( map '', @pay_labels ), + ], + 'style' => [ FS::UI::Web::cust_styles(), + #'b', '', '', '', '', 'b', ], + '', '', '', '', 'b', + ( map '', @pay_labels ), + ], + 'color' => [ + FS::UI::Web::cust_colors(), + '', + '', + '', + '', + '', + '', + ( map '', @pay_labels ), + ], + %opt, + ) +%> +<%init> + +my %opt = @_; + +#actually need to auto-generate other things too for a passed-in ranges to work +my $ranges = $opt{'ranges'} ? delete($opt{'ranges'}) : [ + [ 0, 30 ], + [ 30, 60 ], + [ 60, 90 ], + [ 90, 0 ], + [ 0, 0 ], +]; + +my $range_sub = delete($opt{'range_sub'}); #or die + +my $offset = 0; +if($cgi->param('as_of')) { + $offset = int((time - parse_datetime($cgi->param('as_of'))) / 86400); + $opt{'title'} .= ' ('.$cgi->param('as_of').')' if $offset > 0; +} + +#my $range_cols = join(',', map &{$range_sub}( @$_ ), @ranges ); +my $range_cols = join(',', map call_range_sub($range_sub, @$_, 'offset' => $offset ), @$ranges ); + +my $select_count_pkgs = FS::cust_main->select_count_pkgs_sql; + +my $active_sql = FS::cust_pkg->active_sql; +my $inactive_sql = FS::cust_pkg->inactive_sql; +my $suspended_sql = FS::cust_pkg->suspended_sql; +my $cancelled_sql = FS::cust_pkg->cancelled_sql; + +my $packages_cols = <<END; + ( $select_count_pkgs ) AS num_pkgs_sql, + ( $select_count_pkgs AND $active_sql ) AS active_pkgs, + ( $select_count_pkgs AND $inactive_sql ) AS inactive_pkgs, + ( $select_count_pkgs AND $suspended_sql ) AS suspended_pkgs, + ( $select_count_pkgs AND $cancelled_sql ) AS cancelled_pkgs +END + +my @where = (); + +unless ( $cgi->param('all_customers') ) { +# Exclude entire cust_main records where the balance is >0 + my $days = 0; + if ( $cgi->param('days') =~ /^\s*(\d+)\s*$/ ) { + $days = $1; + } + + push @where, + call_range_sub($range_sub, $days + $offset, 0, 'no_as'=>1). ' > 0'; # != 0'; +} + +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + my $agentnum = $1; + push @where, "agentnum = $agentnum"; +} + +#status (false laziness w/cust_main::search_sql + +#prospect active inactive suspended cancelled +if ( grep { $cgi->param('status') eq $_ } FS::cust_main->statuses() ) { + my $method = $cgi->param('status'). '_sql'; + #push @where, $class->$method(); + push @where, FS::cust_main->$method(); +} + +#here is the agent virtualization +push @where, $FS::CurrentUser::CurrentUser->agentnums_sql; + +my $where = join(' AND ', @where); +$where = "WHERE $where" if $where; + +my $count_sql = "select count(*) from cust_main $where"; + +my $sql_query = { + 'table' => 'cust_main', + 'hashref' => {}, + 'select' => join(',', + #'cust_main.*', + 'custnum', + $range_cols, + $packages_cols, + FS::UI::Web::cust_sql_fields(), + 'payby', + ), + 'extra_sql' => $where, + 'order_by' => "order by coalesce(lower(company), ''), lower(last)", +}; + +my $total_sql = + "SELECT ". + join(',', map call_range_sub( $range_sub, @$_, 'offset' => $offset, 'sum'=>1 ), @$ranges). + " FROM cust_main $where"; + +my $total_sth = dbh->prepare($total_sql) or die dbh->errstr; +$total_sth->execute or die "error executing $total_sql: ". $total_sth->errstr; +my $row = $total_sth->fetchrow_hashref(); + +my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ]; + +my (@payby, @pay_head, @pay_labels, @pay_links); + +my %payby = map {$_ => 1} $conf->config('payby'); +if(%payby) { + push @payby, 'CARD' if ($payby{'CARD'} or $payby{'DCRD'}); + push @payby, 'CHEK' if ($payby{'CHEK'} or $payby{'DCHK'}); +} +else { + @payby = ('CARD','CHEK') +} + +if($opt{'payment_links'} && $curuser->access_right('Process payment') && @payby) { + my %label = ( CARD => 'Card', + CHEK => 'E-Check' ); + push @pay_head, ({nodownload => 1}) foreach @payby; + $pay_head[0] = { label => 'Process', + nodownload => 1, + colspan => scalar(@payby) }; + + @pay_labels = (map { my $payby = $_; + my $label = $label{$payby}; + sub {($payby eq $_[0]->payby) ? "<b>$label (on file)</b>" : $label} + } @payby ); + + @pay_links = (map { [ "${p}misc/payment.cgi?payby=$_;custnum=", 'custnum' ] } + @payby ); +} + +</%init> +<%once> + +my $conf = new FS::Conf; +my $curuser = $FS::CurrentUser::CurrentUser; + +my $money_char = $conf->config('money_char') || '$'; + +#Example: +# +# my $balance = balance( +# $start, $end, +# 'no_as' => 1, #set to true when using in a WHERE clause (supress AS clause) +# #or 0 / omit when using in a SELECT clause as a column +# # ("AS balance_$start_$end") +# 'sum' => 1, #set to true to get a SUM() of the values, for totals +# +# #obsolete? options for totals (passed to cust_main::balance_date_sql) +# 'total' => 1, #set to true to remove all customer comparison clauses +# 'join' => $join, #JOIN clause +# 'where' => \@where, #WHERE clause hashref (elements "AND"ed together) +# ) + +sub call_range_sub { + my($range_sub, $start, $end, %opt) = @_; + + my $as = $opt{'no_as'} ? '' : " AS rangecol_${start}_$end"; + + my $sql = &{$range_sub}( $start, $end, $opt{'offset'} ); #%opt? + + $sql = "SUM($sql)" if $opt{'sum'}; + + $sql.$as; + +} + +sub format_rangecol { #closures help alot + my $range = shift; + sub { sprintf( $money_char.'%.2f', shift->get("rangecol_$range") ) }; +} + +</%once> diff --git a/httemplate/search/elements/cust_pay_or_refund.html b/httemplate/search/elements/cust_pay_or_refund.html new file mode 100755 index 000000000..4f83d0ab6 --- /dev/null +++ b/httemplate/search/elements/cust_pay_or_refund.html @@ -0,0 +1,345 @@ +<%doc> + +Examples: + + include( 'elements/cust_pay_or_refund.html', + 'thing' => 'pay', + 'amount_field' => 'paid', + 'name_singular' => 'payment', + 'name_verb' => 'paid', + ) + + include( 'elements/cust_pay_or_refund.html', + 'thing' => 'refund', + 'amount_field' => 'refund', + 'name_singular' => 'refund', + 'name_verb' => 'refunded', + ) + + include( 'elements/cust_pay_or_refund.html', + 'thing' => 'pay_pending', + 'amount_field' => 'paid', + 'name_singular' => 'pending payment', + 'name_verb' => 'pending', + 'disable_link' => 1, + 'disable_by' => 1, + 'html_init' => '', + 'addl_header' => [], + 'addl_fields' => [], + 'redirect_empty' => $redirect_empty, + ) + + include( 'elements/cust_pay_or_refund.html', + 'table' => 'h_cust_pay', + 'amount_field' => 'paid', + 'name_singular' => 'payment', + 'name_verb' => 'paid', + 'pre_header' => [ 'Transaction', 'By' ], + 'pre_fields' => [ 'history_action', 'history_user' ], + ) + +</%doc> +<% include( 'search.html', + 'title' => $title, + 'name_singular' => $name_singular, + 'query' => $sql_query, + 'count_query' => $count_query, + 'count_addl' => [ '$%.2f total '.$opt{name_verb}, ], + 'redirect_empty' => $opt{'redirect_empty'}, + 'header' => \@header, + 'fields' => \@fields, + 'align' => $align, + 'links' => \@links, + 'color' => \@color, + 'style' => \@style, + ) +%> +<%init> + +my %opt = @_; + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('Financial reports'); + +my $table = $opt{'table'} || 'cust_'.$opt{'thing'}; + +my $amount_field = $opt{'amount_field'}; +my $name_singular = $opt{'name_singular'}; + +my $title = "\u$name_singular Search Results"; + +my $link = ''; +if ( ( $curuser->access_right('View invoices') #XXX for now + || $curuser->access_right('View customer payments') + ) + && ! $opt{'disable_link'} + ) +{ + + my $key; + my $q = ''; + if ( $table eq 'cust_pay_void' ) { + $key = 'paynum'; + $q .= 'void=1;'; + } elsif ( $table eq /^cust_(\w+)$/ ) { + $key = $1.'num'; + } + + if ( $key ) { + $q .= "$key="; + $link = [ "${p}view/$table.html?$q", $key ] + } +} + +my $cust_link = sub { + my $cust_thing = shift; + $cust_thing->cust_main_custnum + ? [ "${p}view/cust_main.cgi?", 'custnum' ] + : ''; +}; + +my @header = (); +my @fields = (); +my $align = ''; +my @links = (); +if ( $opt{'pre_header'} ) { + push @header, @{ $opt{'pre_header'} }; + $align .= 'c' x scalar(@{ $opt{'pre_header'} }); + push @links, map '', @{ $opt{'pre_header'} }; + push @fields, @{ $opt{'pre_fields'} }; +} + +push @header, "\u$name_singular", + 'Amount', + 'Date', +; +$align .= 'rrr'; +push @links, '', '', ''; +push @fields, 'payby_payinfo_pretty', + sub { sprintf('$%.2f', shift->$amount_field() ) }, + sub { time2str('%b %d %Y', shift->_date ) }, +; + +unless ( $opt{'disable_by'} ) { + push @header, 'By'; + $align .= 'c'; + push @links, ''; + push @fields, sub { my $o = shift->otaker; + $o = 'auto billing' if $o eq 'fs_daily'; + $o = 'customer self-service' if $o eq 'fs_selfservice'; + $o; + }; +} + +push @header, FS::UI::Web::cust_header(); +$align .= FS::UI::Web::cust_aligns(); +push @links, map { $_ ne 'Cust. Status' ? $cust_link : '' } + FS::UI::Web::cust_header(); +my @color = ( ( map '', @fields ), FS::UI::Web::cust_colors() ); +my @style = ( ( map '', @fields ), FS::UI::Web::cust_styles() ); +push @fields, \&FS::UI::Web::cust_fields; + +push @header, @{ $opt{'addl_header'} } + if $opt{'addl_header'}; +push @fields, @{ $opt{'addl_fields'} } + if $opt{'addl_fields'}; + +my( $count_query, $sql_query ); +if ( $cgi->param('magic') ) { + + my @search = (); + my $orderby; + if ( $cgi->param('magic') eq '_date' ) { + + if ( $cgi->param('agentnum') && $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @search, "agentnum = $1"; # $search{'agentnum'} = $1; + my $agent = qsearchs('agent', { 'agentnum' => $1 } ); + die "unknown agentnum $1" unless $agent; + $title = $agent->agent. " $title"; + } + + if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { + push @search, "custnum = $1"; + } + + if ( $cgi->param('payby') ) { + $cgi->param('payby') =~ + /^(CARD|CHEK|BILL|PREP|CASH|WEST|MCRD)(-(VisaMC|Amex|Discover|Maestro))?$/ + or die "illegal payby ". $cgi->param('payby'); + push @search, "$table.payby = '$1'"; + if ( $3 ) { + + my $cardtype = $3; + + my $search; + if ( $cardtype eq 'VisaMC' ) { + #avoid posix regexes for portability + $search = + " ( ( substring($table.payinfo from 1 for 1) = '4' ". + " AND substring($table.payinfo from 1 for 4) != '4936' ". + " AND substring($table.payinfo from 1 for 6) ". + " NOT SIMILAR TO '49030[2-9]' ". + " AND substring($table.payinfo from 1 for 6) ". + " NOT SIMILAR TO '49033[5-9]' ". + " AND substring($table.payinfo from 1 for 6) ". + " NOT SIMILAR TO '49110[1-2]' ". + " AND substring($table.payinfo from 1 for 6) ". + " NOT SIMILAR TO '49117[4-9]' ". + " AND substring($table.payinfo from 1 for 6) ". + " NOT SIMILAR TO '49118[1-2]' ". + " )". + " OR substring($table.payinfo from 1 for 2) = '51' ". + " OR substring($table.payinfo from 1 for 2) = '52' ". + " OR substring($table.payinfo from 1 for 2) = '53' ". + " OR substring($table.payinfo from 1 for 2) = '54' ". + " OR substring($table.payinfo from 1 for 2) = '54' ". + " OR substring($table.payinfo from 1 for 2) = '55' ". + " OR substring($table.payinfo from 1 for 2) = '36' ". #Diner's int'l processed as Visa/MC inside US + " ) "; + } elsif ( $cardtype eq 'Amex' ) { + $search = + " ( substring($table.payinfo from 1 for 2 ) = '34' ". + " OR substring($table.payinfo from 1 for 2 ) = '37' ". + " ) "; + } elsif ( $cardtype eq 'Discover' ) { + $search = + " ( substring($table.payinfo from 1 for 4 ) = '6011' ". + " OR substring($table.payinfo from 1 for 2 ) = '65' ". + " OR substring($table.payinfo from 1 for 3 ) = '622' ". #China Union Pay processed as Discover outside CN + " ) "; + } elsif ( $cardtype eq 'Maestro' ) { + $search = + " ( substring($table.payinfo from 1 for 2 ) = '63' ". + " OR substring($table.payinfo from 1 for 2 ) = '67' ". + " OR substring($table.payinfo from 1 for 6 ) = '564182' ". + " OR substring($table.payinfo from 1 for 4 ) = '4936' ". + " OR substring($table.payinfo from 1 for 6 ) ". + " SIMILAR TO '49030[2-9]' ". + " OR substring($table.payinfo from 1 for 6 ) ". + " SIMILAR TO '49033[5-9]' ". + " OR substring($table.payinfo from 1 for 6 ) ". + " SIMILAR TO '49110[1-2]' ". + " OR substring($table.payinfo from 1 for 6 ) ". + " SIMILAR TO '49117[4-9]' ". + " OR substring($table.payinfo from 1 for 6 ) ". + " SIMILAR TO '49118[1-2]' ". + " ) "; + } else { + die "unknown card type $cardtype"; + } + + my $masksearch = $search; + $masksearch =~ s/$table\.payinfo/$table.paymask/gi; + + push @search, + "( $search OR ( $table.paymask IS NOT NULL AND $masksearch ) )"; + + } + } + + if ( $cgi->param('payinfo') ) { + $cgi->param('payinfo') =~ /^\s*(\d+)\s*$/ + or die "illegal payinfo ". $cgi->param('payinfo'); + push @search, "$table.payinfo = '$1'"; + } + + if ( $cgi->param('otaker') =~ /^(\w+)$/ ) { + push @search, "$table.otaker = '$1'"; + } + + #for cust_pay_pending... statusNOT=done + if ( $cgi->param('statusNOT') =~ /^(\w+)$/ ) { + push @search, "status != '$1'"; + } + + my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); + push @search, "_date >= $beginning ", + "_date <= $ending"; + + if ( $table eq 'cust_pay_void' ) { + my($v_beginning, $v_ending) = + FS::UI::Web::parse_beginning_ending($cgi, 'void'); + push @search, "void_date >= $v_beginning ", + "void_date <= $v_ending"; + } + + push @search, FS::UI::Web::parse_lt_gt($cgi, $amount_field ); + + $orderby = '_date'; + + } elsif ( $cgi->param('magic') eq 'paybatch' ) { + + $cgi->param('paybatch') =~ /^([\w\/\:\-\.]+)$/ + or die "illegal paybatch: ". $cgi->param('paybatch'); + + push @search, "paybatch = '$1'"; + + $orderby = "LOWER(company || ' ' || last || ' ' || first )"; + + } else { + die "unknown search magic: ". $cgi->param('magic'); + } + + #for the history search + if ( $cgi->param('history_action') =~ /^([\w,]+)$/ ) { + my @history_action = split(/,/, $1); + push @search, 'history_action IN ('. + join(',', map "'$_'", @history_action ). ')'; + } + + if ( $cgi->param('history_date_beginning') + || $cgi->param('history_date_ending') ) { + my($h_beginning, $h_ending) = + FS::UI::Web::parse_beginning_ending($cgi, 'history_date'); + push @search, "history_date >= $h_beginning ", + "history_date <= $h_ending"; + } + + #here is the agent virtualization + push @search, $curuser->agentnums_sql; + + my $search = ' WHERE '. join(' AND ', @search); + + $count_query = "SELECT COUNT(*), SUM($amount_field) ". + "FROM $table LEFT JOIN cust_main USING ( custnum )". + $search; + + $sql_query = { + 'table' => $table, + 'select' => join(', ', + "$table.*", + 'cust_main.custnum as cust_main_custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'hashref' => {}, + 'extra_sql' => "$search ORDER BY $orderby", + 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )', + }; + +} else { + + #hmm... is this still used? + + $cgi->param('payinfo') =~ /^\s*(\d+)\s*$/ or die "illegal payinfo"; + my $payinfo = $1; + + $cgi->param('payby') =~ /^(\w+)$/ or die "illegal payby"; + my $payby = $1; + + $count_query = "SELECT COUNT(*), SUM($amount_field) FROM $table". + " WHERE payinfo = '$payinfo' AND payby = '$payby'". + " AND ". $curuser->agentnums_sql; + + $sql_query = { + 'table' => $table, + 'hashref' => { 'payinfo' => $payinfo, + 'payby' => $payby }, + 'extra_sql' => $curuser->agentnums_sql. + " ORDER BY _date", + }; + +} + +</%init> diff --git a/httemplate/search/elements/metasearch.html b/httemplate/search/elements/metasearch.html new file mode 100644 index 000000000..b9d3e3ce2 --- /dev/null +++ b/httemplate/search/elements/metasearch.html @@ -0,0 +1,71 @@ +<%doc> + +Example: + + include( 'elements/metasearch.html', + + ### + # required + ### + + 'title' => 'Page title', + + #arrayref of hashrefs suited for passing to elements/search.html + #see that documentation + 'search' => [ + { + query => { 'table' => 'tablename', + #everything else is optional... + 'hashref' => { 'f1' => 'value', + 'f2' => { 'op' => '<', + 'value' => '54', + }, + }, + 'select' => '*', + 'order_by' => 'ORDER BY something', + + }, + count_query => 'SELECT COUNT(*) FROM tablename', + }, + { + query => 'table' => 'anothertablename', + count_query => 'SELECT COUNT(*) FROM anothertablename', + }, + ], + + ### + # optional + ### + + # some HTML callbacks... + 'menubar' => '', #menubar arrayref + 'html_init' => '', #after the header/menubar and before the pager + 'html_form' => '', #after the pager, right before the results + # (only shown if there are results) + # (use this for any form-opening tag rather than + # html_init, to avoid a nested form) + 'html_foot' => '', #at the bottom + 'html_posttotal' => '', #at the bottom + # (these three can be strings or coderefs) + + ); + +</%doc> +% foreach my $search ( @{$opt{search}} ) { +<% include('search.html', + %$search, + 'type' => $type, + 'nohtmlheader' => 1, + ) +%> +% +% } +<%init> + +my(%opt) = @_; +#warn join(' / ', map { "$_ => $opt{$_}" } keys %opt ). "\n"; + +my $type = $cgi->param('_type') =~ /^(csv|\w*\.xls|select|html(-print)?)$/ + ? $1 : 'html' ; + +</%init> diff --git a/httemplate/search/elements/search-csv.html b/httemplate/search/elements/search-csv.html new file mode 100644 index 000000000..9eb1b66d1 --- /dev/null +++ b/httemplate/search/elements/search-csv.html @@ -0,0 +1,54 @@ +% $csv->combine(@$header); #or die $csv->status; +% +<% $opt{no_csv_header} ? '' : $csv->string %>\ +% +% foreach my $row ( @$rows ) { +% +% if ( $opt{'fields'} ) { +% +% my @line = (); +% +% foreach my $field ( @{$opt{'fields'}} ) { +% if ( ref($field) eq 'CODE' ) { +% push @line, map { +% ref($_) eq 'ARRAY' +% ? '(N/A)' #unimplemented +% : $_; +% } +% &{$field}($row); +% } else { +% push @line, $row->$field(); +% } +% } +% +% $csv->combine(@line); #or die $csv->status; +% +% } else { +% $csv->combine(@$row); #or die $csv->status; +% } +% +% +<% $csv->string %>\ +% +% } +<%init> + +my %args = @_; +my $header = $args{'header'}; +my $rows = $args{'rows'}; +my %opt = %{ $args{'opt'} }; + +#http_header('Content-Type' => 'text/comma-separated-values' ); #IE chokes +#http_header('Content-Type' => 'text/plain' ); +http_header('Content-Type' => 'text/csv' ); # So saith RFC 4180 +http_header('Content-Disposition' => + 'attachment;filename="'.($opt{'name'} || PL($opt{'name_singular'}) ).'.csv"'); + +my $quote_char = '"'; +$quote_char = $opt{csv_quote} if exists($opt{csv_quote}); + +my $csv = new Text::CSV_XS { 'always_quote' => $opt{avoid_quote} ? 0 : 1, + 'eol' => "\n", #"\015\012", #"\012" + }; + +</%init> diff --git a/httemplate/search/elements/search-html.html b/httemplate/search/elements/search-html.html new file mode 100644 index 000000000..e5e6ca954 --- /dev/null +++ b/httemplate/search/elements/search-html.html @@ -0,0 +1,477 @@ +% +% if ( exists($opt{'redirect'}) && $opt{'redirect'} +% && scalar(@$rows) == 1 && $total == 1 +% && $type ne 'html-print' +% ) { +% my $redirect = $opt{'redirect'}; +% $redirect = &{$redirect}($rows->[0], $cgi) if ref($redirect) eq 'CODE'; +% my( $url, $method ) = @$redirect; +% redirect( $url. $rows->[0]->$method() ); +% } elsif ( exists($opt{'redirect_empty'}) && ! scalar(@$rows) && $total == 0 +% && $type ne 'html-print' +% && $opt{'redirect_empty'} +% && ( ref($opt{'redirect_empty'}) ne 'CODE' +% || &{$opt{'redirect_empty'}}($cgi) ) +% ) { +% my $redirect = $opt{'redirect_empty'}; +% $redirect = &{$redirect}($cgi) if ref($redirect) eq 'CODE'; +% redirect( $redirect ); +% } else { +% if ( $opt{'name_singular'} ) { +% $opt{'name'} = PL($opt{'name_singular'}); +% } +% ( my $xlsname = $opt{'name'} ) =~ s/\W//g; +% if ( $total == 1 ) { +% if ( $opt{'name_singular'} ) { +% $opt{'name'} = $opt{'name_singular'} +% } else { +% #$opt{'name'} =~ s/s$// if $total == 1; +% $opt{'name'} =~ s/((s)e)?s$/$2/ if $total == 1; +% } +% } +% +% if ( $type eq 'html-print' ) { + + <% $opt{nohtmlheader} + ? '' + : include( '/elements/header-popup.html', $opt{'title'} ) + %> + +% } elsif ( $type eq 'select' ) { + + <% $opt{nohtmlheader} + ? '' + : include( '/elements/header-popup.html', $opt{'title'} ) + %> + <% defined($opt{'html_init'}) + ? ( ref($opt{'html_init'}) + ? &{$opt{'html_init'}}() + : $opt{'html_init'} + ) + : '' + %> + +% } else { +% +% my @menubar = (); +% if ( $opt{'menubar'} ) { +% @menubar = @{ $opt{'menubar'} }; +% #} else { +% # @menubar = ( 'Main menu' => $p ); +% } + + <% $opt{nohtmlheader} + ? '' + : include( '/elements/header.html', $opt{'title'}, + include( '/elements/menubar.html', @menubar ) + ) + %> + + <% defined($opt{'html_init'}) + ? ( ref($opt{'html_init'}) + ? &{$opt{'html_init'}}() + : $opt{'html_init'} + ) + : '' + %> + +% } + +% unless ( $total ) { +% unless ( $opt{'disable_nonefound'} ) { + No matching <% $opt{'name'} %> found.<BR> +% } +% } +% +% if ( $total || $opt{'disableable'} ) { #hmm... and there *are* ones to show?? + + <TABLE> + <TR> + + <TD VALIGN="bottom"> + + <FORM> + +% if (! $opt{'disable_total'}) { + <% $total %> total <% $opt{'name'} %> +% } + +% if ( $confmax && $total > $confmax +% && ! $opt{'disable_maxselect'} +% && $type ne 'html-print' ) +% { +% $cgi->delete('maxrecords'); +% $cgi->param('_dummy', 1); + + ( show <SELECT NAME="maxrecords" onChange="window.location = '<% $self_url %>;maxrecords=' + this.options[this.selectedIndex].value;"> + +% foreach my $max ( map { $_ * $confmax } qw( 1 5 10 25 ) ) { + <OPTION VALUE="<% $max %>" <% ( $maxrecords == $max ) ? 'SELECTED' : '' %>><% $max %></OPTION> +% } + + </SELECT> per page ) + +% $cgi->param('maxrecords', $maxrecords); +% } + +% if ( defined($opt{'html_posttotal'}) && $type ne 'html-print' ) { + <% ref($opt{'html_posttotal'}) + ? &{$opt{'html_posttotal'}}() + : $opt{'html_posttotal'} + %> +% } + <BR> + +% if ( $opt{'count_addl'} ) { +% my $n=0; +% foreach my $count ( @{$opt{'count_addl'}} ) { +% my $data = $count_arrayref->[++$n]; +% if ( ref($count) ) { + <% &{ $count }( $data ) %> +% } else { + <% sprintf( $count, $data ) %><BR> +% } +% } +% } + </FORM> + + </TD> + +% unless ( $opt{'disable_download'} || $type eq 'html-print' ) { + + <TD ALIGN="right"> + + Download full results<BR> + +% $cgi->param('_type', "$xlsname.xls" ); + as <A HREF="<% $self_url %>">Excel spreadsheet</A><BR> + +% $cgi->param('_type', 'csv'); + as <A HREF="<% $self_url %>">CSV file</A><BR> + +% if ( defined($opt{xml_elements}) ) { +% $cgi->param('_type', 'xml'); + as <A HREF="<% $self_url %>">XML file</A><BR> +% } + +% $cgi->param('_type', 'html-print'); + as <A HREF="<% $self_url %>">printable copy</A> + + </TD> +% $cgi->param('_type', "html" ); +% } + + </TR> + <TR> + <TD COLSPAN=2> + +% my $pager = ''; +% unless ( $type eq 'html_print' ) { + + <% $pager = include( '/elements/pager.html', + 'offset' => $offset, + 'num_rows' => scalar(@$rows), + 'total' => $total, + 'maxrecords' => $maxrecords, + ) + %> + + <% defined($opt{'html_form'}) + ? ( ref($opt{'html_form'}) + ? &{$opt{'html_form'}}() + : $opt{'html_form'} + ) + : '' + %> + +% } + + <% include('/elements/table-grid.html') %> + + <TR> +% my $h2 = 0; +% my $colspan = 0; +% foreach my $header ( @{ $opt{header} } ) { +% $colspan-- if $colspan > 0; +% next if $colspan; +% +% my $label = ref($header) ? $header->{label} : $header; +% $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; +% } + + <TR> + +% if ( $opt{'fields'} ) { +% +% my $links = $opt{'links'} ? [ @{$opt{'links'}} ] : ''; +% my $onclicks = $opt{'link_onclicks'} ? [ @{$opt{'link_onclicks'}} ] : []; +% 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->{'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'} ? '</A>' : '' ). +% '</td>'; +% +% } @$rowref ). +% +% '</tr>'; +% } @$tableref ). +% +% '</table>'; +% +% } else { +% $_; +% } +% } +% +% map { +% if ( ref($_) eq 'CODE' ) { +% &{$_}($row); +% } else { +% $row->$_(); +% } +% } +% @{$opt{'fields'}} +% +% ) { +% +% my $class = ( $field =~ /^<TABLE/i ) ? 'inv' : 'grid'; +% +% my $align = $aligns ? shift @$aligns : ''; +% $align = " ALIGN=$align" if $align; +% +% my $a = ''; +% if ( $links ) { +% my $link = shift @$links; +% my $onclick = shift @$onclicks; +% +% 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; +% +% 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>); +% } +% elsif ( $onclick ) { +% $a = qq(<A HREF="javascript:void(0);"$onclick>); +% } +% } +% +% } +% +% 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 %>><% $font %><% $a %><% $s %><% $field %><% $es %><% $a ? '</A>' : '' %><% $font ? '</FONT>' : '' %></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> + + <% $pager %> + + </TD> + </TR> + </TABLE> +% } + +% if ( $type eq 'html-print' ) { +% unless ( $opt{nohtmlheader} ) { + + </BODY></HTML> + +% } +% } else { + + <% defined($opt{'html_foot'}) + ? ( ref($opt{'html_foot'}) + ? &{$opt{'html_foot'}}() + : $opt{'html_foot'} + ) + : '' + %> + + <% $opt{nohtmlheader} + ? '' + : include( '/elements/footer.html' ) + %> + +% } + +% } +<%init> + +my %args = @_; +my $type = $args{'type'}; +my $header = $args{'header'}; +my $rows = $args{'rows'}; +my @link_agentnums = @{ $args{'link_agentnums'} }; +my $null_link = $args{'null_link'}; +my $confmax = $args{'confmax'}; +my $maxrecords = $args{'maxrecords'}; +my $offset = $args{'offset'}; +my %opt = %{ $args{'opt'} }; +my $self_url = $opt{'url'} || $cgi->self_url; + +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; +my $total = $count_arrayref->[0]; + +</%init> diff --git a/httemplate/search/elements/search-xls.html b/httemplate/search/elements/search-xls.html new file mode 100644 index 000000000..8323f55de --- /dev/null +++ b/httemplate/search/elements/search-xls.html @@ -0,0 +1,85 @@ +<% $data %> +<%init> + +my %args = @_; +my $type = $args{'type'}; +my $header = $args{'header'}; +my $rows = $args{'rows'}; +my %opt = %{ $args{'opt'} }; + +#http_header('Content-Type' => 'application/excel' ); #eww +#http_header('Content-Type' => 'application/msexcel' ); #alas +#http_header('Content-Type' => 'application/x-msexcel' ); #? + +#http://support.microsoft.com/kb/199841 +http_header('Content-Type' => 'application/vnd.ms-excel' ); +http_header('Content-Disposition' => + 'attachment;filename="'.($opt{'name'} || PL($opt{'name_singular'}) ).'.xls"'); + +#http://support.microsoft.com/kb/812935 +#http://support.microsoft.com/kb/323308 +$HTML::Mason::Commands::r->headers_out->{'Cache-control'} = 'max-age=0'; + +my $data = ''; +my $XLS = new IO::Scalar \$data; +my $workbook = Spreadsheet::WriteExcel->new($XLS) + or die "Error opening .xls file: $!"; + +my $worksheet = $workbook->add_worksheet(substr($opt{'title'},0,31)); + +$worksheet->protect(); + +my($r,$c) = (0,0); + +my $header_format = $workbook->add_format( + bold => 1, + locked => 1, + bg_color => 55, #22, + bottom => 3, +); + +$worksheet->write($r, $c++, $_, $header_format ) foreach @$header; + +foreach my $row ( @$rows ) { + $r++; + $c = 0; + + if ( $opt{'fields'} ) { + + #my $links = $opt{'links'} ? [ @{$opt{'links'}} ] : ''; + #my $aligns = $opt{'align'} ? [ @{$opt{'align'}} ] : ''; + #could also translate color, size, style into xls equivalents? + my $formats = $opt{'xls_format'} ? [ @{$opt{'xls_format'}} ] : []; + + foreach my $field ( @{$opt{'fields'}} ) { + + my $format = shift @$formats; + $format = &{$format}($row) if ref($format) eq 'CODE'; + $format ||= {}; + my $xls_format = $workbook->add_format(locked=>0, %$format); + + if ( ref($field) eq 'CODE' ) { + foreach my $value ( &{$field}($row) ) { + if ( ref($value) eq 'ARRAY' ) { + $worksheet->write($r, $c++, '(N/A)' ); #unimplemented + } else { + $worksheet->write($r, $c++, $value, $xls_format ); + } + } + } else { + $worksheet->write($r, $c++, $row->$field(), $xls_format ); + } + } + + } else { + my $xls_format = $workbook->add_format(locked=>0); + $worksheet->write($r, $c++, $_, $xls_format ) foreach @$row; + } + +} + +$workbook->close();# or die "Error creating .xls file: $!"; + +http_header('Content-Length' => length($data) ); + +</%init> diff --git a/httemplate/search/elements/search-xml.html b/httemplate/search/elements/search-xml.html new file mode 100644 index 000000000..9f5e9b6c1 --- /dev/null +++ b/httemplate/search/elements/search-xml.html @@ -0,0 +1,88 @@ +% foreach my $row ( @$rows ) { +% +% if (&{$beginrow}($row)){ +<% &{$beginrow}($row) %> +% } +% +% foreach my $i ( 0 .. scalar( @{$opt{'fields'}} ) - 1 ) { +% my $field = $opt{'fields'}->[$i]; +% my $value = ''; +% if ( ref($field) eq 'CODE' ) { +% $value = &{$field}($row); +% $value = '(N/A)' #unimplemented +% if ref($value) eq 'ARRAY'; +% } else { +% $value = $row->$field(); +% } +% +<% &{$beginfield}($row, $i) %><% $value |h %><% &{$endfield}($row, $i) %> +% +% } +% +% if (&{$endrow}($row)) { +<% &{$endrow}($row) %> +% } +% +% } +<%init> + +my %args = @_; +my $header = $args{'header'}; +my $rows = $args{'rows'}; +my %opt = %{ $args{'opt'} }; + +http_header('Content-Type' => 'application/XML' ); # So saith RFC 4180 +http_header('Content-Disposition' => + 'attachment;filename="'.($opt{'name'} || PL($opt{'name_singular'}) ).'.xml"'); + +unless ( $opt{'fields'} ) { + foreach my $i ( 0 .. ( $#{ @$rows[0] } ) ) { + $opt{'fields'}->[$i] = sub { my $row = shift; $row->[$i]; }; + } +} + +my $beginrow = sub { return ''; }; +my $endrow = sub { return ''; }; +if ($opt{xml_row_element}) { + $beginrow = sub { my ($row, $index) = @_; + my $value; + if ( ref($opt{xml_row_element}) eq 'CODE' ) { + $value = &{$opt{xml_row_element}}($row); + } else { + $value = $opt{xml_row_element}; + } + return "<$value>"; + }; + $endrow = sub { my ($row, $index) = @_; + my $value; + if ( ref($opt{xml_row_element}) eq 'CODE' ) { + $value = &{$opt{xml_row_element}}($row); + } else { + $value = $opt{xml_row_element}; + } + return "</$value>"; + }; +} +my $beginfield = sub { my ($row, $index) = @_; + my $value; + if ( ref($opt{xml_elements}->[$index]) eq 'CODE' ) { + $value = &{$opt{xml_elements}->[$index]}($row); + } else { + $value = $opt{xml_elements}->[$index]; + } + return "<$value>"; + }; +my $endfield = sub { my ($row, $index) = @_; + my $value; + if ( ref($opt{xml_elements}->[$index]) eq 'CODE' ) { + $value = &{$opt{xml_elements}->[$index]}($row); + } else { + $value = $opt{xml_elements}->[$index]; + } + return "</$value>"; + }; + +$beginfield = sub { return ''; } if $opt{no_field_elements}; #hmm +$endfield = sub { return ''; } if $opt{no_field_elements}; #hmm + +</%init> diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html new file mode 100644 index 000000000..a258f1721 --- /dev/null +++ b/httemplate/search/elements/search.html @@ -0,0 +1,415 @@ +<%doc> + +Example: + + include( 'elements/search.html', + + ### + # required + ### + + 'title' => 'Page title', + + 'name_singular' => 'item', #singular name for the records returned + #OR# # (preferred, will be pluralized automatically) + 'name' => 'items', #plural name for the records returned + # (deprecated, will be singularlized + # simplisticly) + + #literal SQL query string (deprecated?) or qsearch hashref or arrayref + #of qsearch hashrefs for a union of qsearches + 'query' => { + 'table' => 'tablename', + #everything else is optional... + 'hashref' => { 'field' => 'value', + 'field' => { 'op' => '<', + 'value' => '54', + }, + }, + 'select' => '*', + 'addl_from' => '', #'LEFT JOIN othertable USING ( key )', + 'extra_sql' => '', #'AND otherstuff', #'WHERE onlystuff', + 'order_by' => 'ORDER BY something', + + }, + # "select * from tablename"; + + #required unless 'query' is an SQL query string (shouldn't be...) + 'count_query' => 'SELECT COUNT(*) FROM tablename', + + ### + # recommended / common + ### + + #listref of column labels, <TH> + #recommended unless 'query' is an SQL query string + # (if not specified the database column names will be used) + 'header' => [ '#', + 'Item', + { 'label' => 'Another Item', + + }, + ], + + #listref - each item is a literal column name (or method) or coderef + #if not specified all columns will be shown + 'fields' => [ + 'column', + sub { my $row = shift; $row->column; }, + ], + + #redirect if there's only one item... + # listref of URL base and column name (or method) + # or a coderef that returns the same + 'redirect' => sub { my( $record, $cgi ) = @_; + [ popurl(2).'view/item.html', 'primary_key' ]; + }, + + #redirect if there's no items + # scalar URL or a coderef that returns a URL + 'redirect_empty' => sub { my( $cgi ) = @_; + popurl(2).'view/item.html'; + }, + + ### + # optional + ### + + # some HTML callbacks... + 'menubar' => '', #menubar arrayref + 'html_init' => '', #after the header/menubar and before the pager + 'html_form' => '', #after the pager, right before the results + # (only shown if there are results) + # (use this for any form-opening tag rather than + # html_init, to avoid a nested form) + 'html_foot' => '', #at the bottom + 'html_posttotal' => '', #at the bottom + # (these three can be strings or coderefs) + + 'count_addl' => [], #additional count fields listref of sprintf strings or coderefs + # [ $money_char.'%.2f total paid', ], + + #second (smaller) header line, currently only for HTML + 'header2 => [ '#', + 'Item', + { 'label' => 'Another Item', + + }, + ], + + #listref of column footers + 'footer' => [], + + #disabling things + 'disable_download' => '', # set true to hide the CSV/Excel download links + 'disable_total' => '', # set true to hide the total" + 'disable_maxselect' => '', # set true to disable record/page selection + 'disable_nonefound' => '', # set true to disable the "No matching Xs found" + # message + + #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" + # field, to hide disabled records & have + # "show disabled/hide disabled" links + #(can't be used with a literal query) + 'disabled_statuspos' => 3, #optional position (starting from 0) to insert + #a Status column when showing disabled records + #(query needs to be a qsearch hashref and + # header & fields need to be defined) + + #handling agent virtualization + 'agent_virt' => 1, # set true if this search should be + # agent-virtualized + 'agent_null' => 1, # set true to view global records always + 'agent_null_right' => 'Access Right', # optional right to view global + # records + 'agent_null_right_link' => 'Access Right' # optional right to link to + # global records; defaults to + # same as agent_null_right + '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) + + # link & display properties for fields + + #listref - each item is the empty string, + # 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' => [],` + + #listref - each item is the empty string, + # or a string onClick handler for the corresponding link + # or a coderef that returns string onClick handler + 'link_onclicks' => [], + + #one letter for each column, left/right/center/none + # or pass a listref with full values: [ 'left', 'right', 'center', '' ] + 'align' => 'lrc.', + + #listrefs of ( scalars or coderefs ) + # currently only HTML, maybe eventually Excel too + 'color' => [], + 'size' => [], + 'style' => [], #<B> or <I>, etc. + 'cell_style' => [], #STYLE= attribute of TR, very HTML-specific... + + # Excel-specific listref of ( hashrefs or coderefs ) + # each hashref: http://search.cpan.org/dist/Spreadsheet-WriteExcel/lib/Spreadsheet/WriteExcel.pm#Format_methods_and_Format_properties + 'xls_format' => => [], + + ) + +</%doc> +% if ( $type eq 'csv' ) { +% +<% include('search-csv.html', header=>$header, rows=>$rows, opt=>\%opt ) %> +% +% #} elsif ( $type eq 'excel' ) { +% } elsif ( $type =~ /\.xls$/ ) { +% +<% include('search-xls.html', header=>$header, rows=>$rows, opt=>\%opt ) %> +% +% } elsif ( $type eq 'xml' ) { +% +<% include('search-xml.html', rows=>$rows, opt=>\%opt ) %> +% +% } else { # regular HTML +% +<% include('search-html.html', + type => $type, + header => $header, + rows => $rows, + link_agentnums => \@link_agentnums, + null_link => $null_link, + confmax => $confmax, + maxrecords => $maxrecords, + offset => $offset, + opt => \%opt + ) +%> +% +% } +<%init> + +my(%opt) = @_; +#warn join(' / ', map { "$_ => $opt{$_}" } keys %opt ). "\n"; + +my $curuser = $FS::CurrentUser::CurrentUser; + +my $type = $cgi->param('_type') =~ /^(csv|\w*\.xls|xml|select|html(-print)?)$/ + ? $1 : '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; + my $i = 0; + do { + if( ref($h->[$i]) and exists($h->[$i]->{'nodownload'}) ) { + splice(@{$opt{$_}}, $i, 1) foreach + qw(header footer fields links link_onclicks + align color size style cell_style xls_format); + } + else { + $i++; + } + } while ( exists($h->[$i]) ); +} + +# wtf? +$opt{disable_download} = 0 + if $opt{disable_download} && $curuser->access_right('Configuration download'); + +$opt{disable_download} = 1 + if $opt{really_disable_download}; + +my @link_agentnums = (); +my $null_link = ''; +if ( $opt{'agent_virt'} ) { + + @link_agentnums = $curuser->agentnums; + $null_link = $curuser->access_right( $opt{'agent_null_right_link'} + || $opt{'agent_null_right'} ); + + my $agentnums_sql = $curuser->agentnums_sql( + 'null' => $opt{'agent_null'}, + 'null_right' => $opt{'agent_null_right'}, + 'table' => $opt{'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 ( $opt{'agent_pos'} || $opt{'agent_pos'} eq '0' + and scalar($curuser->agentnums) > 1 ) { + #false laziness w/statuspos above + my $pos = $opt{'agent_pos'}; + + foreach my $att (qw( align color size style cell_style xls_format )) { + $opt{$att} ||= [ map '', @{ $opt{'fields'} } ]; + } + + splice @{ $opt{'header'} }, $pos, 0, 'Agent'; + splice @{ $opt{'align'} }, $pos, 0, 'c'; + splice @{ $opt{'style'} }, $pos, 0, ''; + splice @{ $opt{'size'} }, $pos, 0, ''; + splice @{ $opt{'fields'} }, $pos, 0, + sub { $_[0]->agentnum ? $_[0]->agent->agent : '(global)'; }; + splice @{ $opt{'color'} }, $pos, 0, ''; + splice @{ $opt{'links'} }, $pos, 0, '' #[ 'agent link?', 'agentnum' ] + if $opt{'links'}; + splice @{ $opt{'link_onclicks'} }, $pos, 0, '' + if $opt{'link_onclicks'}; + + } + +} + +if ( $opt{'disableable'} ) { + + unless ( $cgi->param('showdisabled') ) { #modify searches + + $opt{'query'}{'hashref'}{'disabled'} = ''; + $opt{'query'}{'extra_sql'} =~ s/^\s*WHERE/ AND/i; + + $opt{'count_query'} .= + ( $opt{'count_query'} =~ /WHERE/i ? ' AND ' : ' WHERE ' ). + "( disabled = '' OR disabled IS NULL )"; + + } elsif ( $opt{'disabled_statuspos'} + || $opt{'disabled_statuspos'} eq '0' ) { #add status column + + my $pos = $opt{'disabled_statuspos'}; + + foreach my $att (qw( align style color size )) { + $opt{$att} ||= [ map '', @{ $opt{'fields'} } ]; + } + + splice @{ $opt{'header'} }, $pos, 0, 'Status'; + splice @{ $opt{'align'} }, $pos, 0, 'c'; + splice @{ $opt{'style'} }, $pos, 0, 'b'; + splice @{ $opt{'size'} }, $pos, 0, ''; + splice @{ $opt{'fields'} }, $pos, 0, + sub { shift->disabled ? 'DISABLED' : 'Active'; }; + splice @{ $opt{'color'} }, $pos, 0, + sub { shift->disabled ? 'FF0000' : '00CC00'; }; + splice @{ $opt{'links'} }, $pos, 0, '' + if $opt{'links'}; + splice @{ $opt{'link_onlicks'} }, $pos, 0, '' + if $opt{'link_onlicks'}; + } + + #add show/hide disabled links + my $items = $opt{'name'} || PL($opt{'name_singular'}); + if ( $cgi->param('showdisabled') ) { + $cgi->param('showdisabled', 0); + $opt{'html_posttotal'} .= + '( <a href="'. $cgi->self_url. qq!">hide disabled $items</a> )!; #" + $cgi->param('showdisabled', 1); + } else { + $cgi->param('showdisabled', 1); + $opt{'html_posttotal'} .= + '( <a href="'. $cgi->self_url. qq!">show disabled $items</a> )!; #" + $cgi->param('showdisabled', 0); + } + +} + +my $limit = ''; +my($confmax, $maxrecords, $offset ); + +if ( !$type =~ /^(csv|\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:/ + } + + if ( $opt{disableable} && ! $cgi->param('showdisabled') ) { + $opt{count_query} .= + ( ( $opt{count_query} =~ /WHERE/i ) ? ' AND ' : ' WHERE ' ). + "( disabled = '' OR disabled IS NULL )"; + } + + unless ( $type eq 'html-print' ) { + + #setup some pagination things if we're in html mode + + my $conf = new FS::Conf; + $confmax = $conf->config('maxsearchrecordsperpage'); + if ( $cgi->param('maxrecords') =~ /^(\d+)$/ ) { + $maxrecords = $1; + } else { + $maxrecords ||= $confmax; + } + + $limit = $maxrecords ? "LIMIT $maxrecords" : ''; + + $offset = $cgi->param('offset') =~ /^(\d+)$/ ? $1 : 0; + $limit .= " OFFSET $offset" if $offset; + + } + +} + +# run the query + +my $header = [ map { ref($_) ? $_->{'label'} : $_ } @{$opt{header}} ]; +my $rows; +if ( ref($opt{query}) ) { + + my @query; + if (ref($opt{query}) eq 'HASH') { + @query = ( $opt{query} ); + } 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; + } + + #eval "use FS::$opt{'query'};"; + my @param = qw( select table addl_from hashref extra_sql order_by ); + $rows = [ qsearch( [ map { my $query = $_; + ({ map { $_ => $query->{$_} } @param }); + } + @query + ], + 'order_by' => $opt{order_by}. " ". $limit, + ) + ]; +} 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; + + $header ||= $sth->{NAME}; +} + +</%init> diff --git a/httemplate/search/h_cust_pay.html b/httemplate/search/h_cust_pay.html new file mode 100755 index 000000000..99330fadd --- /dev/null +++ b/httemplate/search/h_cust_pay.html @@ -0,0 +1,9 @@ +<% include( 'elements/cust_pay_or_refund.html', + 'table' => 'h_cust_pay', + 'amount_field' => 'paid', + 'name_singular' => 'payment', + 'name_verb' => 'paid', + 'pre_header' => [ 'Transaction', 'By' ], + 'pre_fields' => [ 'history_action', 'history_user' ], + ) +%> diff --git a/httemplate/search/inventory_item.html b/httemplate/search/inventory_item.html new file mode 100644 index 000000000..086c8e92d --- /dev/null +++ b/httemplate/search/inventory_item.html @@ -0,0 +1,198 @@ +<% include( 'elements/search.html', + 'title' => $title, + + 'menubar' => [ 'View inventory classes' => + $p.'browse/inventory_class.html', + 'Upload '. PL($inventory_class->classname)=> + $p.'misc/inventory_item-import.html?'. + "classnum=$classnum" + ], + + 'name' => PL($inventory_class->classname), + + 'query' => { + 'table' => 'inventory_item', + 'hashref' => {}, + 'select' => join(', ', + 'inventory_item.*', + 'part_svc.svcdb', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'extra_sql' => $extra_sql, + 'addl_from' => $addl_from, + }, + + 'count_query' => $count_query, + + 'agent_virt' => 1, + 'agent_null' => 1, + 'agent_pos' => 2, + + 'header' => [ + '#', + $inventory_class->classname, + 'Service', + FS::UI::Web::cust_header(), + '', # checkbox column + ], + + 'fields' => [ + 'itemnum', + 'item', + #'svcnum', #XXX proper full service customer link ala svc_acct + # "unallocated" ? "available" ? + sub { + #this could be way more efficient with a mixin + # like cust_main_Mixin that let us all all the methods + # on data we already have... + my $inventory_item = shift; + my $cust_svc = $inventory_item->cust_svc; + if ( $cust_svc ) { + my($label, $value) = $cust_svc->label; + "$label: $value"; + } else { + '(available)'; + } + }, + + \&FS::UI::Web::cust_fields, + $sub_checkbox, + + ], + 'align' => 'rll'.FS::UI::Web::cust_aligns(), + 'links' => [ + '', + '', + $link, + ( map { $_ ne 'Cust. Status' ? $link_cust : '' } + FS::UI::Web::cust_header() + ), + ], + 'color' => [ + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + 'html_form' => + qq! +<FORM NAME="itemForm" ACTION="$p/misc/inventory_item-move.cgi" METHOD="POST"> +<INPUT TYPE="hidden" NAME="classnum" VALUE="$classnum"> +<INPUT TYPE="hidden" NAME="avail" VALUE="! .$cgi->param('avail') . '">', #' + 'html_foot' => $sub_foot, + ) +%> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('Edit inventory') + || $curuser->access_right('Edit global inventory') + || $curuser->access_right('Configuration'); + +my $classnum = $cgi->param('classnum'); +$classnum =~ /^(\d+)$/ or errorpage("illegal classnum $classnum"); +$classnum = $1; +my $extra_sql = "WHERE inventory_item.classnum = $classnum "; + +my $inventory_class = qsearchs( { + 'table' => 'inventory_class', + 'hashref' => { 'classnum' => $classnum }, +} ); + +my $title = $inventory_class->classname. ' Inventory'; + +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + $extra_sql .= " AND inventory_item.agentnum = $1 "; + my $agent = qsearchs('agent', { 'agentnum' => $1 }) or die "unknown agentnum"; + $title = $agent->agent. " $title"; +} + +#little false laziness with SQL fragments in inventory_class.pm +if ( $cgi->param('avail') ) { + $extra_sql .= ' AND ( svcnum IS NULL OR svcnum = 0 )'; + $title .= ' - Available'; +} elsif ( $cgi->param('used') ) { + $extra_sql .= ' AND svcnum IS NOT NULL AND svcnum > 0'; + $title .= ' - In use'; +} + +my $count_query = + "SELECT COUNT(*) FROM inventory_item $extra_sql"; + +my $link = sub { + my $inventory_item = shift; + if ( $inventory_item->svcnum ) { + + #[ "${p}view/svc_acct.cgi?", 'svcnum' ]; + my $url = svc_url( + 'm' => $m, + 'action' => 'view', + #'svcdb' => $inventory_item->cust_svc->part_svc->svcdb, + 'svcdb' => $inventory_item->svcdb, #we have it from the joined search + 'query' => '', + ); + [ $url, 'svcnum' ]; + } else { + ''; + } +}; +my $link_cust = sub { + my $inventory_item = shift; + if ( $inventory_item->custnum ) { + [ "${p}view/cust_main.cgi?", 'custnum' ]; + } else { + ''; + } +}; + +my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. + ' LEFT JOIN part_svc USING ( svcpart ) '. + ' LEFT JOIN cust_pkg USING ( pkgnum ) '. + ' LEFT JOIN cust_main USING ( custnum ) '; +my $areboxes = 0; + +my $sub_checkbox = sub { + my $item = $_[0]; + my $itemnum = $item->itemnum; + #return '' if $item->svcnum; + $areboxes = 1; + return qq!<INPUT NAME="itemnum$itemnum" TYPE="checkbox" VALUE="1">!; +}; + +my $sub_foot = sub { + return if !$areboxes; + my $foot = +'<BR><INPUT TYPE="button" VALUE="Select all" onClick="setAll(true)"> +<INPUT TYPE="button" VALUE="Unselect all" onClick="setAll(false)"> +<BR><INPUT TYPE="submit" NAME="action" VALUE="Move to agent"> +<SELECT NAME="move_agentnum">'; + foreach my $agent ($curuser->agents) { + $foot .= '<OPTION VALUE="'.$agent->agentnum.'">'. + $agent->agent . '</OPTION> + '; + } + $foot .= '</SELECT> +<SCRIPT TYPE="text/javascript"> + function setAll(setTo) { + theForm = document.itemForm; + for (i=0,n=theForm.elements.length;i<n;i++) + if (theForm.elements[i].name.indexOf("itemnum") != -1) + theForm.elements[i].checked = setTo; + } +</SCRIPT>'; + $foot; +}; + + + + +</%init> diff --git a/httemplate/search/mailinglistmember.html b/httemplate/search/mailinglistmember.html new file mode 100644 index 000000000..ee395f416 --- /dev/null +++ b/httemplate/search/mailinglistmember.html @@ -0,0 +1,57 @@ +<% include('elements/search.html', + 'title' => $title, + 'name_singular' => 'member', + 'query' => $query, + 'count_query' => $count_query, + 'header' => [ 'Email address' ], + 'fields' => [ $email_sub, ], #just this one for now + 'html_init' => $html_init, + ) +%> +<%init> + +#XXX ACL: +#make sure the mailing list is attached to a customer service i can see/view + +$cgi->param('listnum') =~ /^(\d+)$/ or die 'illegal listnum'; +my $listnum = $1; + +my $mailinglist = qsearchs('mailinglist', { 'listnum' => $listnum }) + or die "unknown listnum $listnum"; +my $title = $mailinglist->listname. ' mailing list'; + +my $svc_mailinglist = $mailinglist->svc_mailinglist; + +my $query = { + 'table' => 'mailinglistmember', + 'hashref' => { 'listnum' => $listnum }, +}; + +my $count_query = "SELECT COUNT(*) FROM mailinglistmember WHERE listnum = $listnum"; + +my $email_sub = sub { + my $member = shift; + my $r = $member->email; #just this one for now + my $a = qq[<A HREF="javascript:areyousure('$r', ]. $member->membernum. ')">'; + $r .= " (${a}remove</A>)"; + $r; +}; + +my $html_init = ''; +if ( $svc_mailinglist ) { + my $svcnum = $svc_mailinglist->svcnum; + my $label = encode_entities($svc_mailinglist->label); + $html_init .= qq[<A HREF="${p}/view/svc_mailinglist.cgi?$svcnum">View customer mailing list: $label</A><BR><BR>]; +} + +$html_init .= <<"END"; +<SCRIPT TYPE="text/javascript"> + function areyousure(email,membernum) { + if ( confirm('Are you sure you want to remove ' + email + ' from this mailing list?') ) + window.location.href="${p}misc/delete-mailinglistmember.html?" + membernum; + + } +</SCRIPT> +END + +</%init> diff --git a/httemplate/search/part_pkg.html b/httemplate/search/part_pkg.html new file mode 100644 index 000000000..915dbf448 --- /dev/null +++ b/httemplate/search/part_pkg.html @@ -0,0 +1,213 @@ +<% include( 'elements/search.html', + 'title' => $title, + 'name' => $name, + 'header' => \@header, + 'query' => { 'select' => $select, + 'table' => 'part_pkg', + 'addl_from' => $addl_from, + 'hashref' => {}, + 'extra_sql' => $extra_sql, + 'order_by' => "ORDER BY $order_by", + }, + 'count_query' => $count_query, + 'fields' => \@fields, + 'links' => \@links, + 'align' => $align, + ) +%> +<%init> + +#this is about reports about packages definitions (starting w/commission ones) +# while browse/part_pkg.cgi is config->package definitions + +my $curuser = $FS::CurrentUser::CurrentUser; +die "access denied" + unless $curuser->access_right('Financial reports'); + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my $title = 'Package definition report'; +my $name = 'package definition'; + +my $select = ''; +my $addl_from = ''; +my @where = (); +my @order_by = (); +my @header = (); +my @fields = (); +my @links = (); +my $align = ''; + +if (1) { #commission reports + + if (1) { #employee commission reports + + $select = 'DISTINCT usernum, username, part_pkg.*'; + + $addl_from .= ' CROSS JOIN access_user '; + + if ( $cgi->param('usernum') =~ /^(\d+)$/ ) { + + #XXX in this context, agent virt for employees, not package defs + my $access_user = qsearchs('access_user', { 'usernum' => $1, }) + or die "unknown usernum"; + + $title = $access_user->name; + + } else { + + push @header, 'Employee'; + push @fields, sub { shift->get('username'); }; #access_user->name + push @links, ''; #link to employee edit w/ACL? + $align .= 'c'; + + push @order_by, 'usernum'; #join to username? we're mostly interested in grouping rather than order + + $title = 'Employee'; + + } + + } elsif (0) { #agent commission reports + + if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + + #agent virt + my $agent = qsearchs('agent', { 'agentnum' => $1 }) + or die "unknown agentnum"; + + $title = $agent->agent; + + push @header, 'Agent'; + push @fields, sub { 'XXXagent' }; + push @links, ''; #link to agent edit w/ACL? + $align .= 'c'; + + push @order_by, 'agentnum'; #join to agent? we're mostly interested in grouping rather than order + + } else { + $title = 'Agent'; + } + + } + + $title .= ' commission report'; + $name = "commissionable $name"; + + +} + +push @header, 'Package definition'; +push @fields, 'pkg_comment'; +push @links, ''; #link to pkg definition edit w/ACL? +$align .= 'l'; + +if (1) { #commission reports + + my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); + + my $match = ''; + if (1) { #employee commission reports + $match = 'cust_pkg.usernum = access_user.usernum'; + } elsif (0) { #agent commission reports + $match = 'cust_main.agentnum = agent.agentnum'; + } + + my $from_cust_bill_pkg_where = "FROM cust_bill_pkg + LEFT JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_pkg USING ( pkgnum ) + WHERE cust_bill_pkg.pkgnum > 0 + AND cust_bill._date >= $beginning + AND cust_bill._date <= $ending "; + my $and = " AND $match + AND cust_pkg.pkgpart = part_pkg.pkgpart"; + + push @where, "EXISTS( SELECT 1 $from_cust_bill_pkg_where $and )"; + + push @header, '#'; # of sales'; + push @links, ''; #link to detail report + $align .= 'r'; + push @fields, 'num_cust_pkg'; + $select .= ", ( SELECT COUNT(DISTINCT pkgnum) + $from_cust_bill_pkg_where $and ) + AS num_cust_pkg"; +# push @fields, sub { +# my $part_pkg = shift; +# my $sql = +# #"SELECT COUNT( SELECT DISTINCT pkgnum $from_cust_bill_pkg_where )"; +# "SELECT COUNT(DISTINCT pkgnum) $from_cust_bill_pkg_where"; +# my $sth = dbh->prepare($sql) or die dbh->errstr; +# $sth->execute or die $sth->errstr; +# $sth->fetchrow_arrayref->[0]; +# }; + + push @header, 'Sales'; + push @links, ''; #link to detail report + $align .= 'r'; +# push @fields, sub { $money_char. sprintf('%.2f', shift->get('pkg_sales')); }; +# $select .= +# ", SUM( SELECT setup+recur $from_cust_bill_pkg_where ) AS pkg_sales"; + push @fields, sub { + my $part_pkg = shift; + my $sql = "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) $from_cust_bill_pkg_where AND pkgpart = ? AND "; + my @arg = ($part_pkg->pkgpart); + if (1) { #employee commission reports + $sql .= 'usernum = ?'; + push @arg, $part_pkg->get('usernum'); + } elsif (0) { #agent commission reports + $match = 'cust_main.agentnum = agent.agentnum'; + } + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute(@arg) or die $sth->errstr; + $money_char. sprintf('%.2f', $sth->fetchrow_arrayref->[0] ); + }; + + push @header, 'Commission'; + push @links, ''; #link to detail report + $align .= 'r'; + #push @fields, sub { $money_char. sprintf('%.2f', shift->get('pkg_commission')); }; + push @fields, sub { + my $part_pkg = shift; + my $sql = "SELECT SUM(amount) FROM cust_credit + LEFT JOIN cust_event USING ( eventnum ) + LEFT JOIN part_event USING ( eventpart ) + LEFT JOIN cust_pkg ON ( cust_event.tablenum = cust_pkg.pkgnum ) + WHERE eventnum IS NOT NULL + AND action IN ( 'pkg_employee_credit', + 'pkg_employee_credit_pkg' + ) + AND cust_credit._date >= $beginning + AND cust_credit._date <= $ending + AND pkgpart = ? + AND cust_credit.custnum = ? + "; + my @arg = ($part_pkg->pkgpart); + if (1) { #employee commission reports + + #XXX in this context, agent virt for employees, not package defs + my $access_user = qsearchs('access_user', { 'usernum' => $part_pkg->get('usernum'), }) + or die "unknown usernum"; + + return 0 unless $access_user->user_custnum; + push @arg, $access_user->user_custnum; + + } elsif (0) { #agent commission reports + push @arg, 'XXXagent_custnum'; #$agent->agent_custnum + } + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute(@arg) or die $sth->errstr; + $money_char. sprintf('%.2f', $sth->fetchrow_arrayref->[0] ); + + }; + +} + +push @order_by, 'pkgpart'; #pkg? + +$select ||= 'part_pkg.*'; +my $extra_sql = scalar(@where) ? 'WHERE ' . join(' AND ', @where) : ''; +my $order_by = join(', ', @order_by); + +my $count_query = "SELECT COUNT(*) FROM part_pkg $addl_from $extra_sql"; + +</%init> diff --git a/httemplate/search/pay_batch.cgi b/httemplate/search/pay_batch.cgi new file mode 100755 index 000000000..ebd323e13 --- /dev/null +++ b/httemplate/search/pay_batch.cgi @@ -0,0 +1,130 @@ +<% include( 'elements/search.html', + 'title' => 'Payment Batches', + 'name_singular' => 'batch', + 'query' => { 'table' => 'pay_batch', + 'hashref' => $hashref, + 'extra_sql' => "$extra_sql ORDER BY batchnum DESC", + }, + 'count_query' => "$count_query $extra_sql", + 'header' => [ 'Batch', + 'Type', + 'First Download', + 'Last Upload', + 'Item Count', + 'Amount', + 'Status', + ], + 'align' => 'rcllrrc', + 'fields' => [ 'batchnum', + sub { + FS::payby->shortname(shift->payby); + }, + sub { + my $self = shift; + my $_date = $self->download; + if ( $_date ) { + time2str("%a %b %e %T %Y", $_date); + } elsif ( $self->status eq 'O' ) { + 'Download batch'; + } else { + ''; + } + }, + sub { + my $self = shift; + my $_date = $self->upload; + if ( $_date ) { + time2str("%a %b %e %T %Y", $_date); + } elsif ( $self->status eq 'I' ) { + 'Upload results'; + } else { + ''; + } + }, + sub { + my $st = "SELECT COUNT(*) from cust_pay_batch WHERE batchnum=" . shift->batchnum; + my $sth = dbh->prepare($st) + or die dbh->errstr. "doing $st"; + $sth->execute + or die "Error executing \"$st\": ". $sth->errstr; + $sth->fetchrow_arrayref->[0]; + }, + sub { + my $st = "SELECT SUM(amount) from cust_pay_batch WHERE batchnum=" . shift->batchnum; + my $sth = dbh->prepare($st) + or die dbh->errstr. "doing $st"; + $sth->execute + or die "Error executing \"$st\": ". $sth->errstr; + $sth->fetchrow_arrayref->[0]; + }, + sub { + $statusmap{shift->status}; + }, + ], + 'links' => [ + $link, + '', + sub { shift->status eq 'O' ? $link : '' }, + sub { shift->status eq 'I' ? $link : '' }, + ], + 'size' => [ + '', + '', + sub { shift->status eq 'O' ? "+1" : '' }, + sub { shift->status eq 'I' ? "+1" : '' }, + ], + 'style' => [ + '', + '', + sub { shift->status eq 'O' ? "b" : '' }, + sub { shift->status eq 'I' ? "b" : '' }, + ], + ) + +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports') + || $FS::CurrentUser::CurrentUser->access_right('Process batches'); + +my %statusmap = ('I'=>'In Transit', 'O'=>'Open', 'R'=>'Resolved'); +my $hashref = {}; +my $count_query = 'SELECT COUNT(*) FROM pay_batch'; + +my($begin, $end) = ( '', '' ); + +my @where; +if ( $cgi->param('beginning') + && $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/ ) { + $begin = parse_datetime($1); + push @where, "download >= $begin"; +} +if ( $cgi->param('ending') + && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) { + $end = parse_datetime($1) + 86399; + push @where, "download < $end"; +} + +my @status; +if ( $cgi->param('open') ) { + push @status, "O"; +} + +if ( $cgi->param('intransit') ) { + push @status, "I"; +} + +if ( $cgi->param('resolved') ) { + push @status, "R"; +} + +push @where, + scalar(@status) ? q!(status='! . join(q!' OR status='!, @status) . q!')! + : q!status='X'!; # kludgy, X is unused at present + +my $extra_sql = scalar(@where) ? 'WHERE ' . join(' AND ', @where) : ''; + +my $link = [ "${p}search/cust_pay_batch.cgi?dcln=1;batchnum=", 'batchnum' ]; + +</%init> diff --git a/httemplate/search/pay_batch.html b/httemplate/search/pay_batch.html new file mode 100644 index 000000000..5907169d8 --- /dev/null +++ b/httemplate/search/pay_batch.html @@ -0,0 +1,33 @@ +<% include('/elements/header.html', 'Batch criteria' ) %> + +<FORM ACTION="pay_batch.cgi" METHOD="GET"> +<INPUT TYPE="hidden" NAME="magic" VALUE="_date"> + +<TABLE> + <% include( '/elements/tr-input-beginning_ending.html' ) %> + <TR> + <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="open" VALUE="1" CHECKED></TD> + <TD>Show open batches</TD> + </TR> + <TR> + <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="intransit" VALUE="1" CHECKED></TD> + <TD>Show in-transit batches</TD> + </TR> + <TR> + <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="resolved" VALUE="1" CHECKED></TD> + <TD>Show resolved batches</TD> + </TR> +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Batches"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +</%init> diff --git a/httemplate/search/phone_avail.html b/httemplate/search/phone_avail.html new file mode 100644 index 000000000..2388d25ff --- /dev/null +++ b/httemplate/search/phone_avail.html @@ -0,0 +1,102 @@ +<% include( 'elements/search.html', + 'title' => 'Phone Number (DID) Search Results', + 'name_singular' => 'phone number', + 'query' => { + 'table' => 'phone_avail', + 'hashref' => {}, + 'select' => join(', ', + 'phone_avail.*', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'extra_sql' => $search, + 'addl_from' => $addl_from, + }, + 'count_query' => $count_query, + 'header' => [ '#', + 'State', + 'Phone Number', + 'Export', + 'Service', + FS::UI::Web::cust_header(), + ], + 'fields' => [ + 'availnum', + 'state', + sub { my $pn = shift; + '+'. $pn->countrycode. ' '. + $pn->npa. ' '. $pn->nxx. '-'. $pn->station; + }, + 'exportnum', #XXX + #sub { }, + 'svcnum', #XXX + \&FS::UI::Web::cust_fields, + ], + 'align' => 'rllll'.FS::UI::Web::cust_aligns(), + 'links' => [ + '', + '', + '', + '', #XXX #$export_link + '', #XXX #$svc_link + ( map { $_ ne 'Cust. Status' ? $link_cust : '' } + FS::UI::Web::cust_header() + ), + ], + 'color' => [ + '', + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Import'); + +my @search = (); + +if ( $cgi->param('availbatch') =~ /^([\w\/\:\-\.]+)$/ ) { + push @search, "availbatch = '$1'"; +} + +# #here is the agent virtualization +# push @search, $FS::CurrentUser::CurrentUser->agentnums_sql; + +my $search = scalar(@search) + ? ' WHERE '. join(' AND ', @search) + : ''; + + +my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. + #' LEFT JOIN part_svc USING ( svcpart ) '. + ' LEFT JOIN cust_pkg USING ( pkgnum ) '. + ' LEFT JOIN cust_main USING ( custnum ) '; + +my $count_query = "SELECT COUNT(*) FROM phone_avail $search"; #$addl_from? + +my $link_cust = sub { + my $phone_avail = shift; + if ( $phone_avail->svcnum ) { + my $cust_svc = $phone_avail->svc_phone->cust_svc; + if ( $cust_svc->pkgnum ) { + #my $cust_main = $cust_svc->cust_pkg->cust_main; + return [ "${p}view/cust_main.cgi?", 'custnum' ]; + } + } + ''; +}; + +</%init> diff --git a/httemplate/search/prepay_credit.html b/httemplate/search/prepay_credit.html new file mode 100644 index 000000000..36403511b --- /dev/null +++ b/httemplate/search/prepay_credit.html @@ -0,0 +1,67 @@ +<% include( 'elements/search.html', + 'title' => 'Unused Prepaid Cards'. + ($agent ? ' for '. $agent->agent : ''), + 'menubar' => [ + 'Generate cards' => $p.'edit/prepay_credit.cgi', + ], + 'name' => 'prepaid cards', + 'query' => { 'table' => 'prepay_credit', + 'hashref' => $hashref, + }, + 'count_query' => $count_query, + #'redirect' => $link, + 'header' => [ '#', qw(Amount Time Upload Download Total Agent) ], + 'fields' => [ + 'identifier', + sub { sprintf('$%.2f', shift->amount ) }, + sub { my $c = shift; + $c->seconds ? duration_exact($c->seconds) : '' + }, + sub { my $c = shift; + $c->upbytes + ? FS::UI::bytecount::bytecount_unexact($c->upbytes) + : '' + }, + sub { my $c = shift; + $c->downbytes + ? FS::UI::bytecount::bytecount_unexact($c->downbytes) + : '' + }, + sub { my $c = shift; + $c->totalbytes + ? FS::UI::bytecount::bytecount_unexact($c->totalbytes) + : '' + }, + sub { my $agent = shift->agent; + $agent ? $agent->agent : ''; + }, + ], + 'links' => [ + '', + '', + '', + '', + '', + '', + sub { my $agent = shift->agent; + $agent ? [ "${p}edit/agent.cgi?", 'agentnum' ] : ''; + }, + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $agent = ''; +my $hashref = {}; +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { +$hashref->{agentnum} = $1; +$agent = qsearchs('agent', { 'agentnum' => $1 } ); +} + +my $count_query = 'SELECT COUNT(*) FROM prepay_credit'; +$count_query .= ' WHERE agentnum = '. $agent->agentnum if $agent; + +</%init> diff --git a/httemplate/search/prospect_main.html b/httemplate/search/prospect_main.html new file mode 100644 index 000000000..12e3e1812 --- /dev/null +++ b/httemplate/search/prospect_main.html @@ -0,0 +1,74 @@ +<% include('elements/search.html', + 'title' => 'Prospect Search Results', + 'name_singular' => 'prospect', + 'query' => $query, + 'count_query' => $count_query, + 'header' => [ '#', + 'Prospect', + 'Contact(s)', + ], + 'fields' => [ 'prospectnum', + 'company', + sub { + my $pm = shift; + [ map { + [ { 'data' => $_->line, }, ]; + } + $pm->contact + ]; + }, + ], + 'links' => [ '', + $link, + '', #link to contact edit??? + ], + 'agent_virt' => 1, + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List prospects'); + +my %search_hash = (); + +#$search_hash{'query'} = $cgi->keywords; + +#scalars +my @scalars = qw ( + agentnum +); + +for my $param ( @scalars ) { + $search_hash{$param} = scalar( $cgi->param($param) ) + if $cgi->param($param); +} + +#lists +#for my $param () { +# $search_hash{$param} = [ $cgi->param($param) ]; +#} + +# parse dates +#foreach my $field (qw( signupdate )) { +# +# my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field); +# +# next if $beginning == 0 && $ending == 4294967295; +# #or $disable{$cgi->param('status')}->{$field}; +# +# $search_hash{$field} = [ $beginning, $ending ]; +# +#} + +my $query = FS::prospect_main->search(\%search_hash); +my $count_query = delete($query->{'count_query'}); +#my @extra_headers = @{ delete($query->{'extra_headers'}) }; +#my @extra_fields = @{ delete($query->{'extra_fields'}) }; + +my $link = sub { + my $prospect_main = shift; + [ "${p}view/prospect_main.html?", 'prospectnum' ]; +}; + +</%init> diff --git a/httemplate/search/queue.html b/httemplate/search/queue.html new file mode 100644 index 000000000..e5f7aed6a --- /dev/null +++ b/httemplate/search/queue.html @@ -0,0 +1,142 @@ +<% include( 'elements/search.html', + 'title' => 'Job Queue', + 'name' => 'jobs', + 'html_form' => qq!<FORM NAME="jobForm" ACTION="$p/misc/queue.cgi" METHOD="POST">!, + 'query' => { 'table' => 'queue', + 'hashref' => $hashref, + 'extra_sql' => 'ORDER BY jobnum', + }, + 'count_query' => $count_query, + 'header' => [ '#', + 'Job', + 'Args', + 'Date', + 'Status', + 'Account', # unless $hashref->{'svcnum'} + '', # checkbox column + ], + 'fields' => [ + 'jobnum', + 'job', + sub { + my $queue = shift; + if ( $dangerous + || $queue->job !~ /^FS::part_export::/ + || !$noactions + ) + { + encode_entities( join(' ', $queue->args) ); + } else { + ''; + } + }, + sub { + time2str( "%a %b %e %T %Y", shift->_date ); + }, + sub { + my $queue = shift; + my $jobnum = $queue->jobnum; + my $status = $queue->status; + $status .= ': '. $queue->statustext + if $queue->statustext; + my @queue_depend = $queue->queue_depend; + $status .= ' (waiting for '. + join(', ', map { $_->depend_jobnum } + @queue_depend + ). + ')' + if @queue_depend; + my $changable = $dangerous + || ( ! $noactions + && $status =~ /^failed/ + || $status =~ /^locked/ + || $status =~ /^done/ + ); + if ( $changable ) { + $status .= qq! (!; + $status .= + qq! <A HREF="$p/misc/queue.cgi?jobnum=$jobnum&action=new">retry</A> |! + unless $status =~ /^done/; + $status .= + qq! <A HREF="$p/misc/queue.cgi?jobnum=$jobnum&action=del">remove</A> )!; + } + $status; + }, + sub { + my $queue = shift; + # return '' if $hashref->{'svcnum'} + my $cust_svc = $queue->cust_svc; + my $account; + if ( $cust_svc ) { + my $table = $cust_svc->part_svc->svcdb; + my $label = ( $cust_svc->label )[1]; + qq!<A HREF="../view/$table.cgi?!. $queue->svcnum. + qq!">$label</A>!; + } else { + ''; + } + }, + sub { + my $queue = shift; + my $jobnum = $queue->jobnum; + my $status = $queue->status; + my $changable = $dangerous + || ( ! $noactions + && $status eq 'failed' + || $status eq 'locked' + ); + if ( $changable ) { + $areboxes = 1; + qq!<INPUT NAME="jobnum$jobnum" TYPE="checkbox" VALUE="1">!; + } else { + ''; + } + }, + ], + #'links' => [ + # '', + # '', + # '', + # '', + # '', + # '', #$acct_link, + # '', + # ], + 'html_foot' => sub { + if ( $areboxes ) { + '<BR><INPUT TYPE="button" VALUE="select all" onClick="setAll(true)">'. + '<INPUT TYPE="button" VALUE="unselect all" onClick="setAll(false)">'. + '<BR><INPUT TYPE="submit" NAME="action" VALUE="retry selected">'. + '<INPUT TYPE="submit" NAME="action" VALUE="remove selected"><BR>'. + '<SCRIPT TYPE="text/javascript">'. + ' function setAll(setTo) { '. + ' theForm = document.jobForm;'. + ' for (i=0,n=theForm.elements.length;i<n;i++)'. + ' if (theForm.elements[i].name.indexOf("jobnum") != -1)'. + ' theForm.elements[i].checked = setTo;'. + ' }'. + '</SCRIPT>'; + } else { + ''; + } + }, + ) + +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Job queue'); + +my $hashref = {}; + +my $conf = new FS::Conf; +my $dangerous = $conf->exists('queue_dangerous_controls'); + +my $noactions = 0; + +my $count_query = 'SELECT COUNT(*) FROM queue'; # + $hashref + +my $areboxes = 0; + +</%init> diff --git a/httemplate/search/reg_code.html b/httemplate/search/reg_code.html new file mode 100644 index 000000000..f7d6d2061 --- /dev/null +++ b/httemplate/search/reg_code.html @@ -0,0 +1,40 @@ +<% include( 'elements/search.html', + 'title' => 'Unused Registration Codes for '. + $agent->agent, + 'name' => 'registration codes', + 'query' => { 'table' => 'reg_code', + 'hashref' => { 'agentnum' => $agentnum, }, + }, + 'count_query' => $count_query, + #'redirect' => $link, + 'header' => [ qw(Code Packages) ], + 'fields' => [ + 'code', + sub { + map { + qq!<A HREF="${p}edit/part_pkg.cgi?!. $_->pkgpart. '">'. + $_->pkg_comment(nopkgpart => 1). + '</A><BR>' + } $_[0]->part_pkg + }, + ], + 'links' => [ + '', + #$plink, + '', + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $agentnum = $cgi->param('agentnum'); +$agentnum =~ /^(\d+)$/ or errorpage("illegal agentnum $agentnum"); +$agentnum = $1; +my $agent = qsearchs('agent', { 'agentnum' => $agentnum } ); + +my $count_query = "SELECT COUNT(*) FROM reg_code WHERE agentnum = $agentnum"; + +</%init> diff --git a/httemplate/search/report_477.html b/httemplate/search/report_477.html new file mode 100755 index 000000000..bc2a95806 --- /dev/null +++ b/httemplate/search/report_477.html @@ -0,0 +1,201 @@ +<% include('/elements/header.html', 'FCC Form 477 Report' ) %> + +<FORM ACTION="477.html" METHOD="GET"> +<INPUT TYPE="hidden" NAME="magic" VALUE="active"> + + <TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"> + <FONT SIZE="+1">Search options</FONT> + </TH> + </TR> + + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'disable_empty' => 0, + ) + %> + + <% include( '/elements/tr-select-pkg_class.html', + 'pre_options' => [ '0' => 'all' ], + 'empty_label' => '(empty class)', + ) + %> + + <SCRIPT type="text/javascript"> + function partchange(what) { + var id = 'part' + what.value; + var element = document.getElementById(id); + if (what.checked) { + element.style.display = ''; + } else { + element.style.display = 'none'; + } + } + </SCRIPT> + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Enable part IA?', + 'field' => 'part', + 'value' => 'IA', + 'onchange' => 'partchange(this)', + ) + %> + + <TR id='partIA' style="display:none"><TD>Part IA</TD><TD><TABLE> + <TR><TD>Download speeds</TD><TD> + <TABLE> +% foreach my $speed ( @FS::Report::FCC_477::download ) { + <TR> + <TH><% $speed %></TH> + <TD> + <% include( '/elements/select-table.html', + 'table' => 'part_pkg_report_option', + 'name_col' => 'name', + 'hashref' => { 'disabled' => '' }, + 'element_name' => 'part1_column_option', + 'disable_empty' => 1, + ) + %> + </TD> + </TR> +% } + </TABLE></TD> + <TD>Upload speeds</TD><TD> + <TABLE> +% foreach my $speed ( @FS::Report::FCC_477::upload ) { + <TR> + <TH><% $speed %></TH> + <TD> + <% include( '/elements/select-table.html', + 'table' => 'part_pkg_report_option', + 'name_col' => 'name', + 'hashref' => { 'disabled' => '' }, + 'element_name' => 'part1_row_option', + 'disable_empty' => 1, + ) + %> + </TD> + </TR> +% } + </TABLE></TD></TR> + <TR><TD>Technologies</TD><TD> + <TABLE> +% my $i = 0; +% foreach my $tech ( @FS::Report::FCC_477::technology ) { + <TR> + <TH><% $tech %></TH> + <TD> + <% include( '/elements/select-table.html', + 'table' => 'part_pkg_report_option', + 'name_col' => 'name', + 'hashref' => { 'disabled' => '' }, + 'element_name' => "part1_technology_option_$i", + 'empty_label' => '(omit)', + ) + %> + </TD> + </TR> +% $i++ +% } + </TABLE></TD></TR> + </TABLE></TD></TR> + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Enable part IIA?', + 'field' => 'part', + 'value' => 'IIA', + 'onchange' => 'partchange(this)', + ) + %> + + <TR id='partIIA' style="display:none"><TD>Part IIA</TD><TD><TABLE> +% foreach my $option ( @FS::Report::FCC_477::part2aoption ) { + <TR> + <TH><% $option %></TH> + <TD> + <% include( '/elements/select-table.html', + 'table' => 'part_pkg_report_option', + 'name_col' => 'name', + 'hashref' => { 'disabled' => '' }, + 'element_name' => 'part2a_row_option', + ) + %> + </TD> + </TR> +% } + </TABLE></TD></TR> + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Enable part IIB?', + 'field' => 'part', + 'value' => 'IIB', + 'onchange' => 'partchange(this)', + ) + %> + + <TR id='partIIB' style="display:none"><TD>Part IIB</TD><TD><TABLE> +% foreach my $option ( @FS::Report::FCC_477::part2boption ) { + <TR> + <TH><% $option %></TH> + <TD> + <% include( '/elements/select-table.html', + 'table' => 'part_pkg_report_option', + 'name_col' => 'name', + 'hashref' => { 'disabled' => '' }, + 'element_name' => 'part2b_row_option', + ) + %> + </TD> + </TR> +% } + </TABLE></TD></TR> + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Enable part IV?', + 'field' => 'part', + 'value' => 'IV', + 'onchange' => 'partchange(this)', + ) + %> + + <TR id='partIV' style="display:none"><TD>Part IV</TD><TD><TABLE> + <% include( '/elements/tr-textarea.html', + 'label' => 'Explanatory notes', + 'id' => 'partIV', + 'field' => 'notes', + 'rows' => 15, + 'cols' => 80, + ) + %> + </TABLE></TD></TR> + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Enable part V?', + 'field' => 'part', + 'value' => 'V', + ) + %> + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Enable part VI?', + 'field' => 'part', + 'value' => 'VI', + ) + %> + + </TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List packages'); + +</%init> diff --git a/httemplate/search/report_agent_inventory.html b/httemplate/search/report_agent_inventory.html new file mode 100644 index 000000000..af66043a6 --- /dev/null +++ b/httemplate/search/report_agent_inventory.html @@ -0,0 +1,26 @@ +<% include('/elements/header.html', 'Inventory summary per agent' ) %> + +<FORM ACTION="agent_inventory.html" METHOD="GET"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + +%# select agents + +%# select inventory classes + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Search"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); +#XXX List inventory + +</%init> + diff --git a/httemplate/search/report_cdr.html b/httemplate/search/report_cdr.html new file mode 100644 index 000000000..a50e4db4c --- /dev/null +++ b/httemplate/search/report_cdr.html @@ -0,0 +1,148 @@ +<% include('/elements/header.html', 'Call Detail Record Search' ) %> + +<FORM ACTION="cdr.html" METHOD="GET"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"> + <FONT SIZE="+1">Search options</FONT> + </TH> + </TR> + + <TR> + <TD ALIGN="right">Status: </TD> + <TD> + <SELECT NAME="freesidestatus"> + <OPTION VALUE="">(all) + <OPTION VALUE="NULL">unprocessed + <OPTION VALUE="done">processed + </SELECT> + </TD> + </TR> + +% #if ( ) { # disable for everyone not using termination billing... +% foreach my $termpart ( 1..1 ) { #qsearch('part_termination + + <TR> + <TD ALIGN="right">Termination Status: </TD> + <TD> + <SELECT NAME="termpart<%$termpart%>status"> + <OPTION VALUE="">(all) + <OPTION VALUE="NULL">unprocessed + <OPTION VALUE="done">processed + </SELECT> + </TD> + </TR> + +% } +% #} + + <% include ( '/elements/tr-input-beginning_ending.html' ) %> + + <TR> + <TD ALIGN="right">Source #: </TD> + <TD> + <INPUT TYPE="text" NAME="src"> + </TD> + </TR> + + <TR> + <TD ALIGN="right">Destination #: </TD> + <TD> + <INPUT TYPE="text" NAME="dst"> + </TD> + </TR> + + <TR> + <TD ALIGN="right">Destination Context: </TD> + <TD> + <INPUT TYPE="text" NAME="dcontext"> + </TD> + </TR> + + + <TR> + <TD ALIGN="right">Charged Party #: </TD> + <TD> + <INPUT TYPE="text" NAME="charged_party"> + </TD> + </TR> + + <% include( '/elements/tr-input-lessthan_greaterthan.html', + 'label' => 'Duration (sec)', + 'field' => 'duration', + ) + %> + + <% include( '/elements/tr-input-lessthan_greaterthan.html', + 'label' => 'Billable duration (sec)', + 'field' => 'billsec', + ) + %> + + <% include( '/elements/tr-select-cdrbatch.html' ) %> + + <TR> + <TD ALIGN="right">Acct ID (one per-line):</TD> + <TD><TEXTAREA NAME="acctid"></TEXTAREA></TD> + </TR> + + <TR> + <TH CLASS="background" COLSPAN=2> </TH> + </TR> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Display options</FONT></TH> + </TR> + + <INPUT TYPE="hidden" NAME="show" VALUE="1"> + + <TR> + <TD COLSPAN=2> + <% include('/elements/checkboxes.html', + 'names_list' => $names_list, + 'element_name_prefix' => 'show_', + 'checked_callback' => sub { $show_default{$_[1]} }, + # my($cgi, $name) = @_; + ) + %> + </TD> + </TR> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Search Call Detail Records"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List rating data'); + +my @fields = fields('cdr'); +my $labels = FS::cdr->table_info->{'fields'}; + +#XXX config +my @show_default = qw( + calldate clid src dst dcontext charged_party + startdate answerdate enddate duration billsec + disposition amaflags accountcode userfield + rated_price upstream_price carrierid + svcnum freesidestatus freesiderewritestatus +); +my %show_default = map { $_=>1 } @show_default; + +my $names_list = [ map { + [ $_ => { + 'label' => 'Show '. ( $labels->{$_} || $_ ) + } + ] + } + @fields + ]; + +</%init> diff --git a/httemplate/search/report_cust_bill.html b/httemplate/search/report_cust_bill.html new file mode 100644 index 000000000..00d566a62 --- /dev/null +++ b/httemplate/search/report_cust_bill.html @@ -0,0 +1,51 @@ +<% include('/elements/header.html', 'Invoice Report' ) %> + +<FORM ACTION="cust_bill.html" METHOD="GET"> +<INPUT TYPE="hidden" NAME="magic" VALUE="_date"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0 + + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'label' => 'Invoices for agent: ', + 'disable_empty' => 0, + ) + %> + + <% include( '/elements/tr-input-beginning_ending.html' ) %> + + <% include( '/elements/tr-input-lessthan_greaterthan.html', + label => 'Charged', + field => 'charged', + ) + %> + + <% include( '/elements/tr-input-lessthan_greaterthan.html', + label => 'Owed', + field => 'owed', + ) + %> + + <TR> + <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="open" VALUE="1" CHECKED></TD> + <TD>Show only open invoices</TD> + </TR> + <TR> + <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="newest_percust" VALUE="1"></TD> + <TD>Show only the single most recent invoice per-customer</TD> + </TR> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List invoices'); + +</%init> diff --git a/httemplate/search/report_cust_bill_pkg_discount.html b/httemplate/search/report_cust_bill_pkg_discount.html new file mode 100644 index 000000000..f1879d4a9 --- /dev/null +++ b/httemplate/search/report_cust_bill_pkg_discount.html @@ -0,0 +1,47 @@ +<% include('/elements/header.html', 'Discount report' ) %> + +<FORM ACTION="cust_bill_pkg_discount.html" METHOD="GET"> + + +<TABLE> + + <% include( '/elements/tr-select-otaker.html', + 'label' => 'Discounts by employee: ', + 'otakers' => \@otakers, + ) + %> + + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'label' => 'for agent: ', + 'disable_empty' => 0, + ) + %> + + <% include( '/elements/tr-input-beginning_ending.html' ) %> + + <% include( '/elements/tr-input-lessthan_greaterthan.html', + 'label' => 'Amount', + 'field' => 'amount', + ) + %> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $sth = dbh->prepare("SELECT DISTINCT otaker FROM cust_pkg_discount") + or die dbh->errstr; +$sth->execute or die $sth->errstr; +my @otakers = map { $_->[0] } @{$sth->fetchall_arrayref}; + +</%init> diff --git a/httemplate/search/report_cust_credit.html b/httemplate/search/report_cust_credit.html new file mode 100644 index 000000000..9c719b787 --- /dev/null +++ b/httemplate/search/report_cust_credit.html @@ -0,0 +1,48 @@ +<% include('/elements/header.html', 'Credit report' ) %> + +<FORM ACTION="cust_credit.html" METHOD="GET"> +<INPUT TYPE="hidden" NAME="magic" VALUE="_date"> + +<TABLE> + + <% include( '/elements/tr-select-otaker.html', + 'label' => 'Credits by employee: ', + 'otakers' => \@otakers, + ) + %> + + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'label' => 'for agent: ', + 'disable_empty' => 0, + ) + %> + + <% include( '/elements/tr-input-beginning_ending.html' ) %> + + <% include( '/elements/tr-input-lessthan_greaterthan.html', + 'label' => 'Amount', + 'field' => 'amount', + ) + %> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $sth = dbh->prepare("SELECT DISTINCT otaker FROM cust_credit") + or die dbh->errstr; +$sth->execute or die $sth->errstr; +my @otakers = map { $_->[0] } @{$sth->fetchall_arrayref}; + +</%init> diff --git a/httemplate/search/report_cust_event.html b/httemplate/search/report_cust_event.html new file mode 100644 index 000000000..e0d6242b2 --- /dev/null +++ b/httemplate/search/report_cust_event.html @@ -0,0 +1,49 @@ +<% include( + '/elements/header.html', + ( $cgi->param('failed') ? 'Failed billing events' : 'Billing events' ), + ) +%> + + <FORM ACTION="cust_event.html" METHOD="GET"> + <INPUT TYPE="hidden" NAME="failed" VALUE="<% $cgi->param('failed') ? 1 : 0 %>"> + <TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Search options</FONT></TH> + </TR> + + <% include( '/elements/tr-select-agent.html', 'disable_empty'=>0 ) %> + + <% include( '/elements/tr-select-cust_main-status.html', + 'label' => 'Status' + ) + %> + + <% include( '/elements/tr-select-payby.html', + 'label' => 'Customer payment type', + 'payby_type' => 'cust', + 'multiple' => 1, + 'all_selected' => 1, + ) + %> + + <% include( '/elements/tr-select-part_event.html', + 'label' => 'Events', + 'multiple' => 1, + 'all_selected' => 1, + ) + %> + + <% include( '/elements/tr-input-beginning_ending.html' ) %> + + </TABLE> + <BR><INPUT TYPE="submit" VALUE="Get Report"> + </FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Billing event reports'); + +</%init> diff --git a/httemplate/search/report_cust_main-zip.html b/httemplate/search/report_cust_main-zip.html new file mode 100644 index 000000000..00cb9ed2c --- /dev/null +++ b/httemplate/search/report_cust_main-zip.html @@ -0,0 +1,70 @@ +<% include('/elements/header.html', 'Zip code report') %> + + <FORM ACTION="cust_main-zip.html" METHOD="GET"> + + <TABLE> + + <TR> + <TD ALIGN="right">Billing or service zip</TD> + <TD> + <SELECT NAME="column"> + <OPTION VALUE="zip">Billing zip + <OPTION VALUE="ship_zip">Service zip + </SELECT> + </TD> + </TR> + + <TR> + <TD ALIGN="right">Ignore +4 for US zip codes</TD> + <TD><INPUT TYPE="checkbox" NAME="ignore_plus4" VALUE="yes" CHECKED> </TD> + </TR> + + <TR> + <TD ALIGN="right">Show customers with status</TD> + <TD> + <SELECT NAME="status"> + <OPTION VALUE="">all + <OPTION VALUE="prospect">prospect (no packages ever) + <OPTION SELECTED VALUE="uncancel">all except cancelled + <OPTION VALUE="active">active recurring packages + <OPTION VALUE="susp">suspended + <OPTION VALUE="cancel">cancelled + </SELECT> + </TD> + </TR> + + <TR> + <TD ALIGN="right">Limit to customers with provisioned service</TD> + <TD> + <SELECT NAME="svcdb"> + <OPTION VALUE="">(no) + <OPTION VALUE="svc_acct">Account (svc_acct) + <OPTION VALUE="svc_broadband">Broadband service (svc_broadband) + <OPTION VALUE="svc_domain">Domain (svc_domain) + <OPTION VALUE="svc_external">External service (svc_external) + <OPTION VALUE="svc_forward">Mail forward (svc_foward) + <OPTION VALUE="svc_pbx">PBX (svc_pbx) + <OPTION VALUE="svc_phone">Phone number (svc_phone) + <OPTION VALUE="svc_www">Hosting (svc_www) + </SELECT> + </TD> + </TR> + + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'label' => 'For agent: ', + 'disable_empty' => 0, + ) + %> + + </TABLE> + <BR><INPUT TYPE="submit" VALUE="Get Report"> + </FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List zip codes'); + +</%init> diff --git a/httemplate/search/report_cust_main.html b/httemplate/search/report_cust_main.html new file mode 100755 index 000000000..eb1a66273 --- /dev/null +++ b/httemplate/search/report_cust_main.html @@ -0,0 +1,154 @@ +<% include('/elements/header.html', 'Customer Report' ) %> + +<FORM ACTION="cust_main.html" METHOD="GET"> +<INPUT TYPE="hidden" NAME="magic" VALUE="bill"> + + <TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Search options</FONT></TH> + </TR> + + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar($cgi->param('agentnum')), + 'disable_empty' => 0, + ) + %> + + <% include( '/elements/tr-select-cust_main-status.html', + 'label' => 'Status' + ) + %> + + <% include( '/elements/tr-select-cust_class.html', + 'label' => 'Class', + 'multiple' => 1, + 'pre_options' => [ '' => '(none)' ], + 'all_selected' => 1, + ) + %> + +% foreach my $field (qw( signupdate )) { + + <TR> + <TD ALIGN="right" VALIGN="center"><% $label{$field} %></TD> + <TD> + <TABLE> + <% include( '/elements/tr-input-beginning_ending.html', + prefix => $field, + layout => 'horiz', + ) + %> + </TABLE> + </TD> + </TR> + +% } + + <% include( '/elements/tr-select-payby.html', + 'payby_type' => 'cust', + 'multiple' => 1, + 'all_selected' => 1, + ) + %> + + <TR> + <TD ALIGN="right">Payment expiration before</TD> + <TD> + <SELECT NAME="paydate_month" DISABLED> +% foreach my $month ( 1 .. 12 ) { + <OPTION VALUE="<% $month %>"><% $month %></OPTION> +% } + </SELECT> + / + <SELECT NAME="paydate_year" onChange="paydate_year_changed(this);"> + <OPTION VALUE=""></OPTION> +% my $lastyear = (localtime(time))[5] + 1899; +% foreach my $year ( $lastyear .. $lastyear+12 ) { + <OPTION VALUE="<% $year %>"><% $year %></OPTION> +% } + </SELECT> + </TD> + </TR> + + <SCRIPT TYPE="text/javascript"> + function paydate_year_changed(what) { + var value = what.options[what.selectedIndex].value; + var month_select = what.form.paydate_month; + if ( value == '' ) { + month_select.disabled = true; + } else { + month_select.disabled = false; + } + } + </SCRIPT> + + <TR> + <TD ALIGN="right">Invoice terms</TD> + <TD> + <% include( '/elements/select-terms.html', + 'pre_options' => [ '' => 'all' ], + 'empty_value' => 'NULL', + ) + %> + </TD> + </TR> + + <% include( '/elements/tr-input-lessthan_greaterthan.html', + label => 'Current balance', + field => 'current_balance', + ) + %> + + <TR> + <TD ALIGN="right" VALIGN="center">Include cancelled packages</TD> + <TD><INPUT TYPE="checkbox" NAME="cancelled_pkgs"></TD> + </TR> + +% if ( $conf->exists('cust_main-require_censustract') ) { + + <TR> + <TD ALIGN="right" VALIGN="center">Without census tract</TD> + <TD><INPUT TYPE="checkbox" NAME="no_censustract"></TD> + </TR> + +% } + + <TR> + <TH CLASS="background" COLSPAN=2> </TH> + </TR> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Display options</FONT></TH> + </TR> + <% include( '/elements/tr-select-cust-fields.html' ) %> + + <TR> + <TD ALIGN="right" VALIGN="center">Add package columns</TD> + <TD><INPUT TYPE="checkbox" NAME="flattened_pkgs"></TD> + </TR> + </TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless ( $FS::CurrentUser::CurrentUser->access_right('List customers') && + $FS::CurrentUser::CurrentUser->access_right('List packages') + ); + +my $conf = new FS::Conf; + +</%init> +<%once> + +my %label = ( + 'signupdate' => 'Signup date', +); + +</%once> diff --git a/httemplate/search/report_cust_pay.html b/httemplate/search/report_cust_pay.html new file mode 100644 index 000000000..6c10a2e4d --- /dev/null +++ b/httemplate/search/report_cust_pay.html @@ -0,0 +1,116 @@ +<% include('/elements/header.html', $title ) %> + +<FORM ACTION="<% $void ? 'cust_pay_void.html' : 'cust_pay.cgi' %>" METHOD="GET"> +<INPUT TYPE="hidden" NAME="magic" VALUE="_date"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"> + <FONT SIZE="+1">Search options</FONT> + </TH> + </TR> + + <TR> + <TD ALIGN="right">Payments of type: </TD> + <TD> + <SELECT NAME="payby" onChange="payby_changed(this)"> + <OPTION VALUE="">all</OPTION> + <OPTION VALUE="CARD">credit card (all)</OPTION> + <OPTION VALUE="CARD-VisaMC">credit card (Visa/MasterCard)</OPTION> + <OPTION VALUE="CARD-Amex">credit card (American Express)</OPTION> + <OPTION VALUE="CARD-Discover">credit card (Discover)</OPTION> + <OPTION VALUE="CARD-Maestro">credit card (Maestro/Switch/Solo)</OPTION> + <OPTION VALUE="CHEK">electronic check / ACH</OPTION> + <OPTION VALUE="BILL">check</OPTION> + <OPTION VALUE="PREP">prepaid card</OPTION> + <OPTION VALUE="CASH">cash</OPTION> + <OPTION VALUE="WEST">Western Union</OPTION> + <OPTION VALUE="MCRD">manual credit card</OPTION> + </SELECT> + </TD> + </TR> + + <SCRIPT TYPE="text/javascript"> + + function payby_changed(what) { + if ( what.options[what.selectedIndex].value == 'BILL' ) { + document.getElementById('checkno_caption').style.color = '#000000'; + what.form.payinfo.disabled = false; + what.form.payinfo.style.backgroundColor = '#ffffff'; + } else { + document.getElementById('checkno_caption').style.color = '#bbbbbb'; + what.form.payinfo.disabled = true; + what.form.payinfo.style.backgroundColor = '#dddddd'; + } + } + + </SCRIPT> + + <TR> + <TD ALIGN="right"><FONT ID="checkno_caption" COLOR="#bbbbbb">Check #: </FONT></TD> + <TD> + <INPUT TYPE="text" NAME="payinfo" DISABLED STYLE="background-color: #dddddd"> + </TD> + </TR> + + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar($cgi->param('agentnum')), + 'label' => 'for agent: ', + 'disable_empty' => 0, + ) + %> + + <% include( '/elements/tr-select-otaker.html' ) %> + + <TR> + <TD ALIGN="right" VALIGN="center">Payment</TD> + <TD> + <TABLE> + <% include( '/elements/tr-input-beginning_ending.html', + layout => 'horiz', + ) + %> + </TABLE> + </TD> + </TR> + +% if ( $void ) { + <TR> + <TD ALIGN="right" VALIGN="center">Voided</TD> + <TD> + <TABLE> + <% include( '/elements/tr-input-beginning_ending.html', + prefix => 'void', + layout => 'horiz', + ) + %> + </TABLE> + </TD> + </TR> +% } + + <% include( '/elements/tr-input-lessthan_greaterthan.html', + 'label' => 'Amount', + 'field' => 'paid', + ) + %> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $void = $cgi->param('void') ? 1 : 0; + +my $title = $void ? 'Voided payment report' : 'Payment report'; + +</%init> diff --git a/httemplate/search/report_cust_pay_batch.html b/httemplate/search/report_cust_pay_batch.html new file mode 100644 index 000000000..2d3ef068a --- /dev/null +++ b/httemplate/search/report_cust_pay_batch.html @@ -0,0 +1,44 @@ +<% include('/elements/header.html', 'Batch payment report' ) %> + +<FORM ACTION="cust_pay_batch.cgi" METHOD="GET"> + +<TABLE> + + <TR> + <TD ALIGN="right">Payments of type: </TD> + <TD> + <SELECT NAME="payby"> + <OPTION VALUE="">all</OPTION> + <OPTION VALUE="CARD">credit card</OPTION> + <OPTION VALUE="CHEK">electronic check / ACH</OPTION> + </SELECT> + </TD> + </TR> + + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'label' => 'For agent: ', + 'disable_empty' => 0 + ) + %> + + <% include( '/elements/tr-input-beginning_ending.html' ) %> + + <TR> + <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="dcln" VALUE="1" CHECKED></TD> + <TD>Include approved items</TD> + </TR> +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +</%init> diff --git a/httemplate/search/report_cust_pkg.html b/httemplate/search/report_cust_pkg.html new file mode 100755 index 000000000..58fcf619e --- /dev/null +++ b/httemplate/search/report_cust_pkg.html @@ -0,0 +1,208 @@ +<% include('/elements/header.html', $title ) %> + +<FORM ACTION="cust_pkg.cgi" METHOD="GET"> +<INPUT TYPE="hidden" NAME="magic" VALUE="bill"> +<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>"> + + <TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"> + <FONT SIZE="+1">Search options</FONT> + </TH> + </TR> + +% unless ( $custnum ) { + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'disable_empty' => 0, + ) + %> +% } + + <% include( '/elements/tr-select-cust_pkg-status.html', + 'onchange' => 'status_changed(this);', + ) + %> + + <SCRIPT TYPE="text/javascript"> + + function status_changed(what) { + +% foreach my $status ( '', FS::cust_pkg->statuses() ) { + + if ( what.options[what.selectedIndex].value == '<% $status %>' ) { + +% foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) { +% if ( $disable{$status}->{$field} ) { + + what.form.<% $field %>_beginning_text.disabled = true; + what.form.<% $field %>_ending_text.disabled = true; + what.form.<% $field %>_beginning_text.style.backgroundColor = '#dddddd'; + what.form.<% $field %>_ending_text.style.backgroundColor = '#dddddd'; + + what.form.<% $field %>_beginning_button.style.display = 'none'; + what.form.<% $field %>_ending_button.style.display = 'none'; + what.form.<% $field %>_beginning_disabled.style.display = ''; + what.form.<% $field %>_ending_disabled.style.display = ''; + +% } else { + + what.form.<% $field %>_beginning_text.disabled = false; + what.form.<% $field %>_ending_text.disabled = false; + what.form.<% $field %>_beginning_text.style.backgroundColor = '#ffffff'; + what.form.<% $field %>_ending_text.style.backgroundColor = '#ffffff'; + + what.form.<% $field %>_beginning_button.style.display = ''; + what.form.<% $field %>_ending_button.style.display = ''; + what.form.<% $field %>_beginning_disabled.style.display = 'none'; + what.form.<% $field %>_ending_disabled.style.display = 'none'; + +% } +% } + + } + +% } + + } + + </SCRIPT> + + <% include( '/elements/tr-select-pkg_class.html', + 'pre_options' => [ '0' => 'all' ], + 'empty_label' => '(empty class)', + ) + %> + +% if ( scalar( qsearch( 'part_pkg_report_option', { 'disabled' => '' } ) ) ) { + + <% include( '/elements/tr-select-table.html', + 'label' => 'Report classes', + 'table' => 'part_pkg_report_option', + 'name_col' => 'name', + 'hashref' => { 'disabled' => '' }, + 'element_name' => 'report_option', + 'multiple' => 'multiple', + ) + %> + +% } + +% foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) { + + <TR> + <TD ALIGN="right" VALIGN="center"><% $label{$field} %></TD> + <TD> + <TABLE> + <% include( '/elements/tr-input-beginning_ending.html', + prefix => $field, + layout => 'horiz', + ) + %> + </TABLE> + </TD> + </TR> + +% } + + <SCRIPT TYPE="text/javascript"> + + function custom_changed(what) { + + if ( what.checked ) { + + what.form.pkgpart.disabled = true; + what.form.pkgpart.style.backgroundColor = '#dddddd'; + + } else { + + what.form.pkgpart.disabled = false; + what.form.pkgpart.style.backgroundColor = '#ffffff'; + + } + + } + + </SCRIPT> + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Custom packages', + 'field' => 'custom', + 'value' => 1, + 'onchange' => 'custom_changed(this);', + ) + %> + + <% include( '/elements/tr-selectmultiple-part_pkg.html' ) %> + + <TR> + <TH CLASS="background" COLSPAN=2> </TH> + </TR> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Display options</FONT></TH> + </TR> + <% include( '/elements/tr-select-cust-fields.html' ) %> + + </TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List packages'); + +my $title = 'Package Report'; + +my $custnum = ''; +if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { + $custnum = $1; + my $cust_main = qsearchs({ + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $custnum }, + 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, + }) or die "unknown custnum $custnum"; + $title .= ': '. $cust_main->name; +} + +</%init> +<%once> + +my %label = ( + 'setup' => 'Setup', + 'last_bill' => 'Last bill', + 'bill' => 'Next bill', + 'adjourn' => 'Adjourns', + 'susp' => 'Suspended', + 'expire' => 'Expires', + 'cancel' => 'Cancelled', +); + +#false laziness w/cust_pkg.cgi +my %disable = ( + 'all' => {}, + 'not yet billed' => { 'setup'=>1, 'last_bill'=>1, 'bill'=>1, 'adjourn'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, }, + 'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'adjourn'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, }, + 'active' => { 'susp'=>1, 'cancel'=>1 }, + 'suspended' => { 'cancel' => 1 }, + 'cancelled' => {}, + '' => {}, +); + +#hmm? +my %checkbox = ( + 'setup' => 0, + 'last_bill' => 0, + 'bill' => 0, + 'susp' => 1, + 'expire' => 1, + 'cancel' => 1, +); + +</%once> diff --git a/httemplate/search/report_cust_pkg_discount.html b/httemplate/search/report_cust_pkg_discount.html new file mode 100644 index 000000000..7ebd44f75 --- /dev/null +++ b/httemplate/search/report_cust_pkg_discount.html @@ -0,0 +1,50 @@ +<% include('/elements/header.html', 'Package discount report' ) %> + +<FORM ACTION="cust_pkg_discount.html" METHOD="GET"> + + +<TABLE> + + <TR> + <TD>Discount status</TD> + <TD> + <SELECT NAME="status"> + <OPTION VALUE="active">Active + <OPTION VALUE="expired">Expired + <OPTION VALUE="">(all) + </SELECT> + </TD> + </TR> + + <% include( '/elements/tr-select-otaker.html', + 'label' => 'Discounts by employee: ', + 'otakers' => \@otakers, + ) + %> + + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'label' => 'for agent: ', + 'disable_empty' => 0, + ) + %> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $sth = dbh->prepare("SELECT DISTINCT otaker FROM cust_pkg_discount") + or die dbh->errstr; +$sth->execute or die $sth->errstr; +my @otakers = map { $_->[0] } @{$sth->fetchall_arrayref}; + +</%init> diff --git a/httemplate/search/report_cust_refund.html b/httemplate/search/report_cust_refund.html new file mode 100644 index 000000000..4d311001e --- /dev/null +++ b/httemplate/search/report_cust_refund.html @@ -0,0 +1,116 @@ +<% include('/elements/header.html', $title ) %> + +<FORM ACTION="<% $void ? 'cust_refund_void.html' : 'cust_refund.html' %>" METHOD="GET"> +<INPUT TYPE="hidden" NAME="magic" VALUE="_date"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"> + <FONT SIZE="+1">Search options</FONT> + </TH> + </TR> + + <TR> + <TD ALIGN="right">Refunds of type: </TD> + <TD> + <SELECT NAME="payby" onChange="payby_changed(this)"> + <OPTION VALUE="">all</OPTION> + <OPTION VALUE="CARD">credit card (all)</OPTION> + <OPTION VALUE="CARD-VisaMC">credit card (Visa/MasterCard)</OPTION> + <OPTION VALUE="CARD-Amex">credit card (American Express)</OPTION> + <OPTION VALUE="CARD-Discover">credit card (Discover)</OPTION> + <OPTION VALUE="CARD-Maestro">credit card (Maestro/Switch/Solo)</OPTION> + <OPTION VALUE="CHEK">electronic check / ACH</OPTION> + <OPTION VALUE="BILL">check</OPTION> + <OPTION VALUE="PREP">prepaid card</OPTION> + <OPTION VALUE="CASH">cash</OPTION> + <OPTION VALUE="WEST">Western Union</OPTION> + <OPTION VALUE="MCRD">manual credit card</OPTION> + </SELECT> + </TD> + </TR> + + <SCRIPT TYPE="text/javascript"> + + function payby_changed(what) { + if ( what.options[what.selectedIndex].value == 'BILL' ) { + document.getElementById('checkno_caption').style.color = '#000000'; + what.form.payinfo.disabled = false; + what.form.payinfo.style.backgroundColor = '#ffffff'; + } else { + document.getElementById('checkno_caption').style.color = '#bbbbbb'; + what.form.payinfo.disabled = true; + what.form.payinfo.style.backgroundColor = '#dddddd'; + } + } + + </SCRIPT> + + <TR> + <TD ALIGN="right"><FONT ID="checkno_caption" COLOR="#bbbbbb">Check #: </FONT></TD> + <TD> + <INPUT TYPE="text" NAME="payinfo" DISABLED STYLE="background-color: #dddddd"> + </TD> + </TR> + + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar($cgi->param('agentnum')), + 'label' => 'for agent: ', + 'disable_empty' => 0, + ) + %> + + <% include( '/elements/tr-select-otaker.html' ) %> + + <TR> + <TD ALIGN="right" VALIGN="center">Refund</TD> + <TD> + <TABLE> + <% include( '/elements/tr-input-beginning_ending.html', + layout => 'horiz', + ) + %> + </TABLE> + </TD> + </TR> + +% if ( $void ) { + <TR> + <TD ALIGN="right" VALIGN="center">Voided</TD> + <TD> + <TABLE> + <% include( '/elements/tr-input-beginning_ending.html', + prefix => 'void', + layout => 'horiz', + ) + %> + </TABLE> + </TD> + </TR> +% } + + <% include( '/elements/tr-input-lessthan_greaterthan.html', + 'label' => 'Amount', + 'field' => 'paid', + ) + %> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $void = $cgi->param('void') ? 1 : 0; + +my $title = $void ? 'Voided refund report' : 'Refund report'; + +</%init> diff --git a/httemplate/search/report_employee_commission.html b/httemplate/search/report_employee_commission.html new file mode 100644 index 000000000..51afad3b5 --- /dev/null +++ b/httemplate/search/report_employee_commission.html @@ -0,0 +1,30 @@ +<% include('/elements/header.html', 'Employee commission report' ) %> + +<FORM ACTION="part_pkg.html"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + +%# +%# <% include( '/elements/tr-select-agent.html', +%# 'curr_value' => scalar( $cgi->param('agentnum') ), +%# 'disable_empty' => 0, +%# ) +%# %> +%# + +<% include( '/elements/tr-select-user.html' ) %> + +<% include( '/elements/tr-input-beginning_ending.html', ) %> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +</%init> diff --git a/httemplate/search/report_h_cust_pay.html b/httemplate/search/report_h_cust_pay.html new file mode 100644 index 000000000..fe7c4a9fa --- /dev/null +++ b/httemplate/search/report_h_cust_pay.html @@ -0,0 +1,124 @@ +<% include('/elements/header.html', 'Payment transaction history' ) %> + +<FORM ACTION="h_cust_pay.html" METHOD="GET"> +<INPUT TYPE="hidden" NAME="magic" VALUE="_date"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"> + <FONT SIZE="+1">Search options</FONT> + </TH> + </TR> + +%#history stuff + <TR> + <TD ALIGN="right">Search transactions for: </TD> + <TD> + <SELECT NAME="history_action"> + <OPTION VALUE="insert,replace_old,replace_new,delete">(all changes) + <OPTION VALUE="delete">Insertions + <OPTION VALUE="replace_old,replace_new">Replacements + <OPTION VALUE="delete">Deletions + </SELECT> + </TD> + </TR> + + <TR> + <TD ALIGN="right" VALIGN="center">Transaction date: </TD> + <TD> + <TABLE> + <% include( '/elements/tr-input-beginning_ending.html', + prefix => 'history_date', + layout => 'horiz', + ) + %> + </TABLE> + </TD> + </TR> +%#eo history stuff + + <TR> + <TD ALIGN="right">Payments of type: </TD> + <TD> + <SELECT NAME="payby" onChange="payby_changed(this)"> + <OPTION VALUE="">all</OPTION> + <OPTION VALUE="CARD">credit card (all)</OPTION> + <OPTION VALUE="CARD-VisaMC">credit card (Visa/MasterCard)</OPTION> + <OPTION VALUE="CARD-Amex">credit card (American Express)</OPTION> + <OPTION VALUE="CARD-Discover">credit card (Discover)</OPTION> + <OPTION VALUE="CARD-Maestro">credit card (Maestro/Switch/Solo)</OPTION> + <OPTION VALUE="CHEK">electronic check / ACH</OPTION> + <OPTION VALUE="BILL">check</OPTION> + <OPTION VALUE="PREP">prepaid card</OPTION> + <OPTION VALUE="CASH">cash</OPTION> + <OPTION VALUE="WEST">Western Union</OPTION> + <OPTION VALUE="MCRD">manual credit card</OPTION> + </SELECT> + </TD> + </TR> + + <SCRIPT TYPE="text/javascript"> + + function payby_changed(what) { + if ( what.options[what.selectedIndex].value == 'BILL' ) { + document.getElementById('checkno_caption').style.color = '#000000'; + what.form.payinfo.disabled = false; + what.form.payinfo.style.backgroundColor = '#ffffff'; + } else { + document.getElementById('checkno_caption').style.color = '#bbbbbb'; + what.form.payinfo.disabled = true; + what.form.payinfo.style.backgroundColor = '#dddddd'; + } + } + + </SCRIPT> + + <TR> + <TD ALIGN="right"><FONT ID="checkno_caption" COLOR="#bbbbbb">Check #: </FONT></TD> + <TD> + <INPUT TYPE="text" NAME="payinfo" DISABLED STYLE="background-color: #dddddd"> + </TD> + </TR> + + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar($cgi->param('agentnum')), + 'label' => 'for agent: ', + 'disable_empty' => 0, + ) + %> + + <% include( '/elements/tr-select-otaker.html' ) %> + + <TR> + <TD ALIGN="right" VALIGN="center">Payment</TD> + <TD> + <TABLE> + <% include( '/elements/tr-input-beginning_ending.html', + layout => 'horiz', + ) + %> + </TABLE> + </TD> + </TR> + + <% include( '/elements/tr-input-lessthan_greaterthan.html', + 'label' => 'Amount', + 'field' => 'paid', + ) + %> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +</%init> diff --git a/httemplate/search/report_newtax.cgi b/httemplate/search/report_newtax.cgi new file mode 100755 index 000000000..6a2cbb0d1 --- /dev/null +++ b/httemplate/search/report_newtax.cgi @@ -0,0 +1,207 @@ +<% include("/elements/header.html", "$agentname Tax Report - ". + ( $beginning + ? time2str('%h %o %Y ', $beginning ) + : '' + ). + 'through '. + ( $ending == 4294967295 + ? 'now' + : time2str('%h %o %Y', $ending ) + ) + ) +%> + +<% include('/elements/table-grid.html') %> + + <TR> + <TH CLASS="grid" BGCOLOR="#cccccc"></TH> + <TH CLASS="grid" BGCOLOR="#cccccc"></TH> + <TH CLASS="grid" BGCOLOR="#cccccc">Tax collected</TH> + <TH CLASS="grid" BGCOLOR="#cccccc"> </TH> + <TH CLASS="grid" BGCOLOR="#cccccc"></TH> + <TH CLASS="grid" BGCOLOR="#cccccc">Tax credited</TH> + </TR> +% my $bgcolor1 = '#eeeeee'; +% my $bgcolor2 = '#ffffff'; +% my $bgcolor; +% +% foreach my $tax ( @taxes ) { +% +% if ( $bgcolor eq $bgcolor1 ) { +% $bgcolor = $bgcolor2; +% } else { +% $bgcolor = $bgcolor1; +% } +% +% my $link = ''; +% if ( $tax->{'label'} ne 'Total' ) { +% $link = ';'. $tax->{'url_param'}; +% } +% + + <TR> + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $tax->{'label'} %></TD> + <% $tax->{base} ? qq!<TD CLASS="grid" BGCOLOR="$bgcolor"></TD>! : '' %> + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right"> + <A HREF="<% $baselink. $link %>;istax=1"><% $money_char %><% sprintf('%.2f', $tax->{'tax'} ) %></A> + </TD> + <% !($tax->{base}) ? qq!<TD CLASS="grid" BGCOLOR="$bgcolor"></TD>! : '' %> + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"></TD> + <% $tax->{base} ? qq!<TD CLASS="grid" BGCOLOR="$bgcolor"></TD>! : '' %> + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right"> + <A HREF="<% $baselink. $link %>;istax=1;iscredit=rate"><% $money_char %><% sprintf('%.2f', $tax->{'credit'} ) %></A> + </TD> + <% !($tax->{base}) ? qq!<TD CLASS="grid" BGCOLOR="$bgcolor"></TD>! : '' %> + </TR> +% } + +</TABLE> + +</BODY> +</HTML> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); + +my $join_cust = " + JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_main USING ( custnum ) +"; + +my $join_loc = "LEFT JOIN cust_bill_pkg_tax_rate_location USING ( billpkgnum )"; +my $join_tax_loc = "LEFT JOIN tax_rate_location USING ( taxratelocationnum )"; + +my $addl_from = " $join_cust $join_loc $join_tax_loc "; + +my $where = "WHERE _date >= $beginning AND _date <= $ending "; + +my $agentname = ''; +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + my $agent = qsearchs('agent', { 'agentnum' => $1 } ); + die "agent not found" unless $agent; + $agentname = $agent->agent; + $where .= ' AND cust_main.agentnum = '. $agent->agentnum; +} + +# my ( $location_sql, @location_param ) = FS::cust_pkg->location_sql; +# $where .= " AND $location_sql"; +#my @taxparam = ( 'itemdesc', @location_param ); +# now something along the lines of geocode matching ? +#$where .= FS::cust_pkg->_location_sql_where('cust_tax_location');; +my @taxparam = ( 'itemdesc', 'tax_rate_location.state', 'tax_rate_location.county', 'tax_rate_location.city', 'cust_bill_pkg_tax_rate_location.locationtaxid' ); + +my $select = 'DISTINCT itemdesc,locationtaxid,tax_rate_location.state,tax_rate_location.county,tax_rate_location.city'; + +my $tax = 0; +my $credit = 0; +my %taxes = (); +my %basetaxes = (); +foreach my $t (qsearch({ table => 'cust_bill_pkg', + select => $select, + hashref => { pkgpart => 0 }, + addl_from => $addl_from, + extra_sql => $where, + }) + ) +{ + my @params = map { my $f = $_; $f =~ s/.*\.//; $f } @taxparam; + my $label = join('~', map { $t->$_ } @params); + $label = 'Tax'. $label if $label =~ /^~/; + unless ( exists( $taxes{$label} ) ) { + my ($baselabel, @trash) = split /~/, $label; + + $taxes{$label}->{'label'} = join(', ', split(/~/, $label) ); + $taxes{$label}->{'url_param'} = + join(';', map { "$_=". uri_escape($t->$_) } @params); + + my $taxwhere = "FROM cust_bill_pkg $addl_from $where AND payby != 'COMP' ". + "AND ". join( ' AND ', map { "( $_ = ? OR ? = '' AND $_ IS NULL)" } @taxparam ); + + my $sql = "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) ". + " $taxwhere AND cust_bill_pkg.pkgnum = 0"; + + my $x = scalar_sql($t, [ map { $_, $_ } @params ], $sql ); + $tax += $x; + $taxes{$label}->{'tax'} += $x; + + my $creditfrom = " JOIN cust_credit_bill_pkg USING (billpkgnum,billpkgtaxratelocationnum) "; + my $creditwhere = "FROM cust_bill_pkg $addl_from $creditfrom $where ". + "AND payby != 'COMP' ". + "AND ". join( ' AND ', map { "( $_ = ? OR ? = '' AND $_ IS NULL)" } @taxparam ); + + $sql = "SELECT SUM(cust_credit_bill_pkg.amount) ". + " $creditwhere AND cust_bill_pkg.pkgnum = 0"; + + my $y = scalar_sql($t, [ map { $_, $_ } @params ], $sql ); + $credit += $y; + $taxes{$label}->{'credit'} += $y; + + unless ( exists( $taxes{$baselabel} ) ) { + + $basetaxes{$baselabel}->{'label'} = $baselabel; + $basetaxes{$baselabel}->{'url_param'} = "itemdesc=$baselabel"; + $basetaxes{$baselabel}->{'base'} = 1; + + } + + $basetaxes{$baselabel}->{'tax'} += $x; + $basetaxes{$baselabel}->{'credit'} += $y; + + } + + # calculate customer-exemption for this tax + # calculate package-exemption for this tax + # calculate monthly exemption (texas tax) for this tax + # count up all the cust_tax_exempt_pkg records associated with + # the actual line items. +} + + +#ordering +my @taxes = (); + +foreach my $tax ( sort { $a cmp $b } keys %taxes ) { + my ($base, @trash) = split '~', $tax; + my $basetax = delete( $basetaxes{$base} ); + if ($basetax) { + if ( $basetax->{tax} == $taxes{$tax}->{tax} ) { + $taxes{$tax}->{base} = 1; + } else { + push @taxes, $basetax; + } + } + push @taxes, $taxes{$tax}; +} + +push @taxes, { + 'label' => 'Total', + 'url_param' => '', + 'tax' => $tax, + 'credit' => $credit, + 'base' => 1, +}; + +#-- + +#false laziness w/FS::Report::Table::Monthly (sub should probably be moved up +#to FS::Report or FS::Record or who the fuck knows where) +sub scalar_sql { + my( $r, $param, $sql ) = @_; + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute( map $r->$_(), @$param ) + or die "Unexpected error executing statement $sql: ". $sth->errstr; + $sth->fetchrow_arrayref->[0] || 0; +} + +my $dateagentlink = "begin=$beginning;end=$ending"; +$dateagentlink .= ';agentnum='. $cgi->param('agentnum') + if length($agentname); +my $baselink = $p. "search/cust_bill_pkg.cgi?$dateagentlink"; + +</%init> diff --git a/httemplate/search/report_newtax.html b/httemplate/search/report_newtax.html new file mode 100755 index 000000000..2588b48d3 --- /dev/null +++ b/httemplate/search/report_newtax.html @@ -0,0 +1,23 @@ +<% include('/elements/header.html', 'Tax Report' ) %> + +<FORM ACTION="report_queued_newtax.cgi" METHOD="GET"> + +<TABLE> + + <% include( '/elements/tr-select-agent.html', 'disable_empty'=>0 ) %> + + <% include( '/elements/tr-input-beginning_ending.html' ) %> + +</TABLE> + +<BR><INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +</%init> diff --git a/httemplate/search/report_prepaid_income.cgi b/httemplate/search/report_prepaid_income.cgi new file mode 100644 index 000000000..bfb699b54 --- /dev/null +++ b/httemplate/search/report_prepaid_income.cgi @@ -0,0 +1,235 @@ +<% include("/elements/header.html", 'Prepaid Income (Unearned Revenue) Report') %> + +<% include( '/elements/table-grid.html' ) %> + + <TR> +% if ( scalar(@agentnums) > 1 ) { + <TH CLASS="grid" BGCOLOR="#cccccc">Agent</TH> +% } + <TH CLASS="grid" BGCOLOR="#cccccc"><% $actual_label %>Unearned Revenue</TH> +% if ( $legacy ) { + <TH CLASS="grid" BGCOLOR="#cccccc">Legacy Unearned Revenue</TH> +% } + </TR> + +% my $bgcolor1 = '#eeeeee'; +% my $bgcolor2 = '#ffffff'; +% my $bgcolor; +% +% push @agentnums, 0 unless scalar(@agentnums) < 2; +% foreach my $agentnum (@agentnums) { +% +% if ( $bgcolor eq $bgcolor1 ) { +% $bgcolor = $bgcolor2; +% } else { +% $bgcolor = $bgcolor1; +% } +% +% my $alink = $agentnum ? "$link;agentnum=$agentnum" : $link; +% +% my $agent_name = 'Total'; +% if ( $agentnum ) { +% my $agent = qsearchs('agent', { 'agentnum' => $agentnum }) +% or die "unknown agentnum $agentnum"; +% $agent_name = $agent->agent; +% } + + <TR> + +% if ( scalar(@agentnums) > 1 ) { + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $agent_name |h %></TD> +% } + + <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="<% $alink %>"><% $money_char %><% $total{$agentnum} %></A></TD> + +% if ( $legacy ) { + <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>"> + <% $now == $time ? $money_char.$total_legacy{$agentnum} : '<i>N/A</i>'%> + </TD> +% } + + </TR> + +% } + +</TABLE> + +<BR> +<% $actual_label %><% $actual_label ? 'u' : 'U' %>nearned revenue +is the amount of unearned revenue +<% $actual_label ? 'Freeside has actually' : '' %> +invoiced for packages with longer-than monthly terms. + +% if ( $legacy ) { + <BR><BR> + Legacy unearned revenue is the amount of unearned revenue represented by + customer packages. This number may be larger than actual unearned + revenue if you have imported longer-than monthly customer packages from + a previous billing system. +% } + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my $legacy = $conf->exists('enable_legacy_prepaid_income'); +my $actual_label = $legacy ? 'Actual ' : ''; + +#doesn't yet deal with daily/weekly packages + +my $time = time; + +my $now = $cgi->param('date') && parse_datetime($cgi->param('date')) || $time; +$now =~ /^(\d+)$/ or die "unparsable date?"; +$now = $1; + +my $link = "cust_bill_pkg.cgi?nottax=1;unearned_now=$now"; + +my $curuser = $FS::CurrentUser::CurrentUser; + +my $agentnum = ''; +my @agentnums = (); +$agentnum ? ($agentnum) : $curuser->agentnums; +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + @agentnums = ($1); + #XXX#push @where, "agentnum = $agentnum"; + #XXX#$link .= ";agentnum=$agentnum"; +} else { + @agentnums = $curuser->agentnums; +} + +my @where = (); + +#here is the agent virtualization +push @where, $curuser->agentnums_sql( 'table'=>'cust_main' ); + +#well, because cust_bill_pkg.cgi has it and without it the numbers don't match.. +push @where , " payby != 'COMP' " + unless $cgi->param('include_comp_cust'); + +my %total = (); +my %total_legacy = (); +foreach my $agentnum (@agentnums) { + + my $where = join(' AND ', @where, "cust_main.agentnum = $agentnum"); + $where = "AND $where" if $where; + + my( $total, $total_legacy ) = ( 0, 0 ); + + # my @cust_bill_pkg = + # grep { $_->cust_pkg && $_->cust_pkg->part_pkg->freq !~ /^([01]|\d+[hdw])$/ } + # qsearch({ + # 'select' => 'cust_bill_pkg.*', + # 'table' => 'cust_bill_pkg', + # 'addl_from' => ' LEFT JOIN cust_bill USING ( invnum ) '. + # ' LEFT JOIN cust_main USING ( custnum ) ', + # 'hashref' => { + # 'recur' => { op=>'!=', value=>0 }, + # 'sdate' => { op=>'<', value=>$now }, + # 'edate' => { op=>'>', value=>$now }, + # }, + # 'extra_sql' => $where, + # }); + # + # foreach my $cust_bill_pkg ( @cust_bill_pkg) { + # my $period = $cust_bill_pkg->edate - $cust_bill_pkg->sdate; + # + # my $elapsed = $now - $cust_bill_pkg->sdate; + # $elapsed = 0 if $elapsed < 0; + # + # my $remaining = 1 - $elapsed/$period; + # + # my $unearned = $remaining * $cust_bill_pkg->recur; + # $total += $unearned; + # + # } + + #re-written in sql: + + #false laziness w/cust_bill_pkg.cgi + + my $float = 'REAL'; #'DOUBLE PRECISION'; + + my $period = "CAST(cust_bill_pkg.edate - cust_bill_pkg.sdate AS $float)"; + my $elapsed = "(CASE WHEN cust_bill_pkg.sdate > $now + THEN 0 + ELSE ($now - cust_bill_pkg.sdate) + END)"; + #my $elapsed = "CAST($unearned - cust_bill_pkg.sdate AS $float)"; + + my $remaining = "(1 - $elapsed/$period)"; + + my $select = "SUM($remaining * cust_bill_pkg.recur)"; + + #[...] + + my $sql = "SELECT $select FROM cust_bill_pkg + LEFT JOIN cust_pkg USING ( pkgnum ) + LEFT JOIN part_pkg USING ( pkgpart ) + LEFT JOIN cust_main USING ( custnum ) + WHERE pkgpart > 0 + AND sdate < $now + AND edate > $now + AND cust_bill_pkg.recur != 0 + AND part_pkg.freq != '0' + AND part_pkg.freq != '1' + AND part_pkg.freq NOT LIKE '%h' + AND part_pkg.freq NOT LIKE '%d' + AND part_pkg.freq NOT LIKE '%w' + $where + "; + + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute or die $sth->errstr; + my $total = $sth->fetchrow_arrayref->[0]; + + $total = sprintf('%.2f', $total); + $total{$agentnum} = $total; + $total{0} += $total; + + if ( $legacy ) { + + #not yet rewritten in sql, but now not enabled by default + + my @cust_pkg = + grep { $_->part_pkg->recur != 0 + && $_->part_pkg->freq !~ /^([01]|\d+[dw])$/ + } + qsearch({ + 'select' => 'cust_pkg.*', + 'table' => 'cust_pkg', + 'addl_from' => ' LEFT JOIN cust_main USING ( custnum ) ', + 'hashref' => { 'bill' => { op=>'>', value=>$now } }, + 'extra_sql' => $where, + }); + + foreach my $cust_pkg ( @cust_pkg ) { + my $period = $cust_pkg->bill - $cust_pkg->last_bill; + + my $elapsed = $now - $cust_pkg->last_bill; + $elapsed = 0 if $elapsed < 0; + + my $remaining = 1 - $elapsed/$period; + + my $unearned = $remaining * $cust_pkg->part_pkg->recur; #!! only works for flat/legacy + $total_legacy += $unearned; + + } + + $total_legacy = sprintf('%.2f', $total_legacy); + $total_legacy{$agentnum} = $total_legacy; + $total_legacy{0} += $total_legacy; + + } + +} + +$total{0} = sprintf('%.2f', $total{0}); +$total_legacy{0} = sprintf('%.2f', $total_legacy{0}); + +</%init> diff --git a/httemplate/search/report_prepaid_income.html b/httemplate/search/report_prepaid_income.html new file mode 100644 index 000000000..061b24c68 --- /dev/null +++ b/httemplate/search/report_prepaid_income.html @@ -0,0 +1,64 @@ +<% include('/elements/header.html','Prepaid Income (Unearned Revenue) Report')%> + +<% include('/elements/init_calendar.html') %> + +<FORM ACTION="report_prepaid_income.cgi" METHOD="GET"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"> + <FONT SIZE="+1">Search options</FONT> + </TH> + </TR> + + <TR> + <TD>As of </TD> + <TD> + <INPUT TYPE="text" NAME="date" ID="date_text" VALUE="now"> + <IMG SRC="../images/calendar.png" ID="date_button" STYLE="cursor: pointer" TITLE="Select date"> + </TD> + </TR> + <TR> + <TD> + </TD> + <TD><FONT SIZE="-1"><i>m/d/y</i></FONT></TD> + </TR> + + <TR> + <TD COLSPAN=2> </TD> + </TR> + + <% include( '/elements/tr-select-agent.html', 'disable_empty'=>0 ) %> + + <TR> + <TD COLSPAN=2> </TD> + </TR> + + <TR> + <TD COLSPAN=2 ALIGN="center"><INPUT TYPE="submit" VALUE="Generate report"></TD> + </TR> + +</TABLE> + +<SCRIPT TYPE="text/javascript"> + Calendar.setup({ + inputField: "date_text", + ifFormat: "<% $date_format %>", + button: "date_button", + align: "BR" + }); +</SCRIPT> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; +my $date_format = $conf->config('date_format') || '%m/%d/%Y'; + +</%init> diff --git a/httemplate/search/report_prospect_main.html b/httemplate/search/report_prospect_main.html new file mode 100644 index 000000000..4834c2047 --- /dev/null +++ b/httemplate/search/report_prospect_main.html @@ -0,0 +1,32 @@ +<% include('/elements/header.html', 'Prospect Report' ) %> + +<FORM ACTION="prospect_main.html" METHOD="GET"> + + <TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Search options</FONT></TH> + </TR> + + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar($cgi->param('agentnum')), + 'disable_empty' => 0, + ) + %> + + </TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List prospects'); + +my $conf = new FS::Conf; + +</%init> diff --git a/httemplate/search/report_queued_newtax.cgi b/httemplate/search/report_queued_newtax.cgi new file mode 100755 index 000000000..1d5813ece --- /dev/null +++ b/httemplate/search/report_queued_newtax.cgi @@ -0,0 +1,16 @@ +<% include("/elements/header.html", "Queue Tax Report") %> +<% include("/elements/error.html") %> +% unless ($error) { + <CENTER> + Report queued. Check the job queue for status. + </CENTER> +% } +<% include("/elements/footer.html") %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $error = FS::tax_rate::queue_liability_report($cgi); + +</%init> diff --git a/httemplate/search/report_receivables.cgi b/httemplate/search/report_receivables.cgi new file mode 100755 index 000000000..3696ed40d --- /dev/null +++ b/httemplate/search/report_receivables.cgi @@ -0,0 +1,52 @@ +<% include( 'elements/cust_main_dayranges.html', + 'title' => 'Accounts Receivable Aging Summary', + 'range_sub' => \&balance, + 'payment_links' => 1, + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Receivables report') + or $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +</%init> +<%once> + +#Example: +# +# my $balance = balance( +# $start, $end, $offset, +# 'no_as' => 1, #set to true when using in a WHERE clause (supress AS clause) +# #or 0 / omit when using in a SELECT clause as a column +# # ("AS balance_$start_$end") +# 'sum' => 1, #set to true to get a SUM() of the values, for totals +# +# #obsolete? options for totals (passed to cust_main::balance_date_sql) +# 'total' => 1, #set to true to remove all customer comparison clauses +# 'join' => $join, #JOIN clause +# 'where' => \@where, #WHERE clause hashref (elements "AND"ed together) +# ) + +sub balance { + my($start, $end, $offset) = @_; #, %opt ? + #handle start and end ranges (86400 = 24h * 60m * 60s) + my $str2time = str2time_sql; + my $closing = str2time_sql_closing; + + # $end == 0 means "+infinity", while $start == 0 really means 0 + # so we should always include a start condition + $start = "( $str2time now() $closing - ". ($start + $offset) * 86400 . ' )'; + # but only include an end condition if $end != 0 + $end = $end ? + "( $str2time now() $closing - ". ($end + $offset) * 86400 . ' )' + : ''; + + #$opt{'unapplied_date'} = 1; + + FS::cust_main->balance_date_sql( $start, $end, 'unapplied_date'=>1, + 'cutoff' => "( $str2time now() $closing - ".$offset * 86400 . ')' ); + +} + +</%once> diff --git a/httemplate/search/report_receivables.html b/httemplate/search/report_receivables.html new file mode 100755 index 000000000..912ef26b4 --- /dev/null +++ b/httemplate/search/report_receivables.html @@ -0,0 +1,47 @@ +<% include('/elements/header.html', 'Accounts Receivable Aging Summary' ) %> + +<FORM NAME="OneTrueForm" ACTION="report_receivables.cgi" METHOD="GET"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"> + <FONT SIZE="+1">Search options</FONT> + </TH> + </TR> + + <% include( '/elements/tr-select-agent.html', 'disable_empty'=>0 ) %> + + <% include( '/elements/tr-select-cust_main-status.html', + 'label' => 'Customer Status' + ) + %> + + <TR> + <TD ALIGN="right">Customers</TD> + <TD> + <INPUT TYPE="radio" NAME="all_customers" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.days.disabled=true; document.OneTrueForm.days.style.backgroundColor = '#dddddd'; } else { document.OneTrueForm.days.disabled=false; document.OneTrueForm.days.style.backgroundColor = '#ffffff'; }">All customers (even those without an outstanding balance)<BR> + <INPUT TYPE="radio" NAME="all_customers" VALUE="0" CHECKED onClick="if ( ! this.checked ) { document.OneTrueForm.days.disabled=true; document.OneTrueForm.days.style.backgroundColor = '#dddddd'; } else { document.OneTrueForm.days.disabled=false; document.OneTrueForm.days.style.backgroundColor = '#ffffff'; }">Customers with a balance over <INPUT NAME="days" TYPE="text" SIZE=4 MAXLENGTH=3 VALUE="0"> days old + </TD> + </TR> + <% include( '/elements/tr-input-date-field.html', { + 'name' => 'as_of', + 'value' => time, + 'label' => 'As of date ', + 'format' => FS::Conf->new->config('date_format') || '%m/%d/%Y', + } ) %> + +</TABLE> + +<BR><INPUT TYPE="submit" VALUE="Get Report"> +</FORM> + +<% include('/elements/footer.html') %> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Receivables report') + or $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +</%init> diff --git a/httemplate/search/report_rt_transaction.html b/httemplate/search/report_rt_transaction.html new file mode 100644 index 000000000..9b7b7cbbb --- /dev/null +++ b/httemplate/search/report_rt_transaction.html @@ -0,0 +1,24 @@ +<% include('/elements/header.html', 'Time worked report criteria' ) %> + +<FORM ACTION="rt_transaction.html" METHOD="GET"> + +<TABLE> + + <% include ( '/elements/tr-input-beginning_ending.html' ) %> + + <% include ( '/elements/tr-select-otaker.html' ) %> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Search"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List rating data'); + +</%init> diff --git a/httemplate/search/report_sql.html b/httemplate/search/report_sql.html new file mode 100644 index 000000000..995330894 --- /dev/null +++ b/httemplate/search/report_sql.html @@ -0,0 +1,23 @@ +<% include('/elements/header.html', 'SQL Query' ) %> + +<FORM ACTION="sql.html" METHOD="GET"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + <TR> + <TD ALIGN="right" VALIGN="top">SELECT </TD> + <TD><TEXTAREA NAME="sql"></TEXTAREA></TD> + </TR> +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Query"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Raw SQL'); + +</%init> diff --git a/httemplate/search/report_svc_acct.html b/httemplate/search/report_svc_acct.html new file mode 100755 index 000000000..c7fac4631 --- /dev/null +++ b/httemplate/search/report_svc_acct.html @@ -0,0 +1,134 @@ +<% include('/elements/header.html', $title ) %> + +<FORM ACTION="svc_acct.cgi" METHOD="GET"> +<INPUT TYPE="hidden" NAME="magic" VALUE="advanced"> +<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>"> + + <TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Search options</FONT></TH> + </TR> + +% unless ( $custnum ) { + <% include( '/elements/tr-select-agent.html', + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'disable_empty' => 0, + ) + %> + +% # just this customer's domains? + <% include( '/elements/tr-select-domain.html', + 'element_name' => 'domsvc', + 'curr_value' => scalar( $cgi->param('domsvc') ), + 'disable_empty' => 0, + ) + %> +% } + + <SCRIPT type="text/javascript"> + function toggle(what) { + label = document.getElementById (what + '_label'); + field = document.getElementById ( what + '_invert'); + if (field.value == 1) { + field.value = 0; + } else { + field.value = 1; + } + if (field.value == 1) { + label.firstChild.nodeValue = 'Did not ' + label.firstChild.nodeValue; + }else{ + text = label.firstChild.nodeValue; + label.firstChild.nodeValue = text.replace(/Did not /, ''); + } + } + </SCRIPT> +% foreach my $field (qw( last_login last_logout )) { +% my $invert = $field."_invert"; + + <TR> + <TD> + <TABLE> + <TR> + <TD ALIGN="right" VALIGN="center" ID="<% $field."_label" %>"> + <% $label{$field} %> + </TD> + <TD> + <INPUT NAME="<% $invert %>" ID="<% $invert %>" TYPE="hidden"> + <A HREF="javascript:void(0)" onClick="toggle('<% $field %>'); return false;">Invert</A> + </TD> + </TR> + </TABLE> + </TD> + <TD> + <TABLE> + <% include( '/elements/tr-input-beginning_ending.html', + prefix => $field, + layout => 'horiz', + ) + %> + </TABLE> + </TD> + </TR> + +% } + + <% include( '/elements/tr-selectmultiple-part_pkg.html' ) %> + + <TR> + <TH CLASS="background" COLSPAN=2> </TH> + </TR> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Display options</FONT></TH> + </TR> + +% #move to /elements/tr-select-cust_pkg-fields if anything else needs it... + <TR> + <TD ALIGN="right">Package fields</TD> + <TD> + <SELECT NAME="cust_pkg_fields"> + <OPTION VALUE="">(none) + <OPTION VALUE="setup,last_bill,bill,cancel">Setup date | Last bill date | Next bill date | Cancel date + </SELECT> + </TD> + </TR> + + <% include( '/elements/tr-select-cust-fields.html' ) %> + + </TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List packages'); #? + +my $title = 'Account Report'; + +#false laziness w/report_cust_pkg.html +my $custnum = ''; +if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { + $custnum = $1; + my $cust_main = qsearchs({ + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $custnum }, + 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, + }) or die "unknown custnum $custnum"; + $title .= ': '. $cust_main->name; +} + +</%init> +<%once> + +my %label = ( + 'last_login' => 'Last login', + 'last_logout' => 'Last logout', +); + +</%once> diff --git a/httemplate/search/report_svc_phone.html b/httemplate/search/report_svc_phone.html new file mode 100644 index 000000000..9f1042608 --- /dev/null +++ b/httemplate/search/report_svc_phone.html @@ -0,0 +1,32 @@ +<% include('/elements/header.html', 'Phone number total usage' ) %> + +<FORM ACTION="svc_phone.cgi" METHOD="GET"> + +<INPUT TYPE="hidden" NAME="magic" VALUE="all"> +<INPUT TYPE="hidden" NAME="usage_total" VALUE="1"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + +%# <TR> +%# <TH CLASS="background" COLSPAN=2 ALIGN="left"> +%# <FONT SIZE="+1">Search options</FONT> +%# </TH> +%# </TR> + + <% include ( '/elements/tr-input-beginning_ending.html', prefix=>'usage' ) %> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Search phone numbers"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +#? 'List services' ? something new? +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List rating data'); + +</%init> diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi new file mode 100755 index 000000000..803b7d48f --- /dev/null +++ b/httemplate/search/report_tax.cgi @@ -0,0 +1,786 @@ +<% include("/elements/header.html", "$agentname Tax Report - ". + ( $beginning + ? time2str('%h %o %Y ', $beginning ) + : '' + ). + 'through '. + ( $ending == 4294967295 + ? 'now' + : time2str('%h %o %Y', $ending ) + ) + ) +%> + +<% include('/elements/table-grid.html') %> + + <TR> + <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH> + <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=9>Sales</TH> + <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH> + <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Rate</TH> + <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH> + <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Tax owed</TH> +% unless ( $cgi->param('show_taxclasses') ) { + <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Tax invoiced</TH> + <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH> + <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Tax credited</TH> + <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH> + <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Tax collected</TH> +% } + </TR> + + <TR> + <TH CLASS="grid" BGCOLOR="#cccccc">Total</TH> + <TH CLASS="grid" BGCOLOR="#cccccc"></TH> + <TH CLASS="grid" BGCOLOR="#cccccc">Non-taxable<BR><FONT SIZE=-1>(tax-exempt customer)</FONT></TH> + <TH CLASS="grid" BGCOLOR="#cccccc"></TH> + <TH CLASS="grid" BGCOLOR="#cccccc">Non-taxable<BR><FONT SIZE=-1>(tax-exempt package)</FONT></TH> + <TH CLASS="grid" BGCOLOR="#cccccc"></TH> + <TH CLASS="grid" BGCOLOR="#cccccc">Non-taxable<BR><FONT SIZE=-1>(monthly exemption)</FONT></TH> + <TH CLASS="grid" BGCOLOR="#cccccc"></TH> + <TH CLASS="grid" BGCOLOR="#cccccc">Taxable</TH> + </TR> + +% my $bgcolor1 = '#eeeeee'; +% my $bgcolor2 = '#ffffff'; +% my $bgcolor; +% +% foreach my $region ( @regions ) { +% +% my $link = ''; +% if ( $region->{'label'} eq $out ) { +% $link = ';out=1'; +% } else { +% $link = ';'. $region->{'url_param'} +% if $region->{'url_param'}; +% } +% +% if ( $bgcolor eq $bgcolor1 ) { +% $bgcolor = $bgcolor2; +% } else { +% $bgcolor = $bgcolor1; +% } +% +% #my $diff = 0; +% my $hicolor = $bgcolor; +% unless ( $cgi->param('show_taxclasses') ) { +% my $diff = abs( sprintf( '%.2f', $region->{'owed'} ) +% - sprintf( '%.2f', $region->{'tax'} ) +% ); +% if ( $diff > 0.02 ) { +% # $hicolor = $hicolor eq '#eeeeee' ? '#eeee66' : '#ffff99'; +% #} elsif ( $diff ) { +% $hicolor = $hicolor eq '#eeeeee' ? '#eeee99' : '#ffffcc'; +% } +% } +% +% +% my $td = qq(TD CLASS="grid" BGCOLOR="$bgcolor"); +% my $tdh = qq(TD CLASS="grid" BGCOLOR="$hicolor"); +% my $bigmath = '<FONT FACE="sans-serif" SIZE="+1"><B>'; +% my $bme = '</B></FONT>'; + + <TR> + <<%$td%>><% $region->{'label'} %></TD> + <<%$td%> ALIGN="right"> + <A HREF="<% $baselink. $link %>;nottax=1" + ><% &$money_sprintf( $region->{'total'} ) %></A> + </TD> + <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD> + <<%$td%> ALIGN="right"> + <A HREF="<% $baselink. $link %>;nottax=1;cust_tax=Y" + ><% &$money_sprintf( $region->{'exempt_cust'} ) %></A> + </TD> + <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD> + <<%$td%> ALIGN="right"> + <A HREF="<% $baselink. $link %>;nottax=1;pkg_tax=Y" + ><% &$money_sprintf( $region->{'exempt_pkg'} ) %></A> + </TD> + <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD> + <<%$td%> ALIGN="right"> + <A HREF="<% $exemptlink. $link %>" + ><% &$money_sprintf( $region->{'exempt_monthly'} ) %></A> + </TD> + <<%$td%>><FONT SIZE="+1"><B> = </B></FONT></TD> + <<%$td%> ALIGN="right"> + <A HREF="<% $baselink. $link %>;nottax=1;taxable=1" + ><% &$money_sprintf( $region->{'taxable'} ) %></A> + </TD> + <<%$td%>><% $region->{'label'} eq 'Total' ? '' : "$bigmath X $bme" %></TD> + <<%$td%> ALIGN="right"><% $region->{'rate'} %></TD> + <<%$td%>><% $region->{'label'} eq 'Total' ? '' : "$bigmath = $bme" %></TD> + <<%$tdh%> ALIGN="right"> + <% &$money_sprintf( $region->{'owed'} ) %> + </TD> + +% unless ( $cgi->param('show_taxclasses') ) { +% my $invlink = $region->{'url_param_inv'} +% ? ';'. $region->{'url_param_inv'} +% : $link; + + <<%$tdh%> ALIGN="right"> + <A HREF="<% $baselink. $invlink %>;istax=1" + ><% &$money_sprintf( $region->{'tax'} ) %></A> + </TD> + <<%$tdh%>><FONT SIZE="+1"><B> - </B></FONT></TD> + <<%$tdh%> ALIGN="right"> + <A HREF="<% $creditlink. $invlink %>;istax=1" + ><% &$money_sprintf( $region->{'credit'} ) %></A> + </TD> + <<%$tdh%>><FONT SIZE="+1"><B> = </B></FONT></TD> + <<%$tdh%> ALIGN="right"> + <% &$money_sprintf( $region->{'tax'} - $region->{'credit'} ) %> + </TD> +% } + + </TR> +% } + +</TABLE> + +% if ( $cgi->param('show_taxclasses') ) { + + <BR> + <% include('/elements/table-grid.html') %> + <TR> + <TH CLASS="grid" BGCOLOR="#cccccc"></TH> + <TH CLASS="grid" BGCOLOR="#cccccc">Tax invoiced</TH> + <TH CLASS="grid" BGCOLOR="#cccccc"></TH> + <TH CLASS="grid" BGCOLOR="#cccccc">Tax credited</TH> + <TH CLASS="grid" BGCOLOR="#cccccc"></TH> + <TH CLASS="grid" BGCOLOR="#cccccc">Tax collected</TH> + </TR> + +% #some false laziness w/above +% $bgcolor1 = '#eeeeee'; +% $bgcolor2 = '#ffffff'; +% +% foreach my $region ( @base_regions ) { +% +% my $link = ''; +% if ( $region->{'label'} eq $out ) { +% $link = ';out=1'; +% } else { +% $link = ';'. $region->{'url_param'} +% if $region->{'url_param'}; +% } +% +% if ( $bgcolor eq $bgcolor1 ) { +% $bgcolor = $bgcolor2; +% } else { +% $bgcolor = $bgcolor1; +% } +% my $td = qq(TD CLASS="grid" BGCOLOR="$bgcolor"); +% my $tdh = qq(TD CLASS="grid" BGCOLOR="$bgcolor"); +% +% #? +% my $invlink = $region->{'url_param_inv'} +% ? ';'. $region->{'url_param_inv'} +% : $link; + + <TR> + <<%$td%>><% $region->{'label'} %></TD> + <<%$td%> ALIGN="right"> + <A HREF="<% $baselink. $link %>;istax=1" + ><% &$money_sprintf( $region->{'tax'} ) %></A> + </TD> + <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD> + <<%$tdh%> ALIGN="right"> + <A HREF="<% $creditlink. $invlink %>;istax=1" + ><% &$money_sprintf( $region->{'credit'} ) %></A> + </TD> + <<%$td%>><FONT SIZE="+1"><B> = </B></FONT></TD> + <<%$tdh%> ALIGN="right"> + <% &$money_sprintf( $region->{'tax'} - $region->{'credit'} ) %> + </TD> + </TR> + +% } + +% if ( $bgcolor eq $bgcolor1 ) { +% $bgcolor = $bgcolor2; +% } else { +% $bgcolor = $bgcolor1; +% } +% my $td = qq(TD CLASS="grid" BGCOLOR="$bgcolor"); + + <TR> + <<%$td%>>Total</TD> + <<%$td%> ALIGN="right"> + <A HREF="<% $baselink %>;istax=1" + ><% &$money_sprintf( $tot_tax ) %></A> + </TD> + <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD> + <<%$td%> ALIGN="right"> + <A HREF="<% $creditlink %>;istax=1" + ><% &$money_sprintf( $tot_credit ) %></A> + </TD> + <<%$td%>><FONT SIZE="+1"><B> = </B></FONT></TD> + <<%$td%> ALIGN="right"> + <% &$money_sprintf( $tot_tax - $tot_credit ) %> + </TD> + </TR> + + </TABLE> + +% } + +<% include('/elements/footer.html') %> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; + +my $user = getotaker; + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); + +my $join_cust = ' JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_main USING ( custnum ) '; +my $join_cust_pkg = $join_cust. + ' LEFT JOIN cust_pkg USING ( pkgnum ) + LEFT JOIN part_pkg USING ( pkgpart ) '; +$join_cust_pkg .= ' LEFT JOIN cust_location USING ( locationnum )' + if $conf->exists('tax-pkg_address'); + +my $from_join_cust_pkg = " FROM cust_bill_pkg $join_cust_pkg "; + +my $where = "WHERE _date >= $beginning AND _date <= $ending "; + +my( $location_sql, @base_param ) = FS::cust_pkg->location_sql; +$where .= " AND $location_sql "; + +my $agentname = ''; +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + my $agent = qsearchs('agent', { 'agentnum' => $1 } ); + die "agent not found" unless $agent; + $agentname = $agent->agent; + $where .= ' AND cust_main.agentnum = '. $agent->agentnum; +} + +sub gotcust { + my $table = shift; + my $prefix = @_ ? shift : ''; + " + ( $table.${prefix}city = cust_main_county.city + OR cust_main_county.city = '' + OR cust_main_county.city IS NULL ) + AND ( $table.${prefix}county = cust_main_county.county + OR cust_main_county.county = '' + OR cust_main_county.county IS NULL ) + AND ( $table.${prefix}state = cust_main_county.state + OR cust_main_county.state = '' + OR cust_main_county.state IS NULL ) + AND ( $table.${prefix}country = cust_main_county.country ) + "; +} + +my $gotcust; +if ( $conf->exists('tax-ship_address') ) { + + $gotcust = " + ( cust_main_county.country = cust_main.country + OR cust_main_county.country = cust_main.ship_country + ) + + AND + + ( + ( ( ship_last IS NULL OR ship_last = '' ) + AND ". gotcust('cust_main'). " + ) + OR + ( ship_last IS NOT NULL AND ship_last != '' + AND ". gotcust('cust_main', 'ship_'). " + ) + ) + "; + +} else { + + $gotcust = gotcust('cust_main'); + +} +if ( $conf->exists('tax-pkg_address') ) { + $gotcust = " + ( cust_pkg.locationnum IS NULL AND $gotcust) + OR ( cust_pkg.locationnum IS NOT NULL AND ". gotcust('cust_location'). " )"; + $gotcust = + "WHERE 0 < ( SELECT COUNT(*) FROM cust_pkg + LEFT JOIN cust_main USING ( custnum ) + LEFT JOIN cust_location USING ( locationnum ) + WHERE $gotcust + LIMIT 1 + ) + "; +} else { + $gotcust = + "WHERE 0 < ( SELECT COUNT(*) FROM cust_main WHERE $gotcust LIMIT 1 )"; +} + +my $out = 'Out of taxable region(s)'; +my %regions = (); + +foreach my $r ( qsearch({ 'table' => 'cust_main_county', + 'extra_sql' => $gotcust, + }) + ) +{ + #warn $r->county. ' '. $r->state. ' '. $r->country. "\n"; + + my $label = getlabel($r); + $regions{$label}->{'label'} = $label; + + $regions{$label}->{$_} = $r->$_() for (qw( county state country )); #taxname? + + my @url_param = qw( county state country taxname ); + push @url_param, 'city' if $cgi->param('show_cities') && $r->city(); + + $regions{$label}->{'url_param'} = + join(';', map "$_=".uri_escape($r->$_()), @url_param ); + + my @param = @base_param; + my $mywhere = $where; + + if ( $r->taxclass ) { + + $mywhere .= " AND taxclass = ? "; + push @param, 'taxclass'; + $regions{$label}->{'url_param'} .= ';taxclass='. uri_escape($r->taxclass); + #no, always# if $cgi->param('show_taxclasses'); + + $regions{$label}->{'taxclass'} = $r->taxclass; + + } else { + + $regions{$label}->{'url_param'} .= ';taxclassNULL=1' + if $cgi->param('show_taxclasses'); + + my $same_sql = $r->sql_taxclass_sameregion; + $mywhere .= " AND $same_sql" if $same_sql; + + } + + my $fromwhere = "$from_join_cust_pkg $mywhere"; # AND payby != 'COMP' "; + +# my $label = getlabel($r); +# $regions{$label}->{'label'} = $label; + + my $nottax = 'pkgnum != 0'; + + ## calculate total for this region + + my $t_sql = + "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) $fromwhere AND $nottax"; + my $t = scalar_sql($r, \@param, $t_sql); + $regions{$label}->{'total'} += $t; + + #if ( $label eq $out ) # && $t ) { + # warn "adding $t for ". + # join('/', map $r->$_, qw( taxclass county state country ) ). "\n"; + # #warn $t_sql if $r->state eq 'FL'; + #} + + ## calculate customer-exemption for this region + +## my $taxable = $t; + +# my($taxable, $x_cust) = (0, 0); +# foreach my $e ( grep { $r->get($_.'tax') !~ /^Y/i } +# qw( cust_bill_pkg.setup cust_bill_pkg.recur ) ) { +# $taxable += scalar_sql($r, \@param, +# "SELECT SUM($e) $fromwhere AND $nottax AND ( tax != 'Y' OR tax IS NULL )" +# ); +# +# $x_cust += scalar_sql($r, \@param, +# "SELECT SUM($e) $fromwhere AND $nottax AND tax = 'Y'" +# ); +# } + + #false laziness -ish w/report_tax.cgi + my $cust_exempt; + if ( $r->taxname ) { + my $q_taxname = dbh->quote($r->taxname); + $cust_exempt = + "( tax = 'Y' + OR EXISTS ( SELECT 1 FROM cust_main_exemption + WHERE cust_main_exemption.custnum = cust_main.custnum + AND cust_main_exemption.taxname = $q_taxname + ) + ) + "; + } else { + $cust_exempt = " tax = 'Y' "; + } + + my $x_cust = scalar_sql($r, \@param, + "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) + $fromwhere AND $nottax AND $cust_exempt " + ); + + $regions{$label}->{'exempt_cust'} += $x_cust; + + ## calculate package-exemption for this region + + my $x_pkg = scalar_sql($r, \@param, + "SELECT SUM( + ( CASE WHEN part_pkg.setuptax = 'Y' + THEN cust_bill_pkg.setup + ELSE 0 + END + ) + + + ( CASE WHEN part_pkg.recurtax = 'Y' + THEN cust_bill_pkg.recur + ELSE 0 + END + ) + ) + $fromwhere + AND $nottax + AND ( + ( part_pkg.setuptax = 'Y' AND cust_bill_pkg.setup > 0 ) + OR ( part_pkg.recurtax = 'Y' AND cust_bill_pkg.recur > 0 ) + ) + AND ( tax != 'Y' OR tax IS NULL ) + " + ); + $regions{$label}->{'exempt_pkg'} += $x_pkg; + + ## calculate monthly exemption (texas tax) for this region + + # count up all the cust_tax_exempt_pkg records associated with + # the actual line items. + + my $x_monthly = scalar_sql($r, \@param, + "SELECT SUM(amount) + FROM cust_tax_exempt_pkg + JOIN cust_bill_pkg USING ( billpkgnum ) + $join_cust_pkg + $mywhere" + ); + $regions{$label}->{'exempt_monthly'} += $x_monthly; + + my $taxable = $t - $x_cust - $x_pkg - $x_monthly; + $regions{$label}->{'taxable'} += $taxable; + + $regions{$label}->{'owed'} += $taxable * ($r->tax/100); + + if ( defined($regions{$label}->{'rate'}) + && $regions{$label}->{'rate'} != $r->tax.'%' ) { + $regions{$label}->{'rate'} = 'variable'; + } else { + $regions{$label}->{'rate'} = $r->tax.'%'; + } + +} + +my $distinct = "country, state, county, city, + CASE WHEN taxname IS NULL THEN '' ELSE taxname END AS taxname"; +my $taxclass_distinct = + #a little bit unsure of this part... test? + #ah, it looks like it winds up being irrelevant as ->{'tax'} + # from $regions is not displayed when show_taxclasses is on + ( $cgi->param('show_taxclasses') + ? " CASE WHEN taxclass IS NULL THEN '' ELSE taxclass END " + : " '' " + )." AS taxclass"; + + +my %qsearch = ( + 'select' => "DISTINCT $distinct, $taxclass_distinct", + 'table' => 'cust_main_county', + 'hashref' => {}, + 'extra_sql' => $gotcust, +); + +my $taxfromwhere = " FROM cust_bill_pkg $join_cust "; +my $taxwhere = $where; +if ( $conf->exists('tax-pkg_address') ) { + + $taxfromwhere .= 'LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum ) + LEFT JOIN cust_location USING ( locationnum ) '; + + #quelle kludge + $taxwhere =~ s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g; + +} +my $creditfromwhere = $taxfromwhere. + " JOIN cust_credit_bill_pkg USING (billpkgnum"; +$creditfromwhere .= " ,billpkgtaxlocationnum" + if $conf->exists('tax-pkg_address'); +$creditfromwhere .= ")"; + +$taxfromwhere .= " $taxwhere "; #AND payby != 'COMP' "; +$creditfromwhere .= " $taxwhere AND billpkgtaxratelocationnum IS NULL"; #AND payby != 'COMP' "; + +my @taxparam = @base_param; + + +#should i be a cust_main_county method or something +#need to pass in $taxfromwhere & @taxparam??? +my $_taxamount_sub = sub { + my $r = shift; + + #match itemdesc if necessary! + my $named_tax = + $r->taxname + ? 'AND itemdesc = '. dbh->quote($r->taxname) + : "AND ( itemdesc IS NULL OR itemdesc = '' OR itemdesc = 'Tax' )"; + + my $sql = "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) ". + " $taxfromwhere AND cust_bill_pkg.pkgnum = 0 $named_tax"; + + scalar_sql($r, \@taxparam, $sql ); +}; + +my $_creditamount_sub = sub { + my $r = shift; + + #match itemdesc if necessary! + my $named_tax = + $r->taxname + ? 'AND itemdesc = '. dbh->quote($r->taxname) + : "AND ( itemdesc IS NULL OR itemdesc = '' OR itemdesc = 'Tax' )"; + + my $sql = "SELECT SUM(cust_credit_bill_pkg.amount) ". + " $creditfromwhere AND cust_bill_pkg.pkgnum = 0 $named_tax"; + + scalar_sql($r, \@taxparam, $sql ); +}; + +#tax-report_groups filtering +my($group_op, $group_value) = ( '', '' ); +if ( $cgi->param('report_group') =~ /^(=|!=) (.*)$/ ) { + ( $group_op, $group_value ) = ( $1, $2 ); +} +my $group_test = sub { + my $label = shift; + return 1 unless $group_op; #in case we get called inadvertantly + if ( $label eq $out ) { #don't display "out of taxable region" in this case + 0; + } elsif ( $group_op eq '=' ) { + $label =~ /^$group_value/; + } elsif ( $group_op eq '!=' ) { + $label !~ /^$group_value/; + } else { + die "guru meditation #00de: group_op $group_op\n"; + } +}; + +my $tot_tax = 0; +my $tot_credit = 0; +#foreach my $label ( keys %regions ) { +foreach my $r ( qsearch(\%qsearch) ) { + + #warn join('-', map { $r->$_() } qw( country state county taxname ) )."\n"; + + my $label = getlabel($r); + if ( $group_op ) { + next unless &{$group_test}($label); + } + + #my $fromwhere = $join_pkg. $where. " AND payby != 'COMP' "; + #my @param = @base_param; + + my $x = &{$_taxamount_sub}($r); + + $regions{$label}->{'tax'} += $x; + $tot_tax += $x unless $cgi->param('show_taxclasses'); + + ## calculate credit for this region + + $x = &{$_creditamount_sub}($r); + + $regions{$label}->{'credit'} += $x; + $tot_credit += $x unless $cgi->param('show_taxclasses'); + +} + +my %base_regions = (); +if ( $cgi->param('show_taxclasses') ) { + + $qsearch{'select'} = "DISTINCT $distinct"; + foreach my $r ( qsearch(\%qsearch) ) { + + my $x = &{$_taxamount_sub}($r); + + my $base_label = getlabel($r, 'no_taxclass'=>1 ); + $base_regions{$base_label}->{'label'} = $base_label; + + $base_regions{$base_label}->{'url_param'} = + join(';', map "$_=". uri_escape($r->$_()), + qw( county state country taxname ) + ); + + $base_regions{$base_label}->{'tax'} += $x; + $tot_tax += $x; + + ## calculate credit for this region + + $x = &{$_creditamount_sub}($r); + + $base_regions{$base_label}->{'credit'} += $x; + $tot_credit += $x; + + } + +} + +my @regions = keys %regions; + +#tax-report_groups filtering +@regions = grep &{$group_test}($_), @regions + if $group_op; + +#calculate totals +my( $total, $tot_taxable, $tot_owed ) = ( 0, 0, 0 ); +my( $exempt_cust, $exempt_pkg, $exempt_monthly, $tot_credit ) = ( 0, 0, 0, 0 ); +my %taxclasses = (); +my %county = (); +my %state = (); +my %country = (); +foreach (@regions) { + $total += $regions{$_}->{'total'}; + $tot_taxable += $regions{$_}->{'taxable'}; + $tot_owed += $regions{$_}->{'owed'}; + $exempt_cust += $regions{$_}->{'exempt_cust'}; + $exempt_pkg += $regions{$_}->{'exempt_pkg'}; + $exempt_monthly += $regions{$_}->{'exempt_monthly'}; + $tot_credit += $regions{$_}->{'credit'}; + $taxclasses{$regions{$_}->{'taxclass'}} = 1 + if $regions{$_}->{'taxclass'}; + $county{$regions{$_}->{'county'}} = 1; + $state{$regions{$_}->{'state'}} = 1; + $country{$regions{$_}->{'country'}} = 1; +} + +my $total_url_param = ''; +my $total_url_param_invoiced = ''; +if ( $group_op ) { + + my @country = keys %country; + warn "WARNING: multiple countries on this grouped report; total links broken" + if scalar(@country) > 1; + my $country = $country[0]; + + my @state = keys %state; + warn "WARNING: multiple countries on this grouped report; total links broken" + if scalar(@state) > 1; + my $state = $state[0]; + + $total_url_param_invoiced = + $total_url_param = + 'report_group='.uri_escape("$group_op $group_value").';'. + join(';', map 'taxclass='.uri_escape($_), keys %taxclasses ); + $total_url_param .= ';'. + "country=$country;state=".uri_escape($state).';'. + join(';', map 'county='.uri_escape($_), keys %county ) ; + +} + +#ordering +@regions = + map $regions{$_}, + sort { ( ($a eq $out) cmp ($b eq $out) ) || ($b cmp $a) } + @regions; + +my @base_regions = + map $base_regions{$_}, + sort { ( ($a eq $out) cmp ($b eq $out) ) || ($b cmp $a) } + keys %base_regions; + +#add total line +push @regions, { + 'label' => 'Total', + 'url_param' => $total_url_param, + 'url_param_inv' => $total_url_param_invoiced, + 'total' => $total, + 'exempt_cust' => $exempt_cust, + 'exempt_pkg' => $exempt_pkg, + 'exempt_monthly' => $exempt_monthly, + 'taxable' => $tot_taxable, + 'rate' => '', + 'owed' => $tot_owed, + 'tax' => $tot_tax, + 'credit' => $tot_credit, +}; + +#-- + +my $money_char = $conf->config('money_char') || '$'; +my $money_sprintf = sub { + $money_char. sprintf('%.2f', shift ); +}; + +sub getlabel { + my $r = shift; + my %opt = @_; + + my $label; + if ( + $r->tax == 0 + && ! scalar( qsearch('cust_main_county', { 'city' => $r->city, + 'county' => $r->county, + 'state' => $r->state, + 'country' => $r->country, + 'tax' => { op=>'>', value=>0 }, + } + ) + ) + + ) { + #kludge to avoid "will not stay shared" warning + my $out = 'Out of taxable region(s)'; + $label = $out; +# } elsif ( $r->taxname && count_taxname($r->taxname) == 1 ) { +# $label = $r->taxname; +## $regions{$label}->{'taxname'} = $label; +## push @{$regions{$label}->{$_}}, $r->$_() foreach qw( county state country ); + } else { + $label = $r->country; + $label = $r->state.", $label" if $r->state; + $label = $r->county." county, $label" if $r->county; + $label = $r->city. ", $label" if $r->city && $cgi->param('show_cities'); + $label = "$label (". $r->taxclass. ")" + if $r->taxclass + && $cgi->param('show_taxclasses') + && ! $opt{'no_taxclass'}; + $label = $r->taxname. " ($label)" if $r->taxname; + } + return $label; +} + +#my %count_taxname = (); #cache +#sub count_taxname { +# my $taxname = shift; +# return $count_taxname{$taxname} if exists $count_taxname{$taxname}; +# my $sql = 'SELECT COUNT(*) FROM cust_main_county WHERE taxname = ?'; +# my $sth = dbh->prepare($sql) or die dbh->errstr; +# $sth->execute( $taxname ) +# or die "Unexpected error executing statement $sql: ". $sth->errstr; +# $count_taxname{$taxname} = $sth->fetchrow_arrayref->[0]; +#} + +#false laziness w/FS::Report::Table::Monthly (sub should probably be moved up +#to FS::Report or FS::Record or who the fuck knows where) +sub scalar_sql { + my( $r, $param, $sql ) = @_; + #warn "$sql\n"; + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute( map $r->$_(), @$param ) + or die "Unexpected error executing statement $sql: ". $sth->errstr; + $sth->fetchrow_arrayref->[0] || 0; +} + +my $dateagentlink = "begin=$beginning;end=$ending"; +$dateagentlink .= ';agentnum='. $cgi->param('agentnum') + if length($agentname); +my $baselink = $p. "search/cust_bill_pkg.cgi?$dateagentlink"; +my $exemptlink = $p. "search/cust_tax_exempt_pkg.cgi?$dateagentlink"; +my $creditlink = $p. "search/cust_credit_bill_pkg.html?$dateagentlink"; + +</%init> diff --git a/httemplate/search/report_tax.html b/httemplate/search/report_tax.html new file mode 100755 index 000000000..2ab0e0b2e --- /dev/null +++ b/httemplate/search/report_tax.html @@ -0,0 +1,79 @@ +<% include('/elements/header.html', 'Tax Report' ) %> + +<FORM ACTION="report_tax.cgi" METHOD="GET"> + +<TABLE> + +% if ( $conf->config('tax-report_groups') ) { +% my @lines = $conf->config('tax-report_groups'); + + <TR> + <TD ALIGN="right">Tax group</TD> + <TD> + <SELECT NAME="report_group"> + + <OPTION VALUE="">all</OPTION> + +% foreach my $line ( @lines ) { +% $line =~ /^\s*(.+)\s+(=|!=)\s+(.*)\s*$/ #or next; +% or do { warn "bad report_group line: $line\n"; next; }; +% my($label, $op, $value) = ($1, $2, $3); + + <OPTION VALUE="<% "$op $value" %>"><% $label %></OPTION> +% } + + </SELECT> + </TD> + </TR> + +% } + + <% include( '/elements/tr-select-agent.html', 'disable_empty'=>0 ) %> + + <% include( '/elements/tr-input-beginning_ending.html' ) %> + +% if ( $city ) { + <TR> + <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_cities" VALUE="1"></TD> + <TD>Show cities</TD> + </TR> +% } + +% if ( $conf->exists('enable_taxclasses') ) { + <TR> + <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_taxclasses" VALUE="1"></TD> + <TD>Show tax classes</TD> + </TR> +% } + +% my @pkg_class = qsearch('pkg_class', {}); +% if ( @pkg_class ) { + <TR> + <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_pkgclasses" VALUE="1"></TD> + <TD>Show package classes</TD> + </TR> +% } + +</TABLE> + +<BR><INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; + +my $city_sql = "SELECT COUNT(*) FROM cust_main_county + WHERE city != '' AND city IS NOT NULL + LIMIT 1"; + +my $city_sth = dbh->prepare($city_sql) or die dbh->errstr; +$city_sth->execute or die $city_sth->errstr; +my $city = $city_sth->fetchrow_arrayref->[0]; + +</%init> diff --git a/httemplate/search/report_timeworked.html b/httemplate/search/report_timeworked.html new file mode 100644 index 000000000..492e738ad --- /dev/null +++ b/httemplate/search/report_timeworked.html @@ -0,0 +1,28 @@ +<% include( '/elements/header.html', 'Time Worked' ) %> + +<FORM ACTION="timeworked.html" METHOD="GET"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"> + <FONT SIZE="+1">Search options</FONT> + </TH> + </TR> + + <% include ('/elements/tr-input-beginning_ending.html') %> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Time queue'); + +</%init> diff --git a/httemplate/search/report_unapplied_cust_pay.html b/httemplate/search/report_unapplied_cust_pay.html new file mode 100755 index 000000000..d2dd9e71d --- /dev/null +++ b/httemplate/search/report_unapplied_cust_pay.html @@ -0,0 +1,47 @@ +<% include('/elements/header.html', 'Unapplied Payments Aging Summary' ) %> +%# 'Prepaid Balance Aging Summary', #??? + +<FORM NAME="OneTrueForm" ACTION="unapplied_cust_pay.html" METHOD="GET"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"> + <FONT SIZE="+1">Search options</FONT> + </TH> + </TR> + + <% include( '/elements/tr-select-agent.html', 'disable_empty'=>0 ) %> + + <% include( '/elements/tr-select-cust_main-status.html', + 'label' => 'Customer Status' + ) + %> + + <TR> + <TD ALIGN="right">Customers</TD> + <TD> + <INPUT TYPE="radio" NAME="all_customers" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.days.disabled=true; document.OneTrueForm.days.style.backgroundColor = '#dddddd'; } else { document.OneTrueForm.days.disabled=false; document.OneTrueForm.days.style.backgroundColor = '#ffffff'; }">All customers (even those without unapplied payments)<BR> + <INPUT TYPE="radio" NAME="all_customers" VALUE="0" CHECKED onClick="if ( ! this.checked ) { document.OneTrueForm.days.disabled=true; document.OneTrueForm.days.style.backgroundColor = '#dddddd'; } else { document.OneTrueForm.days.disabled=false; document.OneTrueForm.days.style.backgroundColor = '#ffffff'; }">Customers with unapplied payments over <INPUT NAME="days" TYPE="text" SIZE=4 MAXLENGTH=3 VALUE="0"> days old + </TD> + </TR> + <% include( '/elements/tr-input-date-field.html', { + 'name' => 'as_of', + 'value' => time, + 'label' => 'As of date ', + 'format' => FS::Conf->new->config('date_format') || '%m/%d/%Y', + } ) %> + +</TABLE> + +<BR><INPUT TYPE="submit" VALUE="Get Report"> +</FORM> + +<% include('/elements/footer.html') %> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +</%init> diff --git a/httemplate/search/rt_transaction.html b/httemplate/search/rt_transaction.html new file mode 100644 index 000000000..651f2896d --- /dev/null +++ b/httemplate/search/rt_transaction.html @@ -0,0 +1,96 @@ +<% include('elements/search.html', + 'title' => 'Time worked', + 'name_singular' => 'transaction', + 'query' => $query, + 'count_query' => $count_query, + 'count_addl' => [ $format_seconds_sub, ], + 'header' => [ 'Ticket #', + 'Ticket', + 'Date', + 'Time', + ], + 'fields' => [ 'ticketid', + sub { encode_entities(shift->get('subject')) }, + 'created', + sub { my $seconds = shift->get('transaction_time'); + &{ $format_seconds_sub }( $seconds ); + }, + ], + 'links' => [ + $link, + $link, + '', + '', + ], + ) +%> +<%once> + +my $format_seconds_sub = sub { + my $seconds = shift; + (($seconds < 0) ? '-' : '') . concise(duration($seconds)); +}; + +</%once> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List rating data'); + +#some amount of false laziness w/timeworked.html... + +my $transactiontime = " + CASE transactions.type when 'Set' + THEN (to_number(newvalue,'999999')-to_number(oldvalue, '999999')) * 60 + ELSE timetaken*60 + END +"; + +my $join = 'JOIN Tickets ON Transactions.ObjectId = Tickets.Id '. + 'JOIN Users ON Transactions.Creator = Users.Id '; + +my $where = " + WHERE objecttype='RT::Ticket' + AND ( ( Transactions.Type = 'Set' + AND Transactions.Field = 'TimeWorked' + AND Transactions.NewValue != Transactions.OldValue ) + OR ( ( Transactions.Type='Create' OR Transactions.Type='Comment' OR Transactions.Type='Correspond' ) + AND Transactions.TimeTaken > 0 + ) + ) +"; +#AND transaction_time != 0 +#AND $wheretimeleft + +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). "'"; + $where .= " AND Transactions.Created >= $beginning "; +} +if ( $ending < 4294967295 ) { + $ending = "TIMESTAMP '". time2str('%Y-%m-%d %X', $ending). "'"; + $where .= " AND Transactions.Created <= $ending "; +} + +if ( $cgi->param('otaker') && $cgi->param('otaker') =~ /^([\w\.\-]+)$/ ) { + $where .= " AND Users.name = '$1' "; +} + +my $query = { + 'select' => "Transactions.*, Tickets.Id AS ticketid, Tickets.Subject, Users.name as otaker, $transactiontime AS transaction_time", + #'table' => 'Transactions', + 'table' => 'transactions', + 'addl_from' => $join. + 'LEFT JOIN acct_rt_transaction '. + ' ON Transactions.Id = acct_rt_transaction.transaction_id', + 'extra_sql' => $where, + 'order by' => 'ORDER BY Created', +}; + +my $count_query = + "SELECT COUNT(*), SUM($transactiontime) FROM Transactions $join $where"; + +my $link = [ "${p}rt/Ticket/Display.html?id=", sub { shift->get('id'); } ]; + +</%init> diff --git a/httemplate/search/sql.html b/httemplate/search/sql.html new file mode 100644 index 000000000..df9b8cddb --- /dev/null +++ b/httemplate/search/sql.html @@ -0,0 +1,13 @@ +<% include( 'elements/search.html', + 'title' => 'Query Results', + 'name' => 'rows', + 'query' => 'SELECT '. ( $cgi->param('sql') + || errorpage('Empty query') ), + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Raw SQL'); + +</%init> diff --git a/httemplate/search/sqlradius.cgi b/httemplate/search/sqlradius.cgi new file mode 100644 index 000000000..cca121179 --- /dev/null +++ b/httemplate/search/sqlradius.cgi @@ -0,0 +1,328 @@ +<% include( '/elements/header.html', 'RADIUS Sessions') %> + +% ### +% # and finally, display the thing +% ### +% +% foreach my $part_export ( +% #grep $_->can('usage_sessions'), qsearch( 'part_export' ) +% qsearch( 'part_export', { 'exporttype' => 'sqlradius' } ), +% qsearch( 'part_export', { 'exporttype' => 'sqlradius_withdomain' } ) +% ) { +% %user2svc_acct = (); +% +% my $efields = tie my %efields, 'Tie::IxHash', %fields; +% delete $efields{'framedipaddress'} if $part_export->option('hide_ip'); +% if ( $part_export->option('hide_data') ) { +% delete $efields{$_} foreach qw(acctinputoctets acctoutputoctets); +% } +% if ( $part_export->option('show_called_station') ) { +% $efields->Splice(1, 0, +% 'calledstationid' => { +% 'name' => 'Destination', +% 'attrib' => 'Called-Station-ID', +% 'fmt' => +% sub { length($_[0]) ? shift : ' '; }, +% 'align' => 'left', +% }, +% ); +% } +% +% + + <% $part_export->exporttype %> to <% $part_export->machine %><BR> + <% include( '/elements/table-grid.html' ) %> +% my $bgcolor1 = '#eeeeee'; +% my $bgcolor2 = '#ffffff'; +% my $bgcolor; + + <TR> +% foreach my $field ( keys %efields ) { + + <TH CLASS="grid" BGCOLOR="#cccccc"> + <% $efields{$field}->{name} %><BR> + <FONT SIZE=-2><% $efields{$field}->{attrib} %></FONT> + </TH> + +% } + </TR> + +% foreach my $session ( +% @{ $part_export->usage_sessions( { +% 'stoptime_start' => $beginning, +% 'stoptime_end' => $ending, +% 'open_sessions' => $open_sessions, +% 'starttime_start' => $starttime_beginning, +% 'starttime_end' => $starttime_ending, +% 'svc_acct' => $cgi_svc_acct, +% 'ip' => $ip, +% 'prefix' => $prefix, +% } ) +% } +% ) { +% if ( $bgcolor eq $bgcolor1 ) { +% $bgcolor = $bgcolor2; +% } else { +% $bgcolor = $bgcolor1; +% } + + <TR> +% foreach my $field ( keys %efields ) { +% my $html = &{ $efields{$field}->{fmt} }( $session->{$field}, +% $session, +% $part_export, +% ); +% my $class = ( $html =~ /<TABLE/ ? 'inv' : 'grid' ); + + <TD CLASS="<%$class%>" BGCOLOR="<% $bgcolor %>" ALIGN="<% $efields{$field}->{align} %>"> + <% $html %> + </TD> +% } + </TR> + +% } + +</TABLE> +<BR><BR> + +% } + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List rating data'); + +### +# parse cgi params +### + +#sort of false laziness w/cust_pay.cgi +my( $beginning, $ending ) = ( '', '' ); +if ( $cgi->param('stoptime_beginning') + && $cgi->param('stoptime_beginning') =~ /^([ 0-9\-\/\:\w]{0,54})$/ ) { + $beginning = parse_datetime($1); +} +if ( $cgi->param('stoptime_ending') + && $cgi->param('stoptime_ending') =~ /^([ 0-9\-\/\:\w]{0,54})$/ ) { + $ending = parse_datetime($1); # + 86399; +} +if ( $cgi->param('begin') && $cgi->param('begin') =~ /^(\d+)$/ ) { + $beginning = $1; +} +if ( $cgi->param('end') && $cgi->param('end') =~ /^(\d+)$/ ) { + $ending = $1; +} + +my $open_sessions = ''; +if ( $cgi->param('open_sessions') =~ /^(\d*)$/ ) { + $open_sessions = $1; +} + +my( $starttime_beginning, $starttime_ending ) = ( '', '' ); +if ( $cgi->param('starttime_beginning') + && $cgi->param('starttime_beginning') =~ /^([ 0-9\-\/\:\w]{0,54})$/ ) { + $starttime_beginning = parse_datetime($1); +} +if ( $cgi->param('starttime_ending') + && $cgi->param('starttime_ending') =~ /^([ 0-9\-\/\:\w]{0,54})$/ ) { + $starttime_ending = parse_datetime($1); # + 86399; +} + +my $cgi_svc_acct = ''; +if ( $cgi->param('svcnum') =~ /^(\d+)$/ ) { + $cgi_svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $1 } ); +} elsif ( $cgi->param('username') =~ /^([^@]+)\@([^@]+)$/ ) { + my %search = { 'username' => $1 }; + my $svc_domain = qsearchs('svc_domain', { 'domain' => $2 } ); + if ( $svc_domain ) { + $search{'domsvc'} = $svc_domain->svcnum; + } else { + delete $search{'username'}; + } + $cgi_svc_acct = qsearchs( 'svc_acct', \%search ) + if keys %search; +} elsif ( $cgi->param('username') =~ /^(.+)$/ ) { + $cgi_svc_acct = qsearchs( 'svc_acct', { 'username' => $1 } ); +} + +my $ip = ''; +if ( $cgi->param('ip') =~ /^((\d+\.){3}\d+)$/ ) { + $ip = $1; +} + +my $prefix = $cgi->param('prefix'); +$prefix =~ s/\D//g; +if ( $prefix =~ /^(\d+)$/ ) { + $prefix = $1; + $prefix = "011$prefix" unless $prefix =~ /^1/; +} else { + $prefix = ''; +} + +### +# field formatting subroutines +### + +my %user2svc_acct = (); +my $user_format = sub { + my ( $user, $session, $part_export ) = @_; + + my $svc_acct = ''; + if ( exists $user2svc_acct{$user} ) { + $svc_acct = $user2svc_acct{$user}; + } else { + my %search = (); + if ( $part_export->exporttype eq 'sqlradius_withdomain' ) { + my $domain; + if ( $user =~ /^([^@]+)\@([^@]+)$/ ) { + $search{'username'} = $1; + $domain = $2; + } else { + $search{'username'} = $user; + $domain = $session->{'realm'}; + } + my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } ); + if ( $svc_domain ) { + $search{'domsvc'} = $svc_domain->svcnum; + } else { + delete $search{'username'}; + } + } elsif ( $part_export->exporttype eq 'sqlradius' ) { + $search{'username'} = $user; + } else { + die 'unknown export type '. $part_export->exporttype. + " for $part_export\n"; + } + if ( keys %search ) { + my @svc_acct = + grep { qsearchs( 'export_svc', { + 'exportnum' => $part_export->exportnum, + 'svcpart' => $_->cust_svc->svcpart, + } ) + } qsearch( 'svc_acct', \%search ); + if ( @svc_acct ) { + warn 'multiple svc_acct records for user $user found; '. + 'using first arbitrarily' + if scalar(@svc_acct) > 1; + $user2svc_acct{$user} = $svc_acct = shift @svc_acct; + } + } + } + + if ( $svc_acct ) { + my $svcnum = $svc_acct->svcnum; + qq(<A HREF="${p}view/svc_acct.cgi?$svcnum"><B>$user</B></A>); + } else { + "<B>$user</B>"; + } + +}; + +my $customer_format = sub { + my( $unused, $session ) = @_; + return ' ' unless exists $user2svc_acct{$session->{'username'}}; + my $svc_acct = $user2svc_acct{$session->{'username'}}; + my $cust_pkg = $svc_acct->cust_svc->cust_pkg; + return ' ' unless $cust_pkg; + my $cust_main = $cust_pkg->cust_main; + + qq!<A HREF="${p}view/cust_main.cgi?!. $cust_main->custnum. '">'. + $cust_pkg->cust_main->name. '</A>'; +}; + +my $time_format = sub { + my $time = shift; + return ' ' if $time == 0; + my $pretty = time2str('%T%P %a %b %o %Y', $time ); + $pretty =~ s/ (\d)(st|dn|rd|th)/$1$2/; + $pretty; +}; + +my $duration_format = sub { + my $seconds = shift; + my $hour = int($seconds/3600); + my $min = int( ($seconds%3600) / 60 ); + my $sec = $seconds%60; + '<TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0>'. + '<TR><TD CLASS="inv" ALIGN="right">'. + ( $hour ? "<B>$hour</B>h" : ' ' ). + '</TD><TD CLASS="inv" ALIGN="right">'. + ( ( $hour || $min ) ? "<B>$min</B>m" : ' ' ). + '</TD><TD CLASS="inv" ALIGN="right">'. + "<B>$sec</B>s". + '</TD></TR></TABLE>'; +}; + +my $octets_format = sub { + my $octets = shift; + my $megs = $octets / 1048576; + sprintf('<B>%.3f</B> megs', $megs); + #my $gigs = $octets / 1073741824 + #sprintf('<B>%.3f</B> gigabytes', $gigs); +}; + +### +# the fields +### + +tie my %fields, 'Tie::IxHash', + 'username' => { + name => 'User', + attrib => 'UserName', + fmt => $user_format, + align => 'left', + }, + 'realm' => { + name => 'Realm', + attrib => 'Realm', + align => 'left', + }, + 'dummy' => { + name => 'Customer', + attrib => '', + fmt => $customer_format, + align => 'left', + }, + 'framedipaddress' => { + name => 'IP Address', + attrib => 'Framed-IP-Address', + fmt => sub { my $ip = shift; + length($ip) ? $ip : ' '; + }, + align => 'right', + }, + 'acctstarttime' => { + name => 'Start time', + attrib => 'Acct-Start-Time', + fmt => $time_format, + align => 'left', + }, + 'acctstoptime' => { + name => 'End time', + attrib => 'Acct-Stop-Time', + fmt => $time_format, + align => 'left', + }, + 'acctsessiontime' => { + name => 'Duration', + attrib => 'Acct-Session-Time', + fmt => $duration_format, + align => 'right', + }, + 'acctinputoctets' => { + name => 'Upload', # (from user)', + attrib => 'Acct-Input-Octets', + fmt => $octets_format, + align => 'right', + }, + 'acctoutputoctets' => { + name => 'Download', # (to user)', + attrib => 'Acct-Output-Octets', + fmt => $octets_format, + align => 'right', + }, +; +$fields{$_}->{fmt} ||= sub { length($_[0]) ? shift : ' '; } + foreach keys %fields; + +</%init> diff --git a/httemplate/search/sqlradius.html b/httemplate/search/sqlradius.html new file mode 100644 index 000000000..8c405982f --- /dev/null +++ b/httemplate/search/sqlradius.html @@ -0,0 +1,123 @@ +<% include( '/elements/header.html', 'Search RADIUS sessions' ) %> + +<FORM NAME="OneTrueForm" ACTION="sqlradius.cgi" METHOD="GET"> +% #include( '/elements/table.html' ) + +<% ntable('#cccccc') %> +<TR> + <TD ALIGN="right">Username: </TD> + <TD><INPUT TYPE="text" NAME="username"></TD> +</TR> +<TR> + <TD></TD> + <TD><FONT SIZE="-1"><I>(leave blank to show all users)</I></FONT></TD> +</TR> +% my @part_export = qsearch( 'part_export', { 'exporttype' => 'sqlradius' } ); +% push @part_export, +% qsearch( 'part_export', { 'exporttype' => 'sqlradius_withdomain' } ); +% +% if ( grep { ! $_->option('hide_ip') } @part_export ) { + + <TR> + <TD ALIGN="right">IP address: </TD> + <TD><INPUT TYPE="text" NAME="ip"></TD> + </TR> + <TR> + <TD></TD> + <TD><FONT SIZE="-1"><I>(leave blank to show all IPs)</I></FONT></TD> + </TR> +% } +% if ( grep { $_->option('show_called_station') } @part_export ) { + + <TR> + <TD ALIGN="right">Destination prefix:</TD> + <TD><INPUT TYPE="text" NAME="prefix"></TD> + </TR> + <TR> + <TD></TD> + <TD><FONT SIZE="-1"><I>(country code or country code and prefix)</I></FONT></TD> + </TR> + <TR> + <TD></TD> + <TD><FONT SIZE="-1"><I>(leave blank to show all destinations)</I></FONT></TD> + </TR> +% } + + <TR> + <TD>Show:</TD> + <TD> + <INPUT TYPE="radio" NAME="open_sessions" VALUE="0" onClick="open_changed(this);" CHECKED>Completed sessions<BR> + <INPUT TYPE="radio" NAME="open_sessions" VALUE="1" onClick="open_changed(this);">Open sessions + </TD> + </TR> + + <TR> + <TH COLSPAN=2>Session start</TD> + </TR> + + <% include( '/elements/tr-input-beginning_ending.html', + 'prefix' => 'starttime', + 'input_time' => 1, + ) + %> + + <SCRIPT TYPE="text/javascript"> + + function open_changed(what) { + + var value=get_open_value(what); + if ( value == '1' ) { + what.form.stoptime_beginning_text.disabled = true; + what.form.stoptime_ending_text.disabled = true; + what.form.stoptime_beginning_text.style.backgroundColor = '#dddddd'; + what.form.stoptime_ending_text.style.backgroundColor = '#dddddd'; + what.form.stoptime_beginning_button.style.display = 'none'; + what.form.stoptime_ending_button.style.display = 'none'; + what.form.stoptime_beginning_disabled.style.display = ''; + what.form.stoptime_ending_disabled.style.display = ''; + } else if ( value == '0' ) { + what.form.stoptime_beginning_text.disabled = false; + what.form.stoptime_ending_text.disabled = false; + what.form.stoptime_beginning_text.style.backgroundColor = '#ffffff'; + what.form.stoptime_ending_text.style.backgroundColor = '#ffffff'; + what.form.stoptime_beginning_button.style.display = ''; + what.form.stoptime_ending_button.style.display = ''; + what.form.stoptime_beginning_disabled.style.display = 'none'; + what.form.stoptime_ending_disabled.style.display = 'none'; + } + + } + + function get_open_value(what) { + var rad_val = ''; + for (var i=0; i < what.form.open_sessions.length; i++) { + if (what.form.open_sessions[i].checked) { + var rad_val = what.form.open_sessions[i].value; + } + } + return rad_val; + } + + </SCRIPT> + + <TR> + <TH COLSPAN=2>Session end</TD> + </TR> + + <% include( '/elements/tr-input-beginning_ending.html', + 'prefix' => 'stoptime', + 'input_time' => 1, + ) + %> + +</TABLE> +<BR><INPUT TYPE="submit" VALUE="View sessions"> +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List rating data'); + +</%init> diff --git a/httemplate/search/svc_acct.cgi b/httemplate/search/svc_acct.cgi new file mode 100755 index 000000000..1407d9e30 --- /dev/null +++ b/httemplate/search/svc_acct.cgi @@ -0,0 +1,334 @@ +<% include( 'elements/search.html', + 'title' => 'Account Search Results', + 'name' => 'accounts', + 'query' => $sql_query, + 'count_query' => $count_query, + 'redirect' => $link, + 'header' => \@header, + 'fields' => \@fields, + 'links' => \@links, + 'align' => $align, + 'color' => \@color, + 'style' => \@style, + 'footer' => \@footer, + ) +%> +<%once> + +#false laziness w/ClientAPI/MyAccount.pm +sub format_time { + my $support = shift; + (($support < 0) ? '-' : '' ). int(abs($support)/3600)."h".sprintf("%02d",(abs($support)%3600)/60)."m"; +} + +sub timelast { + my( $svc_acct, $last, $permonth ) = @_; + + my $sql = " + SELECT SUM(support) FROM acct_rt_transaction + LEFT JOIN Transactions + ON Transactions.Id = acct_rt_transaction.transaction_id + WHERE svcnum = ? + AND Transactions.Created >= ? + "; + + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute( $svc_acct->svcnum, + time2str('%Y-%m-%d %X', time - $last*86400 ) + ) + or die $sth->errstr; + + my $seconds = $sth->fetchrow_arrayref->[0]; + + #my $return = (($seconds < 0) ? '-' : '') . concise(duration($seconds)); + my $return = (($seconds < 0) ? '-' : '') . format_time($seconds); + + $return .= sprintf(' (%.2fx)', $seconds / $permonth ) if $permonth; + + $return; + +} + +</%once> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" unless $curuser->access_right('List services'); + +my $link = [ "${p}view/svc_acct.cgi?", 'svcnum' ]; +my $link_cust = sub { + my $svc_acct = shift; + if ( $svc_acct->custnum ) { + [ "${p}view/cust_main.cgi?", 'custnum' ]; + } else { + ''; + } +}; + +my %search_hash = (); +my @extra_sql = (); + +my @header = ( 'Service', 'Account' ); +my @fields = ( 'svc', 'email' ); +my @links = ( $link, $link ); +my $align = 'll'; +my @color = ( '', '' ); +my @style = ( '', '' ); +my @footer = (); + +my $conf = new FS::Conf; + +if ( $conf->exists('report-showpasswords') #its a terrible idea + && $curuser->access_right('List service passwords') #but if you insist... + ) +{ + push @header, 'Password'; + push @fields, 'get_cleartext_password'; + push @links, $link; + $align .= 'l'; + push @color, ''; + push @style, ''; +} + +push @header, 'Real Name'; +push @fields, 'finger'; +push @links, $link; +$align .= 'l'; +push @color, ''; +push @style, ''; + +#hide the UID, its much less useful these days +if ( $cgi->param('show_uid') ) { #XXX add a checkbox + push @header, 'UID'; + push @fields, 'uid'; + push @links, $link; + $align .= 'l'; + push @color, ''; + push @style, ''; +} + +push @header, 'Last Login'; +push @fields, 'last_login_text'; +push @links, $link; +$align .= 'r'; +push @color, ''; +push @style, ''; + + +for (qw( domain domsvc agentnum custnum popnum svcpart cust_fields )) { + $search_hash{$_} = $cgi->param($_) if length($cgi->param($_)); +} + +my $timepermonth = ''; + +my $orderby = 'ORDER BY svcnum'; +if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { + + $search_hash{'unlinked'} = 1 + if $cgi->param('magic') eq 'unlinked'; + + my $sortby = ''; + if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { + $sortby = $1; + $sortby = "LOWER($sortby)" + if $sortby eq 'username'; + push @extra_sql, "$sortby IS NOT NULL" #XXX search_hash + if $sortby eq 'uid' || $sortby eq 'seconds' || $sortby eq 'last_login'; + $orderby = "ORDER BY $sortby"; + } + + if ( $sortby eq 'seconds' ) { + my $tot_time = 0; + #push @header, 'Time remaining'; + push @header, 'Time'; + push @fields, sub { my $svc_acct = shift; + $tot_time += $svc_acct->seconds; + format_time($svc_acct->seconds); + }; + push @links, ''; + $align .= 'r'; + push @color, ''; + push @style, ''; + + @footer = ( 'Total', '', '', '', + sub { format_time($tot_time) }, #time + ); + + if ( $conf->exists('svc_acct-display_paid_time_remaining') ) { + my $tot_paid_time = 0; + my %tot = ( '30'=>0, '60'=>0, '90'=>0 ); + push @header, 'Paid time', 'Last 30', 'Last 60', 'Last 90'; + 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; + + #my $timepermonth = $part_pkg->option('seconds'); + $timepermonth = $part_pkg->option('seconds'); + $timepermonth = $timepermonth / $part_pkg->freq + if $part_pkg->freq =~ /^\d+$/ && $part_pkg->freq != 0; + + #my $recur = $part_pkg->calc_recur($cust_pkg); + my $recur = $part_pkg->base_recur($cust_pkg); + + return format_time($seconds) unless $timepermonth && $recur; + + my $balance = $cust_pkg->cust_main->balance; + my $periods_unpaid = $balance / $recur; + my $time_unpaid = $periods_unpaid * $timepermonth; + $time_unpaid *= $part_pkg->freq + if $part_pkg->freq =~ /^\d+$/ && $part_pkg->freq != 0; + $tot_paid_time += $seconds-$time_unpaid; + format_time($seconds-$time_unpaid). + sprintf(' (%.2fx monthly)', ( $seconds-$time_unpaid ) / $timepermonth ); + }, + sub { timelast( shift, 30, $timepermonth ); }, + sub { timelast( shift, 60, $timepermonth ); }, + sub { timelast( shift, 90, $timepermonth ); }, + ; + push @links, '', '', '', ''; + $align .= 'rrrr'; + push @color, '', '', '', ''; + push @style, '', '', '', ''; + push @footer, + sub { format_time($tot_paid_time) }, #paid time + '', #XXX sub { $tot{'30'} }, #30 + '', #XXX sub { $tot{'60'} }, #60 + '', #XXX sub { $tot{'90'} }, #90 + ; + } + + push @footer, '', ''; + + } + +} elsif ( $cgi->param('magic') =~ /^nologin$/ ) { + + if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { + my $sortby = $1; + $sortby = "LOWER($sortby)" + if $sortby eq 'username'; + push @extra_sql, "last_login IS NULL"; + $orderby = "ORDER BY $sortby"; + } + +} elsif ( $cgi->param('magic') =~ /^advanced$/ ) { + + $orderby = ""; + + $search_hash{'pkgpart'} = [ $cgi->param('pkgpart') ]; + + foreach my $field (qw( last_login last_logout )) { + + my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field); + + next if $beginning == 0 && $ending == 4294967295; + + if ($cgi->param($field."_invert")) { + push @extra_sql, + "(svc_acct.$field IS NULL OR ". + "svc_acct.$field < $beginning AND ". + "svc_acct.$field > $ending)"; + } else { + push @extra_sql, + "svc_acct.$field IS NOT NULL", + "svc_acct.$field >= $beginning", + "svc_acct.$field <= $ending"; + } + + $orderby ||= "ORDER BY svc_acct.$field" . + ($cgi->param($field."_invert") ? ' DESC' : ''); + + } + + $orderby ||= "ORDER BY svcnum"; + +} elsif ( $cgi->param('popnum') ) { + $orderby = "ORDER BY LOWER(username)"; +} elsif ( $cgi->param('svcpart') ) { + $orderby = "ORDER BY uid"; + #$orderby = "ORDER BY svcnum"; +} else { + $orderby = "ORDER BY uid"; + + my @username_sql; + + my %username_type; + foreach ( $cgi->param('username_type') ) { + $username_type{$_}++; + } + + $cgi->param('username') =~ /^([\w\-\.\&]+)$/; #untaint username_text + my $username = $1; + + push @username_sql, "username ILIKE '$username'" + if $username_type{'Exact'} + || $username_type{'Fuzzy'}; + + push @username_sql, "username ILIKE '\%$username\%'" + if $username_type{'Substring'} + || $username_type{'All'}; + + if ( $username_type{'Fuzzy'} || $username_type{'All'} ) { + &FS::svc_acct::check_and_rebuild_fuzzyfiles; + my $all_username = &FS::svc_acct::all_username; + + my %username; + if ( $username_type{'Fuzzy'} || $username_type{'All'} ) { + foreach ( amatch($username, [ qw(i) ], @$all_username) ) { + $username{$_}++; + } + } + + #if ($username_type{'Sound-alike'}) { + #} + + push @username_sql, "username = '$_'" + foreach (keys %username); + + } + + push @extra_sql, '( '. join( ' OR ', @username_sql). ' )'; + +} + +my $date_format = $conf->config('date_format') || '%m/%d/%Y'; + +$cgi->param('cust_pkg_fields') =~ /^([\w\,]*)$/ or die "bad cust_pkg_fields"; +my @pkg_fields = split(',', $1); +foreach my $pkg_field ( @pkg_fields ) { + ( my $header = ucfirst($pkg_field) ) =~ s/_/ /; #:/ + push @header, $header; + + #not the most efficient to do it every field, but this is of niche use. so far + push @fields, sub { my $svc_acct = shift; + my $cust_pkg = $svc_acct->cust_svc->cust_pkg or return ''; + my $value = $cust_pkg->get($pkg_field);#closures help alot + $value ? time2str('%b %d %Y', $value ) : ''; + }; + + push @links, ''; + $align .= 'c'; + push @color, ''; + push @style, ''; + +} + +push @header, FS::UI::Web::cust_header($cgi->param('cust_fields')); +push @fields, \&FS::UI::Web::cust_fields, +push @links, map { $_ ne 'Cust. Status' ? $link_cust : '' } + FS::UI::Web::cust_header($cgi->param('cust_fields')); +$align .= FS::UI::Web::cust_aligns(); +push @color, FS::UI::Web::cust_colors(); +push @style, FS::UI::Web::cust_styles(); + +$search_hash{'order_by'} = $orderby; +$search_hash{'where'} = \@extra_sql; + +my $sql_query = FS::svc_acct->search(\%search_hash); +my $count_query = delete($sql_query->{'count_query'}); + +</%init> diff --git a/httemplate/search/svc_broadband.cgi b/httemplate/search/svc_broadband.cgi new file mode 100755 index 000000000..d0b102957 --- /dev/null +++ b/httemplate/search/svc_broadband.cgi @@ -0,0 +1,123 @@ +<% include( 'elements/search.html', + 'title' => 'Broadband Search Results', + 'name' => 'broadband services', + 'query' => $sql_query, + 'count_query' => $count_query, + 'redirect' => [ popurl(2). "view/svc_broadband.cgi?", 'svcnum' ], + 'header' => [ '#', + 'Service', + 'Router', + 'IP Address', + FS::UI::Web::cust_header(), + ], + 'fields' => [ 'svcnum', + 'svc', + sub { $routerbyblock{shift->blocknum}->routername; }, + 'ip_addr', + \&FS::UI::Web::cust_fields, + ], + 'links' => [ $link, + $link, + $link_router, + $link, + ( map { $_ ne 'Cust. Status' ? $link_cust : '' } + FS::UI::Web::cust_header() + ), + ], + 'align' => 'rllr'. FS::UI::Web::cust_aligns(), + 'color' => [ + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List services'); + +my $conf = new FS::Conf; + +my $orderby = 'ORDER BY svcnum'; +my %svc_broadband = (); +my @extra_sql = (); +if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { + + push @extra_sql, 'pkgnum IS NULL' + if $cgi->param('magic') eq 'unlinked'; + + if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { + my $sortby = $1; + $orderby = "ORDER BY $sortby"; + } + +} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) { + push @extra_sql, "svcpart = $1"; +} elsif ( $cgi->param('ip_addr') =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) { + push @extra_sql, "ip_addr = '$1'"; +} + +my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. + ' LEFT JOIN part_svc USING ( svcpart ) '. + ' LEFT JOIN cust_pkg USING ( pkgnum ) '. + ' LEFT JOIN cust_main USING ( custnum ) '; + +push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( + 'null_right' => 'View/link unlinked services' + ); + +my $extra_sql = ''; +if ( @extra_sql ) { + $extra_sql = ( keys(%svc_broadband) ? ' AND ' : ' WHERE ' ). + join(' AND ', @extra_sql ); +} + +my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from "; +#if ( keys %svc_broadband ) { +# $count_query .= ' WHERE '. +# join(' AND ', map "$_ = ". dbh->quote($svc_broadband{$_}), +# keys %svc_broadband +# ); +#} +$count_query .= $extra_sql; + +my $sql_query = { + 'table' => 'svc_broadband', + 'hashref' => {}, #\%svc_broadband, + 'select' => join(', ', + 'svc_broadband.*', + 'part_svc.svc', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'extra_sql' => $extra_sql, + 'addl_from' => $addl_from, +}; + +my %routerbyblock = (); +foreach my $router (qsearch('router', {})) { + foreach ($router->addr_block) { + $routerbyblock{$_->blocknum} = $router; + } +} + +my $link = [ $p.'view/svc_broadband.cgi?', 'svcnum' ]; + +#XXX get the router link working +my $link_router = sub { my $routernum = $routerbyblock{shift->blocknum}->routernum; + [ $p.'view/router.cgi?'.$routernum, 'routernum' ]; + }; + +my $link_cust = [ $p.'view/cust_main.cgi?', 'custnum' ]; + +</%init> diff --git a/httemplate/search/svc_domain.cgi b/httemplate/search/svc_domain.cgi new file mode 100755 index 000000000..08ffdba5e --- /dev/null +++ b/httemplate/search/svc_domain.cgi @@ -0,0 +1,112 @@ +<% include( 'elements/search.html', + 'title' => "Domain Search Results", + 'name' => 'domains', + 'query' => $sql_query, + 'count_query' => $count_query, + 'redirect' => $link, + 'header' => [ '#', + 'Service', + 'Domain', + FS::UI::Web::cust_header(), + ], + 'fields' => [ 'svcnum', + 'svc', + 'domain', + \&FS::UI::Web::cust_fields, + ], + 'links' => [ $link, + $link, + $link, + ( map { $_ ne 'Cust. Status' ? $link_cust : '' } + FS::UI::Web::cust_header() + ), + ], + 'align' => 'rll'. FS::UI::Web::cust_aligns(), + 'color' => [ + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List services'); + +my $conf = new FS::Conf; + +my $orderby = 'ORDER BY svcnum'; +my %svc_domain = (); +my @extra_sql = (); +if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { + + push @extra_sql, 'pkgnum IS NULL' + if $cgi->param('magic') eq 'unlinked'; + + if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { + my $sortby = $1; + $orderby = "ORDER BY $sortby"; + } + +} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) { + push @extra_sql, "svcpart = $1"; +} else { + $cgi->param('domain') =~ /^([\w\-\.]+)$/; + $svc_domain{'domain'} = $1; +} + +my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. + ' LEFT JOIN part_svc USING ( svcpart ) '. + ' LEFT JOIN cust_pkg USING ( pkgnum ) '. + ' LEFT JOIN cust_main USING ( custnum ) '; + +#here is the agent virtualization +push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( + 'null_right' => 'View/link unlinked services' + ); + +my $extra_sql = ''; +if ( @extra_sql ) { + $extra_sql = ( keys(%svc_domain) ? ' AND ' : ' WHERE ' ). + join(' AND ', @extra_sql ); +} + +my $count_query = "SELECT COUNT(*) FROM svc_domain $addl_from "; +if ( keys %svc_domain ) { + $count_query .= ' WHERE '. + join(' AND ', map "$_ = ". dbh->quote($svc_domain{$_}), + keys %svc_domain + ); +} +$count_query .= $extra_sql; + +my $sql_query = { + 'table' => 'svc_domain', + 'hashref' => \%svc_domain, + 'select' => join(', ', + 'svc_domain.*', + 'part_svc.svc', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'extra_sql' => "$extra_sql $orderby", + 'addl_from' => $addl_from, +}; + +my $link = [ "${p}view/svc_domain.cgi?", 'svcnum' ]; + +#smaller false laziness w/svc_*.cgi here +my $link_cust = sub { + my $svc_x = shift; + $svc_x->custnum ? [ "${p}view/cust_main.cgi?", 'custnum' ] : ''; +}; + +</%init> diff --git a/httemplate/search/svc_external.cgi b/httemplate/search/svc_external.cgi new file mode 100755 index 000000000..f0617542a --- /dev/null +++ b/httemplate/search/svc_external.cgi @@ -0,0 +1,135 @@ +<% include( 'elements/search.html', + 'title' => 'External service search results', + 'name' => 'external services', + 'query' => $sql_query, + 'count_query' => $count_query, + 'redirect' => $redirect, + 'header' => [ '#', + 'Service', + ( FS::Msgcat::_gettext('svc_external-id') || 'External ID' ), + ( FS::Msgcat::_gettext('svc_external-title') || 'Title' ), + FS::UI::Web::cust_header(), + ], + 'fields' => [ 'svcnum', + 'svc', + 'id', + 'title', + \&FS::UI::Web::cust_fields, + ], + 'links' => [ $link, + $link, + $link, + $link, + ( map { $_ ne 'Cust. Status' ? $link_cust : '' } + FS::UI::Web::cust_header() + ), + ], + 'align' => 'rlrr'. + FS::UI::Web::cust_aligns(), + 'color' => [ + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List services'); + +my $conf = new FS::Conf; + +my %svc_external; +my @extra_sql = (); +my $orderby = 'ORDER BY svcnum'; + +my $link = [ "${p}view/svc_external.cgi?", 'svcnum' ]; +my $redirect = $link; + +if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { + + push @extra_sql, 'pkgnum IS NULL' + if $cgi->param('magic') eq 'unlinked'; + + if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { + my $sortby = $1; + $orderby = "ORDER BY $sortby"; + } + +} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) { + + push @extra_sql, "svcpart = $1"; + +} elsif ( $cgi->param('title') =~ /^(.*)$/ ) { + + $svc_external{'title'} = $1; + $orderby = 'ORDER BY id'; + + # is this linked from anywhere??? + # if( $cgi->param('history') == 1 ) { + # @h_svc_external=qsearch('h_svc_external',{ title => $1 }); + # } + +} elsif ( $cgi->param('id') =~ /^([\w\-\.]+)$/ ) { + + $svc_external{'id'} = $1; + +} + +my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. + ' LEFT JOIN part_svc USING ( svcpart ) '. + ' LEFT JOIN cust_pkg USING ( pkgnum ) '. + ' LEFT JOIN cust_main USING ( custnum ) '; + +#here is the agent virtualization +push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( + 'null_right' => 'View/link unlinked services' + ); + +my $extra_sql = ''; +if ( @extra_sql ) { + $extra_sql = ( keys(%svc_external) ? ' AND ' : ' WHERE ' ). + join(' AND ', @extra_sql ); +} + +my $count_query = "SELECT COUNT(*) FROM svc_external $addl_from "; +if ( keys %svc_external ) { + $count_query .= ' WHERE '. + join(' AND ', map "$_ = ". dbh->quote($svc_external{$_}), + keys %svc_external + ); +} +$count_query .= $extra_sql; + +my $sql_query = { + 'table' => 'svc_external', + 'hashref' => \%svc_external, + 'select' => join(', ', + 'svc_external.*', + 'part_svc.svc', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'extra_sql' => "$extra_sql $orderby", + 'addl_from' => $addl_from, +}; + +#smaller false laziness w/svc_*.cgi here +my $link_cust = sub { + my $svc_x = shift; + $svc_x->custnum ? [ "${p}view/cust_main.cgi?", 'custnum' ] : ''; +}; + + +</%init> diff --git a/httemplate/search/svc_forward.cgi b/httemplate/search/svc_forward.cgi new file mode 100755 index 000000000..2bcd0c8c8 --- /dev/null +++ b/httemplate/search/svc_forward.cgi @@ -0,0 +1,146 @@ +<% include( 'elements/search.html', + 'title' => "Mail forward Search Results", + 'name' => 'mail forwards', + 'query' => $sql_query, + 'count_query' => $count_query, + 'redirect' => $link, + 'header' => [ '#', + 'Service', + 'Mail to', + 'Forwards to', + FS::UI::Web::cust_header(), + ], + 'fields' => [ 'svcnum', + 'svc', + $format_src, + $format_dst, + \&FS::UI::Web::cust_fields, + ], + 'links' => [ $link, + $link, + $link_src, + $link_dst, + ( map { $_ ne 'Cust. Status' ? $link_cust : '' } + FS::UI::Web::cust_header() + ), + ], + 'align' => 'rlll'. FS::UI::Web::cust_aligns(), + 'color' => [ + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List services'); + +my $conf = new FS::Conf; + +my $orderby = 'ORDER BY svcnum'; +my @extra_sql = (); +if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { + + push @extra_sql, 'pkgnum IS NULL' + if $cgi->param('magic') eq 'unlinked'; + + if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { + my $sortby = $1; + $orderby = "ORDER BY $sortby"; + } + +} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) { + push @extra_sql, "svcpart = $1"; +} + +my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. + ' LEFT JOIN part_svc USING ( svcpart ) '. + ' LEFT JOIN cust_pkg USING ( pkgnum ) '. + ' LEFT JOIN cust_main USING ( custnum ) '; + +#here is the agent virtualization +push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( + 'null_right' => 'View/link unlinked services' + ); + +my $extra_sql = + scalar(@extra_sql) + ? ' WHERE '. join(' AND ', @extra_sql ) + : ''; + +my $count_query = "SELECT COUNT(*) FROM svc_forward $addl_from $extra_sql"; +my $sql_query = { + 'table' => 'svc_forward', + 'hashref' => {}, + 'select' => join(', ', + 'svc_forward.*', + 'part_svc.svc', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'extra_sql' => "$extra_sql $orderby", + 'addl_from' => $addl_from, +}; + +# <TH>Service #<BR><FONT SIZE=-1>(click to view forward)</FONT></TH> +# <TH>Mail to<BR><FONT SIZE=-1>(click to view account)</FONT></TH> +# <TH>Forwards to<BR><FONT SIZE=-1>(click to view account)</FONT></TH> + +my $link = [ "${p}view/svc_forward.cgi?", 'svcnum' ]; + +my $format_src = sub { + my $svc_forward = shift; + if ( $svc_forward->srcsvc_acct ) { + $svc_forward->srcsvc_acct->email; + } else { + my $src = $svc_forward->src; + $src = "<I>(anything)</I>$src" if $src =~ /^@/; + $src; + } +}; + +my $link_src = sub { + my $svc_forward = shift; + if ( $svc_forward->srcsvc_acct ) { + [ "${p}view/svc_acct.cgi?", 'srcsvc' ]; + } else { + ''; + } +}; + +my $format_dst = sub { + my $svc_forward = shift; + if ( $svc_forward->dstsvc_acct ) { + $svc_forward->dstsvc_acct->email; + } else { + $svc_forward->dst; + } +}; + +my $link_dst = sub { + my $svc_forward = shift; + if ( $svc_forward->dstsvc_acct ) { + [ "${p}view/svc_acct.cgi?", 'dstsvc' ]; + } else { + ''; + } +}; + +#smaller false laziness w/svc_*.cgi here +my $link_cust = sub { + my $svc_x = shift; + $svc_x->custnum ? [ "${p}view/cust_main.cgi?", 'custnum' ] : ''; +}; + +</%init> diff --git a/httemplate/search/svc_phone.cgi b/httemplate/search/svc_phone.cgi new file mode 100644 index 000000000..0ad458b72 --- /dev/null +++ b/httemplate/search/svc_phone.cgi @@ -0,0 +1,174 @@ +<% include( 'elements/search.html', + 'title' => "Phone number search results", + 'name' => 'phone numbers', + 'query' => $sql_query, + 'count_query' => $count_query, + 'redirect' => $redirect, + 'header' => [ '#', + 'Service', + 'Country code', + 'Phone number', + @header, + FS::UI::Web::cust_header(), + ], + 'fields' => [ 'svcnum', + 'svc', + 'countrycode', + 'phonenum', + @fields, + \&FS::UI::Web::cust_fields, + ], + 'links' => [ $link, + $link, + $link, + $link, + ( map '', @header ), + ( map { $_ ne 'Cust. Status' ? $link_cust : '' } + FS::UI::Web::cust_header() + ), + ], + 'align' => 'rlrr'. + join('', map 'r', @header). + FS::UI::Web::cust_aligns(), + 'color' => [ + '', + '', + '', + '', + ( map '', @header ), + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + ( map '', @header ), + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List services'); + +my $conf = new FS::Conf; + +my @select = (); +my %svc_phone = (); +my @extra_sql = (); +my $orderby = 'ORDER BY svcnum'; + +my @header = (); +my @fields = (); +my $link = [ "${p}view/svc_phone.cgi?", 'svcnum' ]; +my $redirect = $link; + +if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { + + push @extra_sql, 'pkgnum IS NULL' + if $cgi->param('magic') eq 'unlinked'; + + if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { + my $sortby = $1; + $orderby = "ORDER BY $sortby"; + } + + if ( $cgi->param('usage_total') ) { + + my($beginning,$ending) = FS::UI::Web::parse_beginning_ending($cgi, 'usage'); + + $redirect = ''; + + #my $and_date = " AND startdate >= $beginning AND startdate <= $ending "; + my $and_date = " AND enddate >= $beginning AND enddate <= $ending "; + + my $fromwhere = " FROM cdr WHERE cdr.svcnum = svc_phone.svcnum $and_date"; + + #more efficient to join against cdr just once... this will do for now + push @select, map { " ( SELECT SUM($_) $fromwhere ) AS $_ " } + qw( billsec rated_price ); + + my $money_char = $conf->config('money_char') || '$'; + + push @header, 'Minutes', 'Billed'; + push @fields, + sub { sprintf('%.3f', shift->get('billsec') / 60 ); }, + sub { $money_char. sprintf('%.2f', shift->get('rated_price') ); }; + + #XXX and termination... (this needs a config to turn on, not by default) + if ( 1 ) { # $conf->exists('cdr-termination_hack') { #} + + my $f_w = + " FROM cdr_termination LEFT JOIN cdr USING ( acctid ) ". + " WHERE cdr.carrierid = CAST(svc_phone.phonenum AS BIGINT) ". # XXX connectone-specific, has to match svc_external.id :/ + $and_date; + + push @select, + " ( SELECT SUM(billsec) $f_w ) AS term_billsec ", + " ( SELECT SUM(cdr_termination.rated_price) $f_w ) AS term_rated_price"; + + push @header, 'Term Min', 'Term Billed'; + push @fields, + sub { sprintf('%.3f', shift->get('term_billsec') / 60 ); }, + sub { $money_char. sprintf('%.2f', shift->get('rated_price') ); }; + + } + + + } + +} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) { + push @extra_sql, "svcpart = $1"; +} else { + $cgi->param('phonenum') =~ /^([\d\- ]+)$/; + ( $svc_phone{'phonenum'} = $1 ) =~ s/\D//g; +} + +my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. + ' LEFT JOIN part_svc USING ( svcpart ) '. + ' LEFT JOIN cust_pkg USING ( pkgnum ) '. + ' LEFT JOIN cust_main USING ( custnum ) '; + +#here is the agent virtualization +push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( + 'null_right' => 'View/link unlinked services' + ); + +my $extra_sql = ''; +if ( @extra_sql ) { + $extra_sql = ( keys(%svc_phone) ? ' AND ' : ' WHERE ' ). + join(' AND ', @extra_sql ); +} + +my $count_query = "SELECT COUNT(*) FROM svc_phone $addl_from "; +if ( keys %svc_phone ) { + $count_query .= ' WHERE '. + join(' AND ', map "$_ = ". dbh->quote($svc_phone{$_}), + keys %svc_phone + ); +} +$count_query .= $extra_sql; + +my $sql_query = { + 'table' => 'svc_phone', + 'hashref' => \%svc_phone, + 'select' => join(', ', + 'svc_phone.*', + 'part_svc.svc', + @select, + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'extra_sql' => "$extra_sql $orderby", + 'addl_from' => $addl_from, +}; + +#smaller false laziness w/svc_*.cgi here +my $link_cust = sub { + my $svc_x = shift; + $svc_x->custnum ? [ "${p}view/cust_main.cgi?", 'custnum' ] : ''; +}; + +</%init> diff --git a/httemplate/search/svc_www.cgi b/httemplate/search/svc_www.cgi new file mode 100755 index 000000000..2e3c4615b --- /dev/null +++ b/httemplate/search/svc_www.cgi @@ -0,0 +1,113 @@ +<% include( 'elements/search.html', + 'title' => 'Virtual Host Search Results', + 'name' => 'virtual hosts', + 'query' => $sql_query, + 'count_query' => $count_query, + 'redirect' => $link, + 'header' => [ '#', + 'Service', + 'Zone', + 'User', + FS::UI::Web::cust_header(), + ], + 'fields' => [ 'svcnum', + 'svc', + sub { $_[0]->domain_record->zone }, + sub { + my $svc_www = shift; + my $svc_acct = $svc_www->svc_acct; + $svc_acct + ? $svc_acct->email + : ''; + }, + \&FS::UI::Web::cust_fields, + ], + 'links' => [ $link, + $link, + '', + $ulink, + ( map { $_ ne 'Cust. Status' ? $link_cust : '' } + FS::UI::Web::cust_header() + ), + ], + 'align' => 'rlll'. FS::UI::Web::cust_aligns(), + 'color' => [ + '', + '', + '', + '', + FS::UI::Web::cust_colors(), + ], + 'style' => [ + '', + '', + '', + '', + FS::UI::Web::cust_styles(), + ], + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List services'); + +#my $conf = new FS::Conf; + +my $orderby = 'ORDER BY svcnum'; +my @extra_sql = (); +if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { + + push @extra_sql, 'pkgnum IS NULL' + if $cgi->param('magic') eq 'unlinked'; + + if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { + my $sortby = $1; + $orderby = "ORDER BY $sortby"; + } + +} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) { + push @extra_sql, "svcpart = $1"; +} + +my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. + ' LEFT JOIN part_svc USING ( svcpart ) '. + ' LEFT JOIN cust_pkg USING ( pkgnum ) '. + ' LEFT JOIN cust_main USING ( custnum ) '; + +#here is the agent virtualization +push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( + 'null_right' => 'View/link unlinked services' + ); + +my $extra_sql = + scalar(@extra_sql) + ? ' WHERE '. join(' AND ', @extra_sql ) + : ''; + + +my $count_query = "SELECT COUNT(*) FROM svc_www $addl_from $extra_sql"; +my $sql_query = { + 'table' => 'svc_www', + 'hashref' => {}, + 'select' => join(', ', + 'svc_www.*', + 'part_svc.svc', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(), + ), + 'extra_sql' => "$extra_sql $orderby", + 'addl_from' => $addl_from, +}; + +my $link = [ "${p}view/svc_www.cgi?", 'svcnum', ]; +#my $dlink = [ "${p}view/svc_www.cgi?", 'svcnum', ]; +my $ulink = [ "${p}view/svc_acct.cgi?", 'usersvc', ]; + +#smaller false laziness w/svc_*.cgi here +my $link_cust = sub { + my $svc_x = shift; + $svc_x->custnum ? [ "${p}view/cust_main.cgi?", 'custnum' ] : ''; +}; + +</%init> diff --git a/httemplate/search/timeworked.html b/httemplate/search/timeworked.html new file mode 100644 index 000000000..d07cd4f59 --- /dev/null +++ b/httemplate/search/timeworked.html @@ -0,0 +1,130 @@ +<% include( 'elements/search.html', + 'title' => 'Time Worked', + 'name' => 'time', + 'html_form' => qq!<FORM NAME="timeForm" ACTION="${p}misc/timeworked.html" METHOD="POST">!, + 'query' => $query, + 'count_query' => $count_query, + 'header' => [ '#', + 'Ticket', + 'Date', + 'Time', + '', # checkbox column + ], + 'fields' => [ sub { shift->[0] }, + sub { encode_entities(shift->[1]) }, + sub { shift->[2] }, + sub { my $seconds = shift->[3]; + (($seconds < 0) ? '-' : '') . + concise(duration($seconds)); + }, + sub { + my $row = shift; + my $seconds = $row->[3]; + my $id = $row->[4]; + qq!<INPUT NAME="transactionid$id" TYPE="checkbox" VALUE="1">!. + qq!<INPUT NAME="seconds$id" TYPE="hidden" VALUE="$seconds">!; + }, + ], + 'links' => [ + $link, + $link, + '', + '', + '', + ], + 'html_foot' => $html_foot, + ) + +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Time queue'); + +my @groupby = (); + +my $transactiontime = " + CASE Transactions.Type WHEN 'Set' + THEN (TO_NUMBER(NewValue,'999999')-TO_NUMBER(OldValue, '999999')) * 60 + ELSE TimeTaken*60 + END +"; + +push @groupby, qw( Transactions.Type NewValue OldValue TimeTaken ); + +my $appliedtimeclause = "COALESCE (SUM(acct_rt_transaction.seconds), 0)"; + +my $appliedtimeselect = " + COALESCE( + ( SELECT SUM(seconds) FROM acct_rt_transaction + WHERE transaction_id = Transactions.id + ), + 0 + ) +"; + +push @groupby, "Transactions.id"; + +my $wheretimeleft = "$transactiontime != $appliedtimeselect"; + +push @groupby, "Tickets.id"; +push @groupby, "Tickets.Subject"; +push @groupby, "Transactions.Created"; + +my $groupby = join(',', @groupby); + +my $where = " + WHERE ObjectType='RT::Ticket' + AND ( ( Transactions.Type='Set' AND Field='TimeWorked' ) + OR Transactions.Type='Create' + OR Transactions.Type='Comment' + OR Transactions.Type='Correspond' + ) + AND $wheretimeleft +"; + #AND $wheretimeleft + +my $str2time_sql = str2time_sql; +my $closing = str2time_sql_closing; + +my($begin, $end) = FS::UI::Web::parse_beginning_ending($cgi); +$where .= " AND $str2time_sql Transactions.Created $closing >= $begin ". + " AND $str2time_sql Transactions.Created $closing <= $end "; + +my $query = " + SELECT Tickets.id, Tickets.Subject, + TO_CHAR(Transactions.Created, 'Dy Mon DD HH24:MI:SS YYYY'), + $transactiontime-$appliedtimeclause, + Transactions.id + FROM Transactions + JOIN Tickets ON Transactions.ObjectId = Tickets.id + LEFT JOIN acct_rt_transaction + ON Transactions.id = acct_rt_transaction.transaction_id + $where + GROUP BY $groupby + ORDER BY Transactions.Created +"; + +my $count_query = "SELECT COUNT(*) FROM Transactions $where"; + +my $link = [ "${p}rt/Ticket/Display.html?id=", sub { shift->[0]; } ]; + +my $html_foot = qq' + <INPUT TYPE="hidden" NAME="begin" VALUE="$begin"> + <INPUT TYPE="hidden" NAME="end" VALUE="$end"> + <BR> + <INPUT TYPE="button" VALUE="select all" onClick="setAll(true)"> + <INPUT TYPE="button" VALUE="unselect all" onClick="setAll(false)"> + <BR> + <INPUT TYPE="submit" NAME="action" VALUE="Assign to accounts"><BR> + <SCRIPT TYPE="text/javascript"> + function setAll(setTo) { + theForm = document.timeForm; + for (i=0,n=theForm.elements.length;i<n;i++) + if (theForm.elements[i].name.indexOf("transactionid") != -1) + theForm.elements[i].checked = setTo; + } + </SCRIPT> +'; + +</%init> diff --git a/httemplate/search/unapplied_cust_pay.html b/httemplate/search/unapplied_cust_pay.html new file mode 100755 index 000000000..73361c00b --- /dev/null +++ b/httemplate/search/unapplied_cust_pay.html @@ -0,0 +1,30 @@ +<% include( 'elements/cust_main_dayranges.html', + #'title' => 'Prepaid Balance Aging Summary', #??? + 'title' => 'Unapplied Payments Aging Summary', + 'range_sub' => \&unapplied_payments, + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +</%init> +<%once> + +sub unapplied_payments { + my($start, $end, $offset) = @_; + + #handle start and end ranges (86400 = 24h * 60m * 60s) + my $str2time = str2time_sql; + my $closing = str2time_sql_closing; + $start = "( $str2time now() $closing - ".($start + $offset) * 86400 . ' )'; + $end = $end ? + "( $str2time now() $closing - ".($end + $offset) * 86400 . ' )' + : ''; + + FS::cust_main->unapplied_payments_date_sql( $start, $end ); + +} + +</%once> |