%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.
%doc>
<& 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,
);
%init>