1 package FS::cust_main::Billing;
4 use vars qw( $conf $DEBUG $me );
7 use List::Util qw( min );
9 use FS::Record qw( qsearch qsearchs dbdef );
10 use FS::Misc::DateTime qw( day_end );
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;
18 use FS::tax_rate_location;
19 use FS::cust_bill_pkg_tax_location;
20 use FS::cust_bill_pkg_tax_rate_location;
22 use FS::part_event_condition;
26 # 1 is mostly method/subroutine entry and options
27 # 2 traces progress of some operations
28 # 3 is even more information including possibly sensitive data
30 $me = '[FS::cust_main::Billing]';
32 install_callback FS::UID sub {
34 #yes, need it for stuff below (prolly should be cached)
39 FS::cust_main::Billing - Billing mixin for cust_main
45 These methods are available on FS::cust_main objects.
51 =item bill_and_collect
53 Cancels and suspends any packages due, generates bills, applies payments and
54 credits, and applies collection events to run cards, send bills and notices,
57 By default, warns on errors and continues with the next operation (but see the
60 Options are passed as name-value pairs. Currently available options are:
66 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:
70 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
74 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.
78 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
82 If set true, re-charges setup fees.
86 If set any errors prevent subsequent operations from continusing. If set
87 specifically to "return", returns the error (or false, if there is no error).
88 Any other true value causes errors to die.
92 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)
96 Optional FS::queue entry to receive status updates.
100 Options are passed to the B<bill> and B<collect> methods verbatim, so all
101 options of those methods are also available.
105 sub bill_and_collect {
106 my( $self, %options ) = @_;
108 my $log = FS::Log->new('bill_and_collect');
109 $log->debug('start', object => $self, agentnum => $self->agentnum);
113 #$options{actual_time} not $options{time} because freeside-daily -d is for
114 #pre-printing invoices
116 $options{'actual_time'} ||= time;
117 my $job = $options{'job'};
119 my $actual_time = ( $conf->exists('next-bill-ignore-time')
120 ? day_end( $options{actual_time} )
121 : $options{actual_time}
124 $job->update_statustext('0,cleaning expired packages') if $job;
125 $error = $self->cancel_expired_pkgs( $actual_time );
127 $error = "Error expiring custnum ". $self->custnum. ": $error";
128 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
129 elsif ( $options{fatal} ) { die $error; }
130 else { warn $error; }
133 $error = $self->suspend_adjourned_pkgs( $actual_time );
135 $error = "Error adjourning custnum ". $self->custnum. ": $error";
136 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
137 elsif ( $options{fatal} ) { die $error; }
138 else { warn $error; }
141 $error = $self->unsuspend_resumed_pkgs( $actual_time );
143 $error = "Error resuming custnum ".$self->custnum. ": $error";
144 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
145 elsif ( $options{fatal} ) { die $error; }
146 else { warn $error; }
149 $job->update_statustext('20,billing packages') if $job;
150 $error = $self->bill( %options );
152 $error = "Error billing custnum ". $self->custnum. ": $error";
153 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
154 elsif ( $options{fatal} ) { die $error; }
155 else { warn $error; }
158 $job->update_statustext('50,applying payments and credits') if $job;
159 $error = $self->apply_payments_and_credits;
161 $error = "Error applying custnum ". $self->custnum. ": $error";
162 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
163 elsif ( $options{fatal} ) { die $error; }
164 else { warn $error; }
167 $job->update_statustext('70,running collection events') if $job;
168 unless ( $conf->exists('cancelled_cust-noevents')
169 && ! $self->num_ncancelled_pkgs
171 $error = $self->collect( %options );
173 $error = "Error collecting custnum ". $self->custnum. ": $error";
174 if ($options{fatal} && $options{fatal} eq 'return') { return $error; }
175 elsif ($options{fatal} ) { die $error; }
176 else { warn $error; }
179 $job->update_statustext('100,finished') if $job;
180 $log->debug('finish', object => $self, agentnum => $self->agentnum);
186 sub cancel_expired_pkgs {
187 my ( $self, $time, %options ) = @_;
189 my @cancel_pkgs = $self->ncancelled_pkgs( {
190 'extra_sql' => " AND expire IS NOT NULL AND expire > 0 AND expire <= $time "
195 CUST_PKG: foreach my $cust_pkg ( @cancel_pkgs ) {
196 my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
199 if ( $cust_pkg->change_to_pkgnum ) {
201 my $new_pkg = FS::cust_pkg->by_key($cust_pkg->change_to_pkgnum);
203 push @errors, 'can\'t change pkgnum '.$cust_pkg->pkgnum.' to pkgnum '.
204 $cust_pkg->change_to_pkgnum.'; not expiring';
207 $error = $cust_pkg->change( 'cust_pkg' => $new_pkg,
208 'unprotect_svcs' => 1 );
209 $error = '' if ref $error eq 'FS::cust_pkg';
211 } else { # just cancel it
212 $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum,
213 'reason_otaker' => $cpr->otaker,
219 push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
222 join(' / ', @errors);
226 sub suspend_adjourned_pkgs {
227 my ( $self, $time, %options ) = @_;
229 my @susp_pkgs = $self->ncancelled_pkgs( {
231 " AND ( susp IS NULL OR susp = 0 )
232 AND ( ( bill IS NOT NULL AND bill != 0 AND bill < $time )
233 OR ( adjourn IS NOT NULL AND adjourn != 0 AND adjourn <= $time )
238 #only because there's no SQL test for is_prepaid :/
240 grep { ( $_->part_pkg->is_prepaid
245 && $_->adjourn <= $time
253 foreach my $cust_pkg ( @susp_pkgs ) {
254 my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
255 if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
256 my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
257 'reason_otaker' => $cpr->otaker
261 push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
264 join(' / ', @errors);
268 sub unsuspend_resumed_pkgs {
269 my ( $self, $time, %options ) = @_;
271 my @unsusp_pkgs = $self->ncancelled_pkgs( {
272 'extra_sql' => " AND resume IS NOT NULL AND resume > 0 AND resume <= $time "
277 foreach my $cust_pkg ( @unsusp_pkgs ) {
278 my $error = $cust_pkg->unsuspend( 'time' => $time );
279 push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
282 join(' / ', @errors);
288 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
289 conjunction with the collect method by calling B<bill_and_collect>.
291 If there is an error, returns the error, otherwise returns false.
293 Options are passed as name-value pairs. Currently available options are:
299 If set true, re-charges setup fees.
303 If set true then only bill recurring charges, not setup, usage, one time
308 If set, then override the normal frequency and look for a part_pkg_discount
309 to take at that frequency. This is appropriate only when the normal
310 frequency for all packages is monthly, and is an error otherwise. Use
311 C<pkg_list> to limit the set of packages included in billing.
315 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:
319 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
323 An array ref of specific packages (objects) to attempt billing, instead trying all of them.
325 $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
329 A hashref of pkgparts to exclude from this billing run (can also be specified as a comma-separated scalar).
333 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.
337 This boolean value informs the us that the package is being cancelled. This
338 typically might mean not charging the normal recurring fee but only usage
339 fees since the last billing. Setup charges may be charged. Not all package
340 plans support this feature (they tend to charge 0).
344 Prevent the resetting of usage limits during this call.
348 Do not save the generated bill in the database. Useful with return_bill
352 A list reference on which the generated bill(s) will be returned.
356 Optional terms to be printed on this invoice. Otherwise, customer-specific
357 terms or the default terms are used.
364 my( $self, %options ) = @_;
366 return '' if $self->payby eq 'COMP';
368 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
370 warn "$me bill customer ". $self->custnum. "\n"
373 my $time = $options{'time'} || time;
374 my $invoice_time = $options{'invoice_time'} || $time;
376 my $cmp_time = ( $conf->exists('next-bill-ignore-time')
381 $options{'not_pkgpart'} ||= {};
382 $options{'not_pkgpart'} = { map { $_ => 1 }
383 split(/\s*,\s*/, $options{'not_pkgpart'})
385 unless ref($options{'not_pkgpart'});
387 local $SIG{HUP} = 'IGNORE';
388 local $SIG{INT} = 'IGNORE';
389 local $SIG{QUIT} = 'IGNORE';
390 local $SIG{TERM} = 'IGNORE';
391 local $SIG{TSTP} = 'IGNORE';
392 local $SIG{PIPE} = 'IGNORE';
394 my $oldAutoCommit = $FS::UID::AutoCommit;
395 local $FS::UID::AutoCommit = 0;
398 warn "$me acquiring lock on customer ". $self->custnum. "\n"
401 $self->select_for_update; #mutex
403 warn "$me running pre-bill events for customer ". $self->custnum. "\n"
406 my $error = $self->do_cust_event(
407 'debug' => ( $options{'debug'} || 0 ),
408 'time' => $invoice_time,
409 'check_freq' => $options{'check_freq'},
410 'stage' => 'pre-bill',
412 unless $options{no_commit};
414 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
418 warn "$me done running pre-bill events for customer ". $self->custnum. "\n"
421 #keep auto-charge and non-auto-charge line items separate
422 my @passes = ( '', 'no_auto' );
424 my %cust_bill_pkg = map { $_ => [] } @passes;
427 # find the packages which are due for billing, find out how much they are
428 # & generate invoice database.
431 my %total_setup = map { my $z = 0; $_ => \$z; } @passes;
432 my %total_recur = map { my $z = 0; $_ => \$z; } @passes;
434 my %taxlisthash = map { $_ => {} } @passes;
436 my @precommit_hooks = ();
438 $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ]; #param checks?
440 foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
442 next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
444 warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG;
446 #? to avoid use of uninitialized value errors... ?
447 $cust_pkg->setfield('bill', '')
448 unless defined($cust_pkg->bill);
450 #my $part_pkg = $cust_pkg->part_pkg;
452 my $real_pkgpart = $cust_pkg->pkgpart;
453 my %hash = $cust_pkg->hash;
455 # we could implement this bit as FS::part_pkg::has_hidden, but we already
456 # suffer from performance issues
457 $options{has_hidden} = 0;
458 my @part_pkg = $cust_pkg->part_pkg->self_and_bill_linked;
459 $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
461 # if this package was changed from another package,
462 # and it hasn't been billed since then,
463 # and package balances are enabled,
464 if ( $cust_pkg->change_pkgnum
465 and $cust_pkg->change_date >= ($cust_pkg->last_bill || 0)
466 and $cust_pkg->change_date < $invoice_time
467 and $conf->exists('pkg-balances') )
469 # _transfer_balance will also create the appropriate credit
470 my @transfer_items = $self->_transfer_balance($cust_pkg);
471 # $part_pkg[0] is the "real" part_pkg
472 my $pass = ($cust_pkg->no_auto || $part_pkg[0]->no_auto) ?
474 push @{ $cust_bill_pkg{$pass} }, @transfer_items;
475 # treating this as recur, just because most charges are recur...
476 ${$total_recur{$pass}} += $_->recur foreach @transfer_items;
479 foreach my $part_pkg ( @part_pkg ) {
481 $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
483 my $pass = ($cust_pkg->no_auto || $part_pkg->no_auto) ? 'no_auto' : '';
485 my $next_bill = $cust_pkg->getfield('bill') || 0;
487 # let this run once if this is the last bill upon cancellation
488 while ( $next_bill <= $cmp_time or $options{cancel} ) {
490 $self->_make_lines( 'part_pkg' => $part_pkg,
491 'cust_pkg' => $cust_pkg,
492 'precommit_hooks' => \@precommit_hooks,
493 'line_items' => $cust_bill_pkg{$pass},
494 'setup' => $total_setup{$pass},
495 'recur' => $total_recur{$pass},
496 'tax_matrix' => $taxlisthash{$pass},
498 'real_pkgpart' => $real_pkgpart,
499 'options' => \%options,
502 # Stop if anything goes wrong
505 # or if we're not incrementing the bill date.
506 last if ($cust_pkg->getfield('bill') || 0) == $next_bill;
508 # or if we're letting it run only once
509 last if $options{cancel};
511 $next_bill = $cust_pkg->getfield('bill') || 0;
513 #stop if -o was passed to freeside-daily
514 last if $options{'one_recur'};
517 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
521 } #foreach my $part_pkg
523 } #foreach my $cust_pkg
525 #if the customer isn't on an automatic payby, everything can go on a single
527 #if ( $cust_main->payby !~ /^(CARD|CHEK)$/ ) {
528 #merge everything into one list
531 foreach my $pass (@passes) { # keys %cust_bill_pkg ) {
533 my @cust_bill_pkg = _omit_zero_value_bundles(@{ $cust_bill_pkg{$pass} });
535 next unless @cust_bill_pkg; #don't create an invoice w/o line items
537 warn "$me billing pass $pass\n"
538 #.Dumper(\@cust_bill_pkg)."\n"
541 if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
542 !$conf->exists('postal_invoice-recurring_only')
546 my $postal_pkg = $self->charge_postal_fee();
547 if ( $postal_pkg && !ref( $postal_pkg ) ) {
549 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
550 return "can't charge postal invoice fee for customer ".
551 $self->custnum. ": $postal_pkg";
553 } elsif ( $postal_pkg ) {
555 my $real_pkgpart = $postal_pkg->pkgpart;
556 # we could implement this bit as FS::part_pkg::has_hidden, but we already
557 # suffer from performance issues
558 $options{has_hidden} = 0;
559 my @part_pkg = $postal_pkg->part_pkg->self_and_bill_linked;
560 $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
562 foreach my $part_pkg ( @part_pkg ) {
563 my %postal_options = %options;
564 delete $postal_options{cancel};
566 $self->_make_lines( 'part_pkg' => $part_pkg,
567 'cust_pkg' => $postal_pkg,
568 'precommit_hooks' => \@precommit_hooks,
569 'line_items' => \@cust_bill_pkg,
570 'setup' => $total_setup{$pass},
571 'recur' => $total_recur{$pass},
572 'tax_matrix' => $taxlisthash{$pass},
574 'real_pkgpart' => $real_pkgpart,
575 'options' => \%postal_options,
578 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
583 # it's silly to have a zero value postal_pkg, but....
584 @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
590 my $listref_or_error =
591 $self->calculate_taxes( \@cust_bill_pkg, $taxlisthash{$pass}, $invoice_time);
593 unless ( ref( $listref_or_error ) ) {
594 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
595 return $listref_or_error;
598 foreach my $taxline ( @$listref_or_error ) {
599 ${ $total_setup{$pass} } =
600 sprintf('%.2f', ${ $total_setup{$pass} } + $taxline->setup );
601 push @cust_bill_pkg, $taxline;
605 warn "adding tax adjustments...\n" if $DEBUG > 2;
606 foreach my $cust_tax_adjustment (
607 qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum,
613 my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
615 my $itemdesc = $cust_tax_adjustment->taxname;
616 $itemdesc = '' if $itemdesc eq 'Tax';
618 push @cust_bill_pkg, new FS::cust_bill_pkg {
624 'itemdesc' => $itemdesc,
625 'itemcomment' => $cust_tax_adjustment->comment,
626 'cust_tax_adjustment' => $cust_tax_adjustment,
627 #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
632 my $charged = sprintf('%.2f', ${ $total_setup{$pass} } + ${ $total_recur{$pass} } );
634 my @cust_bill = $self->cust_bill;
635 my $balance = $self->balance;
636 my $previous_balance = scalar(@cust_bill)
637 ? ( $cust_bill[$#cust_bill]->billing_balance || 0 )
640 $previous_balance += $cust_bill[$#cust_bill]->charged
641 if scalar(@cust_bill);
642 #my $balance_adjustments =
643 # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged);
645 warn "creating the new invoice\n" if $DEBUG;
646 #create the new invoice
647 my $cust_bill = new FS::cust_bill ( {
648 'custnum' => $self->custnum,
649 '_date' => $invoice_time,
650 'charged' => $charged,
651 'billing_balance' => $balance,
652 'previous_balance' => $previous_balance,
653 'invoice_terms' => $options{'invoice_terms'},
654 'cust_bill_pkg' => \@cust_bill_pkg,
656 $error = $cust_bill->insert unless $options{no_commit};
658 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
659 return "can't create invoice for customer #". $self->custnum. ": $error";
661 push @{$options{return_bill}}, $cust_bill if $options{return_bill};
663 } #foreach my $pass ( keys %cust_bill_pkg )
665 foreach my $hook ( @precommit_hooks ) {
668 } unless $options{no_commit};
670 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
671 return "$@ running precommit hook $hook\n";
675 $dbh->commit or die $dbh->errstr if $oldAutoCommit && !$options{no_commit};
680 #discard bundled packages of 0 value
681 sub _omit_zero_value_bundles {
684 my @cust_bill_pkg = ();
685 my @cust_bill_pkg_bundle = ();
687 my $discount_show_always = 0;
689 foreach my $cust_bill_pkg ( @in ) {
691 $discount_show_always = ($cust_bill_pkg->get('discounts')
692 && scalar(@{$cust_bill_pkg->get('discounts')})
693 && $conf->exists('discount-show-always'));
695 warn " pkgnum ". $cust_bill_pkg->pkgnum. " sum $sum, ".
696 "setup_show_zero ". $cust_bill_pkg->setup_show_zero.
697 "recur_show_zero ". $cust_bill_pkg->recur_show_zero. "\n"
700 if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) {
701 push @cust_bill_pkg, @cust_bill_pkg_bundle
703 || ($sum == 0 && ( $discount_show_always
704 || grep {$_->recur_show_zero || $_->setup_show_zero}
705 @cust_bill_pkg_bundle
708 @cust_bill_pkg_bundle = ();
712 $sum += $cust_bill_pkg->setup + $cust_bill_pkg->recur;
713 push @cust_bill_pkg_bundle, $cust_bill_pkg;
717 push @cust_bill_pkg, @cust_bill_pkg_bundle
719 || ($sum == 0 && ( $discount_show_always
720 || grep {$_->recur_show_zero || $_->setup_show_zero}
721 @cust_bill_pkg_bundle
725 warn " _omit_zero_value_bundles: ". scalar(@in).
726 '->'. scalar(@cust_bill_pkg). "\n" #. Dumper(@cust_bill_pkg). "\n"
733 =item calculate_taxes LINEITEMREF TAXHASHREF INVOICE_TIME
735 Generates tax line items (see L<FS::cust_bill_pkg>) for this customer.
736 Usually used internally by bill method B<bill>.
738 If there is an error, returns the error, otherwise returns reference to a
739 list of line items suitable for insertion.
745 An array ref of the line items being billed.
749 A strange beast. The keys to this hash are internal identifiers consisting
750 of the name of the tax object type, a space, and its unique identifier ( e.g.
751 'cust_main_county 23' ). The values of the hash are listrefs. The first
752 item in the list is the tax object. The remaining items are either line
753 items or floating point values (currency amounts).
755 The taxes are calculated on this entity. Calculated exemption records are
756 transferred to the LINEITEMREF items on the assumption that they are related.
762 This specifies the date appearing on the associated invoice. Some
763 jurisdictions (i.e. Texas) have tax exemptions which are date sensitive.
769 sub calculate_taxes {
770 my ($self, $cust_bill_pkg, $taxlisthash, $invoice_time) = @_;
772 # $taxlisthash is a hashref
773 # keys are identifiers, values are arrayrefs
774 # each arrayref starts with a tax object (cust_main_county or tax_rate)
775 # then any cust_bill_pkg objects the tax applies to
777 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
779 warn "$me calculate_taxes\n"
780 #.Dumper($self, $cust_bill_pkg, $taxlisthash, $invoice_time). "\n"
783 my @tax_line_items = ();
785 # keys are tax names (as printed on invoices / itemdesc )
786 # values are arrayrefs of taxlisthash keys (internal identifiers)
789 # keys are taxlisthash keys (internal identifiers)
790 # values are (cumulative) amounts
793 # keys are taxlisthash keys (internal identifiers)
794 # values are arrayrefs of cust_bill_pkg_tax_location hashrefs
795 my %tax_location = ();
797 # keys are taxlisthash keys (internal identifiers)
798 # values are arrayrefs of cust_bill_pkg_tax_rate_location hashrefs
799 my %tax_rate_location = ();
801 # keys are taxlisthash keys (internal identifiers!)
802 # values are arrayrefs of cust_tax_exempt_pkg objects
805 foreach my $tax ( keys %$taxlisthash ) {
806 # $tax is a tax identifier (intersection of a tax definition record
807 # and a cust_bill_pkg record)
808 my $tax_object = shift @{ $taxlisthash->{$tax} };
809 # $tax_object is a cust_main_county or tax_rate
810 # (with billpkgnum, pkgnum, locationnum set)
811 # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg component objects
812 # (setup, recurring, usage classes)
813 warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
814 warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2;
815 # taxline calculates the tax on all cust_bill_pkgs in the
816 # first (arrayref) argument, and returns a hashref of 'name'
817 # (the line item description) and 'amount'.
818 # It also calculates exemptions and attaches them to the cust_bill_pkgs
820 my $taxables = $taxlisthash->{$tax};
821 my $exemptions = $tax_exemption{$tax} ||= [];
822 my $taxline = $tax_object->taxline(
824 'custnum' => $self->custnum,
825 'invoice_time' => $invoice_time,
826 'exemptions' => $exemptions,
828 return $taxline unless ref($taxline);
830 unshift @{ $taxlisthash->{$tax} }, $tax_object;
832 if ( $tax_object->isa('FS::cust_main_county') ) {
833 # then $taxline is a real line item
834 push @{ $taxname{ $taxline->itemdesc } }, $taxline;
837 # leave this as is for now
839 my $name = $taxline->{'name'};
840 my $amount = $taxline->{'amount'};
842 #warn "adding $amount as $name\n";
843 $taxname{ $name } ||= [];
844 push @{ $taxname{ $name } }, $tax;
846 $tax_amount{ $tax } += $amount;
848 # link records between cust_main_county/tax_rate and cust_location
849 $tax_rate_location{ $tax } ||= [];
850 my $taxratelocationnum =
851 $tax_object->tax_rate_location->taxratelocationnum;
852 push @{ $tax_rate_location{ $tax } },
854 'taxnum' => $tax_object->taxnum,
855 'taxtype' => ref($tax_object),
856 'amount' => sprintf('%.2f', $amount ),
857 'locationtaxid' => $tax_object->location,
858 'taxratelocationnum' => $taxratelocationnum,
860 } #if ref($tax_object)...
861 } #foreach keys %$taxlisthash
863 #consolidate and create tax line items
864 warn "consolidating and generating...\n" if $DEBUG > 2;
865 foreach my $taxname ( keys %taxname ) {
866 my @cust_bill_pkg_tax_location;
867 my @cust_bill_pkg_tax_rate_location;
868 my $tax_cust_bill_pkg = FS::cust_bill_pkg->new({
873 'itemdesc' => $taxname,
874 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
875 'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
880 warn "adding $taxname\n" if $DEBUG > 1;
881 foreach my $taxitem ( @{ $taxname{$taxname} } ) {
882 if ( ref($taxitem) eq 'FS::cust_bill_pkg' ) {
883 # then we need to transfer the amount and the links from the
884 # line item to the new one we're creating.
885 $tax_total += $taxitem->setup;
886 foreach my $link ( @{ $taxitem->get('cust_bill_pkg_tax_location') } ) {
887 $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg);
888 push @cust_bill_pkg_tax_location, $link;
892 next if $seen{$taxitem}++;
893 warn "adding $tax_amount{$taxitem}\n" if $DEBUG > 1;
894 $tax_total += $tax_amount{$taxitem};
895 push @cust_bill_pkg_tax_rate_location,
896 map { new FS::cust_bill_pkg_tax_rate_location $_ }
897 @{ $tax_rate_location{ $taxitem } };
900 next unless $tax_total;
902 # we should really neverround this up...I guess it's okay if taxline
903 # already returns amounts with 2 decimal places
904 $tax_total = sprintf('%.2f', $tax_total );
905 $tax_cust_bill_pkg->set('setup', $tax_total);
907 my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
913 if ( $pkg_category and
914 $conf->config('invoice_latexsummary') ||
915 $conf->config('invoice_htmlsummary')
919 my %hash = ( 'section' => $pkg_category->categoryname );
920 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
923 $tax_cust_bill_pkg->set('display', \@display);
925 push @tax_line_items, $tax_cust_bill_pkg;
932 my ($self, %params) = @_;
934 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
936 my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
937 my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
938 my $precommit_hooks = $params{precommit_hooks} or die "no precommit_hooks specified";
939 my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
940 my $total_setup = $params{setup} or die "no setup accumulator specified";
941 my $total_recur = $params{recur} or die "no recur accumulator specified";
942 my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
943 my $time = $params{'time'} or die "no time specified";
944 my (%options) = %{$params{options}};
946 if ( $part_pkg->freq ne '1' and ($options{'freq_override'} || 0) > 0 ) {
947 # this should never happen
948 die 'freq_override billing attempted on non-monthly package '.
953 my $real_pkgpart = $params{real_pkgpart};
954 my %hash = $cust_pkg->hash;
955 my $old_cust_pkg = new FS::cust_pkg \%hash;
960 $cust_pkg->pkgpart($part_pkg->pkgpart);
962 my $cmp_time = ( $conf->exists('next-bill-ignore-time')
973 my @setup_discounts = ();
974 my %setup_param = ( 'discounts' => \@setup_discounts );
975 if ( ! $options{recurring_only}
976 and ! $options{cancel}
977 and ( $options{'resetup'}
978 || ( ! $cust_pkg->setup
979 && ( ! $cust_pkg->start_date
980 || $cust_pkg->start_date <= $cmp_time
982 && ( ! $conf->exists('disable_setup_suspended_pkgs')
983 || ( $conf->exists('disable_setup_suspended_pkgs') &&
984 ! $cust_pkg->getfield('susp')
992 warn " bill setup\n" if $DEBUG > 1;
994 unless ( $cust_pkg->waive_setup ) {
997 $setup = eval { $cust_pkg->calc_setup( $time, \@details, \%setup_param ) };
998 return "$@ running calc_setup for $cust_pkg\n"
1001 $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
1004 $cust_pkg->setfield('setup', $time)
1005 unless $cust_pkg->setup;
1006 #do need it, but it won't get written to the db
1007 #|| $cust_pkg->pkgpart != $real_pkgpart;
1009 $cust_pkg->setfield('start_date', '')
1010 if $cust_pkg->start_date;
1015 # bill recurring fee
1020 my @recur_discounts = ();
1022 if ( ! $cust_pkg->start_date
1023 and ( ! $cust_pkg->susp || $cust_pkg->option('suspend_bill',1)
1024 || ( $part_pkg->option('suspend_bill', 1) )
1025 && ! $cust_pkg->option('no_suspend_bill',1)
1028 ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time )
1029 || ( $part_pkg->plan eq 'voip_cdr'
1030 && $part_pkg->option('bill_every_call')
1035 # XXX should this be a package event? probably. events are called
1036 # at collection time at the moment, though...
1037 $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
1038 if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'};
1039 #don't want to reset usage just cause we want a line item??
1040 #&& $part_pkg->pkgpart == $real_pkgpart;
1042 warn " bill recur\n" if $DEBUG > 1;
1045 # XXX shared with $recur_prog
1046 $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
1050 #over two params! lets at least switch to a hashref for the rest...
1051 my $increment_next_bill = ( $part_pkg->freq ne '0'
1052 && ( $cust_pkg->getfield('bill') || 0 ) <= $cmp_time
1053 && !$options{cancel}
1055 my %param = ( %setup_param,
1056 'precommit_hooks' => $precommit_hooks,
1057 'increment_next_bill' => $increment_next_bill,
1058 'discounts' => \@recur_discounts,
1059 'real_pkgpart' => $real_pkgpart,
1060 'freq_override' => $options{freq_override} || '',
1064 my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
1066 # There may be some part_pkg for which this is wrong. Only those
1067 # which can_discount are supported.
1068 # (the UI should prevent adding discounts to these at the moment)
1070 warn "calling $method on cust_pkg ". $cust_pkg->pkgnum.
1071 " for pkgpart ". $cust_pkg->pkgpart.
1072 " with params ". join(' / ', map "$_=>$param{$_}", keys %param). "\n"
1075 $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
1076 return "$@ running $method for $cust_pkg\n"
1080 $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better
1082 if ( $increment_next_bill ) {
1086 if ( my $main_pkg = $cust_pkg->main_pkg ) {
1087 # supplemental package
1088 # to keep in sync with the main package, simulate billing at
1090 my $main_pkg_freq = $main_pkg->part_pkg->freq;
1091 my $supp_pkg_freq = $part_pkg->freq;
1092 my $ratio = $supp_pkg_freq / $main_pkg_freq;
1093 if ( $ratio != int($ratio) ) {
1094 # the UI should prevent setting up packages like this, but just
1096 return "supplemental package period is not an integer multiple of main package period";
1098 $next_bill = $sdate;
1100 $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq );
1105 $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
1106 return "unparsable frequency: ". $part_pkg->freq
1107 if $next_bill == -1;
1110 #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
1111 # only for figuring next bill date, nothing else, so, reset $sdate again
1113 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1114 #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
1115 $cust_pkg->last_bill($sdate);
1117 $cust_pkg->setfield('bill', $next_bill );
1121 if ( $param{'setup_fee'} ) {
1122 # Add an additional setup fee at the billing stage.
1123 # Used for prorate_defer_bill.
1124 $setup += $param{'setup_fee'};
1125 $unitsetup += $param{'setup_fee'};
1129 if ( defined $param{'discount_left_setup'} ) {
1130 foreach my $discount_setup ( values %{$param{'discount_left_setup'}} ) {
1131 $setup -= $discount_setup;
1137 warn "\$setup is undefined" unless defined($setup);
1138 warn "\$recur is undefined" unless defined($recur);
1139 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1142 # If there's line items, create em cust_bill_pkg records
1143 # If $cust_pkg has been modified, update it (if we're a real pkgpart)
1148 if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
1149 # hmm.. and if just the options are modified in some weird price plan?
1151 warn " package ". $cust_pkg->pkgnum. " modified; updating\n"
1154 my $error = $cust_pkg->replace( $old_cust_pkg,
1155 'depend_jobnum'=>$options{depend_jobnum},
1156 'options' => { $cust_pkg->options },
1158 unless $options{no_commit};
1159 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
1160 if $error; #just in case
1163 $setup = sprintf( "%.2f", $setup );
1164 $recur = sprintf( "%.2f", $recur );
1165 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1166 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1168 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1169 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1172 my $discount_show_always = $conf->exists('discount-show-always')
1173 && ( ($setup == 0 && scalar(@setup_discounts))
1174 || ($recur == 0 && scalar(@recur_discounts))
1179 || (!$part_pkg->hidden && $options{has_hidden}) #include some $0 lines
1180 || $discount_show_always
1181 || ($setup == 0 && $cust_pkg->_X_show_zero('setup'))
1182 || ($recur == 0 && $cust_pkg->_X_show_zero('recur'))
1186 warn " charges (setup=$setup, recur=$recur); adding line items\n"
1189 my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
1191 warn " adding customer package invoice detail: $_\n"
1192 foreach @cust_pkg_detail;
1194 push @details, @cust_pkg_detail;
1196 my $cust_bill_pkg = new FS::cust_bill_pkg {
1197 'pkgnum' => $cust_pkg->pkgnum,
1199 'unitsetup' => $unitsetup,
1201 'unitrecur' => $unitrecur,
1202 'quantity' => $cust_pkg->quantity,
1203 'details' => \@details,
1204 'discounts' => [ @setup_discounts, @recur_discounts ],
1205 'hidden' => $part_pkg->hidden,
1206 'freq' => $part_pkg->freq,
1209 if ( $part_pkg->option('prorate_defer_bill',1)
1210 and !$hash{last_bill} ) {
1211 # both preceding and upcoming, technically
1212 $cust_bill_pkg->sdate( $cust_pkg->setup );
1213 $cust_bill_pkg->edate( $cust_pkg->bill );
1214 } elsif ( $part_pkg->recur_temporality eq 'preceding' ) {
1215 $cust_bill_pkg->sdate( $hash{last_bill} );
1216 $cust_bill_pkg->edate( $sdate - 86399 ); #60s*60m*24h-1
1217 $cust_bill_pkg->edate( $time ) if $options{cancel};
1218 } else { #if ( $part_pkg->recur_temporality eq 'upcoming' ) {
1219 $cust_bill_pkg->sdate( $sdate );
1220 $cust_bill_pkg->edate( $cust_pkg->bill );
1221 #$cust_bill_pkg->edate( $time ) if $options{cancel};
1224 $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
1225 unless $part_pkg->pkgpart == $real_pkgpart;
1227 $$total_setup += $setup;
1228 $$total_recur += $recur;
1234 #unless ( $discount_show_always ) { # oh, for god's sake
1235 my $error = $self->_handle_taxes(
1240 $options{invoice_time},
1242 \%options # I have serious objections to this
1244 return $error if $error;
1247 $cust_bill_pkg->set_display(
1248 part_pkg => $part_pkg,
1249 real_pkgpart => $real_pkgpart,
1252 push @$cust_bill_pkgs, $cust_bill_pkg;
1254 } #if $setup != 0 || $recur != 0
1262 =item _transfer_balance TO_PKG [ FROM_PKGNUM ]
1264 Takes one argument, a cust_pkg object that is being billed. This will
1265 be called only if the package was created by a package change, and has
1266 not been billed since the package change, and package balance tracking
1267 is enabled. The second argument can be an alternate package number to
1268 transfer the balance from; this should not be used externally.
1270 Transfers the balance from the previous package (now canceled) to
1271 this package, by crediting one package and creating an invoice item for
1272 the other. Inserts the credit and returns the invoice item (so that it
1273 can be added to an invoice that's being built).
1275 If the previous package was never billed, and was also created by a package
1276 change, then this will also transfer the balance from I<its> previous
1277 package, and so on, until reaching a package that either has been billed
1278 or was not created by a package change.
1282 my $balance_transfer_reason;
1284 sub _transfer_balance {
1286 my $cust_pkg = shift;
1287 my $from_pkgnum = shift || $cust_pkg->change_pkgnum;
1288 my $from_pkg = FS::cust_pkg->by_key($from_pkgnum);
1292 # if $from_pkg is not the first package in the chain, and it was never
1294 if ( $from_pkg->change_pkgnum and scalar($from_pkg->cust_bill_pkg) == 0 ) {
1295 @transfers = $self->_transfer_balance($cust_pkg, $from_pkg->change_pkgnum);
1298 my $prev_balance = $self->balance_pkgnum($from_pkgnum);
1299 if ( $prev_balance != 0 ) {
1300 $balance_transfer_reason ||= FS::reason->new_or_existing(
1301 'reason' => 'Package balance transfer',
1302 'type' => 'Internal adjustment',
1306 my $credit = FS::cust_credit->new({
1307 'custnum' => $self->custnum,
1308 'amount' => abs($prev_balance),
1309 'reasonnum' => $balance_transfer_reason->reasonnum,
1310 '_date' => $cust_pkg->change_date,
1313 my $cust_bill_pkg = FS::cust_bill_pkg->new({
1315 'recur' => abs($prev_balance),
1316 #'sdate' => $from_pkg->last_bill, # not sure about this
1317 #'edate' => $cust_pkg->change_date,
1318 'itemdesc' => $self->mt('Previous Balance, [_1]',
1319 $from_pkg->part_pkg->pkg),
1322 if ( $prev_balance > 0 ) {
1323 # credit the old package, charge the new one
1324 $credit->set('pkgnum', $from_pkgnum);
1325 $cust_bill_pkg->set('pkgnum', $cust_pkg->pkgnum);
1328 $credit->set('pkgnum', $cust_pkg->pkgnum);
1329 $cust_bill_pkg->set('pkgnum', $from_pkgnum);
1331 my $error = $credit->insert;
1332 die "error transferring package balance from #".$from_pkgnum.
1333 " to #".$cust_pkg->pkgnum.": $error\n" if $error;
1335 push @transfers, $cust_bill_pkg;
1336 } # $prev_balance != 0
1341 =item _handle_taxes PART_PKG TAXLISTHASH CUST_BILL_PKG CUST_PKG TIME PKGPART [ OPTIONS ]
1343 This is _handle_taxes. It's called once for each cust_bill_pkg generated
1344 from _make_lines, along with the part_pkg, cust_pkg, invoice time, the
1345 non-overridden pkgpart, a flag indicating whether the package is being
1346 canceled, and a partridge in a pear tree.
1348 The most important argument is 'taxlisthash'. This is shared across the
1349 entire invoice. It looks like this:
1351 'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
1352 'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
1355 'cust_main_county' can also be 'tax_rate'. The first object in the array
1356 is always the cust_main_county or tax_rate identified by the key.
1358 That "..." is a list of FS::cust_bill_pkg objects that will be fed to
1359 the 'taxline' method to calculate the amount of the tax. This doesn't
1360 happen until calculate_taxes, though.
1366 my $part_pkg = shift;
1367 my $taxlisthash = shift;
1368 my $cust_bill_pkg = shift;
1369 my $cust_pkg = shift;
1370 my $invoice_time = shift;
1371 my $real_pkgpart = shift;
1372 my $options = shift;
1374 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1376 my $location = $cust_pkg->tax_location;
1378 return if ( $self->payby eq 'COMP' ); #dubious
1380 if ( $conf->exists('enable_taxproducts')
1381 && ( scalar($part_pkg->part_pkg_taxoverride)
1382 || $part_pkg->has_taxproduct
1387 # EXTERNAL TAX RATES (via tax_rate)
1388 my %cust_bill_pkg = ();
1392 #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
1393 push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
1395 push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
1396 push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
1398 my $exempt = $conf->exists('cust_class-tax_exempt')
1399 ? ( $self->cust_class ? $self->cust_class->tax : '' )
1401 # standardize this just to be sure
1402 $exempt = ($exempt eq 'Y') ? 'Y' : '';
1406 foreach my $class (@classes) {
1407 my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
1408 return $err_or_ref unless ref($err_or_ref);
1409 $taxes{$class} = $err_or_ref;
1412 unless (exists $taxes{''}) {
1413 my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
1414 return $err_or_ref unless ref($err_or_ref);
1415 $taxes{''} = $err_or_ref;
1420 my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate; # grrr
1421 foreach my $key (keys %tax_cust_bill_pkg) {
1422 # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
1423 # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of
1425 # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that
1426 # apply to $key-class charges.
1427 my @taxes = @{ $taxes{$key} || [] };
1428 my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
1430 my %localtaxlisthash = ();
1431 foreach my $tax ( @taxes ) {
1433 # this is the tax identifier, not the taxname
1434 my $taxname = ref( $tax ). ' '. $tax->taxnum;
1435 # $taxlisthash: keys are "setup", "recur", and usage classes.
1436 # Values are arrayrefs, first the tax object (cust_main_county
1437 # or tax_rate) and then any cust_bill_pkg objects that the
1439 $taxlisthash->{ $taxname } ||= [ $tax ];
1440 push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
1442 $localtaxlisthash{ $taxname } ||= [ $tax ];
1443 push @{ $localtaxlisthash{ $taxname } }, $tax_cust_bill_pkg;
1447 warn "finding taxed taxes...\n" if $DEBUG > 2;
1448 foreach my $tax ( keys %localtaxlisthash ) {
1449 my $tax_object = shift @{ $localtaxlisthash{$tax} };
1450 warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
1452 next unless $tax_object->can('tax_on_tax');
1454 foreach my $tot ( $tax_object->tax_on_tax( $location ) ) {
1455 my $totname = ref( $tot ). ' '. $tot->taxnum;
1457 warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
1459 next unless exists( $localtaxlisthash{ $totname } ); # only increase
1461 warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
1462 # calculate the tax amount that the tax_on_tax will apply to
1463 my $hashref_or_error =
1464 $tax_object->taxline( $localtaxlisthash{$tax},
1465 'custnum' => $self->custnum,
1466 'invoice_time' => $invoice_time,
1468 return $hashref_or_error
1469 unless ref($hashref_or_error);
1471 # and append it to the list of taxable items
1472 $taxlisthash->{ $totname } ||= [ $tot ];
1473 push @{ $taxlisthash->{ $totname } }, $hashref_or_error->{amount};
1481 # INTERNAL TAX RATES (cust_main_county)
1483 # We fetch taxes even if the customer is completely exempt,
1484 # because we need to record that fact.
1486 my @loc_keys = qw( district city county state country );
1487 my %taxhash = map { $_ => $location->$_ } @loc_keys;
1489 $taxhash{'taxclass'} = $part_pkg->taxclass;
1491 warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
1493 my @taxes = (); # entries are cust_main_county objects
1494 my %taxhash_elim = %taxhash;
1495 my @elim = qw( district city county state );
1498 #first try a match with taxclass
1499 @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
1501 if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
1502 #then try a match without taxclass
1503 my %no_taxclass = %taxhash_elim;
1504 $no_taxclass{ 'taxclass' } = '';
1505 @taxes = qsearch( 'cust_main_county', \%no_taxclass );
1508 $taxhash_elim{ shift(@elim) } = '';
1510 } while ( !scalar(@taxes) && scalar(@elim) );
1513 my $tax_id = 'cust_main_county '.$_->taxnum;
1514 $taxlisthash->{$tax_id} ||= [ $_ ];
1515 push @{ $taxlisthash->{$tax_id} }, $cust_bill_pkg;
1524 my $part_pkg = shift;
1526 my $cust_pkg = shift;
1528 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1530 my $geocode = $cust_pkg->tax_location->geocode('cch');
1534 my @taxclassnums = map { $_->taxclassnum }
1535 $part_pkg->part_pkg_taxoverride($class);
1537 unless (@taxclassnums) {
1538 @taxclassnums = map { $_->taxclassnum }
1539 grep { $_->taxable eq 'Y' }
1540 $part_pkg->part_pkg_taxrate('cch', $geocode, $class);
1542 warn "Found taxclassnum values of ". join(',', @taxclassnums)
1547 join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
1549 @taxes = qsearch({ 'table' => 'tax_rate',
1550 'hashref' => { 'geocode' => $geocode, },
1551 'extra_sql' => $extra_sql,
1553 if scalar(@taxclassnums);
1555 warn "Found taxes ".
1556 join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n"
1563 =item collect [ HASHREF | OPTION => VALUE ... ]
1565 (Attempt to) collect money for this customer's outstanding invoices (see
1566 L<FS::cust_bill>). Usually used after the bill method.
1568 Actions are now triggered by billing events; see L<FS::part_event> and the
1569 billing events web interface. Old-style invoice events (see
1570 L<FS::part_bill_event>) have been deprecated.
1572 If there is an error, returns the error, otherwise returns false.
1574 Options are passed as name-value pairs.
1576 Currently available options are:
1582 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.
1586 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1590 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1594 set true to surpress email card/ACH decline notices.
1598 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)
1604 # allows for one time override of normal customer billing method
1609 my( $self, %options ) = @_;
1611 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1613 my $invoice_time = $options{'invoice_time'} || time;
1616 local $SIG{HUP} = 'IGNORE';
1617 local $SIG{INT} = 'IGNORE';
1618 local $SIG{QUIT} = 'IGNORE';
1619 local $SIG{TERM} = 'IGNORE';
1620 local $SIG{TSTP} = 'IGNORE';
1621 local $SIG{PIPE} = 'IGNORE';
1623 my $oldAutoCommit = $FS::UID::AutoCommit;
1624 local $FS::UID::AutoCommit = 0;
1627 $self->select_for_update; #mutex
1630 my $balance = $self->balance;
1631 warn "$me collect customer ". $self->custnum. ": balance $balance\n"
1634 if ( exists($options{'retry_card'}) ) {
1635 carp 'retry_card option passed to collect is deprecated; use retry';
1636 $options{'retry'} ||= $options{'retry_card'};
1638 if ( exists($options{'retry'}) && $options{'retry'} ) {
1639 my $error = $self->retry_realtime;
1641 $dbh->rollback if $oldAutoCommit;
1646 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1648 #never want to roll back an event just because it returned an error
1649 local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1651 $self->do_cust_event(
1652 'debug' => ( $options{'debug'} || 0 ),
1653 'time' => $invoice_time,
1654 'check_freq' => $options{'check_freq'},
1655 'stage' => 'collect',
1660 =item retry_realtime
1662 Schedules realtime / batch credit card / electronic check / LEC billing
1663 events for for retry. Useful if card information has changed or manual
1664 retry is desired. The 'collect' method must be called to actually retry
1667 Implementation details: For either this customer, or for each of this
1668 customer's open invoices, changes the status of the first "done" (with
1669 statustext error) realtime processing event to "failed".
1673 sub retry_realtime {
1676 local $SIG{HUP} = 'IGNORE';
1677 local $SIG{INT} = 'IGNORE';
1678 local $SIG{QUIT} = 'IGNORE';
1679 local $SIG{TERM} = 'IGNORE';
1680 local $SIG{TSTP} = 'IGNORE';
1681 local $SIG{PIPE} = 'IGNORE';
1683 my $oldAutoCommit = $FS::UID::AutoCommit;
1684 local $FS::UID::AutoCommit = 0;
1687 #a little false laziness w/due_cust_event (not too bad, really)
1689 my $join = FS::part_event_condition->join_conditions_sql;
1690 my $order = FS::part_event_condition->order_conditions_sql;
1693 . join ( ' OR ' , map {
1694 my $cust_join = FS::part_event->eventtables_cust_join->{$_} || '';
1695 my $custnum = FS::part_event->eventtables_custnum->{$_};
1696 "( part_event.eventtable = " . dbh->quote($_)
1697 . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key
1698 . " from $_ $cust_join"
1699 . " where $custnum = " . dbh->quote( $self->custnum ) . "))" ;
1700 } FS::part_event->eventtables)
1703 #here is the agent virtualization
1704 my $agent_virt = " ( part_event.agentnum IS NULL
1705 OR part_event.agentnum = ". $self->agentnum. ' )';
1707 #XXX this shouldn't be hardcoded, actions should declare it...
1708 my @realtime_events = qw(
1709 cust_bill_realtime_card
1710 cust_bill_realtime_check
1711 cust_bill_realtime_lec
1715 my $is_realtime_event =
1716 ' part_event.action IN ( '.
1717 join(',', map "'$_'", @realtime_events ).
1720 my $batch_or_statustext =
1721 "( part_event.action = 'cust_bill_batch'
1722 OR ( statustext IS NOT NULL AND statustext != '' )
1726 my @cust_event = qsearch({
1727 'table' => 'cust_event',
1728 'select' => 'cust_event.*',
1729 'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
1730 'hashref' => { 'status' => 'done' },
1731 'extra_sql' => " AND $batch_or_statustext ".
1732 " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
1735 my %seen_invnum = ();
1736 foreach my $cust_event (@cust_event) {
1738 #max one for the customer, one for each open invoice
1739 my $cust_X = $cust_event->cust_X;
1740 next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
1744 or $cust_event->part_event->eventtable eq 'cust_bill'
1747 my $error = $cust_event->retry;
1749 $dbh->rollback if $oldAutoCommit;
1750 return "error scheduling event for retry: $error";
1755 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1760 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
1762 Runs billing events; see L<FS::part_event> and the billing events web
1765 If there is an error, returns the error, otherwise returns false.
1767 Options are passed as name-value pairs.
1769 Currently available options are:
1775 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.
1779 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1783 "collect" (the default) or "pre-bill"
1787 set true to surpress email card/ACH decline notices.
1791 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)
1798 # allows for one time override of normal customer billing method
1802 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1805 my( $self, %options ) = @_;
1807 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1809 my $time = $options{'time'} || time;
1812 local $SIG{HUP} = 'IGNORE';
1813 local $SIG{INT} = 'IGNORE';
1814 local $SIG{QUIT} = 'IGNORE';
1815 local $SIG{TERM} = 'IGNORE';
1816 local $SIG{TSTP} = 'IGNORE';
1817 local $SIG{PIPE} = 'IGNORE';
1819 my $oldAutoCommit = $FS::UID::AutoCommit;
1820 local $FS::UID::AutoCommit = 0;
1823 $self->select_for_update; #mutex
1826 my $balance = $self->balance;
1827 warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
1830 # if ( exists($options{'retry_card'}) ) {
1831 # carp 'retry_card option passed to collect is deprecated; use retry';
1832 # $options{'retry'} ||= $options{'retry_card'};
1834 # if ( exists($options{'retry'}) && $options{'retry'} ) {
1835 # my $error = $self->retry_realtime;
1837 # $dbh->rollback if $oldAutoCommit;
1842 # false laziness w/pay_batch::import_results
1844 my $due_cust_event = $self->due_cust_event(
1845 'debug' => ( $options{'debug'} || 0 ),
1847 'check_freq' => $options{'check_freq'},
1848 'stage' => ( $options{'stage'} || 'collect' ),
1850 unless( ref($due_cust_event) ) {
1851 $dbh->rollback if $oldAutoCommit;
1852 return $due_cust_event;
1855 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1856 #never want to roll back an event just because it or a different one
1858 local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1860 foreach my $cust_event ( @$due_cust_event ) {
1864 #re-eval event conditions (a previous event could have changed things)
1865 unless ( $cust_event->test_conditions ) {
1866 #don't leave stray "new/locked" records around
1867 my $error = $cust_event->delete;
1868 return $error if $error;
1873 local $FS::cust_main::Billing_Realtime::realtime_bop_decline_quiet = 1
1874 if $options{'quiet'};
1875 warn " running cust_event ". $cust_event->eventnum. "\n"
1878 #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
1879 if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
1880 #XXX wtf is this? figure out a proper dealio with return value
1892 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
1894 Inserts database records for and returns an ordered listref of new events due
1895 for this customer, as FS::cust_event objects (see L<FS::cust_event>). If no
1896 events are due, an empty listref is returned. If there is an error, returns a
1897 scalar error message.
1899 To actually run the events, call each event's test_condition method, and if
1900 still true, call the event's do_event method.
1902 Options are passed as a hashref or as a list of name-value pairs. Available
1909 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.
1913 "collect" (the default) or "pre-bill"
1917 "Current time" for the events.
1921 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)
1925 Only return events for the specified eventtable (by default, events of all eventtables are returned)
1929 Explicitly pass the objects to be tested (typically used with eventtable).
1933 Set to true to return the objects, but not actually insert them into the
1940 sub due_cust_event {
1942 my %opt = ref($_[0]) ? %{ $_[0] } : @_;
1945 #my $DEBUG = $opt{'debug'}
1946 $opt{'debug'} ||= 0; # silence some warnings
1947 local($DEBUG) = $opt{'debug'}
1948 if $opt{'debug'} > $DEBUG;
1949 $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1951 warn "$me due_cust_event called with options ".
1952 join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
1955 $opt{'time'} ||= time;
1957 local $SIG{HUP} = 'IGNORE';
1958 local $SIG{INT} = 'IGNORE';
1959 local $SIG{QUIT} = 'IGNORE';
1960 local $SIG{TERM} = 'IGNORE';
1961 local $SIG{TSTP} = 'IGNORE';
1962 local $SIG{PIPE} = 'IGNORE';
1964 my $oldAutoCommit = $FS::UID::AutoCommit;
1965 local $FS::UID::AutoCommit = 0;
1968 $self->select_for_update #mutex
1969 unless $opt{testonly};
1972 # find possible events (initial search)
1975 my @cust_event = ();
1977 my @eventtable = $opt{'eventtable'}
1978 ? ( $opt{'eventtable'} )
1979 : FS::part_event->eventtables_runorder;
1981 my $check_freq = $opt{'check_freq'} || '1d';
1983 foreach my $eventtable ( @eventtable ) {
1986 if ( $opt{'objects'} ) {
1988 @objects = @{ $opt{'objects'} };
1990 } elsif ( $eventtable eq 'cust_main' ) {
1992 @objects = ( $self );
1996 my $cm_join = " LEFT JOIN cust_main USING ( custnum )";
1997 # linkage not needed here because FS::cust_main->$eventtable will
2000 #some false laziness w/Cron::bill bill_where
2002 my $join = FS::part_event_condition->join_conditions_sql( $eventtable);
2003 my $where = FS::part_event_condition->where_conditions_sql($eventtable,
2004 'time'=>$opt{'time'},
2006 $where = $where ? "AND $where" : '';
2008 my $are_part_event =
2009 "EXISTS ( SELECT 1 FROM part_event $join
2010 WHERE check_freq = '$check_freq'
2011 AND eventtable = '$eventtable'
2012 AND ( disabled = '' OR disabled IS NULL )
2018 @objects = $self->$eventtable(
2019 'addl_from' => $cm_join,
2020 'extra_sql' => " AND $are_part_event",
2022 } # if ( !$opt{objects} and $eventtable ne 'cust_main' )
2024 my @e_cust_event = ();
2026 my $linkage = FS::part_event->eventtables_cust_join->{$eventtable} || '';
2028 my $cross = "CROSS JOIN $eventtable $linkage";
2029 $cross .= ' LEFT JOIN cust_main USING ( custnum )'
2030 unless $eventtable eq 'cust_main';
2032 foreach my $object ( @objects ) {
2034 #this first search uses the condition_sql magic for optimization.
2035 #the more possible events we can eliminate in this step the better
2037 my $cross_where = '';
2038 my $pkey = $object->primary_key;
2039 $cross_where = "$eventtable.$pkey = ". $object->$pkey();
2041 my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
2043 FS::part_event_condition->where_conditions_sql( $eventtable,
2044 'time'=>$opt{'time'}
2046 my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
2048 $extra_sql = "AND $extra_sql" if $extra_sql;
2050 #here is the agent virtualization
2051 $extra_sql .= " AND ( part_event.agentnum IS NULL
2052 OR part_event.agentnum = ". $self->agentnum. ' )';
2054 $extra_sql .= " $order";
2056 warn "searching for events for $eventtable ". $object->$pkey. "\n"
2057 if $opt{'debug'} > 2;
2058 my @part_event = qsearch( {
2059 'debug' => ( $opt{'debug'} > 3 ? 1 : 0 ),
2060 'select' => 'part_event.*',
2061 'table' => 'part_event',
2062 'addl_from' => "$cross $join",
2063 'hashref' => { 'check_freq' => $check_freq,
2064 'eventtable' => $eventtable,
2067 'extra_sql' => "AND $cross_where $extra_sql",
2071 my $pkey = $object->primary_key;
2072 warn " ". scalar(@part_event).
2073 " possible events found for $eventtable ". $object->$pkey(). "\n";
2076 push @e_cust_event, map {
2077 $_->new_cust_event($object, 'time' => $opt{'time'})
2082 warn " ". scalar(@e_cust_event).
2083 " subtotal possible cust events found for $eventtable\n"
2086 push @cust_event, @e_cust_event;
2090 warn " ". scalar(@cust_event).
2091 " total possible cust events found in initial search\n"
2099 $opt{stage} ||= 'collect';
2101 grep { my $stage = $_->part_event->event_stage;
2102 $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
2112 @cust_event = grep $_->test_conditions( 'stats_hashref' => \%unsat ),
2115 warn " ". scalar(@cust_event). " cust events left satisfying conditions\n"
2118 warn " invalid conditions not eliminated with condition_sql:\n".
2119 join('', map " $_: ".$unsat{$_}."\n", keys %unsat )
2120 if keys %unsat && $DEBUG; # > 1;
2126 unless( $opt{testonly} ) {
2127 foreach my $cust_event ( @cust_event ) {
2129 my $error = $cust_event->insert();
2131 $dbh->rollback if $oldAutoCommit;
2138 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2144 warn " returning events: ". Dumper(@cust_event). "\n"
2151 =item apply_payments_and_credits [ OPTION => VALUE ... ]
2153 Applies unapplied payments and credits.
2155 In most cases, this new method should be used in place of sequential
2156 apply_payments and apply_credits methods.
2158 A hash of optional arguments may be passed. Currently "manual" is supported.
2159 If true, a payment receipt is sent instead of a statement when
2160 'payment_receipt_email' configuration option is set.
2162 If there is an error, returns the error, otherwise returns false.
2166 sub apply_payments_and_credits {
2167 my( $self, %options ) = @_;
2169 local $SIG{HUP} = 'IGNORE';
2170 local $SIG{INT} = 'IGNORE';
2171 local $SIG{QUIT} = 'IGNORE';
2172 local $SIG{TERM} = 'IGNORE';
2173 local $SIG{TSTP} = 'IGNORE';
2174 local $SIG{PIPE} = 'IGNORE';
2176 my $oldAutoCommit = $FS::UID::AutoCommit;
2177 local $FS::UID::AutoCommit = 0;
2180 $self->select_for_update; #mutex
2182 foreach my $cust_bill ( $self->open_cust_bill ) {
2183 my $error = $cust_bill->apply_payments_and_credits(%options);
2185 $dbh->rollback if $oldAutoCommit;
2186 return "Error applying: $error";
2190 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2195 =item apply_credits OPTION => VALUE ...
2197 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2198 to outstanding invoice balances in chronological order (or reverse
2199 chronological order if the I<order> option is set to B<newest>) and returns the
2200 value of any remaining unapplied credits available for refund (see
2201 L<FS::cust_refund>).
2203 Dies if there is an error.
2211 local $SIG{HUP} = 'IGNORE';
2212 local $SIG{INT} = 'IGNORE';
2213 local $SIG{QUIT} = 'IGNORE';
2214 local $SIG{TERM} = 'IGNORE';
2215 local $SIG{TSTP} = 'IGNORE';
2216 local $SIG{PIPE} = 'IGNORE';
2218 my $oldAutoCommit = $FS::UID::AutoCommit;
2219 local $FS::UID::AutoCommit = 0;
2222 $self->select_for_update; #mutex
2224 unless ( $self->total_unapplied_credits ) {
2225 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2229 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2230 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2232 my @invoices = $self->open_cust_bill;
2233 @invoices = sort { $b->_date <=> $a->_date } @invoices
2234 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2236 if ( $conf->exists('pkg-balances') ) {
2237 # limit @credits to those w/ a pkgnum grepped from $self
2239 foreach my $i (@invoices) {
2240 foreach my $li ( $i->cust_bill_pkg ) {
2241 $pkgnums{$li->pkgnum} = 1;
2244 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
2249 foreach my $cust_bill ( @invoices ) {
2251 if ( !defined($credit) || $credit->credited == 0) {
2252 $credit = pop @credits or last;
2256 if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
2257 $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
2259 $owed = $cust_bill->owed;
2261 unless ( $owed > 0 ) {
2262 push @credits, $credit;
2266 my $amount = min( $credit->credited, $owed );
2268 my $cust_credit_bill = new FS::cust_credit_bill ( {
2269 'crednum' => $credit->crednum,
2270 'invnum' => $cust_bill->invnum,
2271 'amount' => $amount,
2273 $cust_credit_bill->pkgnum( $credit->pkgnum )
2274 if $conf->exists('pkg-balances') && $credit->pkgnum;
2275 my $error = $cust_credit_bill->insert;
2277 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2281 redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2285 my $total_unapplied_credits = $self->total_unapplied_credits;
2287 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2289 return $total_unapplied_credits;
2292 =item apply_payments [ OPTION => VALUE ... ]
2294 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2295 to outstanding invoice balances in chronological order.
2297 #and returns the value of any remaining unapplied payments.
2299 A hash of optional arguments may be passed. Currently "manual" is supported.
2300 If true, a payment receipt is sent instead of a statement when
2301 'payment_receipt_email' configuration option is set.
2303 Dies if there is an error.
2307 sub apply_payments {
2308 my( $self, %options ) = @_;
2310 local $SIG{HUP} = 'IGNORE';
2311 local $SIG{INT} = 'IGNORE';
2312 local $SIG{QUIT} = 'IGNORE';
2313 local $SIG{TERM} = 'IGNORE';
2314 local $SIG{TSTP} = 'IGNORE';
2315 local $SIG{PIPE} = 'IGNORE';
2317 my $oldAutoCommit = $FS::UID::AutoCommit;
2318 local $FS::UID::AutoCommit = 0;
2321 $self->select_for_update; #mutex
2325 my @payments = sort { $b->_date <=> $a->_date }
2326 grep { $_->unapplied > 0 }
2329 my @invoices = sort { $a->_date <=> $b->_date}
2330 grep { $_->owed > 0 }
2333 if ( $conf->exists('pkg-balances') ) {
2334 # limit @payments to those w/ a pkgnum grepped from $self
2336 foreach my $i (@invoices) {
2337 foreach my $li ( $i->cust_bill_pkg ) {
2338 $pkgnums{$li->pkgnum} = 1;
2341 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
2346 foreach my $cust_bill ( @invoices ) {
2348 if ( !defined($payment) || $payment->unapplied == 0 ) {
2349 $payment = pop @payments or last;
2353 if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
2354 $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
2356 $owed = $cust_bill->owed;
2358 unless ( $owed > 0 ) {
2359 push @payments, $payment;
2363 my $amount = min( $payment->unapplied, $owed );
2366 'paynum' => $payment->paynum,
2367 'invnum' => $cust_bill->invnum,
2368 'amount' => $amount,
2370 $cbp->{_date} = $payment->_date
2371 if $options{'manual'} && $options{'backdate_application'};
2372 my $cust_bill_pay = new FS::cust_bill_pay($cbp);
2373 $cust_bill_pay->pkgnum( $payment->pkgnum )
2374 if $conf->exists('pkg-balances') && $payment->pkgnum;
2375 my $error = $cust_bill_pay->insert(%options);
2377 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2381 redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2385 my $total_unapplied_payments = $self->total_unapplied_payments;
2387 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2389 return $total_unapplied_payments;
2399 suspend_adjourned_pkgs
2400 unsuspend_resumed_pkgs
2403 (do_cust_event pre-bill)
2406 (vendor-only) _gather_taxes
2407 _omit_zero_value_bundles
2410 apply_payments_and_credits
2419 L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>