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