diff options
author | Ivan Kohler <ivan@freeside.biz> | 2012-04-26 17:35:14 -0700 |
---|---|---|
committer | Ivan Kohler <ivan@freeside.biz> | 2012-04-26 17:35:14 -0700 |
commit | 2a7f90bbc8958c0674bb470ecd8e4bed00e6a8c4 (patch) | |
tree | e855867f9cfdf6028a27472de24ab0cd7dab7b07 | |
parent | 4f94568dd0bc4c857441ec531e2c936fefa78635 (diff) | |
parent | 6ff1c755b054201c38b0a2a7b6161325af5c0bcf (diff) |
Merge branch 'master' of git.freeside.biz:/home/git/freeside
-rw-r--r-- | FS/FS/ClientAPI/MyAccount.pm | 2 | ||||
-rw-r--r-- | FS/FS/Conf.pm | 25 | ||||
-rw-r--r-- | FS/FS/Conf_compat17.pm | 7 | ||||
-rw-r--r-- | FS/FS/Schema.pm | 2 | ||||
-rw-r--r-- | FS/FS/cdr/cia.pm | 5 | ||||
-rw-r--r-- | FS/FS/cust_location.pm | 36 | ||||
-rw-r--r-- | FS/FS/cust_main_county.pm | 2 | ||||
-rw-r--r-- | FS/FS/cust_pay.pm | 42 | ||||
-rw-r--r-- | FS/FS/cust_pkg.pm | 50 | ||||
-rw-r--r-- | FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm | 3 | ||||
-rw-r--r-- | FS/FS/part_event/Action/cust_bill_spool_csv.pm | 3 | ||||
-rw-r--r-- | FS/FS/part_event/Condition/pkg_dundate_age.pm | 43 | ||||
-rwxr-xr-x | FS/FS/svc_broadband.pm | 6 | ||||
-rw-r--r-- | httemplate/edit/tower.html | 4 | ||||
-rw-r--r-- | httemplate/elements/customer-table.html | 46 | ||||
-rw-r--r-- | httemplate/elements/menu.html | 20 | ||||
-rw-r--r-- | httemplate/misc/batch-cust_pay.html | 357 | ||||
-rw-r--r-- | httemplate/misc/process/batch-cust_pay.cgi | 115 | ||||
-rw-r--r-- | httemplate/misc/xmlhttp-cust_bill-search.html | 18 | ||||
-rw-r--r-- | httemplate/misc/xmlhttp-cust_main-search.cgi | 4 | ||||
-rw-r--r-- | httemplate/search/cust_bill_pkg.cgi | 6 | ||||
-rwxr-xr-x | httemplate/search/report_tax.cgi | 69 | ||||
-rw-r--r-- | httemplate/view/directions.html | 1 |
23 files changed, 708 insertions, 158 deletions
diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 7bc3011d2..e9394e4df 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -729,7 +729,7 @@ sub payment_info { $return{payinfo2} = $payinfo2; $return{paytype} = $cust_main->paytype; $return{paystate} = $cust_main->paystate; - + $return{payname} = $cust_main->payname; # override 'first/last name' default from above, if any. Is instution-name here. (#15819) } if ( $conf->config('prepayment_discounts-credit_type') ) { diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 81443632c..82e5ea89c 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -3052,6 +3052,24 @@ and customer address. Include units.', }, { + 'key' => 'cust_location-label_prefix', + 'section' => 'UI', + 'description' => 'Optional "site ID" to show in the location label', + 'type' => 'select', + 'select_hash' => [ '' => '', + 'CoStAg' => 'CoStAgXXXXX (country, state, agent name, locationnum)', + ], + }, + + { + 'key' => 'cust_location-agent_code', + 'section' => 'UI', + 'description' => 'Optional agent string for cust_location-label_prefix', + 'type' => 'text', + 'per_agent' => 1, + }, + + { 'key' => 'cust_pkg-display_times', 'section' => 'UI', 'description' => 'Display full timestamps (not just dates) for customer packages. Useful if you are doing real-time things like hourly prepaid.', @@ -3952,6 +3970,13 @@ and customer address. Include units.', }, { + 'key' => 'unsuspend_email_admin', + 'section' => '', + 'description' => 'Destination admin email address to enable unsuspension notices', + 'type' => 'text', + }, + + { 'key' => 'email_report-subject', 'section' => '', 'description' => 'Subject for reports emailed by freeside-fetch. Defaults to "Freeside report".', diff --git a/FS/FS/Conf_compat17.pm b/FS/FS/Conf_compat17.pm index 6685935d3..2e4bb055f 100644 --- a/FS/FS/Conf_compat17.pm +++ b/FS/FS/Conf_compat17.pm @@ -2458,6 +2458,13 @@ httemplate/docs/config.html }, { + 'key' => 'unsuspend_email_admin', + 'section' => '', + 'description' => 'Destination admin email address to enable unsuspension notices', + 'type' => 'text', + }, + + { 'key' => 'email_report-subject', 'section' => '', 'description' => 'Subject for reports emailed by freeside-fetch. Defaults to "Freeside report".', diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 3894f65f8..d42b946f2 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -2556,7 +2556,7 @@ sub tables_hashref { 'plan_id', 'varchar', 'NULL', $char_d, '', '', ], 'primary_key' => 'svcnum', - 'unique' => [ [ 'mac_addr' ] ], + 'unique' => [ [ 'ip_addr' ], [ 'mac_addr' ] ], 'index' => [], }, diff --git a/FS/FS/cdr/cia.pm b/FS/FS/cdr/cia.pm index 070f3fb0d..ca44c0fdf 100644 --- a/FS/FS/cdr/cia.pm +++ b/FS/FS/cdr/cia.pm @@ -20,11 +20,12 @@ use FS::cdr qw(_cdr_date_parser_maker); skip(2), # Conference Start Time, Conference End Time _cdr_date_parser_maker('startdate'), # Connect Time _cdr_date_parser_maker('enddate'), # Disconnect Time + skip(1), # Duration sub { my($cdr, $data, $conf, $param) = @_; $cdr->duration($data); $cdr->billsec( $data); - }, # Duration - skip(2), # Roundup Duration, User Name + }, # Roundup Duration + skip(1), # User Name 'dst', # DNIS 'src', # ANI skip(2), # Call Type, Toll Free, diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm index a5250ec05..a99fa17d8 100644 --- a/FS/FS/cust_location.pm +++ b/FS/FS/cust_location.pm @@ -408,6 +408,42 @@ sub dealternize { ''; } +=item location_label + +Returns the label of the location object, with an optional site ID +string (based on the cust_location-label_prefix config option). + +=cut + +sub location_label { + my $self = shift; + my %opt = @_; + my $conf = new FS::Conf; + my $prefix = ''; + my $format = $conf->config('cust_location-label_prefix') || ''; + if ( $format eq 'CoStAg' ) { + my $cust_or_prospect; + if ( $self->custnum ) { + $cust_or_prospect = FS::cust_main->by_key($self->custnum); + } + elsif ( $self->prospectnum ) { + $cust_or_prospect = FS::prospect_main->by_key($self->prospectnum); + } + my $agent = $conf->config('cust_location-agent_code', + $cust_or_prospect->agentnum) + || $cust_or_prospect->agent->agent; + # else this location is invalid + $prefix = uc( join('', + $self->country, + ($self->state =~ /^(..)/), + ($agent =~ /^(..)/), + sprintf('%05d', $self->locationnum) + ) ); + } + $prefix .= ($opt{join_string} || ': ') if $prefix; + $prefix . $self->SUPER::location_label(%opt); +} + =back =head1 BUGS diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm index e937b205c..6316f239a 100644 --- a/FS/FS/cust_main_county.pm +++ b/FS/FS/cust_main_county.pm @@ -176,7 +176,7 @@ with different tax classes. sub sql_taxclass_sameregion { my $self = shift; - my $same_query = 'SELECT taxclass FROM cust_main_county '. + my $same_query = 'SELECT DISTINCT taxclass FROM cust_main_county '. ' WHERE taxnum != ? AND country = ?'; my @same_param = ( 'taxnum', 'country' ); foreach my $opt_field (qw( state county )) { diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index eca291a6e..f81a649db 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -760,6 +760,12 @@ objects. Returns a list, each element representing the status of inserting the corresponding payment - empty. If there is an error inserting any payment, the entire transaction is rolled back, i.e. all payments are inserted or none are. +FS::cust_pay objects may have the pseudo-field 'apply_to', containing a +reference to an array of (uninserted) FS::cust_bill_pay objects. If so, +those objects will be inserted with the paynum of the payment, and for +each one, an error message or an empty string will be inserted into the +list of errors. + For example: my @errors = FS::cust_pay->batch_insert(@cust_pay); @@ -786,19 +792,35 @@ sub batch_insert { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $errors = 0; + my $num_errors = 0; - my @errors = map { - my $error = $_->insert( 'manual' => 1 ); - if ( $error ) { - $errors++; - } else { - $_->cust_main->apply_payments; + my @errors; + foreach my $cust_pay (@_) { + my $error = $cust_pay->insert( 'manual' => 1 ); + push @errors, $error; + $num_errors++ if $error; + + if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) { + + foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) { + if ( $error ) { # insert placeholders if cust_pay wasn't inserted + push @errors, ''; + } + else { + $cust_bill_pay->set('paynum', $cust_pay->paynum); + my $apply_error = $cust_bill_pay->insert; + push @errors, $apply_error || ''; + $num_errors++ if $apply_error; + } + } + + } elsif ( !$error ) { #normal case: apply payments as usual + $cust_pay->cust_main->apply_payments; } - $error; - } @_; - if ( $errors ) { + } + + if ( $num_errors ) { $dbh->rollback if $oldAutoCommit; } else { $dbh->commit or die $dbh->errstr if $oldAutoCommit; diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index bee1b82fb..4359de9a4 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -1239,6 +1239,8 @@ sub unsuspend { } #if $date + my @labels = (); + foreach my $cust_svc ( qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } ) ) { @@ -1258,6 +1260,8 @@ sub unsuspend { $dbh->rollback if $oldAutoCommit; return $error; } + my( $label, $value ) = $cust_svc->label; + push @labels, "$label: $value"; } } @@ -1288,6 +1292,29 @@ sub unsuspend { return $error; } + if ( $conf->config('unsuspend_email_admin') ) { + + my $error = send_email( + 'from' => $conf->config('invoice_from', $self->cust_main->agentnum), + #invoice_from ??? well as good as any + 'to' => $conf->config('unsuspend_email_admin'), + 'subject' => 'FREESIDE NOTIFICATION: Customer package unsuspended', 'body' => [ + "This is an automatic message from your Freeside installation\n", + "informing you that the following customer package has been unsuspended:\n", + "\n", + 'Customer: #'. $self->custnum. ' '. $self->cust_main->name. "\n", + 'Package : #'. $self->pkgnum. " (". $self->part_pkg->pkg_comment. ")\n", + ( map { "Service : $_\n" } @labels ), + ], + ); + + if ( $error ) { + warn "WARNING: can't send unsuspension admin email (unsuspending anyway): ". + "$error\n"; + } + + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no errors @@ -3445,7 +3472,13 @@ sub location_sql { # '?' placeholders in _location_sql_where my $x = $ornull ? 3 : 2; - my @bill_param = ( ('city')x3, ('county')x$x, ('state')x$x, 'country' ); + my @bill_param = ( + ('district')x3, + ('city')x3, + ('county')x$x, + ('state')x$x, + 'country' + ); my $main_where; my @main_param; @@ -3504,16 +3537,17 @@ sub _location_sql_where { $ornull = $ornull ? ' OR ? IS NULL ' : ''; - my $or_empty_city = " OR ( ? = '' AND $table.${prefix}city IS NULL ) "; - my $or_empty_county = " OR ( ? = '' AND $table.${prefix}county IS NULL ) "; - my $or_empty_state = " OR ( ? = '' AND $table.${prefix}state IS NULL ) "; + my $or_empty_city = " OR ( ? = '' AND $table.${prefix}city IS NULL )"; + my $or_empty_county = " OR ( ? = '' AND $table.${prefix}county IS NULL )"; + my $or_empty_state = " OR ( ? = '' AND $table.${prefix}state IS NULL )"; # ( $table.${prefix}city = ? $or_empty_city $ornull ) " - ( $table.${prefix}city = ? OR ? = '' OR CAST(? AS text) IS NULL ) - AND ( $table.${prefix}county = ? $or_empty_county $ornull ) - AND ( $table.${prefix}state = ? $or_empty_state $ornull ) - AND $table.${prefix}country = ? + ( $table.${prefix}district = ? OR ? = '' OR CAST(? AS text) IS NULL ) + AND ( $table.${prefix}city = ? OR ? = '' OR CAST(? AS text) IS NULL ) + AND ( $table.${prefix}county = ? $or_empty_county $ornull ) + AND ( $table.${prefix}state = ? $or_empty_state $ornull ) + AND $table.${prefix}country = ? "; } diff --git a/FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm b/FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm index bf472683f..71bbaa89b 100644 --- a/FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm +++ b/FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm @@ -15,9 +15,10 @@ sub option_fields { ( 'ftpformat' => { label => 'Format', type =>'select', - options => ['default', 'billco'], + options => ['default', 'billco', 'oneline'], option_labels => { 'default' => 'Default', 'billco' => 'Billco', + 'oneline' => 'One line', }, }, 'ftpserver' => 'FTP server', diff --git a/FS/FS/part_event/Action/cust_bill_spool_csv.pm b/FS/FS/part_event/Action/cust_bill_spool_csv.pm index 11ecbc555..1504a4fa9 100644 --- a/FS/FS/part_event/Action/cust_bill_spool_csv.pm +++ b/FS/FS/part_event/Action/cust_bill_spool_csv.pm @@ -15,9 +15,10 @@ sub option_fields { ( 'spoolformat' => { label => 'Format', type => 'select', - options => ['default', 'billco'], + options => ['default', 'billco', 'oneline'], option_labels => { 'default' => 'Default', 'billco' => 'Billco', + 'oneline' => 'One line', }, }, 'spoolbalanceover' => { label => diff --git a/FS/FS/part_event/Condition/pkg_dundate_age.pm b/FS/FS/part_event/Condition/pkg_dundate_age.pm new file mode 100644 index 000000000..2ea2a2041 --- /dev/null +++ b/FS/FS/part_event/Condition/pkg_dundate_age.pm @@ -0,0 +1,43 @@ +package FS::part_event::Condition::pkg_dundate_age; +use base qw( FS::part_event::Condition ); + +use strict; + +sub description { + "Skip until specified #days before package suspension delay date"; +} + + +sub option_fields { + ( + 'age' => { 'label' => 'Time before suspension delay date', + 'type' => 'freq', + }, + ); +} + +sub eventtable_hashref { + { 'cust_main' => 0, + 'cust_bill' => 0, + 'cust_pkg' => 1, + }; +} + +sub condition { + my($self, $cust_pkg, %opt) = @_; + + my $age = $self->option_age_from('age', $opt{'time'} ); + + $cust_pkg->dundate <= $age; +} + +sub condition_sql { + my( $class, $table, %opt ) = @_; + return 'true' unless $table eq 'cust_pkg'; + + my $age = $class->condition_sql_option_age_from('age', $opt{'time'}); + + "COALESCE($table.dundate,0) <= ". $age; +} + +1; diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm index 64cc3770e..82102697d 100755 --- a/FS/FS/svc_broadband.pm +++ b/FS/FS/svc_broadband.pm @@ -543,9 +543,9 @@ sub _check_ip_addr { sub _check_duplicate { my $self = shift; - - $self->lock_table; - + # Not a reliable check because the table isn't locked, but + # that's why we have a unique index. This is just to give a + # friendlier error message. my @dup; @dup = $self->find_duplicates('global', 'ip_addr'); if ( @dup ) { diff --git a/httemplate/edit/tower.html b/httemplate/edit/tower.html index 82513082c..03b488e86 100644 --- a/httemplate/edit/tower.html +++ b/httemplate/edit/tower.html @@ -20,8 +20,8 @@ 'sectornum' => 'Sector', 'disabled' => 'Disabled', 'default_ip_addr' => 'Tower IP address', - 'latitude', => 'Latitude', - 'longitude', => 'Longitude', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', }, &> <%init> diff --git a/httemplate/elements/customer-table.html b/httemplate/elements/customer-table.html index a517ece2a..fc1af69c6 100644 --- a/httemplate/elements/customer-table.html +++ b/httemplate/elements/customer-table.html @@ -41,6 +41,8 @@ Example: <SCRIPT TYPE="text/javascript"> + var num_open_invoices = new Array; + function clearhint_invnum() { if ( this.value == 'Not found' || this.value == 'Multiple' ) { @@ -90,7 +92,7 @@ Example: customer_select.style.display = 'none'; return false; - } else if ( customerArray.length == 5 ) { + } else if ( customerArray.length == 6 ) { custnum_obj.value = customerArray[0]; custnum_obj.style.color = '#000000'; @@ -99,6 +101,7 @@ Example: update_balance_text(searchrow, customerArray[2]); update_status_text( searchrow, customerArray[3]); update_status_color(searchrow, '#'+customerArray[4]); + update_num_open(searchrow, customerArray[5]); customer.style.display = ''; customer_select.style.display = 'none'; @@ -140,6 +143,7 @@ Example: update_balance_text(searchrow, ''); update_status_text(searchrow, ''); update_status_color(searchrow, '#000000'); + update_num_open(searchrow, 0); function search_invnum_update(customers) { @@ -192,6 +196,7 @@ Example: update_balance_text(searchrow, ''); update_status_text( searchrow, ''); update_status_color(searchrow, '#000000'); + update_num_open(searchrow, 0); function search_custnum_update(customers) { @@ -337,6 +342,9 @@ Example: document.getElementById('balance'+rownum+'_text').innerHTML = newval; } + function update_num_open(rownum, newval) { + num_open_invoices[rownum] = newval; + } </SCRIPT> @@ -356,7 +364,7 @@ Example: % my $row = 0; % for ( $row = 0; exists($param->{"custnum$row"}); $row++ ) { - <TR> + <TR id="row<%$row%>" rownum="<%$row%>"> <TD> <INPUT TYPE = "text" NAME = "invnum<% $row %>" @@ -458,19 +466,24 @@ Example: % my $color = $opt{color}->[$col]; % my $font = $color ? qq(<FONT COLOR="$color">) : ''; % my $onchange = ''; -% if ( $opt{footer}->[$col] eq '_TOTAL' ) { +% if ( $opt{onchange}->[$col] ) { +% $onchange = 'onchange="'.$opt{onchange}->[$col].'"'; +% } +% elsif ( $opt{footer}->[$col] eq '_TOTAL' ) { % $total[$col] += $value; % $onchange = $opt{prefix}. "calc_total$col();"; % $onchange = qq(onchange="$onchange" onkeyup="$onchange"); % } <TD ALIGN="<% $align %>"> -% if (! $types->[$col] || $types->[$col] eq 'text') { - <INPUT TYPE = "text" +% my $type = $types->[$col] || 'text'; +% if ($type eq 'text' or $type eq 'checkbox') { + <INPUT TYPE = "<% $type %>" NAME = "<% $name %>" ID = "<% $name %>" SIZE = "<% $size %>" STYLE = "text-align: <% $align %>;" VALUE = "<% $value %>" + rownum = "<% $row %>" <% $onchange %> > % } elsif ($types->[$col] eq 'immutable') { @@ -485,7 +498,7 @@ Example: </TR> % } -<TR> +<TR id="row_total"> <TH COLSPAN=5 ID="<% $opt{'prefix'} %>_TOTAL_TOTAL"> Total <% $row ? $row-1 : 0 %> <% PL($opt{name_singular} || 'customer', ( $row ? $row-1 : 0 ) ) %> @@ -559,7 +572,8 @@ Example: var table = document.getElementById('<% $opt{prefix} %>OneTrueTable'); var tablebody = table.getElementsByTagName('tbody').item(0); - var row = table.insertRow(rownum+1); + var row = table.insertRow(table.rows.length - 1); + row.setAttribute('id', 'row'+rownum); var invnum_cell = document.createElement('TD'); @@ -676,7 +690,7 @@ Example: % } else { % $value = $param->{"$field$row"}; % } - var my_text = document.createTextNode('<% $value %>'); + var my_text = document.createTextNode(<% $value |js_string %>); my_cell.appendChild(my_text); % } @@ -686,10 +700,17 @@ Example: my_input.setAttribute('id', '<% $name %>'+<% $opt{prefix} %>rownum); my_input.style.textAlign = '<% $align{ $opt{align}->[$col] || 'l' } %>'; my_input.setAttribute('size', <% $sizes->[$col] || 10 %>); -% if ($types->[$col] eq 'immutable') { + my_input.setAttribute('rownum', <% $opt{prefix} %>rownum); +% if ( $types->[$col] eq 'immutable' ) { my_input.setAttribute('type', 'hidden'); % } -% if ( $opt{footer}->[$col] eq '_TOTAL' ) { +% elsif ( $types->[$col] eq 'checkbox' ) { + my_input.setAttribute('type', 'checkbox'); +% } +% if ( $opt{onchange}->[$col] ) { + my_input.onchange = <% $opt{onchange}->[$col] %>; +% } +% elsif ( $opt{footer}->[$col] eq '_TOTAL' ) { my_input.onchange = <% $opt{prefix} %>calc_total<%$col%>; my_input.onkeyup = <% $opt{prefix} %>calc_total<%$col%>; % } @@ -713,6 +734,11 @@ Example: + ' <% PL($opt{name_singular} || 'customer') %>'; } +% if ( $opt{add_row_callback} ) { + <% $opt{add_row_callback} %>(<% $opt{prefix} %>rownum, + '<% $opt{prefix} %>'); +% } + <% $opt{prefix} %>rownum++; } diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index b1cbebf34..0f36500b0 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -221,7 +221,8 @@ foreach my $svcdb ( FS::part_svc->svc_tables() ) { } - $report_services{$name} = [ \%report_svc, $longname ]; + $report_services{$name} = [ \%report_svc, $longname ] if + $curuser->access_right("Services: $name"); } @@ -253,14 +254,15 @@ tie my %report_inventory, 'Tie::IxHash', 'Inventory activity' => [ $fsurl.'search/report_h_inventory_item.html', '' ], ; -tie my %report_rating, 'Tie::IxHash', - 'RADIUS sessions' => [ $fsurl.'search/sqlradius.html', '' ], - 'Call Detail Records (CDRs)' => [ $fsurl.'search/report_cdr.html', '' ], - 'Unrateable CDRs' => [ $fsurl.'search/cdr.html?freesidestatus=failed'. - ';cdrbatchnum=_ALL_' ], - 'Time worked' => [ $fsurl.'search/report_rt_transaction.html', '' ], - 'Time worked summary' => [ $fsurl.'search/report_rt_ticket.html', '' ], -; +my @report_rating = (); +push(@report_rating, 'RADIUS sessions' => [ $fsurl.'search/sqlradius.html', '' ]) if $curuser->access_right("Usage: RADIUS sessions"); +push(@report_rating, 'Call Detail Records (CDRs)' => [ $fsurl.'search/report_cdr.html', '' ]) if $curuser->access_right("Usage: Call Detail Records (CDRs)"); +push(@report_rating, 'Unrateable CDRs' => [ $fsurl.'search/cdr.html?freesidestatus=failed'. + ';cdrbatchnum=_ALL_' ]) if $curuser->access_right("Usage: Unrateable CDRs"); +push(@report_rating, 'Time worked' => [ $fsurl.'search/report_rt_transaction.html', '' ]) if $curuser->access_right("Usage: Time worked"); +push(@report_rating, 'Time worked summary' => [ $fsurl.'search/report_rt_ticket.html', '' ]) if $curuser->access_right("Usage: Time worked summary"); + +tie my %report_rating, 'Tie::IxHash', @report_rating; tie my %report_ticketing_statistics, 'Tie::IxHash', 'Tickets per day per Queue' => [ $fsurl.'rt/RTx/Statistics/CallsQueueDay', 'View the number of tickets created, resolved or deleted in a specific Queue, over the requested period of days' ], diff --git a/httemplate/misc/batch-cust_pay.html b/httemplate/misc/batch-cust_pay.html index 2e798652d..887b92489 100644 --- a/httemplate/misc/batch-cust_pay.html +++ b/httemplate/misc/batch-cust_pay.html @@ -1,6 +1,9 @@ -<% include('/elements/header.html', 'Quick payment entry') %> +<& /elements/header.html, { + title => 'Quick payment entry', + etc => 'onload="preload()"' +} &> -<% include('/elements/error.html') %> +<& /elements/error.html &> <SCRIPT TYPE="text/javascript"> function warnUnload() { @@ -14,6 +17,21 @@ function warnUnload() { } window.onbeforeunload = warnUnload; +function add_row_callback(rownum, prefix) { + document.getElementById('enable_app'+rownum).disabled = true; +} + +function custnum_update_callback(rownum, prefix) { + var custnum = document.getElementById('custnum'+rownum).value; + document.getElementById('enable_app'+rownum).disabled = ( + custnum == 0 || + num_open_invoices[rownum] < 2 + ); +% if ( $use_discounts ) { + select_discount_term(rownum, prefix); +% } +} + function select_discount_term(row, prefix) { var custnum_obj = document.getElementById('custnum'+prefix+row); var select_obj = document.getElementById('discount_term'+prefix+row); @@ -46,6 +64,265 @@ function select_discount_term(row, prefix) { discount_terms(custnum_obj.value, select_discount_term_update); } + +var invoices_for_row = new Object; + +function update_invoices(rownum, invoices) { + invoices_for_row[rownum] = new Object; + // only called before create_application_row + for ( var i=0; i<invoices.length; i++ ) { + invoices_for_row[rownum][ invoices[i].invnum ] = invoices[i]; + } +} + +function toggle_application_row(ev, next) { + if (!next) next = function(){}; //optional continuation + var rownum = this.getAttribute('rownum'); + if ( this.checked ) { + var custnum = document.getElementById('custnum'+rownum).value; + if (!custnum) return; + lock_payment_row(rownum, true); + custnum_search_open( custnum, + function(returned) { + update_invoices(rownum, JSON.parse(returned)); + create_application_row(rownum, 0); + next.call(this, rownum); + } + ); + } +} + +function lock_payment_row(rownum, flag) { +% foreach (qw(invnum custnum customer)) { + obj = document.getElementById('<% $_ %>'+rownum); + obj.readOnly = flag; +% } + document.getElementById('enable_app'+rownum).disabled = flag; +} + +function delete_application_row() { + var rownum = this.getAttribute('rownum'); + var appnum = this.getAttribute('appnum'); + var tr_app = document.getElementById('row'+rownum+'.'+appnum); + var select_invnum = document.getElementById('invnum'+rownum+'.'+appnum); + if ( select_invnum.value ) { + invoices_for_row[rownum][ select_invnum.value ] = select_invnum.curr_invoice; + } + + tr_app.parentNode.removeChild(tr_app); + if ( appnum > 0 ) { + document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = ''; + } + else { + lock_payment_row(rownum, false); + document.getElementById('enable_app'+rownum).checked = false; + } +} + +function amount_unapplied(rownum) { + var appnum = 0; + var total = 0; + var payment_amount = parseFloat(document.getElementById('paid'+rownum).value) + || 0; + while (true) { + var input_amount = document.getElementById('amount'+rownum+'.'+appnum); + if ( input_amount ) { + total += parseFloat(input_amount.value || 0); + appnum++; + } + else { + return payment_amount - total; + } + } +} + +var change_app_amount; + +function choose_app_invnum() { + var rownum = this.getAttribute('rownum'); + var appnum = this.getAttribute('appnum'); + var last_invoice = this.curr_invoice; + if ( last_invoice ) { + invoices_for_row[rownum][ last_invoice['invnum'] ] = last_invoice; + } + + if ( this.value ) { + var this_invoice = invoices_for_row[rownum][this.value]; + this.curr_invoice = invoices_for_row[rownum][this.value]; + var span_owed = document.getElementById('owed'+rownum+'.'+appnum); + span_owed.innerHTML = this_invoice['owed'] + ' '; + delete invoices_for_row[rownum][this.value]; + + var input_amount = document.getElementById('amount'+rownum+'.'+appnum); + if ( input_amount.value == '' ) { + input_amount.value = + Math.max( + 0, Math.min( amount_unapplied(rownum), this_invoice['owed']) + ).toFixed(2); + // trigger onchange + change_app_amount.call(input_amount); + } + } +} + +function focus_app_invnum() { +% # invoice numbers just display as invoice numbers + var rownum = this.getAttribute('rownum'); + var add_opt = function(obj, value) { + var o = document.createElement('OPTION'); + o.text = value; + o.value = value; + obj.add(o); + } + this.options.length = 0; + var this_invoice = this.curr_invoice; + if ( this_invoice ) { + add_opt(this, this_invoice.invnum); + } else { + add_opt(this, ''); + } + for ( var x in invoices_for_row[rownum] ) { + add_opt(this, invoices_for_row[rownum][x].invnum); + } +} + +function change_app_amount() { + var rownum = this.getAttribute('rownum'); + var appnum = this.getAttribute('appnum'); +%# maybe some kind of warning if amount_unapplied < 0? +%# only spawn a new application row if there are open invoices left, +%# and this is the highest-numbered application row for the customer, +%# and the sum of the applied amounts is < the amount of the payment, + if ( Object.keys(invoices_for_row[rownum]).length > 0 + && !document.getElementById( 'row'+rownum+'.'+(parseInt(appnum) + 1) ) + && amount_unapplied(rownum) > 0 ) { + + create_application_row(rownum, parseInt(appnum) + 1); + + } +} + +function create_application_row(rownum, appnum) { + var payment_row = document.getElementById('row'+rownum); + var tr_app = document.createElement('TR'); + tr_app.setAttribute('rownum', rownum); + tr_app.setAttribute('appnum', appnum); + tr_app.setAttribute('id', 'row'+rownum+'.'+appnum); + + var td_invnum = document.createElement('TD'); + td_invnum.setAttribute('colspan', 4); + td_invnum.style.textAlign = 'right'; + td_invnum.appendChild( + document.createTextNode('<% mt('Apply to Invoice ') %>') + ); + var select_invnum = document.createElement('SELECT'); + select_invnum.setAttribute('rownum', rownum); + select_invnum.setAttribute('appnum', appnum); + select_invnum.setAttribute('id', 'invnum'+rownum+'.'+appnum); + select_invnum.setAttribute('name', 'invnum'+rownum+'.'+appnum); + select_invnum.style.textAlign = 'right'; + select_invnum.style.width = '50px'; + select_invnum.onchange = choose_app_invnum; + select_invnum.onfocus = focus_app_invnum; + + td_invnum.appendChild(select_invnum); + tr_app.appendChild(td_invnum); + + var td_owed = document.createElement('TD'); + td_owed.style.textAlign= 'right'; + var span_owed = document.createElement('SPAN'); + span_owed.setAttribute('rownum', rownum); + span_owed.setAttribute('appnum', appnum); + span_owed.setAttribute('id', 'owed'+rownum+'.'+appnum); + td_owed.appendChild(span_owed); + tr_app.appendChild(td_owed); + + var td_amount = document.createElement('TD'); + td_amount.style.textAlign = 'right'; + var input_amount = document.createElement('INPUT'); + input_amount.size = 6; + input_amount.setAttribute('rownum', rownum); + input_amount.setAttribute('appnum', appnum); + input_amount.setAttribute('name', 'amount'+rownum+'.'+appnum); + input_amount.setAttribute('id', 'amount'+rownum+'.'+appnum); + input_amount.style.textAlign = 'right'; + input_amount.onchange = change_app_amount; + td_amount.appendChild(input_amount); + tr_app.appendChild(td_amount); + + var td_delete = document.createElement('TD'); + td_delete.setAttribute('colspan', <% scalar(@fields)-2 %>); + var button_delete = document.createElement('INPUT'); + button_delete.setAttribute('rownum', rownum); + button_delete.setAttribute('appnum', appnum); + button_delete.setAttribute('id', 'delete'+rownum+'.'+appnum); + button_delete.setAttribute('type', 'button'); + button_delete.setAttribute('value', 'X'); + button_delete.onclick = delete_application_row; + button_delete.style.color = '#ff0000'; + button_delete.style.fontWeight = 'bold'; + button_delete.style.paddingLeft = '2px'; + button_delete.style.paddingRight = '2px'; + td_delete.appendChild(button_delete); + tr_app.appendChild(td_delete); + + var td_error = document.createElement('TD'); + var span_error = document.createElement('SPAN'); + span_error.setAttribute('rownum', rownum); + span_error.setAttribute('appnum', appnum); + span_error.setAttribute('id', 'error'+rownum+'.'+appnum); + span_error.style.color = '#ff0000'; + td_error.appendChild(span_error); + tr_app.appendChild(td_error); + + if ( appnum > 0 ) { + //remove delete button on the previous row + document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = 'none'; + } + rownum++; + var next_row = document.getElementById('row'+rownum); // always exists + payment_row.parentNode.insertBefore(tr_app, next_row); + +} + +%# for error handling--ugly, but the alternative is translating the whole +%# process of creating rows into Mason +var row_array = <% encode_json(\@rows) %>; +function preload() { + var rownum; + var appnum; + for (rownum=0; rownum < row_array.length; rownum++) { + if ( row_array[rownum].length ) { + var enable = document.getElementById('enable_app'+rownum); + enable.checked = true; + var preload_row = function(r) {//continuation from toggle_application_row + for (appnum=0; appnum < row_array[r].length; appnum++) { + this_app = row_array[r][appnum]; + var x = r + '.' + appnum; + //set invnum + var select_invnum = document.getElementById('invnum'+x); + focus_app_invnum.call(select_invnum); + for (i=0; i<select_invnum.options.length; i++) { + if (select_invnum.options[i].value == this_app.invnum) { + select_invnum.selectedIndex = i; + } + } + choose_app_invnum.call(select_invnum); + //set amount + var input_amount = document.getElementById('amount'+x); + input_amount.value = this_app.amount; + + //set error + var span_error = document.getElementById('error'+x); + span_error.innerHTML = this_app.error; + change_app_amount.call(input_amount); //creates next row + } //for appnum + }; //preload_row function + toggle_application_row.call(enable, null, preload_row); + } // if row_array[rownum].length + } //for rownum +} + </SCRIPT> <% include('/elements/xmlhttp.html', @@ -57,21 +334,26 @@ function select_discount_term(row, prefix) { <FORM ACTION="process/batch-cust_pay.cgi" NAME="OneTrueForm" METHOD="POST" onsubmit="document.OneTrueForm.btnsubmit.disabled=true;window.onbeforeunload = null;"> <!-- <B>Batch</B> <INPUT TYPE="text" NAME="paybatch"><BR><BR> --> +<& /elements/xmlhttp.html, + url => $p.'misc/xmlhttp-cust_bill-search.html', + subs => ['custnum_search_open'] +&> -<% include( "/elements/customer-table.html", - name_singular => 'payment', - header => \@header, - fields => \@fields, - type => \@types, - align => \@align, - size => \@sizes, - color => \@colors, - param => \%param, - footer => \@footer, - footer_align => \@footer_align, - custnum_update_callback => $custnum_update_callback, - ) -%> +<& /elements/customer-table.html, + name_singular => 'payment', + header => \@header, + fields => \@fields, + type => \@types, + align => \@align, + size => \@sizes, + color => \@colors, + param => \%param, + footer => \@footer, + footer_align => \@footer_align, + onchange => \@onchange, + custnum_update_callback => 'custnum_update_callback', + add_row_callback => 'add_row_callback', +&> <BR> <INPUT TYPE="button" VALUE="Post payment batch" name="btnsubmit" onclick="window.onbeforeunload = null; document.OneTrueForm.submit(); this.disabled = true;"> @@ -105,7 +387,8 @@ my @colors = ( '', '' ); my %param = (); my @footer = ( '_TOTAL', '' ); my @footer_align = ( 'r', 'r' ); -my $custnum_update_callback = ''; +my @onchange = ( '', '' );; +my $use_discounts = ''; if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) { #push @header, 'Discount'; @@ -117,9 +400,20 @@ if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) { push @colors, ''; push @footer, ''; push @footer_align, ''; - $custnum_update_callback = 'select_discount_term'; + push @onchange, ''; + $use_discounts = 'Y'; } +push @header, 'Allocate'; +push @fields, 'enable_app'; +push @types, 'checkbox'; +push @align, 'c'; +push @sizes, '0'; +push @colors, ''; +push @footer, ''; +push @footer_align, ''; +push @onchange, 'toggle_application_row'; + #push @header, 'Error'; push @header, ''; push @fields, 'error'; @@ -129,7 +423,34 @@ push @sizes, '0'; push @colors, '#ff0000'; push @footer, ''; push @footer_align, ''; +push @onchange, ''; $m->comp('/elements/handle_uri_query'); +# set up for preloading +my @rows; +my @row_errors; +if ( $cgi->param('error') ) { + my $param = $cgi->Vars; + my $enum = 0; #errors numbered separately + for( my $row = 0; exists($param->{"custnum$row"}); $row++ ) { + $rows[$row] = []; + $row_errors[$row] = $param->{"error$enum"}; + $enum++; + for( my $app = 0; exists($param->{"invnum$row.$app"}); $app++ ) { + next if !$param->{"invnum$row.$app"}; + my %this_app = map { $_ => ($param->{$_.$row.'.'.$app} || '') } + qw( invnum amount ); + $this_app{'error'} = $param->{"error$enum"} || ''; + $param->{"error$enum"} = ''; # don't pass this error through + $rows[$row][$app] = \%this_app; + $enum++; + } + } + for( my $row = 0; $row < @row_errors; $row++ ) { + $param->{"error$row"} = $row_errors[$row]; + } +} +#warn Dumper {rows => \@rows, row_errors => \@row_errors }; + </%init> diff --git a/httemplate/misc/process/batch-cust_pay.cgi b/httemplate/misc/process/batch-cust_pay.cgi index a6b90ea74..1105af943 100644 --- a/httemplate/misc/process/batch-cust_pay.cgi +++ b/httemplate/misc/process/batch-cust_pay.cgi @@ -1,51 +1,69 @@ -% die "access denied" -% unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch'); -% -% my $param = $cgi->Vars; -% -% #my $paybatch = $param->{'paybatch'}; -% my $paybatch = time2str('webbatch-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time); -% -% my @cust_pay = (); -% #my $row = 0; -% #while ( exists($param->{"custnum$row"}) ) { -% for ( my $row = 0; exists($param->{"custnum$row"}); $row++ ) { -% my $custnum = $param->{"custnum$row"}; -% my $cust_main; -% if ( $custnum =~ /^(\d+)$/ and $1 <= 2147483647 ) { -% $cust_main = qsearchs({ -% 'table' => 'cust_main', -% 'hashref' => { 'custnum' => $1 }, -% 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, -% }); -% } -% if ( length($custnum) and !$cust_main ) { # not found, try agent_custid -% $cust_main = qsearchs({ -% 'table' => 'cust_main', -% 'hashref' => { 'agent_custid' => $custnum }, -% 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, -% }); -% } -% $custnum = $cust_main->custnum if $cust_main; -% # if !$cust_main, then this will throw an error on batch_insert -% -% push @cust_pay, new FS::cust_pay { -% 'custnum' => $custnum, -% 'paid' => $param->{"paid$row"}, -% 'payby' => 'BILL', -% 'payinfo' => $param->{"payinfo$row"}, -% 'discount_term' => $param->{"discount_term$row"}, -% 'paybatch' => $paybatch, -% } -% if $param->{"custnum$row"} -% || $param->{"paid$row"} -% || $param->{"payinfo$row"}; -% #$row++; -% } -% -% my @errors = FS::cust_pay->batch_insert(@cust_pay); -% my $num_errors = scalar(grep $_, @errors); -% +<%init> +my $DEBUG = 0; +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch'); + +my $param = $cgi->Vars; +warn Dumper($param) if $DEBUG; + +#my $paybatch = $param->{'paybatch'}; +my $paybatch = time2str('webbatch-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time); + +my @cust_pay = (); +#my $row = 0; +#while ( exists($param->{"custnum$row"}) ) { +for ( my $row = 0; exists($param->{"custnum$row"}); $row++ ) { + my $custnum = $param->{"custnum$row"}; + my $cust_main; + if ( $custnum =~ /^(\d+)$/ and $1 <= 2147483647 ) { + $cust_main = qsearchs({ + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $1 }, + 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, + }); + } + if ( length($custnum) and !$cust_main ) { # not found, try agent_custid + $cust_main = qsearchs({ + 'table' => 'cust_main', + 'hashref' => { 'agent_custid' => $custnum }, + 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, + }); + } + $custnum = $cust_main->custnum if $cust_main; + # if !$cust_main, then this will throw an error on batch_insert + + my $cust_pay = new FS::cust_pay { + 'custnum' => $custnum, + 'paid' => $param->{"paid$row"}, + 'payby' => 'BILL', + 'payinfo' => $param->{"payinfo$row"}, + 'discount_term' => $param->{"discount_term$row"}, + 'paybatch' => $paybatch, + } + if $param->{"custnum$row"} + || $param->{"paid$row"} + || $param->{"payinfo$row"}; + next if !$cust_pay; + #$row++; + + # payment applications, if any + my @cust_bill_pay = (); + for ( my $app = 0; exists($param->{"invnum$row.$app"}); $app++ ) { + next if !$param->{"invnum$row.$app"}; + push @cust_bill_pay, new FS::cust_bill_pay { + 'invnum' => $param->{"invnum$row.$app"}, + 'amount' => $param->{"amount$row.$app"} + }; + } + $cust_pay->set('apply_to', \@cust_bill_pay) if scalar(@cust_bill_pay) > 0; + + push @cust_pay, $cust_pay; + +} + +my @errors = FS::cust_pay->batch_insert(@cust_pay); +my $num_errors = scalar(grep $_, @errors); +</%init> % if ( $num_errors ) { % % $cgi->param('error', "$num_errors error". ($num_errors>1 ? 's' : ''). @@ -65,4 +83,3 @@ % <% $cgi->redirect(popurl(3). "search/cust_pay.html?magic=paybatch;paybatch=$paybatch") %> % } - diff --git a/httemplate/misc/xmlhttp-cust_bill-search.html b/httemplate/misc/xmlhttp-cust_bill-search.html new file mode 100644 index 000000000..46f15d1ab --- /dev/null +++ b/httemplate/misc/xmlhttp-cust_bill-search.html @@ -0,0 +1,18 @@ +<% encode_json(\@return) %> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +die 'access denied' unless $curuser->access_right('View invoices'); +my @return; +if ( $cgi->param('sub') eq 'custnum_search_open' ) { + my $custnum = $cgi->param('arg'); + #warn "searching invoices for $custnum\n"; + my $cust_main = FS::cust_main->by_key($custnum); + @return = map { + +{ $_->hash, + 'owed' => $_->owed } + } $cust_main->open_cust_bill + if $curuser->agentnums_href->{ $cust_main->agentnum }; +} + +</%init> diff --git a/httemplate/misc/xmlhttp-cust_main-search.cgi b/httemplate/misc/xmlhttp-cust_main-search.cgi index 436501e8b..86983e462 100644 --- a/httemplate/misc/xmlhttp-cust_main-search.cgi +++ b/httemplate/misc/xmlhttp-cust_main-search.cgi @@ -12,7 +12,7 @@ % my @cust_main = smart_search( 'search' => $string, % 'no_fuzzy_on_exact' => 1, #pref? % ); -% my $return = [ map [ $_->custnum, $_->name, $_->balance, $_->ucfirst_status, $_->statuscolor ], @cust_main ]; +% my $return = [ map [ $_->custnum, $_->name, $_->balance, $_->ucfirst_status, $_->statuscolor, scalar($_->open_cust_bill) ], @cust_main ]; % <% objToJson($return) %> % } elsif ( $sub eq 'invnum_search' ) { @@ -57,7 +57,7 @@ sub findbycustnum{ 'hashref' => $hashref, 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, }); - return [ $c->custnum, $c->name, $c->balance, $c->ucfirst_status, $c->statuscolor ] + return [ $c->custnum, $c->name, $c->balance, $c->ucfirst_status, $c->statuscolor, scalar($c->open_cust_bill) ] if $c; []; } diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index 94860d3f2..b08024cb0 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -218,7 +218,7 @@ if ( $cgi->param('taxclass') } -my @loc_param = qw( city county state country ); +my @loc_param = qw( district city county state country ); if ( $cgi->param('out') ) { @@ -266,7 +266,7 @@ if ( $cgi->param('out') ) { my %ph = ( 'county' => dbh->quote($_), map { $_ => dbh->quote( $cgi->param($_) ) } - qw( city state country ) + qw( district city state country ) ); my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; @@ -330,7 +330,7 @@ if ( $cgi->param('out') ) { push @where, FS::tax_rate_location->location_sql( map { $_ => (scalar($cgi->param($_)) || '') } - qw( city county state locationtaxid ) + qw( district city county state locationtaxid ) ); } elsif ( $cgi->param('unearned_now') =~ /^(\d+)$/ ) { diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi index 0cd652d83..bfbc5fe5a 100755 --- a/httemplate/search/report_tax.cgi +++ b/httemplate/search/report_tax.cgi @@ -275,7 +275,10 @@ sub gotcust { my $table = shift; my $prefix = @_ ? shift : ''; " - ( $table.${prefix}city = cust_main_county.city + ( $table.${prefix}district = cust_main_county.district + OR cust_main_county.district = '' + OR cust_main_county.district IS NULL ) + AND ( $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 @@ -332,6 +335,7 @@ if ( $conf->exists('tax-pkg_address') ) { } my $out = 'Out of taxable region(s)'; +# these are actually tax labels, not regions my %regions = (); foreach my $r ( qsearch({ 'table' => 'cust_main_county', @@ -341,6 +345,7 @@ foreach my $r ( qsearch({ 'table' => 'cust_main_county', { #warn $r->county. ' '. $r->state. ' '. $r->country. "\n"; + # set up a %regions entry for this region's tax label my $label = getlabel($r); $regions{$label}->{'label'} = $label; @@ -366,6 +371,7 @@ foreach my $r ( qsearch({ 'table' => 'cust_main_county', } else { + # SQL for "taxclass doesn't match any other tax in the region" my $same_sql = $r->sql_taxclass_sameregion; $mywhere .= " AND $same_sql" if $same_sql; @@ -375,42 +381,24 @@ foreach my $r ( qsearch({ 'table' => 'cust_main_county', } + # FROM cust_bill_pkg JOIN (whatever is needed to determine tax location) + # WHERE (matches tax location and agentnum and taxclass) + # takes parameters in @base_param, plus taxclass if there is one 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 + ## calculate total of sales (non-tax line items) 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'; - #} + #$regions{$label}->{subtotals}->{$r->taxnum} = $t; #useful debug ## 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 ) { @@ -486,10 +474,12 @@ foreach my $r ( qsearch({ 'table' => 'cust_main_county', } else { $regions{$label}->{'rate'} = $r->tax.'%'; } - } +#warn Dumper(\%regions); +# $regions{$label} now contains 'total', 'exempt_cust', 'exempt_pkg', +# 'exempt_monthly', summed over each set of regions with the same label. -my $distinct = "country, state, county, city, +my $distinct = "country, state, county, city, district, CASE WHEN taxname IS NULL THEN '' ELSE taxname END AS taxname"; my $taxclass_distinct = #a little bit unsure of this part... test? @@ -528,11 +518,19 @@ $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 +# yes. yes, you should. +# $taxfromwhere: Most of a query to find cust_bill_pkg records linked to a +# customer matching a given state/county/city/district (and within the date +# range for the report). +# @base_param: A list of the fields from cust_main_county to use as parameters. + +# $_taxamount_sub: Takes a cust_main_county and returns the sum of taxes billed +# within the report period for all customers located in that county. If +# the cust_main_county has a taxname, limits to taxes with that name; otherwise +# includes all line items with pkgnum = 0 and description either 'Tax' or empty. -#should i be a cust_main_county method or something -#need to pass in $taxfromwhere & @taxparam??? my $_taxamount_sub = sub { my $r = shift; @@ -545,9 +543,11 @@ my $_taxamount_sub = sub { 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 ); + scalar_sql($r, [ @base_param ], $sql ); }; +# $_creditamount_sub: As above, but returns the sum of credits applied + my $_creditamount_sub = sub { my $r = shift; @@ -560,7 +560,7 @@ my $_creditamount_sub = sub { my $sql = "SELECT SUM(cust_credit_bill_pkg.amount) ". " $creditfromwhere AND cust_bill_pkg.pkgnum = 0 $named_tax"; - scalar_sql($r, \@taxparam, $sql ); + scalar_sql($r, [ @base_param ], $sql ); }; #tax-report_groups filtering @@ -734,7 +734,8 @@ sub getlabel { my $label; if ( $r->tax == 0 - && ! scalar( qsearch('cust_main_county', { 'city' => $r->city, + && ! scalar( qsearch('cust_main_county', { 'district'=> $r->district, + 'city' => $r->city, 'county' => $r->county, 'state' => $r->state, 'country' => $r->country, @@ -747,10 +748,6 @@ sub getlabel { #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; diff --git a/httemplate/view/directions.html b/httemplate/view/directions.html index 599d049c2..f14a11a07 100644 --- a/httemplate/view/directions.html +++ b/httemplate/view/directions.html @@ -22,7 +22,6 @@ body { height: 100%; margin: 0px; padding: 0px } #map_canvas { height: 100%; - margin-right: 320px; } #directions_panel { |