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