8f59d71dbb5c69de66d52e240b1bc3c81462a5b5
[freeside.git] / httemplate / search / future_autobill.html
1 <%doc>
2
3 Report listing upcoming auto-bill transactions
4
5 For every customer with a valid auto-bill payment method,
6 report runs bill_and_collect() for each customer, for each
7 day, from today through the report target date.  After
8 recording the results, all operations are rolled back.
9
10 This report relies on the ability to safely run bill_and_collect(),
11 with all exports and messaging disabled, and then to roll back the
12 results.
13
14 This report takes time.  If 200 customers have automatic
15 payment methods, and requester is looking one week ahead,
16 there will be 1,400 billing and payment cycles simulated
17
18 </%doc>
19 <h4><% $report_subtitle %></h4>
20 <& elements/grid-report.html,
21   title => $report_title,
22   rows => \@rows,
23   cells => \@cells,
24   table_width => "",
25   table_class => 'gridreport',
26   head => '
27     <style type="text/css">
28       table.gridreport { margin: .5em; border: solid 1px #aaa; }
29       th.gridreport { background-color: #ccc; }
30       tr.gridreport:nth-child(even) { background-color: #eee; }
31       tr.gridreport:nth-child(odd)  { background-color: #fff; }
32       td.gridreport { margin: 0 .2em; padding: 0 .4em; }
33     </style>
34   ',
35   suppress_header => $job ? 1 : 0,
36   suppress_footer => $job ? 1 : 0,
37 &>
38 % if ( %pmt_type_subtotal ) {
39     <table class="gridreport" style="margin-left: 2em;">
40       <tr>
41         <th class="gridreport" colspan="2">
42           Summary
43         </th>
44       </tr>
45 %   for my $pmt_type ( sort keys %pmt_type_subtotal ) {
46       <tr class="gridreport">
47         <td class="gridreport" style="text-align: right; margin-right: 1em;">
48           <% sprintf '$%.2f', $pmt_type_subtotal{ $pmt_type } %>
49         </td>
50         <td class="gridreport">
51           <% $pmt_type |h %>
52         </td>
53       </tr>
54 %   }
55 %   $pmt_type_subtotal{Total} += $_ for values %pmt_type_subtotal;
56     <tr class="gridreport" style="border-top: solid 1px #999;">
57       <td class="gridreport" style="text-align: right; margin-right: 1em; border-top: solid 1px #666;">
58         <% sprintf( '$%.2f', $pmt_type_subtotal{Total} ) %>
59       </td>
60       <td class="gridreport" style="border-top: solid 1px #666;">
61         Total
62       </td>
63     </tr>
64     </table>
65 % }
66
67 <%init>
68   use DateTime;
69   use FS::Misc::Savepoint;
70   use FS::Report::Queued::FutureAutobill;
71   use FS::UID qw( dbh );
72
73   die "access denied"
74     unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
75
76   my $job = $FS::Report::Queued::FutureAutobill::job;
77
78   $job->update_statustext('0,Finding customers') if $job;
79
80   my $DEBUG = $cgi->param('DEBUG') || 0;
81
82   my $agentnum = $cgi->param('agentnum')
83     if $cgi->param('agentnum') =~ /^\d+/;
84
85   my $target_dt;
86   my @target_dates;
87
88   # Work with all date/time operations @ 12 noon
89   my %noon = (
90     hour   => 12,
91     minute => 0,
92     second => 0,
93   );
94   my $now_dt = DateTime->now;
95   $now_dt = DateTime->new(
96     month  => $now_dt->month,
97     day    => $now_dt->day,
98     year   => $now_dt->year,
99     %noon,
100   );
101
102   # Get target date from form
103   if ($cgi->param('target_date')) {
104     # DateTime::Format::DateParse would be better
105     my ($mm, $dd, $yy) = split /[\-\/]/,$cgi->param('target_date');
106     ( $yy, $mm, $dd ) = ( $mm, $dd, $yy ) if $mm > 1900;
107
108     $target_dt = DateTime->new(
109       month  => $mm,
110       day    => $dd,
111       year   => $yy,
112       %noon,
113     ) if $mm && $dd && $yy;
114
115     # Catch a date from the past: time only travels in one direction
116     $target_dt = undef
117       unless $target_dt && $now_dt && $now_dt <=  $target_dt;
118   }
119
120   # without a target date, default to tomorrow
121   unless ($target_dt) {
122     $target_dt = $now_dt->clone->add( days => 1 );
123   }
124
125   my $report_title = FS::cust_payby->future_autobill_report_title;
126   my $report_subtitle = sprintf(
127     '(%s through %s)',
128     $now_dt->mdy('/'),
129     $target_dt->mdy('/'),
130   );
131
132   # Create a range of dates from today until the given report date
133   #   (leaving the probably useless 'quick-report' mode, but disabled)
134   if ( 1 || $cgi->param('multiple_billing_dates')) {
135     my $walking_dt = DateTime->from_epoch(epoch => $now_dt->epoch);
136     until ($walking_dt->epoch > $target_dt->epoch) {
137      push @target_dates, $walking_dt->epoch;
138      $walking_dt->add(days => 1);
139     }
140   } else {
141     push @target_dates, $target_dt->epoch;
142   }
143
144   # List all customers with an auto-bill method that's not expired
145   my %cust_payby = map {$_->custnum => $_} qsearch({
146     table     => 'cust_payby',
147     addl_from => 'JOIN cust_main USING (custnum)',
148     hashref   => {  weight  => { op => '>', value => '0' }},
149     order_by  => " ORDER BY weight DESC ",
150     extra_sql =>
151       "AND (
152         cust_payby.payby IN ('CHEK','DCHK','DCHEK')
153         OR ( cust_payby.paydate > '".$target_dt->ymd."')
154       )
155       AND " . $FS::CurrentUser::CurrentUser->agentnums_sql
156       . ($agentnum ? "AND cust_main.agentnum = $agentnum" : ''),
157   });
158
159   my $completion_target = scalar(keys %cust_payby) * scalar( @target_dates );
160   my $completion_progress = 0;
161
162   my $fakebill_time = time();
163   my %abreport;
164   my @rows;
165   my %pmt_type_subtotal;
166
167   local $@;
168   local $SIG{__DIE__};
169
170   eval { # Sandbox
171
172     # Supress COMMIT statements
173     my $oldAutoCommit = $FS::UID::AutoCommit;
174     local $FS::UID::AutoCommit = 0;
175     local $FS::UID::ForceObeyAutoCommit = 1;
176
177     # Suppress notices generated by billing events
178     local $FS::Misc::DISABLE_ALL_NOTICES = 1;
179
180     # Bypass payment processing, recording a fake payment
181     local $FS::cust_main::Billing_Realtime::BOP_TESTING = 1;
182     local $FS::cust_main::Billing_Realtime::BOP_TESTING_SUCCESS = 1;
183
184     my $savepoint_label = 'future_autobill';
185     savepoint_create( $savepoint_label );
186
187     warn sprintf "Report involves %s customers", scalar keys %cust_payby
188       if $DEBUG;
189
190     # Run bill_and_collect(), for each customer with an autobill payment method,
191     # for each day represented in the report
192     for my $custnum (keys %cust_payby) {
193       my $cust_main = qsearchs('cust_main', {custnum => $custnum});
194
195       warn "-- Processing custnum $custnum\n"
196         if $DEBUG;
197
198       # walk forward through billing dates
199       for my $query_epoch (@target_dates) {
200         $FS::cust_main::Billing_Realtime::BOP_TESTING_TIMESTAMP = $query_epoch;
201         my $return_bill = [];
202
203         warn "---- Set billtime to ".
204              DateTime->from_epoch( epoch => $query_epoch )."\n"
205                 if $DEBUG;
206
207         my $error = $cust_main->bill_and_collect(
208           time           => $query_epoch,
209           return_bill    => $return_bill,
210           no_usage_reset => 1,
211           fake           => 1,
212         );
213
214         warn "!!! $error (simulating future billing)\n" if $error;
215
216         my $statustext = sprintf(
217             '%s,Simulating upcoming invoices and payments',
218             int( ( ++$completion_progress / $completion_target ) * 100 )
219         );
220         $job->update_statustext( $statustext ) if $job;
221         warn "[ $completion_progress / $completion_target ] $statustext\n"
222           if $DEBUG;
223
224       }
225
226
227       # Generate report rows from recorded payments in cust_pay
228       for my $cust_pay (
229         qsearch( cust_pay => {
230           custnum => $custnum,
231           _date   => { op => '>=', value => $fakebill_time },
232         })
233       ) {
234         push @rows,{
235           name  => $cust_main->name,
236           _date => $cust_pay->_date,
237           cells => [
238
239             # Customer number
240             { class => 'gridreport', value => $custnum },
241
242             # Customer name / customer link
243             { class => 'gridreport',
244               value =>  qq{<a href="${fsurl}view/cust_main.cgi?${custnum}">} . encode_entities( $cust_main->name ). '</a>',
245               bypass_filter => 1
246             },
247
248             # Amount
249             { class => 'gridreport',
250               value => $cust_pay->paid,
251               format => 'money'
252             },
253
254             # Transaction Date
255             { class => 'gridreport',
256               value => DateTime->from_epoch( epoch => $cust_pay->_date )->ymd
257             },
258
259             # Payment Method
260             { class => 'gridreport',
261               value => encode_entities( $cust_pay->paycardtype || $cust_pay->payby ),
262             },
263
264             # Masked Payment Instrument
265             { class => 'gridreport',
266               value => encode_entities( $cust_pay->paymask ),
267             },
268           ]
269         };
270
271         $pmt_type_subtotal{ $cust_pay->paycardtype || $cust_pay-> payby }
272           += $cust_pay->paid;
273
274       } # /foreach payment
275
276       # Roll back database at the end of each customer
277       # Makes the report slighly slower, but ensures only one customer row
278       #   locked at a time
279
280       warn "-- custnum $custnum -- rollback()\n" if $DEBUG;
281       savepoint_rollback( $savepoint_label );
282       dbh->rollback if $oldAutoCommit;
283
284     } # /foreach $custnum
285   }; # /eval
286   warn("future_autobill.html report generated error $@") if $@;
287
288   # Sort output by date, and format for output to grid-report.html
289   my @cells = [
290       # header row
291       { class => 'gridreport', value => '#',       header => 1 },
292       { class => 'gridreport', value => 'Name',    header => 1 },
293       { class => 'gridreport', value => 'Amount',  header => 1 },
294       { class => 'gridreport', value => 'Date',    header => 1 },
295       { class => 'gridreport', value => 'Type',    header => 1 },
296       { class => 'gridreport', value => 'Account', header => 1 },
297     ];
298   push @cells,
299     map  { $_->{cells} }
300     sort { $a->{_date} <=> $b->{_date} || $a->{name} cmp $b->{name} }
301     @rows;
302
303   # grid-report.html requires a parallel @rows parameter to accompany @cells
304   @rows = map { {class => 'gridreport'} } 1..scalar(@cells);
305
306 </%init>