<%doc> Report listing upcoming auto-bill transactions For every customer with a valid auto-bill payment method, report runs bill_and_collect() for each customer, for each day, from today through the report target date. After recording the results, all operations are rolled back. This report relies on the ability to safely run bill_and_collect(), with all exports and messaging disabled, and then to roll back the results. <& elements/grid-report.html, title => $report_title, rows => \@rows, cells => \@cells, table_width => "", table_class => 'gridreport', head => ' ', &> <%init> use FS::UID qw( dbh myconnect ); die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); my $DEBUG = $cgi->param('DEBUG') || 0; my $target_dt; my @target_dates; # Work with all date/time operations @ 12 noon my %noon = ( hour => 12, minute => 0, second => 0, ); my $now_dt = DateTime->now; $now_dt = DateTime->new( month => $now_dt->month, day => $now_dt->day, year => $now_dt->year, %noon, ); # Get target date from form if ($cgi->param('target_date')) { my ($mm, $dd, $yy) = split /[\-\/]/,$cgi->param('target_date'); $target_dt = DateTime->new( month => $mm, day => $dd, year => $yy, %noon, ) if $mm && $dd & $yy; # Catch a date from the past: time only travels in one direction $target_dt = undef if $target_dt->epoch < $now_dt->epoch; } # without a target date, default to tomorrow unless ($target_dt) { $target_dt = $now_dt->clone->add( days => 1 ); } # Create a range of dates from today until the given report date # (leaving the probably useless 'quick-report' mode, but disabled) if ( 1 || $cgi->param('multiple_billing_dates')) { my $walking_dt = DateTime->from_epoch(epoch => $now_dt->epoch); until ($walking_dt->epoch > $target_dt->epoch) { push @target_dates, $walking_dt->epoch; $walking_dt->add(days => 1); } } else { push @target_dates, $target_dt->epoch; } # List all customers with an auto-bill method that's not expired my %cust_payby = map {$_->custnum => $_} qsearch({ table => 'cust_payby', hashref => { weight => { op => '>', value => '0' }, }, order_by => " ORDER BY weight DESC ", extra_sql => " AND ( payby IN ('CHEK','DCHK') OR ( paydate > '".$target_dt->ymd."') ) ", }); my $fakebill_time = time(); my %abreport; my @rows; local $@; local $SIG{__DIE__}; eval { # Sandbox # Create new database handle and supress all COMMIT statements my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; local $FS::UID::ForceObeyAutoCommit = 1; # Suppress notices generated by billing events local $FS::Misc::DISABLE_ALL_NOTICES = 1; # Bypass payment processing, recording a fake payment local $FS::cust_main::Billing_Realtime::BOP_TESTING = 1; local $FS::cust_main::Billing_Realtime::BOP_TESTING_SUCCESS = 1; warn sprintf "Report involves %s customers", scalar keys %cust_payby if $DEBUG; # Run bill_and_collect(), for each customer with an autobill payment method, # for each day represented in the report for my $custnum (keys %cust_payby) { my $cust_main = qsearchs('cust_main', {custnum => $custnum}); warn "-- Processing custnum $custnum\n" if $DEBUG; # walk forward through billing dates for my $query_epoch (@target_dates) { $FS::cust_main::Billing_Realtime::BOP_TESTING_TIMESTAMP = $query_epoch; my $return_bill = []; warn "---- Set billtime to ". DateTime->from_epoch( epoch => $query_epoch )."\n" if $DEBUG; my $error = $cust_main->bill_and_collect( time => $query_epoch, return_bill => $return_bill, no_usage_reset => 1, fake => 1, ); warn "!!! $error (simulating future billing)\n" if $error; } # Generate report rows from recorded payments in cust_pay for my $cust_pay ( qsearch( cust_pay => { custnum => $custnum, _date => { op => '>=', value => $fakebill_time }, }) ) { push @rows,{ name => $cust_main->name, _date => $cust_pay->_date, cells => [ # Customer number { class => 'gridreport', value => $custnum }, # Customer name / customer link { class => 'gridreport', value => qq{} . encode_entities( $cust_main->name ). '', bypass_filter => 1 }, # Amount { class => 'gridreport', value => $cust_pay->paid, format => 'money' }, # Transaction Date { class => 'gridreport', value => DateTime->from_epoch( epoch => $cust_pay->_date )->ymd }, # Payment Method { class => 'gridreport', value => encode_entities( $cust_pay->paycardtype || $cust_pay->payby ), }, # Masked Payment Instrument { class => 'gridreport', value => encode_entities( $cust_pay->paymask ), }, ] }; } # /foreach payment # Roll back database at the end of each customer # Makes the report slighly slower, but ensures only one customer row # locked at a time warn "-- custnum $custnum -- rollback()\n"; dbh->rollback if $oldAutoCommit; } # /foreach $custnum }; # /eval warn("future_autobill.html report generated error $@") if $@; # Sort output by date, and format for output to grid-report.html my @cells = [ # header row { class => 'gridreport', value => '#', header => 1 }, { class => 'gridreport', value => 'Name', header => 1 }, { class => 'gridreport', value => 'Amount', header => 1 }, { class => 'gridreport', value => 'Date', header => 1 }, { class => 'gridreport', value => 'Type', header => 1 }, { class => 'gridreport', value => 'Account', header => 1 }, ]; push @cells, map { $_->{cells} } sort { $a->{_date} <=> $b->{_date} || $a->{name} cmp $b->{name} } @rows; # grid-report.html requires a parallel @rows parameter to accompany @cells @rows = map { {class => 'gridreport'} } 1..scalar(@cells); # Dynamic report title my $title_types = ''; my $card_count = FS::cust_payby->count_autobill_cards; my $check_count = FS::cust_payby->count_autobill_checks; if ( $card_count && $check_count ) { $title_types = 'Card and Check'; } elsif ( $card_count ) { $title_types = 'Card'; } elsif ( $check_count ) { $title_types = 'Check'; } my $report_title = sprintf( 'Upcoming Auto Bill %s Transactions', $title_types, );