add -o flag to freeside-daily, to only bill for one month, RT#14904
[freeside.git] / FS / FS / cust_main / Billing.pm
1 package FS::cust_main::Billing;
2
3 use strict;
4 use vars qw( $conf $DEBUG $me );
5 use Carp;
6 use Data::Dumper;
7 use List::Util qw( min );
8 use FS::UID qw( dbh );
9 use FS::Record qw( qsearch qsearchs dbdef );
10 use FS::Misc::DateTime qw( day_end );
11 use FS::cust_bill;
12 use FS::cust_bill_pkg;
13 use FS::cust_bill_pkg_display;
14 use FS::cust_bill_pay;
15 use FS::cust_credit_bill;
16 use FS::cust_tax_adjustment;
17 use FS::tax_rate;
18 use FS::tax_rate_location;
19 use FS::cust_bill_pkg_tax_location;
20 use FS::cust_bill_pkg_tax_rate_location;
21 use FS::part_event;
22 use FS::part_event_condition;
23 use FS::pkg_category;
24
25 # 1 is mostly method/subroutine entry and options
26 # 2 traces progress of some operations
27 # 3 is even more information including possibly sensitive data
28 $DEBUG = 0;
29 $me = '[FS::cust_main::Billing]';
30
31 install_callback FS::UID sub { 
32   $conf = new FS::Conf;
33   #yes, need it for stuff below (prolly should be cached)
34 };
35
36 =head1 NAME
37
38 FS::cust_main::Billing - Billing mixin for cust_main
39
40 =head1 SYNOPSIS
41
42 =head1 DESCRIPTION
43
44 These methods are available on FS::cust_main objects.
45
46 =head1 METHODS
47
48 =over 4
49
50 =item bill_and_collect 
51
52 Cancels and suspends any packages due, generates bills, applies payments and
53 credits, and applies collection events to run cards, send bills and notices,
54 etc.
55
56 By default, warns on errors and continues with the next operation (but see the
57 "fatal" flag below).
58
59 Options are passed as name-value pairs.  Currently available options are:
60
61 =over 4
62
63 =item time
64
65 Bills the customer as if it were that time.  Specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion functions.  For example:
66
67  use Date::Parse;
68  ...
69  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
70
71 =item invoice_time
72
73 Used in conjunction with the I<time> option, this option specifies the date of for the generated invoices.  Other calculations, such as whether or not to generate the invoice in the first place, are not affected.
74
75 =item check_freq
76
77 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
78
79 =item resetup
80
81 If set true, re-charges setup fees.
82
83 =item fatal
84
85 If set any errors prevent subsequent operations from continusing.  If set
86 specifically to "return", returns the error (or false, if there is no error).
87 Any other true value causes errors to die.
88
89 =item debug
90
91 Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
92
93 =item job
94
95 Optional FS::queue entry to receive status updates.
96
97 =back
98
99 Options are passed to the B<bill> and B<collect> methods verbatim, so all
100 options of those methods are also available.
101
102 =cut
103
104 sub bill_and_collect {
105   my( $self, %options ) = @_;
106
107   my $error;
108
109   #$options{actual_time} not $options{time} because freeside-daily -d is for
110   #pre-printing invoices
111
112   $options{'actual_time'} ||= time;
113   my $job = $options{'job'};
114
115   $job->update_statustext('0,cleaning expired packages') if $job;
116   $error = $self->cancel_expired_pkgs( day_end( $options{actual_time} ) );
117   if ( $error ) {
118     $error = "Error expiring custnum ". $self->custnum. ": $error";
119     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
120     elsif ( $options{fatal}                                ) { die    $error; }
121     else                                                     { warn   $error; }
122   }
123
124   $error = $self->suspend_adjourned_pkgs( day_end( $options{actual_time} ) );
125   if ( $error ) {
126     $error = "Error adjourning custnum ". $self->custnum. ": $error";
127     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
128     elsif ( $options{fatal}                                ) { die    $error; }
129     else                                                     { warn   $error; }
130   }
131
132   $job->update_statustext('20,billing packages') if $job;
133   $error = $self->bill( %options );
134   if ( $error ) {
135     $error = "Error billing custnum ". $self->custnum. ": $error";
136     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
137     elsif ( $options{fatal}                                ) { die    $error; }
138     else                                                     { warn   $error; }
139   }
140
141   $job->update_statustext('50,applying payments and credits') if $job;
142   $error = $self->apply_payments_and_credits;
143   if ( $error ) {
144     $error = "Error applying custnum ". $self->custnum. ": $error";
145     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
146     elsif ( $options{fatal}                                ) { die    $error; }
147     else                                                     { warn   $error; }
148   }
149
150   $job->update_statustext('70,running collection events') if $job;
151   unless ( $conf->exists('cancelled_cust-noevents')
152            && ! $self->num_ncancelled_pkgs
153   ) {
154     $error = $self->collect( %options );
155     if ( $error ) {
156       $error = "Error collecting custnum ". $self->custnum. ": $error";
157       if    ($options{fatal} && $options{fatal} eq 'return') { return $error; }
158       elsif ($options{fatal}                               ) { die    $error; }
159       else                                                   { warn   $error; }
160     }
161   }
162   $job->update_statustext('100,finished') if $job;
163
164   '';
165
166 }
167
168 sub cancel_expired_pkgs {
169   my ( $self, $time, %options ) = @_;
170   
171   my @cancel_pkgs = $self->ncancelled_pkgs( { 
172     'extra_sql' => " AND expire IS NOT NULL AND expire > 0 AND expire <= $time "
173   } );
174
175   my @errors = ();
176
177   foreach my $cust_pkg ( @cancel_pkgs ) {
178     my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
179     my $error = $cust_pkg->cancel($cpr ? ( 'reason'        => $cpr->reasonnum,
180                                            'reason_otaker' => $cpr->otaker
181                                          )
182                                        : ()
183                                  );
184     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
185   }
186
187   scalar(@errors) ? join(' / ', @errors) : '';
188
189 }
190
191 sub suspend_adjourned_pkgs {
192   my ( $self, $time, %options ) = @_;
193   
194   my @susp_pkgs = $self->ncancelled_pkgs( {
195     'extra_sql' =>
196       " AND ( susp IS NULL OR susp = 0 )
197         AND (    ( bill    IS NOT NULL AND bill    != 0 AND bill    <  $time )
198               OR ( adjourn IS NOT NULL AND adjourn != 0 AND adjourn <= $time )
199             )
200       ",
201   } );
202
203   #only because there's no SQL test for is_prepaid :/
204   @susp_pkgs = 
205     grep {     (    $_->part_pkg->is_prepaid
206                  && $_->bill
207                  && $_->bill < $time
208                )
209             || (    $_->adjourn
210                  && $_->adjourn <= $time
211                )
212            
213          }
214          @susp_pkgs;
215
216   my @errors = ();
217
218   foreach my $cust_pkg ( @susp_pkgs ) {
219     my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
220       if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
221     my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
222                                             'reason_otaker' => $cpr->otaker
223                                           )
224                                         : ()
225                                   );
226     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
227   }
228
229   scalar(@errors) ? join(' / ', @errors) : '';
230
231 }
232
233 =item bill OPTIONS
234
235 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
236 conjunction with the collect method by calling B<bill_and_collect>.
237
238 If there is an error, returns the error, otherwise returns false.
239
240 Options are passed as name-value pairs.  Currently available options are:
241
242 =over 4
243
244 =item resetup
245
246 If set true, re-charges setup fees.
247
248 =item recurring_only
249
250 If set true then only bill recurring charges, not setup, usage, one time
251 charges, etc.
252
253 =item freq_override
254
255 If set, then override the normal frequency and look for a part_pkg_discount
256 to take at that frequency.  This is appropriate only when the normal 
257 frequency for all packages is monthly, and is an error otherwise.  Use
258 C<pkg_list> to limit the set of packages included in billing.
259
260 =item time
261
262 Bills the customer as if it were that time.  Specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion functions.  For example:
263
264  use Date::Parse;
265  ...
266  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
267
268 =item pkg_list
269
270 An array ref of specific packages (objects) to attempt billing, instead trying all of them.
271
272  $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
273
274 =item not_pkgpart
275
276 A hashref of pkgparts to exclude from this billing run (can also be specified as a comma-separated scalar).
277
278 =item invoice_time
279
280 Used in conjunction with the I<time> option, this option specifies the date of for the generated invoices.  Other calculations, such as whether or not to generate the invoice in the first place, are not affected.
281
282 =item cancel
283
284 This boolean value informs the us that the package is being cancelled.  This
285 typically might mean not charging the normal recurring fee but only usage
286 fees since the last billing. Setup charges may be charged.  Not all package
287 plans support this feature (they tend to charge 0).
288
289 =item no_usage_reset
290
291 Prevent the resetting of usage limits during this call.
292
293 =item no_commit
294
295 Do not save the generated bill in the database.  Useful with return_bill
296
297 =item return_bill
298
299 A list reference on which the generated bill(s) will be returned.
300
301 =item invoice_terms
302
303 Optional terms to be printed on this invoice.  Otherwise, customer-specific
304 terms or the default terms are used.
305
306 =back
307
308 =cut
309
310 sub bill {
311   my( $self, %options ) = @_;
312
313   return '' if $self->payby eq 'COMP';
314
315   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
316
317   warn "$me bill customer ". $self->custnum. "\n"
318     if $DEBUG;
319
320   my $time = $options{'time'} || time;
321   my $invoice_time = $options{'invoice_time'} || $time;
322
323   $options{'not_pkgpart'} ||= {};
324   $options{'not_pkgpart'} = { map { $_ => 1 }
325                                   split(/\s*,\s*/, $options{'not_pkgpart'})
326                             }
327     unless ref($options{'not_pkgpart'});
328
329   local $SIG{HUP} = 'IGNORE';
330   local $SIG{INT} = 'IGNORE';
331   local $SIG{QUIT} = 'IGNORE';
332   local $SIG{TERM} = 'IGNORE';
333   local $SIG{TSTP} = 'IGNORE';
334   local $SIG{PIPE} = 'IGNORE';
335
336   my $oldAutoCommit = $FS::UID::AutoCommit;
337   local $FS::UID::AutoCommit = 0;
338   my $dbh = dbh;
339
340   warn "$me acquiring lock on customer ". $self->custnum. "\n"
341     if $DEBUG;
342
343   $self->select_for_update; #mutex
344
345   warn "$me running pre-bill events for customer ". $self->custnum. "\n"
346     if $DEBUG;
347
348   my $error = $self->do_cust_event(
349     'debug'      => ( $options{'debug'} || 0 ),
350     'time'       => $invoice_time,
351     'check_freq' => $options{'check_freq'},
352     'stage'      => 'pre-bill',
353   )
354     unless $options{no_commit};
355   if ( $error ) {
356     $dbh->rollback if $oldAutoCommit && !$options{no_commit};
357     return $error;
358   }
359
360   warn "$me done running pre-bill events for customer ". $self->custnum. "\n"
361     if $DEBUG;
362
363   #keep auto-charge and non-auto-charge line items separate
364   my @passes = ( '', 'no_auto' );
365
366   my %cust_bill_pkg = map { $_ => [] } @passes;
367
368   ###
369   # find the packages which are due for billing, find out how much they are
370   # & generate invoice database.
371   ###
372
373   my %total_setup   = map { my $z = 0; $_ => \$z; } @passes;
374   my %total_recur   = map { my $z = 0; $_ => \$z; } @passes;
375
376   my %taxlisthash = map { $_ => {} } @passes;
377
378   my @precommit_hooks = ();
379
380   $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ];  #param checks?
381   foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
382
383     next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
384
385     warn "  bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG;
386
387     #? to avoid use of uninitialized value errors... ?
388     $cust_pkg->setfield('bill', '')
389       unless defined($cust_pkg->bill);
390  
391     #my $part_pkg = $cust_pkg->part_pkg;
392
393     my $real_pkgpart = $cust_pkg->pkgpart;
394     my %hash = $cust_pkg->hash;
395
396     # we could implement this bit as FS::part_pkg::has_hidden, but we already
397     # suffer from performance issues
398     $options{has_hidden} = 0;
399     my @part_pkg = $cust_pkg->part_pkg->self_and_bill_linked;
400     $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
401  
402     foreach my $part_pkg ( @part_pkg ) {
403
404       $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
405
406       my $pass = ($cust_pkg->no_auto || $part_pkg->no_auto) ? 'no_auto' : '';
407
408       my $next_bill = $cust_pkg->getfield('bill') || 0;
409       my $error;
410       while ( $next_bill <= $time ) {
411         $error =
412           $self->_make_lines( 'part_pkg'            => $part_pkg,
413                               'cust_pkg'            => $cust_pkg,
414                               'precommit_hooks'     => \@precommit_hooks,
415                               'line_items'          => $cust_bill_pkg{$pass},
416                               'setup'               => $total_setup{$pass},
417                               'recur'               => $total_recur{$pass},
418                               'tax_matrix'          => $taxlisthash{$pass},
419                               'time'                => $time,
420                               'real_pkgpart'        => $real_pkgpart,
421                               'options'             => \%options,
422                             );
423
424         # Stop if anything goes wrong
425         last if $error;
426
427         # or if we're not incrementing the bill date.
428         last if ($cust_pkg->getfield('bill') || 0) == $next_bill;
429
430         $next_bill = $cust_pkg->getfield('bill') || 0;
431
432         #stop if -o was passed to freeside-daily
433         last if $options{'one_recur'};
434       }
435       if ($error) {
436         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
437         return $error;
438       }
439
440     } #foreach my $part_pkg
441
442   } #foreach my $cust_pkg
443
444   #if the customer isn't on an automatic payby, everything can go on a single
445   #invoice anyway?
446   #if ( $cust_main->payby !~ /^(CARD|CHEK)$/ ) {
447     #merge everything into one list
448   #}
449
450   foreach my $pass (@passes) { # keys %cust_bill_pkg ) {
451
452     my @cust_bill_pkg = _omit_zero_value_bundles(@{ $cust_bill_pkg{$pass} });
453
454     next unless @cust_bill_pkg; #don't create an invoice w/o line items
455
456     warn "$me billing pass $pass\n"
457            #.Dumper(\@cust_bill_pkg)."\n"
458       if $DEBUG > 2;
459
460     if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
461            !$conf->exists('postal_invoice-recurring_only')
462        )
463     {
464
465       my $postal_pkg = $self->charge_postal_fee();
466       if ( $postal_pkg && !ref( $postal_pkg ) ) {
467
468         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
469         return "can't charge postal invoice fee for customer ".
470           $self->custnum. ": $postal_pkg";
471
472       } elsif ( $postal_pkg ) {
473
474         my $real_pkgpart = $postal_pkg->pkgpart;
475         # we could implement this bit as FS::part_pkg::has_hidden, but we already
476         # suffer from performance issues
477         $options{has_hidden} = 0;
478         my @part_pkg = $postal_pkg->part_pkg->self_and_bill_linked;
479         $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
480
481         foreach my $part_pkg ( @part_pkg ) {
482           my %postal_options = %options;
483           delete $postal_options{cancel};
484           my $error =
485             $self->_make_lines( 'part_pkg'            => $part_pkg,
486                                 'cust_pkg'            => $postal_pkg,
487                                 'precommit_hooks'     => \@precommit_hooks,
488                                 'line_items'          => \@cust_bill_pkg,
489                                 'setup'               => $total_setup{$pass},
490                                 'recur'               => $total_recur{$pass},
491                                 'tax_matrix'          => $taxlisthash{$pass},
492                                 'time'                => $time,
493                                 'real_pkgpart'        => $real_pkgpart,
494                                 'options'             => \%postal_options,
495                               );
496           if ($error) {
497             $dbh->rollback if $oldAutoCommit && !$options{no_commit};
498             return $error;
499           }
500         }
501
502         # it's silly to have a zero value postal_pkg, but....
503         @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
504
505       }
506
507     }
508
509     my $listref_or_error =
510       $self->calculate_taxes( \@cust_bill_pkg, $taxlisthash{$pass}, $invoice_time);
511
512     unless ( ref( $listref_or_error ) ) {
513       $dbh->rollback if $oldAutoCommit && !$options{no_commit};
514       return $listref_or_error;
515     }
516
517     foreach my $taxline ( @$listref_or_error ) {
518       ${ $total_setup{$pass} } =
519         sprintf('%.2f', ${ $total_setup{$pass} } + $taxline->setup );
520       push @cust_bill_pkg, $taxline;
521     }
522
523     #add tax adjustments
524     warn "adding tax adjustments...\n" if $DEBUG > 2;
525     foreach my $cust_tax_adjustment (
526       qsearch('cust_tax_adjustment', { 'custnum'    => $self->custnum,
527                                        'billpkgnum' => '',
528                                      }
529              )
530     ) {
531
532       my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
533
534       my $itemdesc = $cust_tax_adjustment->taxname;
535       $itemdesc = '' if $itemdesc eq 'Tax';
536
537       push @cust_bill_pkg, new FS::cust_bill_pkg {
538         'pkgnum'      => 0,
539         'setup'       => $tax,
540         'recur'       => 0,
541         'sdate'       => '',
542         'edate'       => '',
543         'itemdesc'    => $itemdesc,
544         'itemcomment' => $cust_tax_adjustment->comment,
545         'cust_tax_adjustment' => $cust_tax_adjustment,
546         #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
547       };
548
549     }
550
551     my $charged = sprintf('%.2f', ${ $total_setup{$pass} } + ${ $total_recur{$pass} } );
552
553     my @cust_bill = $self->cust_bill;
554     my $balance = $self->balance;
555     my $previous_balance = scalar(@cust_bill)
556                              ? ( $cust_bill[$#cust_bill]->billing_balance || 0 )
557                              : 0;
558
559     $previous_balance += $cust_bill[$#cust_bill]->charged
560       if scalar(@cust_bill);
561     #my $balance_adjustments =
562     #  sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged);
563
564     warn "creating the new invoice\n" if $DEBUG;
565     #create the new invoice
566     my $cust_bill = new FS::cust_bill ( {
567       'custnum'             => $self->custnum,
568       '_date'               => $invoice_time,
569       'charged'             => $charged,
570       'billing_balance'     => $balance,
571       'previous_balance'    => $previous_balance,
572       'invoice_terms'       => $options{'invoice_terms'},
573       'cust_bill_pkg'       => \@cust_bill_pkg,
574     } );
575     $error = $cust_bill->insert unless $options{no_commit};
576     if ( $error ) {
577       $dbh->rollback if $oldAutoCommit && !$options{no_commit};
578       return "can't create invoice for customer #". $self->custnum. ": $error";
579     }
580     push @{$options{return_bill}}, $cust_bill if $options{return_bill};
581
582   } #foreach my $pass ( keys %cust_bill_pkg )
583
584   foreach my $hook ( @precommit_hooks ) { 
585     eval {
586       &{$hook}; #($self) ?
587     } unless $options{no_commit};
588     if ( $@ ) {
589       $dbh->rollback if $oldAutoCommit && !$options{no_commit};
590       return "$@ running precommit hook $hook\n";
591     }
592   }
593   
594   $dbh->commit or die $dbh->errstr if $oldAutoCommit && !$options{no_commit};
595
596   ''; #no error
597 }
598
599 #discard bundled packages of 0 value
600 sub _omit_zero_value_bundles {
601   my @in = @_;
602
603   my @cust_bill_pkg = ();
604   my @cust_bill_pkg_bundle = ();
605   my $sum = 0;
606   my $discount_show_always = 0;
607
608   foreach my $cust_bill_pkg ( @in ) {
609
610     $discount_show_always = ($cust_bill_pkg->get('discounts')
611                                 && scalar(@{$cust_bill_pkg->get('discounts')})
612                                 && $conf->exists('discount-show-always'));
613
614     warn "  pkgnum ". $cust_bill_pkg->pkgnum. " sum $sum, ".
615          "setup_show_zero ". $cust_bill_pkg->setup_show_zero.
616          "recur_show_zero ". $cust_bill_pkg->recur_show_zero. "\n"
617       if $DEBUG > 0;
618
619     if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) {
620       push @cust_bill_pkg, @cust_bill_pkg_bundle 
621         if $sum > 0
622         || ($sum == 0 && (    $discount_show_always
623                            || grep {$_->recur_show_zero || $_->setup_show_zero}
624                                    @cust_bill_pkg_bundle
625                          )
626            );
627       @cust_bill_pkg_bundle = ();
628       $sum = 0;
629     }
630
631     $sum += $cust_bill_pkg->setup + $cust_bill_pkg->recur;
632     push @cust_bill_pkg_bundle, $cust_bill_pkg;
633
634   }
635
636   push @cust_bill_pkg, @cust_bill_pkg_bundle
637     if $sum > 0
638     || ($sum == 0 && (    $discount_show_always
639                        || grep {$_->recur_show_zero || $_->setup_show_zero}
640                                @cust_bill_pkg_bundle
641                      )
642        );
643
644   warn "  _omit_zero_value_bundles: ". scalar(@in).
645        '->'. scalar(@cust_bill_pkg). "\n" #. Dumper(@cust_bill_pkg). "\n"
646     if $DEBUG > 2;
647
648   (@cust_bill_pkg);
649
650 }
651
652 =item calculate_taxes LINEITEMREF TAXHASHREF INVOICE_TIME
653
654 This is a weird one.  Perhaps it should not even be exposed.
655
656 Generates tax line items (see L<FS::cust_bill_pkg>) for this customer.
657 Usually used internally by bill method B<bill>.
658
659 If there is an error, returns the error, otherwise returns reference to a
660 list of line items suitable for insertion.
661
662 =over 4
663
664 =item LINEITEMREF
665
666 An array ref of the line items being billed.
667
668 =item TAXHASHREF
669
670 A strange beast.  The keys to this hash are internal identifiers consisting
671 of the name of the tax object type, a space, and its unique identifier ( e.g.
672  'cust_main_county 23' ).  The values of the hash are listrefs.  The first
673 item in the list is the tax object.  The remaining items are either line
674 items or floating point values (currency amounts).
675
676 The taxes are calculated on this entity.  Calculated exemption records are
677 transferred to the LINEITEMREF items on the assumption that they are related.
678
679 Read the source.
680
681 =item INVOICE_TIME
682
683 This specifies the date appearing on the associated invoice.  Some
684 jurisdictions (i.e. Texas) have tax exemptions which are date sensitive.
685
686 =back
687
688 =cut
689
690 sub calculate_taxes {
691   my ($self, $cust_bill_pkg, $taxlisthash, $invoice_time) = @_;
692
693   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
694
695   warn "$me calculate_taxes\n"
696        #.Dumper($self, $cust_bill_pkg, $taxlisthash, $invoice_time). "\n"
697     if $DEBUG > 2;
698
699   my @tax_line_items = ();
700
701   # keys are tax names (as printed on invoices / itemdesc )
702   # values are listrefs of taxlisthash keys (internal identifiers)
703   my %taxname = ();
704
705   # keys are taxlisthash keys (internal identifiers)
706   # values are (cumulative) amounts
707   my %tax = ();
708
709   # keys are taxlisthash keys (internal identifiers)
710   # values are listrefs of cust_bill_pkg_tax_location hashrefs
711   my %tax_location = ();
712
713   # keys are taxlisthash keys (internal identifiers)
714   # values are listrefs of cust_bill_pkg_tax_rate_location hashrefs
715   my %tax_rate_location = ();
716
717   foreach my $tax ( keys %$taxlisthash ) {
718     my $tax_object = shift @{ $taxlisthash->{$tax} };
719     warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
720     warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2;
721     my $hashref_or_error =
722       $tax_object->taxline( $taxlisthash->{$tax},
723                             'custnum'      => $self->custnum,
724                             'invoice_time' => $invoice_time
725                           );
726     return $hashref_or_error unless ref($hashref_or_error);
727
728     unshift @{ $taxlisthash->{$tax} }, $tax_object;
729
730     my $name   = $hashref_or_error->{'name'};
731     my $amount = $hashref_or_error->{'amount'};
732
733     #warn "adding $amount as $name\n";
734     $taxname{ $name } ||= [];
735     push @{ $taxname{ $name } }, $tax;
736
737     $tax{ $tax } += $amount;
738
739     $tax_location{ $tax } ||= [];
740     if ( $tax_object->get('pkgnum') || $tax_object->get('locationnum') ) {
741       push @{ $tax_location{ $tax }  },
742         {
743           'taxnum'      => $tax_object->taxnum, 
744           'taxtype'     => ref($tax_object),
745           'pkgnum'      => $tax_object->get('pkgnum'),
746           'locationnum' => $tax_object->get('locationnum'),
747           'amount'      => sprintf('%.2f', $amount ),
748         };
749     }
750
751     $tax_rate_location{ $tax } ||= [];
752     if ( ref($tax_object) eq 'FS::tax_rate' ) {
753       my $taxratelocationnum =
754         $tax_object->tax_rate_location->taxratelocationnum;
755       push @{ $tax_rate_location{ $tax }  },
756         {
757           'taxnum'             => $tax_object->taxnum, 
758           'taxtype'            => ref($tax_object),
759           'amount'             => sprintf('%.2f', $amount ),
760           'locationtaxid'      => $tax_object->location,
761           'taxratelocationnum' => $taxratelocationnum,
762         };
763     }
764
765   }
766
767   #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
768   my %packagemap = map { $_->pkgnum => $_ } @$cust_bill_pkg;
769   foreach my $tax ( keys %$taxlisthash ) {
770     foreach ( @{ $taxlisthash->{$tax} }[1 ... scalar(@{ $taxlisthash->{$tax} })] ) {
771       next unless ref($_) eq 'FS::cust_bill_pkg';
772      
773       my @cust_tax_exempt_pkg = splice( @{ $_->_cust_tax_exempt_pkg } );
774
775       next unless @cust_tax_exempt_pkg; #just avoiding the prob when irrelevant?
776       die "can't distribute tax exemptions: no line item for ".  Dumper($_).
777           " in packagemap ". join(',', sort {$a<=>$b} keys %packagemap). "\n"
778         unless $packagemap{$_->pkgnum};
779
780       push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
781            @cust_tax_exempt_pkg;
782     }
783   }
784
785   #consolidate and create tax line items
786   warn "consolidating and generating...\n" if $DEBUG > 2;
787   foreach my $taxname ( keys %taxname ) {
788     my $tax = 0;
789     my %seen = ();
790     my @cust_bill_pkg_tax_location = ();
791     my @cust_bill_pkg_tax_rate_location = ();
792     warn "adding $taxname\n" if $DEBUG > 1;
793     foreach my $taxitem ( @{ $taxname{$taxname} } ) {
794       next if $seen{$taxitem}++;
795       warn "adding $tax{$taxitem}\n" if $DEBUG > 1;
796       $tax += $tax{$taxitem};
797       push @cust_bill_pkg_tax_location,
798         map { new FS::cust_bill_pkg_tax_location $_ }
799             @{ $tax_location{ $taxitem } };
800       push @cust_bill_pkg_tax_rate_location,
801         map { new FS::cust_bill_pkg_tax_rate_location $_ }
802             @{ $tax_rate_location{ $taxitem } };
803     }
804     next unless $tax;
805
806     $tax = sprintf('%.2f', $tax );
807   
808     my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
809                                                    'disabled'     => '',
810                                                  },
811                                );
812
813     my @display = ();
814     if ( $pkg_category and
815          $conf->config('invoice_latexsummary') ||
816          $conf->config('invoice_htmlsummary')
817        )
818     {
819
820       my %hash = (  'section' => $pkg_category->categoryname );
821       push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
822
823     }
824
825     push @tax_line_items, new FS::cust_bill_pkg {
826       'pkgnum'   => 0,
827       'setup'    => $tax,
828       'recur'    => 0,
829       'sdate'    => '',
830       'edate'    => '',
831       'itemdesc' => $taxname,
832       'display'  => \@display,
833       'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
834       'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
835     };
836
837   }
838
839   \@tax_line_items;
840 }
841
842 sub _make_lines {
843   my ($self, %params) = @_;
844
845   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
846
847   my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
848   my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
849   my $precommit_hooks = $params{precommit_hooks} or die "no package specified";
850   my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
851   my $total_setup = $params{setup} or die "no setup accumulator specified";
852   my $total_recur = $params{recur} or die "no recur accumulator specified";
853   my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
854   my $time = $params{'time'} or die "no time specified";
855   my (%options) = %{$params{options}};
856
857   if ( $part_pkg->freq ne '1' and ($options{'freq_override'} || 0) > 0 ) {
858     # this should never happen
859     die 'freq_override billing attempted on non-monthly package '.
860       $cust_pkg->pkgnum;
861   }
862
863   my $dbh = dbh;
864   my $real_pkgpart = $params{real_pkgpart};
865   my %hash = $cust_pkg->hash;
866   my $old_cust_pkg = new FS::cust_pkg \%hash;
867
868   my @details = ();
869   my $lineitems = 0;
870
871   $cust_pkg->pkgpart($part_pkg->pkgpart);
872
873   ###
874   # bill setup
875   ###
876
877   my $setup = 0;
878   my $unitsetup = 0;
879   my @setup_discounts = ();
880   my %setup_param = ( 'discounts' => \@setup_discounts );
881   if (     ! $options{recurring_only}
882        and ! $options{cancel}
883        and ( $options{'resetup'}
884              || ( ! $cust_pkg->setup
885                   && ( ! $cust_pkg->start_date
886                        || $cust_pkg->start_date <= day_end($time)
887                      )
888                   && ( ! $conf->exists('disable_setup_suspended_pkgs')
889                        || ( $conf->exists('disable_setup_suspended_pkgs') &&
890                             ! $cust_pkg->getfield('susp')
891                           )
892                      )
893                 )
894            )
895      )
896   {
897     
898     warn "    bill setup\n" if $DEBUG > 1;
899
900     unless ( $cust_pkg->waive_setup ) {
901         $lineitems++;
902
903         $setup = eval { $cust_pkg->calc_setup( $time, \@details, \%setup_param ) };
904         return "$@ running calc_setup for $cust_pkg\n"
905           if $@;
906
907         $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
908     }
909
910     $cust_pkg->setfield('setup', $time)
911       unless $cust_pkg->setup;
912           #do need it, but it won't get written to the db
913           #|| $cust_pkg->pkgpart != $real_pkgpart;
914
915     $cust_pkg->setfield('start_date', '')
916       if $cust_pkg->start_date;
917
918   }
919
920   ###
921   # bill recurring fee
922   ### 
923
924   #XXX unit stuff here too
925   my $recur = 0;
926   my $unitrecur = 0;
927   my @recur_discounts = ();
928   my $sdate;
929   if (     ! $cust_pkg->start_date
930        and ( ! $cust_pkg->susp || $part_pkg->option('suspend_bill', 1) )
931        and
932             ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= day_end($time) )
933          || ( $part_pkg->plan eq 'voip_cdr'
934                && $part_pkg->option('bill_every_call')
935             )
936          || $options{cancel}
937   ) {
938
939     # XXX should this be a package event?  probably.  events are called
940     # at collection time at the moment, though...
941     $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
942       if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'};
943       #don't want to reset usage just cause we want a line item??
944       #&& $part_pkg->pkgpart == $real_pkgpart;
945
946     warn "    bill recur\n" if $DEBUG > 1;
947     $lineitems++;
948
949     # XXX shared with $recur_prog
950     $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
951              || $cust_pkg->setup
952              || $time;
953
954     #over two params!  lets at least switch to a hashref for the rest...
955     my $increment_next_bill = ( $part_pkg->freq ne '0'
956                                 && ( $cust_pkg->getfield('bill') || 0 ) <= day_end($time)
957                                 && !$options{cancel}
958                               );
959     my %param = ( %setup_param,
960                   'precommit_hooks'     => $precommit_hooks,
961                   'increment_next_bill' => $increment_next_bill,
962                   'discounts'           => \@recur_discounts,
963                   'real_pkgpart'        => $real_pkgpart,
964                   'freq_override'       => $options{freq_override} || '',
965                   'setup_fee'           => 0,
966                 );
967
968     my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
969
970     # There may be some part_pkg for which this is wrong.  Only those
971     # which can_discount are supported.
972     # (the UI should prevent adding discounts to these at the moment)
973
974     warn "calling $method on cust_pkg ". $cust_pkg->pkgnum.
975          " for pkgpart ". $cust_pkg->pkgpart.
976          " with params ". join(' / ', map "$_=>$param{$_}", keys %param). "\n"
977       if $DEBUG > 2;
978            
979     $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
980     return "$@ running $method for $cust_pkg\n"
981       if ( $@ );
982
983     if ( $increment_next_bill ) {
984
985       my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
986       return "unparsable frequency: ". $part_pkg->freq
987         if $next_bill == -1;
988   
989       #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
990       # only for figuring next bill date, nothing else, so, reset $sdate again
991       # here
992       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
993       #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
994       $cust_pkg->last_bill($sdate);
995
996       $cust_pkg->setfield('bill', $next_bill );
997
998     }
999
1000     if ( $param{'setup_fee'} ) {
1001       # Add an additional setup fee at the billing stage.
1002       # Used for prorate_defer_bill.
1003       $setup += $param{'setup_fee'};
1004       $unitsetup += $param{'setup_fee'};
1005       $lineitems++;
1006     }
1007
1008     if ( defined $param{'discount_left_setup'} ) {
1009         foreach my $discount_setup ( values %{$param{'discount_left_setup'}} ) {
1010             $setup -= $discount_setup;
1011         }
1012     }
1013
1014   }
1015
1016   warn "\$setup is undefined" unless defined($setup);
1017   warn "\$recur is undefined" unless defined($recur);
1018   warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1019   
1020   ###
1021   # If there's line items, create em cust_bill_pkg records
1022   # If $cust_pkg has been modified, update it (if we're a real pkgpart)
1023   ###
1024
1025   if ( $lineitems ) {
1026
1027     if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
1028       # hmm.. and if just the options are modified in some weird price plan?
1029   
1030       warn "  package ". $cust_pkg->pkgnum. " modified; updating\n"
1031         if $DEBUG >1;
1032   
1033       my $error = $cust_pkg->replace( $old_cust_pkg,
1034                                       'depend_jobnum'=>$options{depend_jobnum},
1035                                       'options' => { $cust_pkg->options },
1036                                     )
1037         unless $options{no_commit};
1038       return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
1039         if $error; #just in case
1040     }
1041   
1042     $setup = sprintf( "%.2f", $setup );
1043     $recur = sprintf( "%.2f", $recur );
1044     if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1045       return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1046     }
1047     if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1048       return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1049     }
1050
1051     my $discount_show_always = $conf->exists('discount-show-always')
1052                                && (    ($setup == 0 && scalar(@setup_discounts))
1053                                     || ($recur == 0 && scalar(@recur_discounts))
1054                                   );
1055
1056     if (    $setup != 0
1057          || $recur != 0
1058          || (!$part_pkg->hidden && $options{has_hidden}) #include some $0 lines
1059          || $discount_show_always
1060          || ($setup == 0 && $cust_pkg->_X_show_zero('setup'))
1061          || ($recur == 0 && $cust_pkg->_X_show_zero('recur'))
1062        ) 
1063     {
1064
1065       warn "    charges (setup=$setup, recur=$recur); adding line items\n"
1066         if $DEBUG > 1;
1067
1068       my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
1069       if ( $DEBUG > 1 ) {
1070         warn "      adding customer package invoice detail: $_\n"
1071           foreach @cust_pkg_detail;
1072       }
1073       push @details, @cust_pkg_detail;
1074
1075       my $cust_bill_pkg = new FS::cust_bill_pkg {
1076         'pkgnum'    => $cust_pkg->pkgnum,
1077         'setup'     => $setup,
1078         'unitsetup' => $unitsetup,
1079         'recur'     => $recur,
1080         'unitrecur' => $unitrecur,
1081         'quantity'  => $cust_pkg->quantity,
1082         'details'   => \@details,
1083         'discounts' => [ @setup_discounts, @recur_discounts ],
1084         'hidden'    => $part_pkg->hidden,
1085         'freq'      => $part_pkg->freq,
1086       };
1087
1088       if ( $part_pkg->recur_temporality eq 'preceding' ) {
1089         $cust_bill_pkg->sdate( $hash{last_bill} );
1090         $cust_bill_pkg->edate( $sdate - 86399   ); #60s*60m*24h-1
1091         $cust_bill_pkg->edate( $time ) if $options{cancel};
1092       } else { #if ( $part_pkg->recur_temporality eq 'upcoming' ) {
1093         $cust_bill_pkg->sdate( $sdate );
1094         $cust_bill_pkg->edate( $cust_pkg->bill );
1095         #$cust_bill_pkg->edate( $time ) if $options{cancel};
1096       }
1097
1098       $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
1099         unless $part_pkg->pkgpart == $real_pkgpart;
1100
1101       $$total_setup += $setup;
1102       $$total_recur += $recur;
1103
1104       ###
1105       # handle taxes
1106       ###
1107
1108       unless ( $discount_show_always ) {
1109           my $error = 
1110             $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart, \%options);
1111           return $error if $error;
1112       }
1113
1114       push @$cust_bill_pkgs, $cust_bill_pkg;
1115
1116     } #if $setup != 0 || $recur != 0
1117       
1118   } #if $line_items
1119
1120   '';
1121
1122 }
1123
1124 sub _handle_taxes {
1125   my $self = shift;
1126   my $part_pkg = shift;
1127   my $taxlisthash = shift;
1128   my $cust_bill_pkg = shift;
1129   my $cust_pkg = shift;
1130   my $invoice_time = shift;
1131   my $real_pkgpart = shift;
1132   my $options = shift;
1133
1134   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1135
1136   my %cust_bill_pkg = ();
1137   my %taxes = ();
1138     
1139   my @classes;
1140   #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
1141   push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
1142   push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
1143   push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
1144
1145   if ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) {
1146
1147     if ( $conf->exists('enable_taxproducts')
1148          && ( scalar($part_pkg->part_pkg_taxoverride)
1149               || $part_pkg->has_taxproduct
1150             )
1151        )
1152     {
1153
1154       foreach my $class (@classes) {
1155         my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
1156         return $err_or_ref unless ref($err_or_ref);
1157         $taxes{$class} = $err_or_ref;
1158       }
1159
1160       unless (exists $taxes{''}) {
1161         my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
1162         return $err_or_ref unless ref($err_or_ref);
1163         $taxes{''} = $err_or_ref;
1164       }
1165
1166     } else {
1167
1168       my @loc_keys = qw( city county state country );
1169       my %taxhash;
1170       if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
1171         my $cust_location = $cust_pkg->cust_location;
1172         %taxhash = map { $_ => $cust_location->$_()    } @loc_keys;
1173       } else {
1174         my $prefix = 
1175           ( $conf->exists('tax-ship_address') && length($self->ship_last) )
1176           ? 'ship_'
1177           : '';
1178         %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys;
1179       }
1180
1181       $taxhash{'taxclass'} = $part_pkg->taxclass;
1182
1183       my @taxes = ();
1184       my %taxhash_elim = %taxhash;
1185       my @elim = qw( city county state );
1186       do { 
1187
1188         #first try a match with taxclass
1189         @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
1190
1191         if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
1192           #then try a match without taxclass
1193           my %no_taxclass = %taxhash_elim;
1194           $no_taxclass{ 'taxclass' } = '';
1195           @taxes = qsearch( 'cust_main_county', \%no_taxclass );
1196         }
1197
1198         $taxhash_elim{ shift(@elim) } = '';
1199
1200       } while ( !scalar(@taxes) && scalar(@elim) );
1201
1202       @taxes = grep { ! $_->taxname or ! $self->tax_exemption($_->taxname) }
1203                     @taxes
1204         if $self->cust_main_exemption; #just to be safe
1205
1206       if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
1207         foreach (@taxes) {
1208           $_->set('pkgnum',      $cust_pkg->pkgnum );
1209           $_->set('locationnum', $cust_pkg->locationnum );
1210         }
1211       }
1212
1213       $taxes{''} = [ @taxes ];
1214       $taxes{'setup'} = [ @taxes ];
1215       $taxes{'recur'} = [ @taxes ];
1216       $taxes{$_} = [ @taxes ] foreach (@classes);
1217
1218       # # maybe eliminate this entirely, along with all the 0% records
1219       # unless ( @taxes ) {
1220       #   return
1221       #     "fatal: can't find tax rate for state/county/country/taxclass ".
1222       #     join('/', map $taxhash{$_}, qw(state county country taxclass) );
1223       # }
1224
1225     } #if $conf->exists('enable_taxproducts') ...
1226
1227   }
1228
1229   #what's this doing in the middle of _handle_taxes?  probably should split
1230   #this into three parts above in _make_lines
1231   $cust_bill_pkg->set_display(   part_pkg     => $part_pkg,
1232                                  real_pkgpart => $real_pkgpart,
1233                              );
1234
1235   my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
1236   foreach my $key (keys %tax_cust_bill_pkg) {
1237     my @taxes = @{ $taxes{$key} || [] };
1238     my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
1239
1240     my %localtaxlisthash = ();
1241     foreach my $tax ( @taxes ) {
1242
1243       my $taxname = ref( $tax ). ' '. $tax->taxnum;
1244 #      $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
1245 #                  ' locationnum'. $cust_pkg->locationnum
1246 #        if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
1247
1248       $taxlisthash->{ $taxname } ||= [ $tax ];
1249       push @{ $taxlisthash->{ $taxname  } }, $tax_cust_bill_pkg;
1250
1251       $localtaxlisthash{ $taxname } ||= [ $tax ];
1252       push @{ $localtaxlisthash{ $taxname  } }, $tax_cust_bill_pkg;
1253
1254     }
1255
1256     warn "finding taxed taxes...\n" if $DEBUG > 2;
1257     foreach my $tax ( keys %localtaxlisthash ) {
1258       my $tax_object = shift @{ $localtaxlisthash{$tax} };
1259       warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
1260         if $DEBUG > 2;
1261       next unless $tax_object->can('tax_on_tax');
1262
1263       foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
1264         my $totname = ref( $tot ). ' '. $tot->taxnum;
1265
1266         warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
1267           if $DEBUG > 2;
1268         next unless exists( $localtaxlisthash{ $totname } ); # only increase
1269                                                              # existing taxes
1270         warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
1271         my $hashref_or_error = 
1272           $tax_object->taxline( $localtaxlisthash{$tax},
1273                                 'custnum'      => $self->custnum,
1274                                 'invoice_time' => $invoice_time,
1275                               );
1276         return $hashref_or_error
1277           unless ref($hashref_or_error);
1278         
1279         $taxlisthash->{ $totname } ||= [ $tot ];
1280         push @{ $taxlisthash->{ $totname  } }, $hashref_or_error->{amount};
1281
1282       }
1283     }
1284
1285   }
1286
1287   '';
1288 }
1289
1290 sub _gather_taxes {
1291   my $self = shift;
1292   my $part_pkg = shift;
1293   my $class = shift;
1294   my $cust_pkg = shift;
1295
1296   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1297
1298   my $geocode;
1299   if ( $cust_pkg->locationnum && $conf->exists('tax-pkg_address') ) {
1300     $geocode = $cust_pkg->cust_location->geocode('cch');
1301   } else {
1302     $geocode = $self->geocode('cch');
1303   }
1304
1305   my @taxes = ();
1306
1307   my @taxclassnums = map { $_->taxclassnum }
1308                      $part_pkg->part_pkg_taxoverride($class);
1309
1310   unless (@taxclassnums) {
1311     @taxclassnums = map { $_->taxclassnum }
1312                     grep { $_->taxable eq 'Y' }
1313                     $part_pkg->part_pkg_taxrate('cch', $geocode, $class);
1314   }
1315   warn "Found taxclassnum values of ". join(',', @taxclassnums)
1316     if $DEBUG;
1317
1318   my $extra_sql =
1319     "AND (".
1320     join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
1321
1322   @taxes = qsearch({ 'table' => 'tax_rate',
1323                      'hashref' => { 'geocode' => $geocode, },
1324                      'extra_sql' => $extra_sql,
1325                   })
1326     if scalar(@taxclassnums);
1327
1328   warn "Found taxes ".
1329        join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n" 
1330    if $DEBUG;
1331
1332   [ @taxes ];
1333
1334 }
1335
1336 =item collect [ HASHREF | OPTION => VALUE ... ]
1337
1338 (Attempt to) collect money for this customer's outstanding invoices (see
1339 L<FS::cust_bill>).  Usually used after the bill method.
1340
1341 Actions are now triggered by billing events; see L<FS::part_event> and the
1342 billing events web interface.  Old-style invoice events (see
1343 L<FS::part_bill_event>) have been deprecated.
1344
1345 If there is an error, returns the error, otherwise returns false.
1346
1347 Options are passed as name-value pairs.
1348
1349 Currently available options are:
1350
1351 =over 4
1352
1353 =item invoice_time
1354
1355 Use this time when deciding when to print invoices and late notices on those invoices.  The default is now.  It is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion functions.
1356
1357 =item retry
1358
1359 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1360
1361 =item check_freq
1362
1363 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1364
1365 =item quiet
1366
1367 set true to surpress email card/ACH decline notices.
1368
1369 =item debug
1370
1371 Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
1372
1373 =back
1374
1375 # =item payby
1376 #
1377 # allows for one time override of normal customer billing method
1378
1379 =cut
1380
1381 sub collect {
1382   my( $self, %options ) = @_;
1383
1384   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1385
1386   my $invoice_time = $options{'invoice_time'} || time;
1387
1388   #put below somehow?
1389   local $SIG{HUP} = 'IGNORE';
1390   local $SIG{INT} = 'IGNORE';
1391   local $SIG{QUIT} = 'IGNORE';
1392   local $SIG{TERM} = 'IGNORE';
1393   local $SIG{TSTP} = 'IGNORE';
1394   local $SIG{PIPE} = 'IGNORE';
1395
1396   my $oldAutoCommit = $FS::UID::AutoCommit;
1397   local $FS::UID::AutoCommit = 0;
1398   my $dbh = dbh;
1399
1400   $self->select_for_update; #mutex
1401
1402   if ( $DEBUG ) {
1403     my $balance = $self->balance;
1404     warn "$me collect customer ". $self->custnum. ": balance $balance\n"
1405   }
1406
1407   if ( exists($options{'retry_card'}) ) {
1408     carp 'retry_card option passed to collect is deprecated; use retry';
1409     $options{'retry'} ||= $options{'retry_card'};
1410   }
1411   if ( exists($options{'retry'}) && $options{'retry'} ) {
1412     my $error = $self->retry_realtime;
1413     if ( $error ) {
1414       $dbh->rollback if $oldAutoCommit;
1415       return $error;
1416     }
1417   }
1418
1419   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1420
1421   #never want to roll back an event just because it returned an error
1422   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1423
1424   $self->do_cust_event(
1425     'debug'      => ( $options{'debug'} || 0 ),
1426     'time'       => $invoice_time,
1427     'check_freq' => $options{'check_freq'},
1428     'stage'      => 'collect',
1429   );
1430
1431 }
1432
1433 =item retry_realtime
1434
1435 Schedules realtime / batch  credit card / electronic check / LEC billing
1436 events for for retry.  Useful if card information has changed or manual
1437 retry is desired.  The 'collect' method must be called to actually retry
1438 the transaction.
1439
1440 Implementation details: For either this customer, or for each of this
1441 customer's open invoices, changes the status of the first "done" (with
1442 statustext error) realtime processing event to "failed".
1443
1444 =cut
1445
1446 sub retry_realtime {
1447   my $self = shift;
1448
1449   local $SIG{HUP} = 'IGNORE';
1450   local $SIG{INT} = 'IGNORE';
1451   local $SIG{QUIT} = 'IGNORE';
1452   local $SIG{TERM} = 'IGNORE';
1453   local $SIG{TSTP} = 'IGNORE';
1454   local $SIG{PIPE} = 'IGNORE';
1455
1456   my $oldAutoCommit = $FS::UID::AutoCommit;
1457   local $FS::UID::AutoCommit = 0;
1458   my $dbh = dbh;
1459
1460   #a little false laziness w/due_cust_event (not too bad, really)
1461
1462   my $join = FS::part_event_condition->join_conditions_sql;
1463   my $order = FS::part_event_condition->order_conditions_sql;
1464   my $mine = 
1465   '( '
1466    . join ( ' OR ' , map { 
1467     "( part_event.eventtable = " . dbh->quote($_) 
1468     . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key . " from $_ where custnum = " . dbh->quote( $self->custnum ) . "))" ;
1469    } FS::part_event->eventtables)
1470    . ') ';
1471
1472   #here is the agent virtualization
1473   my $agent_virt = " (    part_event.agentnum IS NULL
1474                        OR part_event.agentnum = ". $self->agentnum. ' )';
1475
1476   #XXX this shouldn't be hardcoded, actions should declare it...
1477   my @realtime_events = qw(
1478     cust_bill_realtime_card
1479     cust_bill_realtime_check
1480     cust_bill_realtime_lec
1481     cust_bill_batch
1482   );
1483
1484   my $is_realtime_event = ' ( '. join(' OR ', map "part_event.action = '$_'",
1485                                                   @realtime_events
1486                                      ).
1487                           ' ) ';
1488
1489   my @cust_event = qsearchs({
1490     'table'     => 'cust_event',
1491     'select'    => 'cust_event.*',
1492     'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
1493     'hashref'   => { 'status' => 'done' },
1494     'extra_sql' => " AND statustext IS NOT NULL AND statustext != '' ".
1495                    " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
1496   });
1497
1498   my %seen_invnum = ();
1499   foreach my $cust_event (@cust_event) {
1500
1501     #max one for the customer, one for each open invoice
1502     my $cust_X = $cust_event->cust_X;
1503     next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
1504                           ? $cust_X->invnum
1505                           : 0
1506                         }++
1507          or $cust_event->part_event->eventtable eq 'cust_bill'
1508             && ! $cust_X->owed;
1509
1510     my $error = $cust_event->retry;
1511     if ( $error ) {
1512       $dbh->rollback if $oldAutoCommit;
1513       return "error scheduling event for retry: $error";
1514     }
1515
1516   }
1517
1518   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1519   '';
1520
1521 }
1522
1523 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
1524
1525 Runs billing events; see L<FS::part_event> and the billing events web
1526 interface.
1527
1528 If there is an error, returns the error, otherwise returns false.
1529
1530 Options are passed as name-value pairs.
1531
1532 Currently available options are:
1533
1534 =over 4
1535
1536 =item time
1537
1538 Use this time when deciding when to print invoices and late notices on those invoices.  The default is now.  It is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion functions.
1539
1540 =item check_freq
1541
1542 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1543
1544 =item stage
1545
1546 "collect" (the default) or "pre-bill"
1547
1548 =item quiet
1549  
1550 set true to surpress email card/ACH decline notices.
1551
1552 =item debug
1553
1554 Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
1555
1556 =back
1557 =cut
1558
1559 # =item payby
1560 #
1561 # allows for one time override of normal customer billing method
1562
1563 # =item retry
1564 #
1565 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1566
1567 sub do_cust_event {
1568   my( $self, %options ) = @_;
1569
1570   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1571
1572   my $time = $options{'time'} || time;
1573
1574   #put below somehow?
1575   local $SIG{HUP} = 'IGNORE';
1576   local $SIG{INT} = 'IGNORE';
1577   local $SIG{QUIT} = 'IGNORE';
1578   local $SIG{TERM} = 'IGNORE';
1579   local $SIG{TSTP} = 'IGNORE';
1580   local $SIG{PIPE} = 'IGNORE';
1581
1582   my $oldAutoCommit = $FS::UID::AutoCommit;
1583   local $FS::UID::AutoCommit = 0;
1584   my $dbh = dbh;
1585
1586   $self->select_for_update; #mutex
1587
1588   if ( $DEBUG ) {
1589     my $balance = $self->balance;
1590     warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
1591   }
1592
1593 #  if ( exists($options{'retry_card'}) ) {
1594 #    carp 'retry_card option passed to collect is deprecated; use retry';
1595 #    $options{'retry'} ||= $options{'retry_card'};
1596 #  }
1597 #  if ( exists($options{'retry'}) && $options{'retry'} ) {
1598 #    my $error = $self->retry_realtime;
1599 #    if ( $error ) {
1600 #      $dbh->rollback if $oldAutoCommit;
1601 #      return $error;
1602 #    }
1603 #  }
1604
1605   # false laziness w/pay_batch::import_results
1606
1607   my $due_cust_event = $self->due_cust_event(
1608     'debug'      => ( $options{'debug'} || 0 ),
1609     'time'       => $time,
1610     'check_freq' => $options{'check_freq'},
1611     'stage'      => ( $options{'stage'} || 'collect' ),
1612   );
1613   unless( ref($due_cust_event) ) {
1614     $dbh->rollback if $oldAutoCommit;
1615     return $due_cust_event;
1616   }
1617
1618   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1619   #never want to roll back an event just because it or a different one
1620   # returned an error
1621   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1622
1623   foreach my $cust_event ( @$due_cust_event ) {
1624
1625     #XXX lock event
1626     
1627     #re-eval event conditions (a previous event could have changed things)
1628     unless ( $cust_event->test_conditions( 'time' => $time ) ) {
1629       #don't leave stray "new/locked" records around
1630       my $error = $cust_event->delete;
1631       return $error if $error;
1632       next;
1633     }
1634
1635     {
1636       local $FS::cust_main::Billing_Realtime::realtime_bop_decline_quiet = 1
1637         if $options{'quiet'};
1638       warn "  running cust_event ". $cust_event->eventnum. "\n"
1639         if $DEBUG > 1;
1640
1641       #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
1642       if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
1643         #XXX wtf is this?  figure out a proper dealio with return value
1644         #from do_event
1645         return $error;
1646       }
1647     }
1648
1649   }
1650
1651   '';
1652
1653 }
1654
1655 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
1656
1657 Inserts database records for and returns an ordered listref of new events due
1658 for this customer, as FS::cust_event objects (see L<FS::cust_event>).  If no
1659 events are due, an empty listref is returned.  If there is an error, returns a
1660 scalar error message.
1661
1662 To actually run the events, call each event's test_condition method, and if
1663 still true, call the event's do_event method.
1664
1665 Options are passed as a hashref or as a list of name-value pairs.  Available
1666 options are:
1667
1668 =over 4
1669
1670 =item check_freq
1671
1672 Search only for events of this check frequency (how often events of this type are checked); currently "1d" (daily, the default) and "1m" (monthly) are recognized.
1673
1674 =item stage
1675
1676 "collect" (the default) or "pre-bill"
1677
1678 =item time
1679
1680 "Current time" for the events.
1681
1682 =item debug
1683
1684 Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
1685
1686 =item eventtable
1687
1688 Only return events for the specified eventtable (by default, events of all eventtables are returned)
1689
1690 =item objects
1691
1692 Explicitly pass the objects to be tested (typically used with eventtable).
1693
1694 =item testonly
1695
1696 Set to true to return the objects, but not actually insert them into the
1697 database.
1698
1699 =back
1700
1701 =cut
1702
1703 sub due_cust_event {
1704   my $self = shift;
1705   my %opt = ref($_[0]) ? %{ $_[0] } : @_;
1706
1707   #???
1708   #my $DEBUG = $opt{'debug'}
1709   local($DEBUG) = $opt{'debug'}
1710     if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG;
1711   $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1712
1713   warn "$me due_cust_event called with options ".
1714        join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
1715     if $DEBUG;
1716
1717   $opt{'time'} ||= time;
1718
1719   local $SIG{HUP} = 'IGNORE';
1720   local $SIG{INT} = 'IGNORE';
1721   local $SIG{QUIT} = 'IGNORE';
1722   local $SIG{TERM} = 'IGNORE';
1723   local $SIG{TSTP} = 'IGNORE';
1724   local $SIG{PIPE} = 'IGNORE';
1725
1726   my $oldAutoCommit = $FS::UID::AutoCommit;
1727   local $FS::UID::AutoCommit = 0;
1728   my $dbh = dbh;
1729
1730   $self->select_for_update #mutex
1731     unless $opt{testonly};
1732
1733   ###
1734   # find possible events (initial search)
1735   ###
1736   
1737   my @cust_event = ();
1738
1739   my @eventtable = $opt{'eventtable'}
1740                      ? ( $opt{'eventtable'} )
1741                      : FS::part_event->eventtables_runorder;
1742
1743   my $check_freq = $opt{'check_freq'} || '1d';
1744
1745   foreach my $eventtable ( @eventtable ) {
1746
1747     my @objects;
1748     if ( $opt{'objects'} ) {
1749
1750       @objects = @{ $opt{'objects'} };
1751
1752     } else {
1753
1754       #my @objects = $self->$eventtable(); # sub cust_main { @{ [ $self ] }; }
1755       if ( $eventtable eq 'cust_main' ) {
1756         @objects = ( $self );
1757       } else {
1758
1759         my $cm_join =
1760           "LEFT JOIN cust_main USING ( custnum )";
1761
1762         #some false laziness w/Cron::bill bill_where
1763
1764         my $join  = FS::part_event_condition->join_conditions_sql( $eventtable);
1765         my $where = FS::part_event_condition->where_conditions_sql($eventtable,
1766                                                            'time'=>$opt{'time'},
1767                                                                   );
1768         $where = $where ? "AND $where" : '';
1769
1770         my $are_part_event = 
1771           "EXISTS ( SELECT 1 FROM part_event $join
1772                       WHERE check_freq = '$check_freq'
1773                         AND eventtable = '$eventtable'
1774                         AND ( disabled = '' OR disabled IS NULL )
1775                         $where
1776                   )
1777           ";
1778         #eofalse
1779
1780         @objects = $self->$eventtable(
1781                      'addl_from' => $cm_join,
1782                      'extra_sql' => " AND $are_part_event",
1783                    );
1784       }
1785
1786     }
1787
1788     my @e_cust_event = ();
1789
1790     my $cross = "CROSS JOIN $eventtable";
1791     $cross .= ' LEFT JOIN cust_main USING ( custnum )'
1792       unless $eventtable eq 'cust_main';
1793
1794     foreach my $object ( @objects ) {
1795
1796       #this first search uses the condition_sql magic for optimization.
1797       #the more possible events we can eliminate in this step the better
1798
1799       my $cross_where = '';
1800       my $pkey = $object->primary_key;
1801       $cross_where = "$eventtable.$pkey = ". $object->$pkey();
1802
1803       my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
1804       my $extra_sql =
1805         FS::part_event_condition->where_conditions_sql( $eventtable,
1806                                                         'time'=>$opt{'time'}
1807                                                       );
1808       my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
1809
1810       $extra_sql = "AND $extra_sql" if $extra_sql;
1811
1812       #here is the agent virtualization
1813       $extra_sql .= " AND (    part_event.agentnum IS NULL
1814                             OR part_event.agentnum = ". $self->agentnum. ' )';
1815
1816       $extra_sql .= " $order";
1817
1818       warn "searching for events for $eventtable ". $object->$pkey. "\n"
1819         if $opt{'debug'} > 2;
1820       my @part_event = qsearch( {
1821         'debug'     => ( $opt{'debug'} > 3 ? 1 : 0 ),
1822         'select'    => 'part_event.*',
1823         'table'     => 'part_event',
1824         'addl_from' => "$cross $join",
1825         'hashref'   => { 'check_freq' => $check_freq,
1826                          'eventtable' => $eventtable,
1827                          'disabled'   => '',
1828                        },
1829         'extra_sql' => "AND $cross_where $extra_sql",
1830       } );
1831
1832       if ( $DEBUG > 2 ) {
1833         my $pkey = $object->primary_key;
1834         warn "      ". scalar(@part_event).
1835              " possible events found for $eventtable ". $object->$pkey(). "\n";
1836       }
1837
1838       push @e_cust_event, map { $_->new_cust_event($object) } @part_event;
1839
1840     }
1841
1842     warn "    ". scalar(@e_cust_event).
1843          " subtotal possible cust events found for $eventtable\n"
1844       if $DEBUG > 1;
1845
1846     push @cust_event, @e_cust_event;
1847
1848   }
1849
1850   warn "  ". scalar(@cust_event).
1851        " total possible cust events found in initial search\n"
1852     if $DEBUG; # > 1;
1853
1854
1855   ##
1856   # test stage
1857   ##
1858
1859   $opt{stage} ||= 'collect';
1860   @cust_event =
1861     grep { my $stage = $_->part_event->event_stage;
1862            $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
1863          }
1864          @cust_event;
1865
1866   ##
1867   # test conditions
1868   ##
1869   
1870   my %unsat = ();
1871
1872   @cust_event = grep $_->test_conditions( 'time'          => $opt{'time'},
1873                                           'stats_hashref' => \%unsat ),
1874                      @cust_event;
1875
1876   warn "  ". scalar(@cust_event). " cust events left satisfying conditions\n"
1877     if $DEBUG; # > 1;
1878
1879   warn "    invalid conditions not eliminated with condition_sql:\n".
1880        join('', map "      $_: ".$unsat{$_}."\n", keys %unsat )
1881     if keys %unsat && $DEBUG; # > 1;
1882
1883   ##
1884   # insert
1885   ##
1886
1887   unless( $opt{testonly} ) {
1888     foreach my $cust_event ( @cust_event ) {
1889
1890       my $error = $cust_event->insert();
1891       if ( $error ) {
1892         $dbh->rollback if $oldAutoCommit;
1893         return $error;
1894       }
1895                                        
1896     }
1897   }
1898
1899   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1900
1901   ##
1902   # return
1903   ##
1904
1905   warn "  returning events: ". Dumper(@cust_event). "\n"
1906     if $DEBUG > 2;
1907
1908   \@cust_event;
1909
1910 }
1911
1912 =item apply_payments_and_credits [ OPTION => VALUE ... ]
1913
1914 Applies unapplied payments and credits.
1915
1916 In most cases, this new method should be used in place of sequential
1917 apply_payments and apply_credits methods.
1918
1919 A hash of optional arguments may be passed.  Currently "manual" is supported.
1920 If true, a payment receipt is sent instead of a statement when
1921 'payment_receipt_email' configuration option is set.
1922
1923 If there is an error, returns the error, otherwise returns false.
1924
1925 =cut
1926
1927 sub apply_payments_and_credits {
1928   my( $self, %options ) = @_;
1929
1930   local $SIG{HUP} = 'IGNORE';
1931   local $SIG{INT} = 'IGNORE';
1932   local $SIG{QUIT} = 'IGNORE';
1933   local $SIG{TERM} = 'IGNORE';
1934   local $SIG{TSTP} = 'IGNORE';
1935   local $SIG{PIPE} = 'IGNORE';
1936
1937   my $oldAutoCommit = $FS::UID::AutoCommit;
1938   local $FS::UID::AutoCommit = 0;
1939   my $dbh = dbh;
1940
1941   $self->select_for_update; #mutex
1942
1943   foreach my $cust_bill ( $self->open_cust_bill ) {
1944     my $error = $cust_bill->apply_payments_and_credits(%options);
1945     if ( $error ) {
1946       $dbh->rollback if $oldAutoCommit;
1947       return "Error applying: $error";
1948     }
1949   }
1950
1951   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1952   ''; #no error
1953
1954 }
1955
1956 =item apply_credits OPTION => VALUE ...
1957
1958 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1959 to outstanding invoice balances in chronological order (or reverse
1960 chronological order if the I<order> option is set to B<newest>) and returns the
1961 value of any remaining unapplied credits available for refund (see
1962 L<FS::cust_refund>).
1963
1964 Dies if there is an error.
1965
1966 =cut
1967
1968 sub apply_credits {
1969   my $self = shift;
1970   my %opt = @_;
1971
1972   local $SIG{HUP} = 'IGNORE';
1973   local $SIG{INT} = 'IGNORE';
1974   local $SIG{QUIT} = 'IGNORE';
1975   local $SIG{TERM} = 'IGNORE';
1976   local $SIG{TSTP} = 'IGNORE';
1977   local $SIG{PIPE} = 'IGNORE';
1978
1979   my $oldAutoCommit = $FS::UID::AutoCommit;
1980   local $FS::UID::AutoCommit = 0;
1981   my $dbh = dbh;
1982
1983   $self->select_for_update; #mutex
1984
1985   unless ( $self->total_unapplied_credits ) {
1986     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1987     return 0;
1988   }
1989
1990   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1991       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1992
1993   my @invoices = $self->open_cust_bill;
1994   @invoices = sort { $b->_date <=> $a->_date } @invoices
1995     if defined($opt{'order'}) && $opt{'order'} eq 'newest';
1996
1997   if ( $conf->exists('pkg-balances') ) {
1998     # limit @credits to those w/ a pkgnum grepped from $self
1999     my %pkgnums = ();
2000     foreach my $i (@invoices) {
2001       foreach my $li ( $i->cust_bill_pkg ) {
2002         $pkgnums{$li->pkgnum} = 1;
2003       }
2004     }
2005     @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
2006   }
2007
2008   my $credit;
2009
2010   foreach my $cust_bill ( @invoices ) {
2011
2012     if ( !defined($credit) || $credit->credited == 0) {
2013       $credit = pop @credits or last;
2014     }
2015
2016     my $owed;
2017     if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
2018       $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
2019     } else {
2020       $owed = $cust_bill->owed;
2021     }
2022     unless ( $owed > 0 ) {
2023       push @credits, $credit;
2024       next;
2025     }
2026
2027     my $amount = min( $credit->credited, $owed );
2028     
2029     my $cust_credit_bill = new FS::cust_credit_bill ( {
2030       'crednum' => $credit->crednum,
2031       'invnum'  => $cust_bill->invnum,
2032       'amount'  => $amount,
2033     } );
2034     $cust_credit_bill->pkgnum( $credit->pkgnum )
2035       if $conf->exists('pkg-balances') && $credit->pkgnum;
2036     my $error = $cust_credit_bill->insert;
2037     if ( $error ) {
2038       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2039       die $error;
2040     }
2041     
2042     redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2043
2044   }
2045
2046   my $total_unapplied_credits = $self->total_unapplied_credits;
2047
2048   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2049
2050   return $total_unapplied_credits;
2051 }
2052
2053 =item apply_payments  [ OPTION => VALUE ... ]
2054
2055 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2056 to outstanding invoice balances in chronological order.
2057
2058  #and returns the value of any remaining unapplied payments.
2059
2060 A hash of optional arguments may be passed.  Currently "manual" is supported.
2061 If true, a payment receipt is sent instead of a statement when
2062 'payment_receipt_email' configuration option is set.
2063
2064 Dies if there is an error.
2065
2066 =cut
2067
2068 sub apply_payments {
2069   my( $self, %options ) = @_;
2070
2071   local $SIG{HUP} = 'IGNORE';
2072   local $SIG{INT} = 'IGNORE';
2073   local $SIG{QUIT} = 'IGNORE';
2074   local $SIG{TERM} = 'IGNORE';
2075   local $SIG{TSTP} = 'IGNORE';
2076   local $SIG{PIPE} = 'IGNORE';
2077
2078   my $oldAutoCommit = $FS::UID::AutoCommit;
2079   local $FS::UID::AutoCommit = 0;
2080   my $dbh = dbh;
2081
2082   $self->select_for_update; #mutex
2083
2084   #return 0 unless
2085
2086   my @payments = sort { $b->_date <=> $a->_date }
2087                  grep { $_->unapplied > 0 }
2088                  $self->cust_pay;
2089
2090   my @invoices = sort { $a->_date <=> $b->_date}
2091                  grep { $_->owed > 0 }
2092                  $self->cust_bill;
2093
2094   if ( $conf->exists('pkg-balances') ) {
2095     # limit @payments to those w/ a pkgnum grepped from $self
2096     my %pkgnums = ();
2097     foreach my $i (@invoices) {
2098       foreach my $li ( $i->cust_bill_pkg ) {
2099         $pkgnums{$li->pkgnum} = 1;
2100       }
2101     }
2102     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
2103   }
2104
2105   my $payment;
2106
2107   foreach my $cust_bill ( @invoices ) {
2108
2109     if ( !defined($payment) || $payment->unapplied == 0 ) {
2110       $payment = pop @payments or last;
2111     }
2112
2113     my $owed;
2114     if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
2115       $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
2116     } else {
2117       $owed = $cust_bill->owed;
2118     }
2119     unless ( $owed > 0 ) {
2120       push @payments, $payment;
2121       next;
2122     }
2123
2124     my $amount = min( $payment->unapplied, $owed );
2125
2126     my $cbp = {
2127       'paynum' => $payment->paynum,
2128       'invnum' => $cust_bill->invnum,
2129       'amount' => $amount,
2130     };
2131     $cbp->{_date} = $payment->_date 
2132         if $options{'manual'} && $options{'backdate_application'};
2133     my $cust_bill_pay = new FS::cust_bill_pay($cbp);
2134     $cust_bill_pay->pkgnum( $payment->pkgnum )
2135       if $conf->exists('pkg-balances') && $payment->pkgnum;
2136     my $error = $cust_bill_pay->insert(%options);
2137     if ( $error ) {
2138       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2139       die $error;
2140     }
2141
2142     redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2143
2144   }
2145
2146   my $total_unapplied_payments = $self->total_unapplied_payments;
2147
2148   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2149
2150   return $total_unapplied_payments;
2151 }
2152
2153 =back
2154
2155 =head1 FLOW
2156
2157   bill_and_collect
2158
2159     cancel_expired_pkgs
2160     suspend_adjourned_pkgs
2161
2162     bill
2163       (do_cust_event pre-bill)
2164       _make_lines
2165         _handle_taxes
2166           (vendor-only) _gather_taxes
2167       _omit_zero_value_bundles
2168       calculate_taxes
2169
2170     apply_payments_and_credits
2171     collect
2172       do_cust_event
2173         due_cust_event
2174
2175 =head1 BUGS
2176
2177 =head1 SEE ALSO
2178
2179 L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>
2180
2181 =cut
2182
2183 1;