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 );
13 use FS::cust_bill_pkg;
14 use FS::cust_bill_pkg_display;
15 use FS::cust_bill_pay;
16 use FS::cust_credit_bill;
17 use FS::cust_tax_adjustment;
19 use FS::tax_rate_location;
20 use FS::cust_bill_pkg_tax_location;
21 use FS::cust_bill_pkg_tax_rate_location;
23 use FS::part_event_condition;
25 use FS::FeeOrigin_Mixin;
29 # 1 is mostly method/subroutine entry and options
30 # 2 traces progress of some operations
31 # 3 is even more information including possibly sensitive data
33 $me = '[FS::cust_main::Billing]';
35 install_callback FS::UID sub {
37 #yes, need it for stuff below (prolly should be cached)
42 FS::cust_main::Billing - Billing mixin for cust_main
48 These methods are available on FS::cust_main objects.
54 =item bill_and_collect
56 Cancels and suspends any packages due, generates bills, applies payments and
57 credits, and applies collection events to run cards, send bills and notices,
60 By default, warns on errors and continues with the next operation (but see the
63 Options are passed as name-value pairs. Currently available options are:
69 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:
73 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
77 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.
81 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
85 If set true, re-charges setup fees.
89 If set any errors prevent subsequent operations from continusing. If set
90 specifically to "return", returns the error (or false, if there is no error).
91 Any other true value causes errors to die.
95 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)
99 Optional FS::queue entry to receive status updates.
103 Options are passed to the B<bill> and B<collect> methods verbatim, so all
104 options of those methods are also available.
108 sub bill_and_collect {
109 my( $self, %options ) = @_;
111 my $log = FS::Log->new('FS::cust_main::Billing::bill_and_collect');
112 my %logopt = (object => $self);
113 $log->debug('start', %logopt);
117 #$options{actual_time} not $options{time} because freeside-daily -d is for
118 #pre-printing invoices
120 $options{'actual_time'} ||= time;
121 my $job = $options{'job'};
123 my $actual_time = ( $conf->exists('next-bill-ignore-time')
124 ? day_end( $options{actual_time} )
125 : $options{actual_time}
128 $job->update_statustext('0,cleaning expired packages') if $job;
129 $log->debug('canceling expired packages', %logopt);
130 $error = $self->cancel_expired_pkgs( $actual_time );
132 $error = "Error expiring custnum ". $self->custnum. ": $error";
133 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
134 elsif ( $options{fatal} ) { die $error; }
135 else { warn $error; }
138 $log->debug('suspending adjourned packages', %logopt);
139 $error = $self->suspend_adjourned_pkgs( $actual_time );
141 $error = "Error adjourning custnum ". $self->custnum. ": $error";
142 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
143 elsif ( $options{fatal} ) { die $error; }
144 else { warn $error; }
147 $log->debug('unsuspending resumed packages', %logopt);
148 $error = $self->unsuspend_resumed_pkgs( $actual_time );
150 $error = "Error resuming custnum ".$self->custnum. ": $error";
151 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
152 elsif ( $options{fatal} ) { die $error; }
153 else { warn $error; }
156 $job->update_statustext('20,billing packages') if $job;
157 $log->debug('billing packages', %logopt);
158 $error = $self->bill( %options );
160 $error = "Error billing custnum ". $self->custnum. ": $error";
161 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
162 elsif ( $options{fatal} ) { die $error; }
163 else { warn $error; }
166 $job->update_statustext('50,applying payments and credits') if $job;
167 $log->debug('applying payments and credits', %logopt);
168 $error = $self->apply_payments_and_credits;
170 $error = "Error applying custnum ". $self->custnum. ": $error";
171 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
172 elsif ( $options{fatal} ) { die $error; }
173 else { warn $error; }
176 # In a batch tax environment, do not run collection if any pending
177 # invoices were created. Collection will run after the next tax batch.
178 my $tax = FS::TaxEngine->new;
179 if ( $tax->info->{batch} and
180 qsearch('cust_bill', { custnum => $self->custnum, pending => 'Y' })
183 warn "skipped collection for custnum ".$self->custnum.
184 " due to pending invoices\n" if $DEBUG;
185 } elsif ( $conf->exists('cancelled_cust-noevents')
186 && ! $self->num_ncancelled_pkgs )
188 warn "skipped collection for custnum ".$self->custnum.
189 " because they have no active packages\n" if $DEBUG;
191 # run collection normally
192 $job->update_statustext('70,running collection events') if $job;
193 $log->debug('running collection events', %logopt);
194 $error = $self->collect( %options );
196 $error = "Error collecting custnum ". $self->custnum. ": $error";
197 if ($options{fatal} && $options{fatal} eq 'return') { return $error; }
198 elsif ($options{fatal} ) { die $error; }
199 else { warn $error; }
203 $job->update_statustext('100,finished') if $job;
204 $log->debug('finish', %logopt);
210 sub cancel_expired_pkgs {
211 my ( $self, $time, %options ) = @_;
213 my @cancel_pkgs = $self->ncancelled_pkgs( {
214 'extra_sql' => " AND expire IS NOT NULL AND expire > 0 AND expire <= $time "
219 my @really_cancel_pkgs;
222 CUST_PKG: foreach my $cust_pkg ( @cancel_pkgs ) {
223 my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
226 if ( $cust_pkg->change_to_pkgnum ) {
228 my $new_pkg = FS::cust_pkg->by_key($cust_pkg->change_to_pkgnum);
230 push @errors, 'can\'t change pkgnum '.$cust_pkg->pkgnum.' to pkgnum '.
231 $cust_pkg->change_to_pkgnum.'; not expiring';
234 $error = $cust_pkg->change( 'cust_pkg' => $new_pkg,
235 'unprotect_svcs' => 1 );
236 $error = '' if ref $error eq 'FS::cust_pkg';
238 } else { # just cancel it
240 push @really_cancel_pkgs, $cust_pkg;
241 push @cancel_reasons, $cpr;
246 if (@really_cancel_pkgs) {
248 my %cancel_opt = ( 'cust_pkg' => \@really_cancel_pkgs,
249 'cust_pkg_reason' => \@cancel_reasons,
253 push @errors, $self->cancel_pkgs(%cancel_opt);
257 join(' / ', @errors);
261 sub suspend_adjourned_pkgs {
262 my ( $self, $time, %options ) = @_;
264 my @susp_pkgs = $self->ncancelled_pkgs( {
266 " AND ( susp IS NULL OR susp = 0 )
267 AND ( ( bill IS NOT NULL AND bill != 0 AND bill < $time )
268 OR ( adjourn IS NOT NULL AND adjourn != 0 AND adjourn <= $time )
273 #only because there's no SQL test for is_prepaid :/
275 grep { ( $_->part_pkg->is_prepaid
280 && $_->adjourn <= $time
288 foreach my $cust_pkg ( @susp_pkgs ) {
289 my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
290 if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
291 my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
292 'reason_otaker' => $cpr->otaker
296 push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
299 join(' / ', @errors);
303 sub unsuspend_resumed_pkgs {
304 my ( $self, $time, %options ) = @_;
306 my @unsusp_pkgs = $self->ncancelled_pkgs( {
307 'extra_sql' => " AND resume IS NOT NULL AND resume > 0 AND resume <= $time "
312 foreach my $cust_pkg ( @unsusp_pkgs ) {
313 my $error = $cust_pkg->unsuspend( 'time' => $time );
314 push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
317 join(' / ', @errors);
323 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
324 conjunction with the collect method by calling B<bill_and_collect>.
326 If there is an error, returns the error, otherwise returns false.
328 Options are passed as name-value pairs. Currently available options are:
334 If set true, re-charges setup fees.
338 If set true then only bill recurring charges, not setup, usage, one time
343 If set, then override the normal frequency and look for a part_pkg_discount
344 to take at that frequency. This is appropriate only when the normal
345 frequency for all packages is monthly, and is an error otherwise. Use
346 C<pkg_list> to limit the set of packages included in billing.
350 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:
354 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
358 An array ref of specific packages (objects) to attempt billing, instead trying all of them.
360 $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
364 A hashref of pkgparts to exclude from this billing run (can also be specified as a comma-separated scalar).
368 Do not bill prepaid packages. Used by freeside-daily.
372 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.
376 This boolean value informs the us that the package is being cancelled. This
377 typically might mean not charging the normal recurring fee but only usage
378 fees since the last billing. Setup charges may be charged. Not all package
379 plans support this feature (they tend to charge 0).
383 Prevent the resetting of usage limits during this call.
387 Do not save the generated bill in the database. Useful with return_bill
391 A list reference on which the generated bill(s) will be returned.
395 Boolean value; indicates that this is an estimate rather than a "tax invoice".
396 This will be passed through to the tax engine, as online tax services
397 sometimes need to know it for reporting purposes. Otherwise it has no effect.
401 Optional terms to be printed on this invoice. Otherwise, customer-specific
402 terms or the default terms are used.
409 my( $self, %options ) = @_;
411 return '' if $self->complimentary eq 'Y';
413 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
414 my $log = FS::Log->new('FS::cust_main::Billing::bill');
415 my %logopt = (object => $self);
417 $log->debug('start', %logopt);
418 warn "$me bill customer ". $self->custnum. "\n"
421 my $time = $options{'time'} || time;
422 my $invoice_time = $options{'invoice_time'} || $time;
424 my $cmp_time = ( $conf->exists('next-bill-ignore-time')
429 $options{'not_pkgpart'} ||= {};
430 $options{'not_pkgpart'} = { map { $_ => 1 }
431 split(/\s*,\s*/, $options{'not_pkgpart'})
433 unless ref($options{'not_pkgpart'});
435 local $SIG{HUP} = 'IGNORE';
436 local $SIG{INT} = 'IGNORE';
437 local $SIG{QUIT} = 'IGNORE';
438 local $SIG{TERM} = 'IGNORE';
439 local $SIG{TSTP} = 'IGNORE';
440 local $SIG{PIPE} = 'IGNORE';
442 my $oldAutoCommit = $FS::UID::AutoCommit;
443 local $FS::UID::AutoCommit = 0;
446 $log->debug('acquiring lock', %logopt);
447 warn "$me acquiring lock on customer ". $self->custnum. "\n"
450 $self->select_for_update; #mutex
452 $log->debug('running pre-bill events', %logopt);
453 warn "$me running pre-bill events for customer ". $self->custnum. "\n"
456 my $error = $self->do_cust_event(
457 'debug' => ( $options{'debug'} || 0 ),
458 'time' => $invoice_time,
459 'check_freq' => $options{'check_freq'},
460 'stage' => 'pre-bill',
462 unless $options{no_commit};
464 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
468 $log->debug('done running pre-bill events', %logopt);
469 warn "$me done running pre-bill events for customer ". $self->custnum. "\n"
472 #keep auto-charge and non-auto-charge line items separate
473 my @passes = ( '', 'no_auto' );
475 my %cust_bill_pkg = map { $_ => [] } @passes;
478 # find the packages which are due for billing, find out how much they are
479 # & generate invoice database.
482 my %total_setup = map { my $z = 0; $_ => \$z; } @passes;
483 my %total_recur = map { my $z = 0; $_ => \$z; } @passes;
485 my @precommit_hooks = ();
487 $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ]; #param checks?
490 my $tax_is_batch = '';
492 $tax_engines{$_} = FS::TaxEngine->new(cust_main => $self,
493 invoice_time => $invoice_time,
494 cancel => $options{cancel},
495 estimate => $options{estimate},
497 $tax_is_batch ||= $tax_engines{$_}->info->{batch};
500 foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
502 next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
504 my $part_pkg = $cust_pkg->part_pkg;
506 next if $options{'no_prepaid'} && $part_pkg->is_prepaid;
508 $log->debug('bill package '. $cust_pkg->pkgnum, %logopt);
509 warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG;
511 #? to avoid use of uninitialized value errors... ?
512 $cust_pkg->setfield('bill', '')
513 unless defined($cust_pkg->bill);
515 my $real_pkgpart = $cust_pkg->pkgpart;
516 my %hash = $cust_pkg->hash;
518 # we could implement this bit as FS::part_pkg::has_hidden, but we already
519 # suffer from performance issues
520 $options{has_hidden} = 0;
521 my @part_pkg = $part_pkg->self_and_bill_linked;
522 $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
524 # if this package was changed from another package,
525 # and it hasn't been billed since then,
526 # and package balances are enabled,
527 if ( $cust_pkg->change_pkgnum
528 and $cust_pkg->change_date >= ($cust_pkg->last_bill || 0)
529 and $cust_pkg->change_date < $invoice_time
530 and $conf->exists('pkg-balances') )
532 # _transfer_balance will also create the appropriate credit
533 my @transfer_items = $self->_transfer_balance($cust_pkg);
534 # $part_pkg[0] is the "real" part_pkg
535 my $pass = ($cust_pkg->no_auto || $part_pkg[0]->no_auto) ?
537 push @{ $cust_bill_pkg{$pass} }, @transfer_items;
538 # treating this as recur, just because most charges are recur...
539 ${$total_recur{$pass}} += $_->recur foreach @transfer_items;
541 # currently not considering separate_bill here, as it's for
542 # one-time charges only
545 foreach my $part_pkg ( @part_pkg ) {
547 $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
550 if ( $cust_pkg->separate_bill ) {
551 # if no_auto is also set, that's fine. we just need to not have
552 # invoices that are both auto and no_auto, and since the package
553 # gets an invoice all to itself, it will only be one or the other.
554 $pass = $cust_pkg->pkgnum;
555 if (!exists $cust_bill_pkg{$pass}) { # it may not exist yet
557 $total_setup{$pass} = do { my $z = 0; \$z };
558 $total_recur{$pass} = do { my $z = 0; \$z };
559 # it also needs its own tax context
560 $tax_engines{$pass} = FS::TaxEngine->new(
562 invoice_time => $invoice_time,
563 cancel => $options{cancel},
564 estimate => $options{estimate},
566 $cust_bill_pkg{$pass} = [];
568 } elsif ( ($cust_pkg->no_auto || $part_pkg->no_auto) ) {
572 my $next_bill = $cust_pkg->getfield('bill') || 0;
574 # let this run once if this is the last bill upon cancellation
575 while ( $next_bill <= $cmp_time or $options{cancel} ) {
577 $self->_make_lines( 'part_pkg' => $part_pkg,
578 'cust_pkg' => $cust_pkg,
579 'precommit_hooks' => \@precommit_hooks,
580 'line_items' => $cust_bill_pkg{$pass},
581 'setup' => $total_setup{$pass},
582 'recur' => $total_recur{$pass},
583 'tax_engine' => $tax_engines{$pass},
585 'real_pkgpart' => $real_pkgpart,
586 'options' => \%options,
589 # Stop if anything goes wrong
592 # or if we're not incrementing the bill date.
593 last if ($cust_pkg->getfield('bill') || 0) == $next_bill;
595 # or if we're letting it run only once
596 last if $options{cancel};
598 $next_bill = $cust_pkg->getfield('bill') || 0;
600 #stop if -o was passed to freeside-daily
601 last if $options{'one_recur'};
604 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
608 } #foreach my $part_pkg
610 } #foreach my $cust_pkg
612 foreach my $pass (@passes) { # keys %cust_bill_pkg )
614 my @cust_bill_pkg = _omit_zero_value_bundles(@{ $cust_bill_pkg{$pass} });
616 warn "$me billing pass $pass\n"
617 #.Dumper(\@cust_bill_pkg)."\n"
624 my @pending_fees = FS::FeeOrigin_Mixin->by_cust($self->custnum,
625 hashref => { 'billpkgnum' => '' }
627 warn "$me found pending fees:\n".Dumper(\@pending_fees)."\n"
628 if @pending_fees and $DEBUG > 1;
630 # determine whether to generate an invoice
631 my $generate_bill = scalar(@cust_bill_pkg) > 0;
633 foreach my $fee (@pending_fees) {
634 $generate_bill = 1 unless $fee->nextbill;
637 # don't create an invoice with no line items, or where the only line
638 # items are fees that are supposed to be held until the next invoice
639 next if !$generate_bill;
643 foreach my $fee_origin (@pending_fees) {
644 my $part_fee = $fee_origin->part_fee;
646 # check whether the fee is applicable before doing anything expensive:
648 # if the fee def belongs to a different agent, don't charge the fee.
649 # event conditions should prevent this, but just in case they don't,
651 if ( $part_fee->agentnum and $part_fee->agentnum != $self->agentnum ) {
652 warn "tried to charge fee#".$part_fee->feepart .
653 " on customer#".$self->custnum." from a different agent.\n";
656 # also skip if it's disabled
657 next if $part_fee->disabled eq 'Y';
659 # Decide which invoice to base the fee on.
660 my $cust_bill = $fee_origin->cust_bill;
662 # Then link it to the current invoice. This isn't the real cust_bill
663 # object that will be inserted--in particular there are no taxes yet.
664 # If you want to charge a fee on the total invoice amount including
665 # taxes, you have to put the fee on the next invoice.
666 $cust_bill = FS::cust_bill->new({
667 'custnum' => $self->custnum,
668 'cust_bill_pkg' => \@cust_bill_pkg,
669 'charged' => ${ $total_setup{$pass} } +
670 ${ $total_recur{$pass} },
673 # If the origin is for a specific package, then only apply the fee to
674 # line items from that package.
675 if ( my $cust_pkg = $fee_origin->cust_pkg ) {
676 my @charge_fee_on_item;
677 my $charge_fee_on_amount = 0;
678 foreach (@cust_bill_pkg) {
679 if ($_->pkgnum == $cust_pkg->pkgnum) {
680 push @charge_fee_on_item, $_;
681 $charge_fee_on_amount += $_->setup + $_->recur;
684 $cust_bill->set('cust_bill_pkg', \@charge_fee_on_item);
685 $cust_bill->set('charged', $charge_fee_on_amount);
688 } # $cust_bill is now set
690 my $fee_item = $part_fee->lineitem($cust_bill) or next;
691 # link this so that we can clear the marker on inserting the line item
692 $fee_item->set('fee_origin', $fee_origin);
693 push @fee_items, $fee_item;
697 # add fees to the invoice
698 foreach my $fee_item (@fee_items) {
700 push @cust_bill_pkg, $fee_item;
701 ${ $total_setup{$pass} } += $fee_item->setup;
702 ${ $total_recur{$pass} } += $fee_item->recur;
704 my $part_fee = $fee_item->part_fee;
705 my $fee_location = $self->ship_location; # I think?
707 my $error = $tax_engines{''}->add_sale($fee_item);
709 return $error if $error;
713 # XXX implementation of fees is supposed to make this go away...
714 if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
715 !$conf->exists('postal_invoice-recurring_only')
719 my $postal_pkg = $self->charge_postal_fee();
720 if ( $postal_pkg && !ref( $postal_pkg ) ) {
722 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
723 return "can't charge postal invoice fee for customer ".
724 $self->custnum. ": $postal_pkg";
726 } elsif ( $postal_pkg ) {
728 my $real_pkgpart = $postal_pkg->pkgpart;
729 # we could implement this bit as FS::part_pkg::has_hidden, but we already
730 # suffer from performance issues
731 $options{has_hidden} = 0;
732 my @part_pkg = $postal_pkg->part_pkg->self_and_bill_linked;
733 $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
735 foreach my $part_pkg ( @part_pkg ) {
736 my %postal_options = %options;
737 delete $postal_options{cancel};
739 $self->_make_lines( 'part_pkg' => $part_pkg,
740 'cust_pkg' => $postal_pkg,
741 'precommit_hooks' => \@precommit_hooks,
742 'line_items' => \@cust_bill_pkg,
743 'setup' => $total_setup{$pass},
744 'recur' => $total_recur{$pass},
745 'tax_engine' => $tax_engines{$pass},
747 'real_pkgpart' => $real_pkgpart,
748 'options' => \%postal_options,
751 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
756 # it's silly to have a zero value postal_pkg, but....
757 @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
764 #XXX does this work with batch tax engines?
765 warn "adding tax adjustments...\n" if $DEBUG > 2;
766 foreach my $cust_tax_adjustment (
767 qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum,
773 my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
775 my $itemdesc = $cust_tax_adjustment->taxname;
776 $itemdesc = '' if $itemdesc eq 'Tax';
778 push @cust_bill_pkg, new FS::cust_bill_pkg {
784 'itemdesc' => $itemdesc,
785 'itemcomment' => $cust_tax_adjustment->comment,
786 'cust_tax_adjustment' => $cust_tax_adjustment,
787 #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
792 my $charged = sprintf('%.2f', ${ $total_setup{$pass} } + ${ $total_recur{$pass} } );
794 my $balance = $self->balance;
796 my $previous_bill = qsearchs({ 'table' => 'cust_bill',
797 'hashref' => { custnum=>$self->custnum },
798 'extra_sql' => 'ORDER BY _date DESC LIMIT 1',
800 my $previous_balance =
802 ? ( $previous_bill->billing_balance + $previous_bill->charged )
805 $log->debug('creating the new invoice', %logopt);
806 warn "creating the new invoice\n" if $DEBUG;
807 #create the new invoice
808 my $cust_bill = new FS::cust_bill ( {
809 'custnum' => $self->custnum,
810 '_date' => $invoice_time,
811 'charged' => $charged,
812 'billing_balance' => $balance,
813 'previous_balance' => $previous_balance,
814 'invoice_terms' => $options{'invoice_terms'},
815 'cust_bill_pkg' => \@cust_bill_pkg,
816 'pending' => 'Y', # clear this after doing taxes
819 if (!$options{no_commit}) {
820 # probably we ought to insert it as pending, and then rollback
821 # without ever un-pending it
822 $error = $cust_bill->insert;
824 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
825 return "can't create invoice for customer #". $self->custnum. ": $error";
830 # calculate and append taxes
831 if ( ! $tax_is_batch) {
833 my $arrayref = eval { $tax_engines{$pass}->calculate_taxes($cust_bill) };
836 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
840 # or should this be in TaxEngine?
842 foreach my $taxline ( @$arrayref ) {
843 $total_tax += $taxline->setup;
844 $taxline->set('invnum' => $cust_bill->invnum); # just to be sure
845 push @cust_bill_pkg, $taxline; # for return_bill
847 if (!$options{no_commit}) {
848 my $error = $taxline->insert;
850 $dbh->rollback if $oldAutoCommit;
857 # add tax to the invoice amount and finalize it
858 ${ $total_setup{$pass} } = sprintf('%.2f', ${ $total_setup{$pass} } + $total_tax);
859 $charged = sprintf('%.2f', $charged + $total_tax);
860 $cust_bill->set('charged', $charged);
861 $cust_bill->set('pending', '');
863 if (!$options{no_commit}) {
864 my $error = $cust_bill->replace;
866 $dbh->rollback if $oldAutoCommit;
871 } # if !$tax_is_batch
872 # if it IS batch, then we'll do all this in process_tax_batch
874 push @{$options{return_bill}}, $cust_bill if $options{return_bill};
876 } #foreach my $pass ( keys %cust_bill_pkg )
878 foreach my $hook ( @precommit_hooks ) {
881 } unless $options{no_commit};
883 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
884 return "$@ running precommit hook $hook\n";
888 $dbh->commit or die $dbh->errstr if $oldAutoCommit && !$options{no_commit};
893 #discard bundled packages of 0 value
894 # XXX we should reconsider whether we even need this
895 sub _omit_zero_value_bundles {
900 my $discount_show_always = $conf->exists('discount-show-always');
903 # Sort @in the same way we do during invoice rendering, so we can identify
904 # bundles. See FS::Template_Mixin::_items_nontax.
905 @in = sort { $a->pkgnum <=> $b->pkgnum or
906 $a->sdate <=> $b->sdate or
907 ($a->pkgpart_override ? 0 : -1) or
908 ($b->pkgpart_override ? 0 : 1) or
909 $b->hidden cmp $a->hidden or
910 $a->pkgpart_override <=> $b->pkgpart_override
913 # this is a pack-and-deliver pattern. every time there's a cust_bill_pkg
914 # _without_ pkgpart_override, that's the start of the new bundle. if there's
915 # an existing bundle, and it contains a nonzero amount (or a zero amount
916 # that's displayable anyway), push all line items in the bundle.
917 foreach my $cust_bill_pkg ( @in ) {
919 if (scalar(@bundle) and !$cust_bill_pkg->pkgpart_override) {
920 # ship out this bundle and reset it
928 # add this item to the current bundle
929 push @bundle, $cust_bill_pkg;
931 # determine if it makes the bundle displayable
932 if ( $cust_bill_pkg->setup > 0
933 or $cust_bill_pkg->recur > 0
934 or $cust_bill_pkg->setup_show_zero
935 or $cust_bill_pkg->recur_show_zero
936 or ($discount_show_always
937 and scalar(@{ $cust_bill_pkg->get('discounts')})
949 warn " _omit_zero_value_bundles: ". scalar(@in).
950 '->'. scalar(@out). "\n" #. Dumper(@out). "\n"
957 my ($self, %params) = @_;
959 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
961 my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
962 my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
963 my $cust_location = $cust_pkg->tax_location;
964 my $precommit_hooks = $params{precommit_hooks} or die "no precommit_hooks specified";
965 my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
966 my $total_setup = $params{setup} or die "no setup accumulator specified";
967 my $total_recur = $params{recur} or die "no recur accumulator specified";
968 my $time = $params{'time'} or die "no time specified";
969 my (%options) = %{$params{options}};
971 my $tax_engine = $params{tax_engine};
973 if ( $part_pkg->freq ne '1' and ($options{'freq_override'} || 0) > 0 ) {
974 # this should never happen
975 die 'freq_override billing attempted on non-monthly package '.
980 my $real_pkgpart = $params{real_pkgpart};
981 my %hash = $cust_pkg->hash;
982 my $old_cust_pkg = new FS::cust_pkg \%hash;
987 $cust_pkg->pkgpart($part_pkg->pkgpart);
989 my $cmp_time = ( $conf->exists('next-bill-ignore-time')
1000 my @setup_discounts = ();
1001 my %setup_param = ( 'discounts' => \@setup_discounts,
1002 'real_pkgpart' => $params{real_pkgpart}
1004 my $setup_billed_currency = '';
1005 my $setup_billed_amount = 0;
1006 # Conditions for setting setup date and charging the setup fee:
1007 # - this is not a recurring-only billing run
1008 # - and the package is not currently being canceled
1009 # - and, unless we're specifically told otherwise via 'resetup':
1010 # - it doesn't already HAVE a setup date
1011 # - or a start date in the future
1012 # - and it's not suspended
1013 # - and it doesn't have an expire date in the past
1015 # The "disable_setup_suspended" option is now obsolete; we never set the
1016 # setup date on a suspended package.
1017 if ( ! $options{recurring_only}
1018 and ! $options{cancel}
1019 and ( $options{'resetup'}
1020 || ( ! $cust_pkg->setup
1021 && ( ! $cust_pkg->start_date
1022 || $cust_pkg->start_date <= $cmp_time
1024 && ( ! $cust_pkg->getfield('susp') )
1027 and ( ! $cust_pkg->expire
1028 || $cust_pkg->expire > $cmp_time )
1032 warn " bill setup\n" if $DEBUG > 1;
1034 unless ( $cust_pkg->waive_setup ) {
1037 $setup = eval { $cust_pkg->calc_setup( $time, \@details, \%setup_param ) };
1038 return "$@ running calc_setup for $cust_pkg\n"
1041 # Only increment unitsetup here if there IS a setup fee.
1042 # prorate_defer_bill may cause calc_setup on a setup-stage package
1043 # to return zero, and the setup fee to be charged later. (This happens
1044 # when it's first billed on the prorate cutoff day. RT#31276.)
1046 $unitsetup = $cust_pkg->base_setup()
1050 if ( $setup_param{'billed_currency'} ) {
1051 $setup_billed_currency = delete $setup_param{'billed_currency'};
1052 $setup_billed_amount = delete $setup_param{'billed_amount'};
1056 if ( $cust_pkg->get('setup') ) {
1058 } elsif ( $cust_pkg->get('start_date') ) {
1059 # this allows start_date to be used to set the first bill date
1060 $cust_pkg->set('setup', $cust_pkg->get('start_date'));
1062 # if unspecified, start it right now
1063 $cust_pkg->set('setup', $time);
1066 $cust_pkg->setfield('start_date', '')
1067 if $cust_pkg->start_date;
1072 # bill recurring fee
1077 my @recur_discounts = ();
1078 my $recur_billed_currency = '';
1079 my $recur_billed_amount = 0;
1082 my $override_quantity;
1084 # Conditions for billing the recurring fee:
1085 # - the package doesn't have a future start date
1086 # - and it's not suspended
1087 # - unless suspend_bill is enabled on the package or package def
1088 # - but still not, if the package is on hold
1089 # - or it's suspended for a delayed cancellation
1090 # - and its next bill date is in the past
1091 # - or it doesn't have a next bill date yet
1092 # - or it's a one-time charge
1093 # - or it's a CDR plan with the "bill_every_call" option
1094 # - or it's being canceled
1095 # - and it doesn't have an expire date in the past (this can happen with
1097 # - again, unless it's being canceled
1098 if ( ! $cust_pkg->start_date
1101 || ( $cust_pkg->susp != $cust_pkg->order_date
1102 && ( $cust_pkg->option('suspend_bill',1)
1103 || ( $part_pkg->option('suspend_bill', 1)
1104 && ! $cust_pkg->option('no_suspend_bill',1)
1108 || $cust_pkg->is_status_delay_cancel
1111 ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time )
1112 || ( $part_pkg->plan eq 'voip_cdr'
1113 && $part_pkg->option('bill_every_call')
1118 ( ! $cust_pkg->expire
1119 || $cust_pkg->expire > $cmp_time
1124 # XXX should this be a package event? probably. events are called
1125 # at collection time at the moment, though...
1126 $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
1127 if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'};
1128 #don't want to reset usage just cause we want a line item??
1129 #&& $part_pkg->pkgpart == $real_pkgpart;
1131 warn " bill recur\n" if $DEBUG > 1;
1134 # XXX shared with $recur_prog
1135 $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
1139 #over two params! lets at least switch to a hashref for the rest...
1140 my $increment_next_bill = ( $part_pkg->freq ne '0'
1141 && ( $cust_pkg->getfield('bill') || 0 ) <= $cmp_time
1142 && !$options{cancel}
1144 my %param = ( %setup_param,
1145 'precommit_hooks' => $precommit_hooks,
1146 'increment_next_bill' => $increment_next_bill,
1147 'discounts' => \@recur_discounts,
1148 'real_pkgpart' => $real_pkgpart,
1149 'freq_override' => $options{freq_override} || '',
1153 my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
1155 # There may be some part_pkg for which this is wrong. Only those
1156 # which can_discount are supported.
1157 # (the UI should prevent adding discounts to these at the moment)
1159 warn "calling $method on cust_pkg ". $cust_pkg->pkgnum.
1160 " for pkgpart ". $cust_pkg->pkgpart.
1161 " with params ". join(' / ', map "$_=>$param{$_}", keys %param). "\n"
1164 $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
1165 return "$@ running $method for $cust_pkg\n"
1168 if ($recur eq 'NOTHING') {
1169 # then calc_cancel (or calc_recur but that's not used) has declined to
1170 # generate a recurring lineitem at all. treat this as zero, but also
1171 # try not to generate a lineitem.
1177 $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better
1179 if ( $param{'billed_currency'} ) {
1180 $recur_billed_currency = delete $param{'billed_currency'};
1181 $recur_billed_amount = delete $param{'billed_amount'};
1184 if ( $param{'override_quantity'} ) {
1185 $override_quantity = $param{'override_quantity'};
1186 $unitrecur = $recur / $override_quantity;
1189 if ( $increment_next_bill ) {
1193 if ( my $main_pkg = $cust_pkg->main_pkg ) {
1194 # supplemental package
1195 # to keep in sync with the main package, simulate billing at
1197 my $main_pkg_freq = $main_pkg->part_pkg->freq;
1198 my $supp_pkg_freq = $part_pkg->freq;
1199 if ( $supp_pkg_freq == 0 or $main_pkg_freq == 0 ) {
1200 # the UI should prevent setting up packages like this, but just
1202 return "unable to calculate supplemental package period ratio";
1204 my $ratio = $supp_pkg_freq / $main_pkg_freq;
1205 if ( $ratio == int($ratio) ) {
1206 # simple case: main package is X months, supp package is X*A months,
1207 # advance supp package to where the main package will be in A cycles.
1208 $next_bill = $sdate;
1210 $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq );
1213 # harder case: main package is X months, supp package is Y months.
1214 # advance supp package by Y months. then if they're within half a
1215 # month of each other, resync them. this may result in the period
1216 # not being exactly Y months.
1217 $next_bill = $part_pkg->add_freq( $sdate, $supp_pkg_freq );
1218 my $main_next_bill = $main_pkg->bill;
1219 if ( $main_pkg->bill <= $time ) {
1220 # then the main package has not yet been billed on this cycle;
1221 # predict what its bill date will be.
1223 $part_pkg->add_freq( $main_next_bill, $main_pkg_freq );
1225 if ( abs($main_next_bill - $next_bill) < 86400*15 ) {
1226 $next_bill = $main_next_bill;
1231 # the normal case, not a supplemental package
1232 $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
1233 return "unparsable frequency: ".
1234 ($options{freq_override} || $part_pkg->freq)
1235 if $next_bill == -1;
1238 #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
1239 # only for figuring next bill date, nothing else, so, reset $sdate again
1241 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1242 #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
1243 $cust_pkg->last_bill($sdate);
1245 $cust_pkg->setfield('bill', $next_bill );
1249 if ( $param{'setup_fee'} ) {
1250 # Add an additional setup fee at the billing stage.
1251 # Used for prorate_defer_bill.
1252 $setup += $param{'setup_fee'};
1253 $unitsetup = $cust_pkg->base_setup();
1257 if ( defined $param{'discount_left_setup'} ) {
1258 foreach my $discount_setup ( values %{$param{'discount_left_setup'}} ) {
1259 $setup -= $discount_setup;
1263 } # end of recurring fee
1265 warn "\$setup is undefined" unless defined($setup);
1266 warn "\$recur is undefined" unless defined($recur);
1267 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1270 # If there's line items, create em cust_bill_pkg records
1271 # If $cust_pkg has been modified, update it (if we're a real pkgpart)
1276 if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
1277 # hmm.. and if just the options are modified in some weird price plan?
1279 warn " package ". $cust_pkg->pkgnum. " modified; updating\n"
1282 my $error = $cust_pkg->replace( $old_cust_pkg,
1283 'depend_jobnum'=>$options{depend_jobnum},
1284 'options' => { $cust_pkg->options },
1286 unless $options{no_commit};
1287 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
1288 if $error; #just in case
1291 $setup = sprintf( "%.2f", $setup );
1292 $recur = sprintf( "%.2f", $recur );
1293 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1294 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1296 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1297 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1300 my $discount_show_always = $conf->exists('discount-show-always')
1301 && ( ($setup == 0 && scalar(@setup_discounts))
1302 || ($recur == 0 && scalar(@recur_discounts))
1307 || (!$part_pkg->hidden && $options{has_hidden}) #include some $0 lines
1308 || $discount_show_always
1309 || ($setup == 0 && $cust_pkg->_X_show_zero('setup'))
1310 || ($recur == 0 && $cust_pkg->_X_show_zero('recur'))
1314 warn " charges (setup=$setup, recur=$recur); adding line items\n"
1317 my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
1319 warn " adding customer package invoice detail: $_\n"
1320 foreach @cust_pkg_detail;
1322 push @details, @cust_pkg_detail;
1324 my $cust_bill_pkg = new FS::cust_bill_pkg {
1325 'pkgnum' => $cust_pkg->pkgnum,
1327 'unitsetup' => sprintf('%.2f', $unitsetup),
1328 'setup_billed_currency' => $setup_billed_currency,
1329 'setup_billed_amount' => $setup_billed_amount,
1331 'unitrecur' => sprintf('%.2f', $unitrecur),
1332 'recur_billed_currency' => $recur_billed_currency,
1333 'recur_billed_amount' => $recur_billed_amount,
1334 'quantity' => $override_quantity || $cust_pkg->quantity,
1335 'details' => \@details,
1336 'discounts' => [ @setup_discounts, @recur_discounts ],
1337 'hidden' => $part_pkg->hidden,
1338 'freq' => $part_pkg->freq,
1341 if ( $part_pkg->option('prorate_defer_bill',1)
1342 and !$hash{last_bill} ) {
1343 # both preceding and upcoming, technically
1344 $cust_bill_pkg->sdate( $cust_pkg->setup );
1345 $cust_bill_pkg->edate( $cust_pkg->bill );
1346 } elsif ( $part_pkg->recur_temporality eq 'preceding' ) {
1347 $cust_bill_pkg->sdate( $hash{last_bill} );
1348 $cust_bill_pkg->edate( $sdate - 86399 ); #60s*60m*24h-1
1349 $cust_bill_pkg->edate( $time ) if $options{cancel};
1350 } else { #if ( $part_pkg->recur_temporality eq 'upcoming' )
1351 $cust_bill_pkg->sdate( $sdate );
1352 $cust_bill_pkg->edate( $cust_pkg->bill );
1353 #$cust_bill_pkg->edate( $time ) if $options{cancel};
1356 $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
1357 unless $part_pkg->pkgpart == $real_pkgpart;
1359 $$total_setup += $setup;
1360 $$total_recur += $recur;
1366 my $error = $tax_engine->add_sale($cust_bill_pkg);
1367 return $error if $error;
1369 $cust_bill_pkg->set_display(
1370 part_pkg => $part_pkg,
1371 real_pkgpart => $real_pkgpart,
1374 push @$cust_bill_pkgs, $cust_bill_pkg;
1376 } #if $setup != 0 || $recur != 0
1384 =item _transfer_balance TO_PKG [ FROM_PKGNUM ]
1386 Takes one argument, a cust_pkg object that is being billed. This will
1387 be called only if the package was created by a package change, and has
1388 not been billed since the package change, and package balance tracking
1389 is enabled. The second argument can be an alternate package number to
1390 transfer the balance from; this should not be used externally.
1392 Transfers the balance from the previous package (now canceled) to
1393 this package, by crediting one package and creating an invoice item for
1394 the other. Inserts the credit and returns the invoice item (so that it
1395 can be added to an invoice that's being built).
1397 If the previous package was never billed, and was also created by a package
1398 change, then this will also transfer the balance from I<its> previous
1399 package, and so on, until reaching a package that either has been billed
1400 or was not created by a package change.
1404 my $balance_transfer_reason;
1406 sub _transfer_balance {
1408 my $cust_pkg = shift;
1409 my $from_pkgnum = shift || $cust_pkg->change_pkgnum;
1410 my $from_pkg = FS::cust_pkg->by_key($from_pkgnum);
1414 # if $from_pkg is not the first package in the chain, and it was never
1416 if ( $from_pkg->change_pkgnum and scalar($from_pkg->cust_bill_pkg) == 0 ) {
1417 @transfers = $self->_transfer_balance($cust_pkg, $from_pkg->change_pkgnum);
1420 my $prev_balance = $self->balance_pkgnum($from_pkgnum);
1421 if ( $prev_balance != 0 ) {
1422 $balance_transfer_reason ||= FS::reason->new_or_existing(
1423 'reason' => 'Package balance transfer',
1424 'type' => 'Internal adjustment',
1428 my $credit = FS::cust_credit->new({
1429 'custnum' => $self->custnum,
1430 'amount' => abs($prev_balance),
1431 'reasonnum' => $balance_transfer_reason->reasonnum,
1432 '_date' => $cust_pkg->change_date,
1435 my $cust_bill_pkg = FS::cust_bill_pkg->new({
1437 'recur' => abs($prev_balance),
1438 #'sdate' => $from_pkg->last_bill, # not sure about this
1439 #'edate' => $cust_pkg->change_date,
1440 'itemdesc' => $self->mt('Previous Balance, [_1]',
1441 $from_pkg->part_pkg->pkg),
1444 if ( $prev_balance > 0 ) {
1445 # credit the old package, charge the new one
1446 $credit->set('pkgnum', $from_pkgnum);
1447 $cust_bill_pkg->set('pkgnum', $cust_pkg->pkgnum);
1450 $credit->set('pkgnum', $cust_pkg->pkgnum);
1451 $cust_bill_pkg->set('pkgnum', $from_pkgnum);
1453 my $error = $credit->insert;
1454 die "error transferring package balance from #".$from_pkgnum.
1455 " to #".$cust_pkg->pkgnum.": $error\n" if $error;
1457 push @transfers, $cust_bill_pkg;
1458 } # $prev_balance != 0
1463 #### vestigial code ####
1465 =item handle_taxes TAXLISTHASH CUST_BILL_PKG [ OPTIONS ]
1467 This is _handle_taxes. It's called once for each cust_bill_pkg generated
1470 TAXLISTHASH is a hashref shared across the entire invoice. It looks like
1473 'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
1474 'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
1477 'cust_main_county' can also be 'tax_rate'. The first object in the array
1478 is always the cust_main_county or tax_rate identified by the key.
1480 That "..." is a list of FS::cust_bill_pkg objects that will be fed to
1481 the 'taxline' method to calculate the amount of the tax. This doesn't
1482 happen until calculate_taxes, though.
1484 OPTIONS may include:
1485 - part_item: a part_pkg or part_fee object to be used as the package/fee
1487 - location: a cust_location to be used as the billing location.
1488 - cancel: true if this package is being billed on cancellation. This
1489 allows tax to be calculated on usage charges only.
1491 If not supplied, part_item will be inferred from the pkgnum or feepart of the
1492 cust_bill_pkg, and location from the pkgnum (or, for fees, the invnum and
1493 the customer's default service location).
1495 This method will also calculate exemptions for any taxes that apply to the
1496 line item (using the C<set_exemptions> method of L<FS::cust_bill_pkg>) and
1497 attach them. This is the only place C<set_exemptions> is called in normal
1504 my $taxlisthash = shift;
1505 my $cust_bill_pkg = shift;
1508 # at this point I realize that we have enough information to infer all this
1509 # stuff, instead of passing around giant honking argument lists
1510 my $location = $options{location} || $cust_bill_pkg->tax_location;
1511 my $part_item = $options{part_item} || $cust_bill_pkg->part_X;
1513 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1515 return if ( $self->payby eq 'COMP' ); #dubious
1517 if ( $conf->config('enable_taxproducts')
1518 && ( scalar($part_item->part_pkg_taxoverride)
1519 || $part_item->has_taxproduct
1524 # EXTERNAL TAX RATES (via tax_rate)
1525 my %cust_bill_pkg = ();
1529 my $usage = $cust_bill_pkg->usage || 0;
1530 push @classes, $cust_bill_pkg->usage_classes if $usage;
1531 push @classes, 'setup' if $cust_bill_pkg->setup and !$options{cancel};
1532 push @classes, 'recur' if ($cust_bill_pkg->recur - $usage)
1533 and !$options{cancel};
1534 # that's better--probably don't even need $options{cancel} now
1535 # but leave it for now, just to be safe
1537 # About $options{cancel}: This protects against charging per-line or
1538 # per-customer or other flat-rate surcharges on a package that's being
1539 # billed on cancellation (which is an out-of-cycle bill and should only
1540 # have usage charges). See RT#29443.
1542 # customer exemption is now handled in the 'taxline' method
1543 #my $exempt = $conf->exists('cust_class-tax_exempt')
1544 # ? ( $self->cust_class ? $self->cust_class->tax : '' )
1546 # standardize this just to be sure
1547 #$exempt = ($exempt eq 'Y') ? 'Y' : '';
1551 unless (exists $taxes{''}) {
1552 # unsure what purpose this serves, but last time I deleted something
1553 # from here just because I didn't see the point, it actually did
1554 # something important.
1555 my $err_or_ref = $self->_gather_taxes($part_item, '', $location);
1556 return $err_or_ref unless ref($err_or_ref);
1557 $taxes{''} = $err_or_ref;
1560 # NO DISINTEGRATIONS.
1561 # my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
1563 # do not call taxline() with any argument except the entire set of
1564 # cust_bill_pkgs on an invoice that are eligible for the tax.
1566 # only calculate exemptions once for each tax rate, even if it's used
1567 # for multiple classes
1570 foreach my $class (@classes) {
1571 my $err_or_ref = $self->_gather_taxes($part_item, $class, $location);
1572 return $err_or_ref unless ref($err_or_ref);
1573 my @taxes = @$err_or_ref;
1577 foreach my $tax ( @taxes ) {
1579 my $tax_id = ref( $tax ). ' '. $tax->taxnum;
1580 # $taxlisthash: keys are tax identifiers ('FS::tax_rate 123456').
1581 # Values are arrayrefs, first the tax object (cust_main_county
1582 # or tax_rate), then the cust_bill_pkg object that the
1583 # tax applies to, then the tax class (setup, recur, usage classnum).
1584 $taxlisthash->{ $tax_id } ||= [ $tax ];
1585 push @{ $taxlisthash->{ $tax_id } }, $cust_bill_pkg, $class;
1587 # determine any exemptions that apply
1588 if (!$tax_seen{$tax_id}) {
1589 $cust_bill_pkg->set_exemptions( $tax, custnum => $self->custnum );
1590 $tax_seen{$tax_id} = 1;
1593 # tax on tax will be done later, when we actually create the tax
1601 # INTERNAL TAX RATES (cust_main_county)
1603 # We fetch taxes even if the customer is completely exempt,
1604 # because we need to record that fact.
1606 my @loc_keys = qw( district city county state country );
1607 my %taxhash = map { $_ => $location->$_ } @loc_keys;
1609 $taxhash{'taxclass'} = $part_item->taxclass;
1611 warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
1613 my @taxes = (); # entries are cust_main_county objects
1614 my %taxhash_elim = %taxhash;
1615 my @elim = qw( district city county state );
1618 #first try a match with taxclass
1619 @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
1621 if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
1622 #then try a match without taxclass
1623 my %no_taxclass = %taxhash_elim;
1624 $no_taxclass{ 'taxclass' } = '';
1625 @taxes = qsearch( 'cust_main_county', \%no_taxclass );
1628 $taxhash_elim{ shift(@elim) } = '';
1630 } while ( !scalar(@taxes) && scalar(@elim) );
1633 my $tax_id = 'cust_main_county '.$_->taxnum;
1634 $taxlisthash->{$tax_id} ||= [ $_ ];
1635 $cust_bill_pkg->set_exemptions($_, custnum => $self->custnum);
1636 push @{ $taxlisthash->{$tax_id} }, $cust_bill_pkg;
1643 =item _gather_taxes PART_ITEM CLASS CUST_LOCATION
1645 Internal method used with vendor-provided tax tables. PART_ITEM is a part_pkg
1646 or part_fee (which will define the tax eligibility of the product), CLASS is
1647 'setup', 'recur', null, or a C<usage_class> number, and CUST_LOCATION is the
1648 location where the service was provided (or billed, depending on
1649 configuration). Returns an arrayref of L<FS::tax_rate> objects that
1650 can apply to this line item.
1656 my $part_item = shift;
1658 my $location = shift;
1660 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1662 my $geocode = $location->geocode('cch');
1664 [ $part_item->tax_rates('cch', $geocode, $class) ]
1668 #### end vestigial code ####
1670 =item collect [ HASHREF | OPTION => VALUE ... ]
1672 (Attempt to) collect money for this customer's outstanding invoices (see
1673 L<FS::cust_bill>). Usually used after the bill method.
1675 Actions are now triggered by billing events; see L<FS::part_event> and the
1676 billing events web interface. Old-style invoice events (see
1677 L<FS::part_bill_event>) have been deprecated.
1679 If there is an error, returns the error, otherwise returns false.
1681 Options are passed as name-value pairs.
1683 Currently available options are:
1689 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.
1693 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1697 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1701 set true to surpress email card/ACH decline notices.
1705 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)
1711 # allows for one time override of normal customer billing method
1716 my( $self, %options ) = @_;
1718 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1720 my $invoice_time = $options{'invoice_time'} || time;
1723 local $SIG{HUP} = 'IGNORE';
1724 local $SIG{INT} = 'IGNORE';
1725 local $SIG{QUIT} = 'IGNORE';
1726 local $SIG{TERM} = 'IGNORE';
1727 local $SIG{TSTP} = 'IGNORE';
1728 local $SIG{PIPE} = 'IGNORE';
1730 my $oldAutoCommit = $FS::UID::AutoCommit;
1731 local $FS::UID::AutoCommit = 0;
1734 $self->select_for_update; #mutex
1737 my $balance = $self->balance;
1738 warn "$me collect customer ". $self->custnum. ": balance $balance\n"
1741 if ( exists($options{'retry_card'}) ) {
1742 carp 'retry_card option passed to collect is deprecated; use retry';
1743 $options{'retry'} ||= $options{'retry_card'};
1745 if ( exists($options{'retry'}) && $options{'retry'} ) {
1746 my $error = $self->retry_realtime;
1748 $dbh->rollback if $oldAutoCommit;
1753 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1755 #never want to roll back an event just because it returned an error
1756 local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1758 $self->do_cust_event(
1759 'debug' => ( $options{'debug'} || 0 ),
1760 'time' => $invoice_time,
1761 'check_freq' => $options{'check_freq'},
1762 'stage' => 'collect',
1767 =item retry_realtime
1769 Schedules realtime / batch credit card / electronic check / LEC billing
1770 events for for retry. Useful if card information has changed or manual
1771 retry is desired. The 'collect' method must be called to actually retry
1774 Implementation details: For either this customer, or for each of this
1775 customer's open invoices, changes the status of the first "done" (with
1776 statustext error) realtime processing event to "failed".
1780 sub retry_realtime {
1783 local $SIG{HUP} = 'IGNORE';
1784 local $SIG{INT} = 'IGNORE';
1785 local $SIG{QUIT} = 'IGNORE';
1786 local $SIG{TERM} = 'IGNORE';
1787 local $SIG{TSTP} = 'IGNORE';
1788 local $SIG{PIPE} = 'IGNORE';
1790 my $oldAutoCommit = $FS::UID::AutoCommit;
1791 local $FS::UID::AutoCommit = 0;
1794 #a little false laziness w/due_cust_event (not too bad, really)
1796 # I guess this is always as of now?
1797 my $join = FS::part_event_condition->join_conditions_sql('', 'time' => time);
1798 my $order = FS::part_event_condition->order_conditions_sql;
1801 . join ( ' OR ' , map {
1802 my $cust_join = FS::part_event->eventtables_cust_join->{$_} || '';
1803 my $custnum = FS::part_event->eventtables_custnum->{$_};
1804 "( part_event.eventtable = " . dbh->quote($_)
1805 . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key
1806 . " from $_ $cust_join"
1807 . " where $custnum = " . dbh->quote( $self->custnum ) . "))" ;
1808 } FS::part_event->eventtables)
1811 #here is the agent virtualization
1812 my $agent_virt = " ( part_event.agentnum IS NULL
1813 OR part_event.agentnum = ". $self->agentnum. ' )';
1815 #XXX this shouldn't be hardcoded, actions should declare it...
1816 my @realtime_events = qw(
1817 cust_bill_realtime_card
1818 cust_bill_realtime_check
1819 cust_bill_realtime_lec
1823 my $is_realtime_event =
1824 ' part_event.action IN ( '.
1825 join(',', map "'$_'", @realtime_events ).
1828 my $batch_or_statustext =
1829 "( part_event.action = 'cust_bill_batch'
1830 OR ( statustext IS NOT NULL AND statustext != '' )
1834 my @cust_event = qsearch({
1835 'table' => 'cust_event',
1836 'select' => 'cust_event.*',
1837 'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
1838 'hashref' => { 'status' => 'done' },
1839 'extra_sql' => " AND $batch_or_statustext ".
1840 " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
1843 my %seen_invnum = ();
1844 foreach my $cust_event (@cust_event) {
1846 #max one for the customer, one for each open invoice
1847 my $cust_X = $cust_event->cust_X;
1848 next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
1852 or $cust_event->part_event->eventtable eq 'cust_bill'
1855 my $error = $cust_event->retry;
1857 $dbh->rollback if $oldAutoCommit;
1858 return "error scheduling event for retry: $error";
1863 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1868 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
1870 Runs billing events; see L<FS::part_event> and the billing events web
1873 If there is an error, returns the error, otherwise returns false.
1875 Options are passed as name-value pairs.
1877 Currently available options are:
1883 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.
1887 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1891 "collect" (the default) or "pre-bill"
1895 set true to surpress email card/ACH decline notices.
1899 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)
1906 # allows for one time override of normal customer billing method
1910 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1913 my( $self, %options ) = @_;
1915 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1917 my $time = $options{'time'} || time;
1920 local $SIG{HUP} = 'IGNORE';
1921 local $SIG{INT} = 'IGNORE';
1922 local $SIG{QUIT} = 'IGNORE';
1923 local $SIG{TERM} = 'IGNORE';
1924 local $SIG{TSTP} = 'IGNORE';
1925 local $SIG{PIPE} = 'IGNORE';
1927 my $oldAutoCommit = $FS::UID::AutoCommit;
1928 local $FS::UID::AutoCommit = 0;
1931 $self->select_for_update; #mutex
1934 my $balance = $self->balance;
1935 warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
1938 # if ( exists($options{'retry_card'}) ) {
1939 # carp 'retry_card option passed to collect is deprecated; use retry';
1940 # $options{'retry'} ||= $options{'retry_card'};
1942 # if ( exists($options{'retry'}) && $options{'retry'} ) {
1943 # my $error = $self->retry_realtime;
1945 # $dbh->rollback if $oldAutoCommit;
1950 # false laziness w/pay_batch::import_results
1952 my $due_cust_event = $self->due_cust_event(
1953 'debug' => ( $options{'debug'} || 0 ),
1955 'check_freq' => $options{'check_freq'},
1956 'stage' => ( $options{'stage'} || 'collect' ),
1958 unless( ref($due_cust_event) ) {
1959 $dbh->rollback if $oldAutoCommit;
1960 return $due_cust_event;
1963 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1964 #never want to roll back an event just because it or a different one
1966 local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1968 foreach my $cust_event ( @$due_cust_event ) {
1972 #re-eval event conditions (a previous event could have changed things)
1973 unless ( $cust_event->test_conditions ) {
1974 #don't leave stray "new/locked" records around
1975 my $error = $cust_event->delete;
1976 return $error if $error;
1981 local $FS::cust_main::Billing_Realtime::realtime_bop_decline_quiet = 1
1982 if $options{'quiet'};
1983 warn " running cust_event ". $cust_event->eventnum. "\n"
1986 #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
1987 if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
1988 #XXX wtf is this? figure out a proper dealio with return value
2000 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
2002 Inserts database records for and returns an ordered listref of new events due
2003 for this customer, as FS::cust_event objects (see L<FS::cust_event>). If no
2004 events are due, an empty listref is returned. If there is an error, returns a
2005 scalar error message.
2007 To actually run the events, call each event's test_condition method, and if
2008 still true, call the event's do_event method.
2010 Options are passed as a hashref or as a list of name-value pairs. Available
2017 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.
2021 "collect" (the default) or "pre-bill"
2025 "Current time" for the events.
2029 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)
2033 Only return events for the specified eventtable (by default, events of all eventtables are returned)
2037 Explicitly pass the objects to be tested (typically used with eventtable).
2041 Set to true to return the objects, but not actually insert them into the
2048 sub due_cust_event {
2050 my %opt = ref($_[0]) ? %{ $_[0] } : @_;
2053 #my $DEBUG = $opt{'debug'}
2054 $opt{'debug'} ||= 0; # silence some warnings
2055 local($DEBUG) = $opt{'debug'}
2056 if $opt{'debug'} > $DEBUG;
2057 $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
2059 warn "$me due_cust_event called with options ".
2060 join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
2063 $opt{'time'} ||= time;
2065 local $SIG{HUP} = 'IGNORE';
2066 local $SIG{INT} = 'IGNORE';
2067 local $SIG{QUIT} = 'IGNORE';
2068 local $SIG{TERM} = 'IGNORE';
2069 local $SIG{TSTP} = 'IGNORE';
2070 local $SIG{PIPE} = 'IGNORE';
2072 my $oldAutoCommit = $FS::UID::AutoCommit;
2073 local $FS::UID::AutoCommit = 0;
2076 $self->select_for_update #mutex
2077 unless $opt{testonly};
2080 # find possible events (initial search)
2083 my @cust_event = ();
2085 my @eventtable = $opt{'eventtable'}
2086 ? ( $opt{'eventtable'} )
2087 : FS::part_event->eventtables_runorder;
2089 my $check_freq = $opt{'check_freq'} || '1d';
2091 foreach my $eventtable ( @eventtable ) {
2094 if ( $opt{'objects'} ) {
2096 @objects = @{ $opt{'objects'} };
2098 } elsif ( $eventtable eq 'cust_main' ) {
2100 @objects = ( $self );
2104 my $cm_join = " LEFT JOIN cust_main USING ( custnum )";
2105 # linkage not needed here because FS::cust_main->$eventtable will
2108 #some false laziness w/Cron::bill bill_where
2110 my $join = FS::part_event_condition->join_conditions_sql( $eventtable,
2111 'time' => $opt{'time'});
2112 my $where = FS::part_event_condition->where_conditions_sql($eventtable,
2113 'time'=>$opt{'time'},
2115 $where = $where ? "AND $where" : '';
2117 my $are_part_event =
2118 "EXISTS ( SELECT 1 FROM part_event $join
2119 WHERE check_freq = '$check_freq'
2120 AND eventtable = '$eventtable'
2121 AND ( disabled = '' OR disabled IS NULL )
2127 @objects = $self->$eventtable(
2128 'addl_from' => $cm_join,
2129 'extra_sql' => " AND $are_part_event",
2131 } # if ( !$opt{objects} and $eventtable ne 'cust_main' )
2133 my @e_cust_event = ();
2135 my $linkage = FS::part_event->eventtables_cust_join->{$eventtable} || '';
2137 my $cross = "CROSS JOIN $eventtable $linkage";
2138 $cross .= ' LEFT JOIN cust_main USING ( custnum )'
2139 unless $eventtable eq 'cust_main';
2141 foreach my $object ( @objects ) {
2143 #this first search uses the condition_sql magic for optimization.
2144 #the more possible events we can eliminate in this step the better
2146 my $cross_where = '';
2147 my $pkey = $object->primary_key;
2148 $cross_where = "$eventtable.$pkey = ". $object->$pkey();
2150 my $join = FS::part_event_condition->join_conditions_sql( $eventtable,
2151 'time' => $opt{'time'});
2153 FS::part_event_condition->where_conditions_sql( $eventtable,
2154 'time'=>$opt{'time'}
2156 my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
2158 $extra_sql = "AND $extra_sql" if $extra_sql;
2160 #here is the agent virtualization
2161 $extra_sql .= " AND ( part_event.agentnum IS NULL
2162 OR part_event.agentnum = ". $self->agentnum. ' )';
2164 $extra_sql .= " $order";
2166 warn "searching for events for $eventtable ". $object->$pkey. "\n"
2167 if $opt{'debug'} > 2;
2168 my @part_event = qsearch( {
2169 'debug' => ( $opt{'debug'} > 3 ? 1 : 0 ),
2170 'select' => 'part_event.*',
2171 'table' => 'part_event',
2172 'addl_from' => "$cross $join",
2173 'hashref' => { 'check_freq' => $check_freq,
2174 'eventtable' => $eventtable,
2177 'extra_sql' => "AND $cross_where $extra_sql",
2181 my $pkey = $object->primary_key;
2182 warn " ". scalar(@part_event).
2183 " possible events found for $eventtable ". $object->$pkey(). "\n";
2186 push @e_cust_event, map {
2187 $_->new_cust_event($object, 'time' => $opt{'time'})
2192 warn " ". scalar(@e_cust_event).
2193 " subtotal possible cust events found for $eventtable\n"
2196 push @cust_event, @e_cust_event;
2200 warn " ". scalar(@cust_event).
2201 " total possible cust events found in initial search\n"
2209 $opt{stage} ||= 'collect';
2211 grep { my $stage = $_->part_event->event_stage;
2212 $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
2222 @cust_event = grep $_->test_conditions( 'stats_hashref' => \%unsat ),
2225 warn " ". scalar(@cust_event). " cust events left satisfying conditions\n"
2228 warn " invalid conditions not eliminated with condition_sql:\n".
2229 join('', map " $_: ".$unsat{$_}."\n", keys %unsat )
2230 if keys %unsat && $DEBUG; # > 1;
2236 unless( $opt{testonly} ) {
2237 foreach my $cust_event ( @cust_event ) {
2239 my $error = $cust_event->insert();
2241 $dbh->rollback if $oldAutoCommit;
2248 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2254 warn " returning events: ". Dumper(@cust_event). "\n"
2261 =item apply_payments_and_credits [ OPTION => VALUE ... ]
2263 Applies unapplied payments and credits.
2264 Payments with the no_auto_apply flag set will not be applied.
2266 In most cases, this new method should be used in place of sequential
2267 apply_payments and apply_credits methods.
2269 A hash of optional arguments may be passed. Currently "manual" is supported.
2270 If true, a payment receipt is sent instead of a statement when
2271 'payment_receipt_email' configuration option is set.
2273 If there is an error, returns the error, otherwise returns false.
2277 sub apply_payments_and_credits {
2278 my( $self, %options ) = @_;
2280 local $SIG{HUP} = 'IGNORE';
2281 local $SIG{INT} = 'IGNORE';
2282 local $SIG{QUIT} = 'IGNORE';
2283 local $SIG{TERM} = 'IGNORE';
2284 local $SIG{TSTP} = 'IGNORE';
2285 local $SIG{PIPE} = 'IGNORE';
2287 my $oldAutoCommit = $FS::UID::AutoCommit;
2288 local $FS::UID::AutoCommit = 0;
2291 $self->select_for_update; #mutex
2293 foreach my $cust_bill ( $self->open_cust_bill ) {
2294 my $error = $cust_bill->apply_payments_and_credits(%options);
2296 $dbh->rollback if $oldAutoCommit;
2297 return "Error applying: $error";
2301 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2306 =item apply_credits OPTION => VALUE ...
2308 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2309 to outstanding invoice balances in chronological order (or reverse
2310 chronological order if the I<order> option is set to B<newest>) and returns the
2311 value of any remaining unapplied credits available for refund (see
2312 L<FS::cust_refund>).
2314 Dies if there is an error.
2322 local $SIG{HUP} = 'IGNORE';
2323 local $SIG{INT} = 'IGNORE';
2324 local $SIG{QUIT} = 'IGNORE';
2325 local $SIG{TERM} = 'IGNORE';
2326 local $SIG{TSTP} = 'IGNORE';
2327 local $SIG{PIPE} = 'IGNORE';
2329 my $oldAutoCommit = $FS::UID::AutoCommit;
2330 local $FS::UID::AutoCommit = 0;
2333 $self->select_for_update; #mutex
2335 unless ( $self->total_unapplied_credits ) {
2336 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2340 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2341 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2343 my @invoices = $self->open_cust_bill;
2344 @invoices = sort { $b->_date <=> $a->_date } @invoices
2345 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2347 if ( $conf->exists('pkg-balances') ) {
2348 # limit @credits to those w/ a pkgnum grepped from $self
2350 foreach my $i (@invoices) {
2351 foreach my $li ( $i->cust_bill_pkg ) {
2352 $pkgnums{$li->pkgnum} = 1;
2355 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
2360 foreach my $cust_bill ( @invoices ) {
2362 if ( !defined($credit) || $credit->credited == 0) {
2363 $credit = pop @credits or last;
2367 if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
2368 $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
2370 $owed = $cust_bill->owed;
2372 unless ( $owed > 0 ) {
2373 push @credits, $credit;
2377 my $amount = min( $credit->credited, $owed );
2379 my $cust_credit_bill = new FS::cust_credit_bill ( {
2380 'crednum' => $credit->crednum,
2381 'invnum' => $cust_bill->invnum,
2382 'amount' => $amount,
2384 $cust_credit_bill->pkgnum( $credit->pkgnum )
2385 if $conf->exists('pkg-balances') && $credit->pkgnum;
2386 my $error = $cust_credit_bill->insert;
2388 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2392 redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2396 my $total_unapplied_credits = $self->total_unapplied_credits;
2398 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2400 return $total_unapplied_credits;
2403 =item apply_payments [ OPTION => VALUE ... ]
2405 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2406 to outstanding invoice balances in chronological order.
2407 Payments with the no_auto_apply flag set will not be applied.
2409 #and returns the value of any remaining unapplied payments.
2411 A hash of optional arguments may be passed. Currently "manual" is supported.
2412 If true, a payment receipt is sent instead of a statement when
2413 'payment_receipt_email' configuration option is set.
2415 Dies if there is an error.
2419 sub apply_payments {
2420 my( $self, %options ) = @_;
2422 local $SIG{HUP} = 'IGNORE';
2423 local $SIG{INT} = 'IGNORE';
2424 local $SIG{QUIT} = 'IGNORE';
2425 local $SIG{TERM} = 'IGNORE';
2426 local $SIG{TSTP} = 'IGNORE';
2427 local $SIG{PIPE} = 'IGNORE';
2429 my $oldAutoCommit = $FS::UID::AutoCommit;
2430 local $FS::UID::AutoCommit = 0;
2433 $self->select_for_update; #mutex
2437 my @payments = grep { !$_->no_auto_apply } $self->unapplied_cust_pay;
2439 my @invoices = $self->open_cust_bill;
2441 if ( $conf->exists('pkg-balances') ) {
2442 # limit @payments to those w/ a pkgnum grepped from $self
2444 foreach my $i (@invoices) {
2445 foreach my $li ( $i->cust_bill_pkg ) {
2446 $pkgnums{$li->pkgnum} = 1;
2449 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
2454 foreach my $cust_bill ( @invoices ) {
2456 if ( !defined($payment) || $payment->unapplied == 0 ) {
2457 $payment = pop @payments or last;
2461 if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
2462 $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
2464 $owed = $cust_bill->owed;
2466 unless ( $owed > 0 ) {
2467 push @payments, $payment;
2471 my $amount = min( $payment->unapplied, $owed );
2474 'paynum' => $payment->paynum,
2475 'invnum' => $cust_bill->invnum,
2476 'amount' => $amount,
2478 $cbp->{_date} = $payment->_date
2479 if $options{'manual'} && $options{'backdate_application'};
2480 my $cust_bill_pay = new FS::cust_bill_pay($cbp);
2481 $cust_bill_pay->pkgnum( $payment->pkgnum )
2482 if $conf->exists('pkg-balances') && $payment->pkgnum;
2483 my $error = $cust_bill_pay->insert(%options);
2485 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2489 redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2493 my $total_unapplied_payments = $self->total_unapplied_payments;
2495 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2497 return $total_unapplied_payments;
2507 suspend_adjourned_pkgs
2508 unsuspend_resumed_pkgs
2511 (do_cust_event pre-bill)
2513 _omit_zero_value_bundles
2516 apply_payments_and_credits
2525 L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>