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