summaryrefslogtreecommitdiff
path: root/httemplate/search/future_autobill.html
blob: 2e723ec79d025d281cc8818e51b288800e73fb76 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
<%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.

This report takes time.  If 200 customers have automatic
payment methods, and requester is looking one week ahead,
there will be 1,400 billing and payment cycles simulated

</%doc>
<h4><% $report_subtitle %></h4>
<& elements/grid-report.html,
  title => $report_title,
  rows => \@rows,
  cells => \@cells,
  table_width => "",
  table_class => 'gridreport',
  head => '
    <style type="text/css">
      table.gridreport { margin: .5em; border: solid 1px #aaa; }
      th.gridreport { background-color: #ccc; }
      tr.gridreport:nth-child(even) { background-color: #eee; }
      tr.gridreport:nth-child(odd)  { background-color: #fff; }
      td.gridreport { margin: 0 .2em; padding: 0 .4em; }
    </style>
  ',
  suppress_header => $job ? 1 : 0,
  suppress_footer => $job ? 1 : 0,
&>
% if ( %pmt_type_subtotal ) {
    <table class="gridreport" style="margin-left: 2em;">
      <tr>
        <th class="gridreport" colspan="2">
          Summary
        </th>
      </tr>
%   for my $pmt_type ( sort keys %pmt_type_subtotal ) {
      <tr class="gridreport">
        <td class="gridreport" style="text-align: right; margin-right: 1em;">
          <% sprintf '$%.2f', $pmt_type_subtotal{ $pmt_type } %>
        </td>
        <td class="gridreport">
          <% $pmt_type |h %>
        </td>
      </tr>
%   }
%   if ( keys %pmt_type_subtotal > 1 ) {
%     $pmt_type_subtotal{Total} += $_ for values %pmt_type_subtotal;
      <tr class="gridreport" style="border-top: solid 1px #999;">
        <td class="gridreport" style="text-align: right; margin-right: 1em; border-top: solid 1px #666;">
          <% sprintf( '$%.2f', $pmt_type_subtotal{Total} ) %>
        </td>
        <td class="gridreport" style="border-top: solid 1px #666;">
          Total
        </td>
      </tr>
      </table>
%   }
% }

<%init>
  use DateTime;
  use FS::Misc::Savepoint;
  use FS::Report::Queued::FutureAutobill;
  use FS::UID qw( dbh );

  die "access denied"
    unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');

  my $job = $FS::Report::Queued::FutureAutobill::job;

  $job->update_statustext('0,Finding customers') if $job;

  my $DEBUG = $cgi->param('DEBUG') || 0;

  my $agentnum = $cgi->param('agentnum')
    if $cgi->param('agentnum') =~ /^\d+/;

  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')) {
    # DateTime::Format::DateParse would be better
    my ($mm, $dd, $yy) = split /[\-\/]/,$cgi->param('target_date');
    ( $yy, $mm, $dd ) = ( $mm, $dd, $yy ) if $mm > 1900;

    $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
      unless $target_dt && $now_dt && $now_dt <=  $target_dt;
  }

  # without a target date, default to tomorrow
  unless ($target_dt) {
    $target_dt = $now_dt->clone->add( days => 1 );
  }

  my $report_title = FS::cust_payby->future_autobill_report_title;
  my $report_subtitle = sprintf(
    '(%s through %s)',
    $now_dt->mdy('/'),
    $target_dt->mdy('/'),
  );

  # 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',
    addl_from => 'JOIN cust_main USING (custnum)',
    hashref   => {  weight  => { op => '>', value => '0' }},
    order_by  => " ORDER BY weight DESC ",
    extra_sql =>
      "AND (
        cust_payby.payby IN ('CHEK','DCHK','DCHEK')
        OR ( cust_payby.paydate > '".$target_dt->ymd."')
      )
      AND " . $FS::CurrentUser::CurrentUser->agentnums_sql
      . ($agentnum ? "AND cust_main.agentnum = $agentnum" : ''),
  });

  my $completion_target = scalar(keys %cust_payby) * scalar( @target_dates );
  my $completion_progress = 0;

  my $fakebill_time = time();
  my %abreport;
  my @rows;
  my %pmt_type_subtotal;

  local $@;
  local $SIG{__DIE__};

  eval { # Sandbox

    # Supress 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;

    my $savepoint_label = 'future_autobill';
    savepoint_create( $savepoint_label );

    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;

        my $statustext = sprintf(
            '%s,Simulating upcoming invoices and payments',
            int( ( ++$completion_progress / $completion_target ) * 100 )
        );
        $job->update_statustext( $statustext ) if $job;
        warn "[ $completion_progress / $completion_target ] $statustext\n"
          if $DEBUG;

      }


      # 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{<a href="${fsurl}view/cust_main.cgi?${custnum}">} . encode_entities( $cust_main->name ). '</a>',
              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 ),
            },
          ]
        };

        $pmt_type_subtotal{ $cust_pay->paycardtype || $cust_pay-> payby }
          += $cust_pay->paid;

      } # /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" if $DEBUG;
      savepoint_rollback( $savepoint_label );
      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);

</%init>