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 foreach my $cust_pkg ( @cancel_pkgs ) {
196 my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
197 my $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum,
198 'reason_otaker' => $cpr->otaker,
203 push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
206 join(' / ', @errors);
210 sub suspend_adjourned_pkgs {
211 my ( $self, $time, %options ) = @_;
213 my @susp_pkgs = $self->ncancelled_pkgs( {
215 " AND ( susp IS NULL OR susp = 0 )
216 AND ( ( bill IS NOT NULL AND bill != 0 AND bill < $time )
217 OR ( adjourn IS NOT NULL AND adjourn != 0 AND adjourn <= $time )
222 #only because there's no SQL test for is_prepaid :/
224 grep { ( $_->part_pkg->is_prepaid
229 && $_->adjourn <= $time
237 foreach my $cust_pkg ( @susp_pkgs ) {
238 my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
239 if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
240 my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
241 'reason_otaker' => $cpr->otaker
245 push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
248 join(' / ', @errors);
252 sub unsuspend_resumed_pkgs {
253 my ( $self, $time, %options ) = @_;
255 my @unsusp_pkgs = $self->ncancelled_pkgs( {
256 'extra_sql' => " AND resume IS NOT NULL AND resume > 0 AND resume <= $time "
261 foreach my $cust_pkg ( @unsusp_pkgs ) {
262 my $error = $cust_pkg->unsuspend( 'time' => $time );
263 push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
266 join(' / ', @errors);
272 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
273 conjunction with the collect method by calling B<bill_and_collect>.
275 If there is an error, returns the error, otherwise returns false.
277 Options are passed as name-value pairs. Currently available options are:
283 If set true, re-charges setup fees.
287 If set true then only bill recurring charges, not setup, usage, one time
292 If set, then override the normal frequency and look for a part_pkg_discount
293 to take at that frequency. This is appropriate only when the normal
294 frequency for all packages is monthly, and is an error otherwise. Use
295 C<pkg_list> to limit the set of packages included in billing.
299 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:
303 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
307 An array ref of specific packages (objects) to attempt billing, instead trying all of them.
309 $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
313 A hashref of pkgparts to exclude from this billing run (can also be specified as a comma-separated scalar).
317 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.
321 This boolean value informs the us that the package is being cancelled. This
322 typically might mean not charging the normal recurring fee but only usage
323 fees since the last billing. Setup charges may be charged. Not all package
324 plans support this feature (they tend to charge 0).
328 Prevent the resetting of usage limits during this call.
332 Do not save the generated bill in the database. Useful with return_bill
336 A list reference on which the generated bill(s) will be returned.
340 Optional terms to be printed on this invoice. Otherwise, customer-specific
341 terms or the default terms are used.
348 my( $self, %options ) = @_;
350 return '' if $self->payby eq 'COMP';
352 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
354 warn "$me bill customer ". $self->custnum. "\n"
357 my $time = $options{'time'} || time;
358 my $invoice_time = $options{'invoice_time'} || $time;
360 $options{'not_pkgpart'} ||= {};
361 $options{'not_pkgpart'} = { map { $_ => 1 }
362 split(/\s*,\s*/, $options{'not_pkgpart'})
364 unless ref($options{'not_pkgpart'});
366 local $SIG{HUP} = 'IGNORE';
367 local $SIG{INT} = 'IGNORE';
368 local $SIG{QUIT} = 'IGNORE';
369 local $SIG{TERM} = 'IGNORE';
370 local $SIG{TSTP} = 'IGNORE';
371 local $SIG{PIPE} = 'IGNORE';
373 my $oldAutoCommit = $FS::UID::AutoCommit;
374 local $FS::UID::AutoCommit = 0;
377 warn "$me acquiring lock on customer ". $self->custnum. "\n"
380 $self->select_for_update; #mutex
382 warn "$me running pre-bill events for customer ". $self->custnum. "\n"
385 my $error = $self->do_cust_event(
386 'debug' => ( $options{'debug'} || 0 ),
387 'time' => $invoice_time,
388 'check_freq' => $options{'check_freq'},
389 'stage' => 'pre-bill',
391 unless $options{no_commit};
393 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
397 warn "$me done running pre-bill events for customer ". $self->custnum. "\n"
400 #keep auto-charge and non-auto-charge line items separate
401 my @passes = ( '', 'no_auto' );
403 my %cust_bill_pkg = map { $_ => [] } @passes;
406 # find the packages which are due for billing, find out how much they are
407 # & generate invoice database.
410 my %total_setup = map { my $z = 0; $_ => \$z; } @passes;
411 my %total_recur = map { my $z = 0; $_ => \$z; } @passes;
413 my %taxlisthash = map { $_ => {} } @passes;
415 my @precommit_hooks = ();
417 $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ]; #param checks?
419 foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
421 next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
423 warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG;
425 #? to avoid use of uninitialized value errors... ?
426 $cust_pkg->setfield('bill', '')
427 unless defined($cust_pkg->bill);
429 #my $part_pkg = $cust_pkg->part_pkg;
431 my $real_pkgpart = $cust_pkg->pkgpart;
432 my %hash = $cust_pkg->hash;
434 # we could implement this bit as FS::part_pkg::has_hidden, but we already
435 # suffer from performance issues
436 $options{has_hidden} = 0;
437 my @part_pkg = $cust_pkg->part_pkg->self_and_bill_linked;
438 $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
440 # if this package was changed from another package,
441 # and it hasn't been billed since then,
442 # and package balances are enabled,
443 if ( $cust_pkg->change_pkgnum
444 and $cust_pkg->change_date >= ($cust_pkg->last_bill || 0)
445 and $cust_pkg->change_date < $invoice_time
446 and $conf->exists('pkg-balances') )
448 # _transfer_balance will also create the appropriate credit
449 my @transfer_items = $self->_transfer_balance($cust_pkg);
450 # $part_pkg[0] is the "real" part_pkg
451 my $pass = ($cust_pkg->no_auto || $part_pkg[0]->no_auto) ?
453 push @{ $cust_bill_pkg{$pass} }, @transfer_items;
454 # treating this as recur, just because most charges are recur...
455 ${$total_recur{$pass}} += $_->recur foreach @transfer_items;
458 foreach my $part_pkg ( @part_pkg ) {
460 $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
462 my $pass = ($cust_pkg->no_auto || $part_pkg->no_auto) ? 'no_auto' : '';
464 my $next_bill = $cust_pkg->getfield('bill') || 0;
466 # let this run once if this is the last bill upon cancellation
467 while ( $next_bill <= $time or $options{cancel} ) {
469 $self->_make_lines( 'part_pkg' => $part_pkg,
470 'cust_pkg' => $cust_pkg,
471 'precommit_hooks' => \@precommit_hooks,
472 'line_items' => $cust_bill_pkg{$pass},
473 'setup' => $total_setup{$pass},
474 'recur' => $total_recur{$pass},
475 'tax_matrix' => $taxlisthash{$pass},
477 'real_pkgpart' => $real_pkgpart,
478 'options' => \%options,
481 # Stop if anything goes wrong
484 # or if we're not incrementing the bill date.
485 last if ($cust_pkg->getfield('bill') || 0) == $next_bill;
487 # or if we're letting it run only once
488 last if $options{cancel};
490 $next_bill = $cust_pkg->getfield('bill') || 0;
492 #stop if -o was passed to freeside-daily
493 last if $options{'one_recur'};
496 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
500 } #foreach my $part_pkg
502 } #foreach my $cust_pkg
504 #if the customer isn't on an automatic payby, everything can go on a single
506 #if ( $cust_main->payby !~ /^(CARD|CHEK)$/ ) {
507 #merge everything into one list
510 foreach my $pass (@passes) { # keys %cust_bill_pkg ) {
512 my @cust_bill_pkg = _omit_zero_value_bundles(@{ $cust_bill_pkg{$pass} });
514 next unless @cust_bill_pkg; #don't create an invoice w/o line items
516 warn "$me billing pass $pass\n"
517 #.Dumper(\@cust_bill_pkg)."\n"
520 if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
521 !$conf->exists('postal_invoice-recurring_only')
525 my $postal_pkg = $self->charge_postal_fee();
526 if ( $postal_pkg && !ref( $postal_pkg ) ) {
528 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
529 return "can't charge postal invoice fee for customer ".
530 $self->custnum. ": $postal_pkg";
532 } elsif ( $postal_pkg ) {
534 my $real_pkgpart = $postal_pkg->pkgpart;
535 # we could implement this bit as FS::part_pkg::has_hidden, but we already
536 # suffer from performance issues
537 $options{has_hidden} = 0;
538 my @part_pkg = $postal_pkg->part_pkg->self_and_bill_linked;
539 $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
541 foreach my $part_pkg ( @part_pkg ) {
542 my %postal_options = %options;
543 delete $postal_options{cancel};
545 $self->_make_lines( 'part_pkg' => $part_pkg,
546 'cust_pkg' => $postal_pkg,
547 'precommit_hooks' => \@precommit_hooks,
548 'line_items' => \@cust_bill_pkg,
549 'setup' => $total_setup{$pass},
550 'recur' => $total_recur{$pass},
551 'tax_matrix' => $taxlisthash{$pass},
553 'real_pkgpart' => $real_pkgpart,
554 'options' => \%postal_options,
557 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
562 # it's silly to have a zero value postal_pkg, but....
563 @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
569 my $listref_or_error =
570 $self->calculate_taxes( \@cust_bill_pkg, $taxlisthash{$pass}, $invoice_time);
572 unless ( ref( $listref_or_error ) ) {
573 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
574 return $listref_or_error;
577 foreach my $taxline ( @$listref_or_error ) {
578 ${ $total_setup{$pass} } =
579 sprintf('%.2f', ${ $total_setup{$pass} } + $taxline->setup );
580 push @cust_bill_pkg, $taxline;
584 warn "adding tax adjustments...\n" if $DEBUG > 2;
585 foreach my $cust_tax_adjustment (
586 qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum,
592 my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
594 my $itemdesc = $cust_tax_adjustment->taxname;
595 $itemdesc = '' if $itemdesc eq 'Tax';
597 push @cust_bill_pkg, new FS::cust_bill_pkg {
603 'itemdesc' => $itemdesc,
604 'itemcomment' => $cust_tax_adjustment->comment,
605 'cust_tax_adjustment' => $cust_tax_adjustment,
606 #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
611 my $charged = sprintf('%.2f', ${ $total_setup{$pass} } + ${ $total_recur{$pass} } );
613 my @cust_bill = $self->cust_bill;
614 my $balance = $self->balance;
615 my $previous_balance = scalar(@cust_bill)
616 ? ( $cust_bill[$#cust_bill]->billing_balance || 0 )
619 $previous_balance += $cust_bill[$#cust_bill]->charged
620 if scalar(@cust_bill);
621 #my $balance_adjustments =
622 # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged);
624 warn "creating the new invoice\n" if $DEBUG;
625 #create the new invoice
626 my $cust_bill = new FS::cust_bill ( {
627 'custnum' => $self->custnum,
628 '_date' => $invoice_time,
629 'charged' => $charged,
630 'billing_balance' => $balance,
631 'previous_balance' => $previous_balance,
632 'invoice_terms' => $options{'invoice_terms'},
633 'cust_bill_pkg' => \@cust_bill_pkg,
635 $error = $cust_bill->insert unless $options{no_commit};
637 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
638 return "can't create invoice for customer #". $self->custnum. ": $error";
640 push @{$options{return_bill}}, $cust_bill if $options{return_bill};
642 } #foreach my $pass ( keys %cust_bill_pkg )
644 foreach my $hook ( @precommit_hooks ) {
647 } unless $options{no_commit};
649 $dbh->rollback if $oldAutoCommit && !$options{no_commit};
650 return "$@ running precommit hook $hook\n";
654 $dbh->commit or die $dbh->errstr if $oldAutoCommit && !$options{no_commit};
659 #discard bundled packages of 0 value
660 sub _omit_zero_value_bundles {
663 my @cust_bill_pkg = ();
664 my @cust_bill_pkg_bundle = ();
666 my $discount_show_always = 0;
668 foreach my $cust_bill_pkg ( @in ) {
670 $discount_show_always = ($cust_bill_pkg->get('discounts')
671 && scalar(@{$cust_bill_pkg->get('discounts')})
672 && $conf->exists('discount-show-always'));
674 warn " pkgnum ". $cust_bill_pkg->pkgnum. " sum $sum, ".
675 "setup_show_zero ". $cust_bill_pkg->setup_show_zero.
676 "recur_show_zero ". $cust_bill_pkg->recur_show_zero. "\n"
679 if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) {
680 push @cust_bill_pkg, @cust_bill_pkg_bundle
682 || ($sum == 0 && ( $discount_show_always
683 || grep {$_->recur_show_zero || $_->setup_show_zero}
684 @cust_bill_pkg_bundle
687 @cust_bill_pkg_bundle = ();
691 $sum += $cust_bill_pkg->setup + $cust_bill_pkg->recur;
692 push @cust_bill_pkg_bundle, $cust_bill_pkg;
696 push @cust_bill_pkg, @cust_bill_pkg_bundle
698 || ($sum == 0 && ( $discount_show_always
699 || grep {$_->recur_show_zero || $_->setup_show_zero}
700 @cust_bill_pkg_bundle
704 warn " _omit_zero_value_bundles: ". scalar(@in).
705 '->'. scalar(@cust_bill_pkg). "\n" #. Dumper(@cust_bill_pkg). "\n"
712 =item calculate_taxes LINEITEMREF TAXHASHREF INVOICE_TIME
714 Generates tax line items (see L<FS::cust_bill_pkg>) for this customer.
715 Usually used internally by bill method B<bill>.
717 If there is an error, returns the error, otherwise returns reference to a
718 list of line items suitable for insertion.
724 An array ref of the line items being billed.
728 A strange beast. The keys to this hash are internal identifiers consisting
729 of the name of the tax object type, a space, and its unique identifier ( e.g.
730 'cust_main_county 23' ). The values of the hash are listrefs. The first
731 item in the list is the tax object. The remaining items are either line
732 items or floating point values (currency amounts).
734 The taxes are calculated on this entity. Calculated exemption records are
735 transferred to the LINEITEMREF items on the assumption that they are related.
741 This specifies the date appearing on the associated invoice. Some
742 jurisdictions (i.e. Texas) have tax exemptions which are date sensitive.
748 sub calculate_taxes {
749 my ($self, $cust_bill_pkg, $taxlisthash, $invoice_time) = @_;
751 # $taxlisthash is a hashref
752 # keys are identifiers, values are arrayrefs
753 # each arrayref starts with a tax object (cust_main_county or tax_rate)
754 # then any cust_bill_pkg objects the tax applies to
756 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
758 warn "$me calculate_taxes\n"
759 #.Dumper($self, $cust_bill_pkg, $taxlisthash, $invoice_time). "\n"
762 my @tax_line_items = ();
764 # keys are tax names (as printed on invoices / itemdesc )
765 # values are arrayrefs of taxlisthash keys (internal identifiers)
768 # keys are taxlisthash keys (internal identifiers)
769 # values are (cumulative) amounts
772 # keys are taxlisthash keys (internal identifiers)
773 # values are arrayrefs of cust_bill_pkg_tax_location hashrefs
774 my %tax_location = ();
776 # keys are taxlisthash keys (internal identifiers)
777 # values are arrayrefs of cust_bill_pkg_tax_rate_location hashrefs
778 my %tax_rate_location = ();
780 # keys are taxlisthash keys (internal identifiers!)
781 # values are arrayrefs of cust_tax_exempt_pkg objects
784 foreach my $tax ( keys %$taxlisthash ) {
785 # $tax is a tax identifier (intersection of a tax definition record
786 # and a cust_bill_pkg record)
787 my $tax_object = shift @{ $taxlisthash->{$tax} };
788 # $tax_object is a cust_main_county or tax_rate
789 # (with billpkgnum, pkgnum, locationnum set)
790 # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg component objects
791 # (setup, recurring, usage classes)
792 warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
793 warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2;
794 # taxline calculates the tax on all cust_bill_pkgs in the
795 # first (arrayref) argument, and returns a hashref of 'name'
796 # (the line item description) and 'amount'.
797 # It also calculates exemptions and attaches them to the cust_bill_pkgs
799 my $taxables = $taxlisthash->{$tax};
800 my $exemptions = $tax_exemption{$tax} ||= [];
801 my $taxline = $tax_object->taxline(
803 'custnum' => $self->custnum,
804 'invoice_time' => $invoice_time,
805 'exemptions' => $exemptions,
807 return $taxline unless ref($taxline);
809 unshift @{ $taxlisthash->{$tax} }, $tax_object;
811 if ( $tax_object->isa('FS::cust_main_county') ) {
812 # then $taxline is a real line item
813 push @{ $taxname{ $taxline->itemdesc } }, $taxline;
816 # leave this as is for now
818 my $name = $taxline->{'name'};
819 my $amount = $taxline->{'amount'};
821 #warn "adding $amount as $name\n";
822 $taxname{ $name } ||= [];
823 push @{ $taxname{ $name } }, $tax;
825 $tax_amount{ $tax } += $amount;
827 # link records between cust_main_county/tax_rate and cust_location
828 $tax_rate_location{ $tax } ||= [];
829 my $taxratelocationnum =
830 $tax_object->tax_rate_location->taxratelocationnum;
831 push @{ $tax_rate_location{ $tax } },
833 'taxnum' => $tax_object->taxnum,
834 'taxtype' => ref($tax_object),
835 'amount' => sprintf('%.2f', $amount ),
836 'locationtaxid' => $tax_object->location,
837 'taxratelocationnum' => $taxratelocationnum,
839 } #if ref($tax_object)...
840 } #foreach keys %$taxlisthash
842 #consolidate and create tax line items
843 warn "consolidating and generating...\n" if $DEBUG > 2;
844 foreach my $taxname ( keys %taxname ) {
845 my @cust_bill_pkg_tax_location;
846 my @cust_bill_pkg_tax_rate_location;
847 my $tax_cust_bill_pkg = FS::cust_bill_pkg->new({
852 'itemdesc' => $taxname,
853 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
854 'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
859 warn "adding $taxname\n" if $DEBUG > 1;
860 foreach my $taxitem ( @{ $taxname{$taxname} } ) {
861 if ( ref($taxitem) eq 'FS::cust_bill_pkg' ) {
862 # then we need to transfer the amount and the links from the
863 # line item to the new one we're creating.
864 $tax_total += $taxitem->setup;
865 foreach my $link ( @{ $taxitem->get('cust_bill_pkg_tax_location') } ) {
866 $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg);
867 push @cust_bill_pkg_tax_location, $link;
871 next if $seen{$taxitem}++;
872 warn "adding $tax_amount{$taxitem}\n" if $DEBUG > 1;
873 $tax_total += $tax_amount{$taxitem};
874 push @cust_bill_pkg_tax_rate_location,
875 map { new FS::cust_bill_pkg_tax_rate_location $_ }
876 @{ $tax_rate_location{ $taxitem } };
879 next unless $tax_total;
881 # we should really neverround this up...I guess it's okay if taxline
882 # already returns amounts with 2 decimal places
883 $tax_total = sprintf('%.2f', $tax_total );
884 $tax_cust_bill_pkg->set('setup', $tax_total);
886 my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
892 if ( $pkg_category and
893 $conf->config('invoice_latexsummary') ||
894 $conf->config('invoice_htmlsummary')
898 my %hash = ( 'section' => $pkg_category->categoryname );
899 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
902 $tax_cust_bill_pkg->set('display', \@display);
904 push @tax_line_items, $tax_cust_bill_pkg;
911 my ($self, %params) = @_;
913 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
915 my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
916 my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
917 my $precommit_hooks = $params{precommit_hooks} or die "no precommit_hooks specified";
918 my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
919 my $total_setup = $params{setup} or die "no setup accumulator specified";
920 my $total_recur = $params{recur} or die "no recur accumulator specified";
921 my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
922 my $time = $params{'time'} or die "no time specified";
923 my (%options) = %{$params{options}};
925 if ( $part_pkg->freq ne '1' and ($options{'freq_override'} || 0) > 0 ) {
926 # this should never happen
927 die 'freq_override billing attempted on non-monthly package '.
932 my $real_pkgpart = $params{real_pkgpart};
933 my %hash = $cust_pkg->hash;
934 my $old_cust_pkg = new FS::cust_pkg \%hash;
939 $cust_pkg->pkgpart($part_pkg->pkgpart);
941 my $cmp_time = ( $conf->exists('next-bill-ignore-time')
952 my @setup_discounts = ();
953 my %setup_param = ( 'discounts' => \@setup_discounts );
954 if ( ! $options{recurring_only}
955 and ! $options{cancel}
956 and ( $options{'resetup'}
957 || ( ! $cust_pkg->setup
958 && ( ! $cust_pkg->start_date
959 || $cust_pkg->start_date <= $cmp_time
961 && ( ! $conf->exists('disable_setup_suspended_pkgs')
962 || ( $conf->exists('disable_setup_suspended_pkgs') &&
963 ! $cust_pkg->getfield('susp')
971 warn " bill setup\n" if $DEBUG > 1;
973 unless ( $cust_pkg->waive_setup ) {
976 $setup = eval { $cust_pkg->calc_setup( $time, \@details, \%setup_param ) };
977 return "$@ running calc_setup for $cust_pkg\n"
980 $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
983 $cust_pkg->setfield('setup', $time)
984 unless $cust_pkg->setup;
985 #do need it, but it won't get written to the db
986 #|| $cust_pkg->pkgpart != $real_pkgpart;
988 $cust_pkg->setfield('start_date', '')
989 if $cust_pkg->start_date;
999 my @recur_discounts = ();
1001 if ( ! $cust_pkg->start_date
1002 and ( ! $cust_pkg->susp || $cust_pkg->option('suspend_bill',1)
1003 || ( $part_pkg->option('suspend_bill', 1) )
1004 && ! $cust_pkg->option('no_suspend_bill',1)
1007 ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time )
1008 || ( $part_pkg->plan eq 'voip_cdr'
1009 && $part_pkg->option('bill_every_call')
1014 # XXX should this be a package event? probably. events are called
1015 # at collection time at the moment, though...
1016 $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
1017 if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'};
1018 #don't want to reset usage just cause we want a line item??
1019 #&& $part_pkg->pkgpart == $real_pkgpart;
1021 warn " bill recur\n" if $DEBUG > 1;
1024 # XXX shared with $recur_prog
1025 $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
1029 #over two params! lets at least switch to a hashref for the rest...
1030 my $increment_next_bill = ( $part_pkg->freq ne '0'
1031 && ( $cust_pkg->getfield('bill') || 0 ) <= $cmp_time
1032 && !$options{cancel}
1034 my %param = ( %setup_param,
1035 'precommit_hooks' => $precommit_hooks,
1036 'increment_next_bill' => $increment_next_bill,
1037 'discounts' => \@recur_discounts,
1038 'real_pkgpart' => $real_pkgpart,
1039 'freq_override' => $options{freq_override} || '',
1043 my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
1045 # There may be some part_pkg for which this is wrong. Only those
1046 # which can_discount are supported.
1047 # (the UI should prevent adding discounts to these at the moment)
1049 warn "calling $method on cust_pkg ". $cust_pkg->pkgnum.
1050 " for pkgpart ". $cust_pkg->pkgpart.
1051 " with params ". join(' / ', map "$_=>$param{$_}", keys %param). "\n"
1054 $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
1055 return "$@ running $method for $cust_pkg\n"
1059 $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better
1061 if ( $increment_next_bill ) {
1065 if ( my $main_pkg = $cust_pkg->main_pkg ) {
1066 # supplemental package
1067 # to keep in sync with the main package, simulate billing at
1069 my $main_pkg_freq = $main_pkg->part_pkg->freq;
1070 my $supp_pkg_freq = $part_pkg->freq;
1071 my $ratio = $supp_pkg_freq / $main_pkg_freq;
1072 if ( $ratio != int($ratio) ) {
1073 # the UI should prevent setting up packages like this, but just
1075 return "supplemental package period is not an integer multiple of main package period";
1077 $next_bill = $sdate;
1079 $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq );
1084 $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
1085 return "unparsable frequency: ". $part_pkg->freq
1086 if $next_bill == -1;
1089 #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
1090 # only for figuring next bill date, nothing else, so, reset $sdate again
1092 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1093 #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
1094 $cust_pkg->last_bill($sdate);
1096 $cust_pkg->setfield('bill', $next_bill );
1100 if ( $param{'setup_fee'} ) {
1101 # Add an additional setup fee at the billing stage.
1102 # Used for prorate_defer_bill.
1103 $setup += $param{'setup_fee'};
1104 $unitsetup += $param{'setup_fee'};
1108 if ( defined $param{'discount_left_setup'} ) {
1109 foreach my $discount_setup ( values %{$param{'discount_left_setup'}} ) {
1110 $setup -= $discount_setup;
1116 warn "\$setup is undefined" unless defined($setup);
1117 warn "\$recur is undefined" unless defined($recur);
1118 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1121 # If there's line items, create em cust_bill_pkg records
1122 # If $cust_pkg has been modified, update it (if we're a real pkgpart)
1127 if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
1128 # hmm.. and if just the options are modified in some weird price plan?
1130 warn " package ". $cust_pkg->pkgnum. " modified; updating\n"
1133 my $error = $cust_pkg->replace( $old_cust_pkg,
1134 'depend_jobnum'=>$options{depend_jobnum},
1135 'options' => { $cust_pkg->options },
1137 unless $options{no_commit};
1138 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
1139 if $error; #just in case
1142 $setup = sprintf( "%.2f", $setup );
1143 $recur = sprintf( "%.2f", $recur );
1144 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1145 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1147 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1148 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1151 my $discount_show_always = $conf->exists('discount-show-always')
1152 && ( ($setup == 0 && scalar(@setup_discounts))
1153 || ($recur == 0 && scalar(@recur_discounts))
1158 || (!$part_pkg->hidden && $options{has_hidden}) #include some $0 lines
1159 || $discount_show_always
1160 || ($setup == 0 && $cust_pkg->_X_show_zero('setup'))
1161 || ($recur == 0 && $cust_pkg->_X_show_zero('recur'))
1165 warn " charges (setup=$setup, recur=$recur); adding line items\n"
1168 my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
1170 warn " adding customer package invoice detail: $_\n"
1171 foreach @cust_pkg_detail;
1173 push @details, @cust_pkg_detail;
1175 my $cust_bill_pkg = new FS::cust_bill_pkg {
1176 'pkgnum' => $cust_pkg->pkgnum,
1178 'unitsetup' => $unitsetup,
1180 'unitrecur' => $unitrecur,
1181 'quantity' => $cust_pkg->quantity,
1182 'details' => \@details,
1183 'discounts' => [ @setup_discounts, @recur_discounts ],
1184 'hidden' => $part_pkg->hidden,
1185 'freq' => $part_pkg->freq,
1188 if ( $part_pkg->option('prorate_defer_bill',1)
1189 and !$hash{last_bill} ) {
1190 # both preceding and upcoming, technically
1191 $cust_bill_pkg->sdate( $cust_pkg->setup );
1192 $cust_bill_pkg->edate( $cust_pkg->bill );
1193 } elsif ( $part_pkg->recur_temporality eq 'preceding' ) {
1194 $cust_bill_pkg->sdate( $hash{last_bill} );
1195 $cust_bill_pkg->edate( $sdate - 86399 ); #60s*60m*24h-1
1196 $cust_bill_pkg->edate( $time ) if $options{cancel};
1197 } else { #if ( $part_pkg->recur_temporality eq 'upcoming' ) {
1198 $cust_bill_pkg->sdate( $sdate );
1199 $cust_bill_pkg->edate( $cust_pkg->bill );
1200 #$cust_bill_pkg->edate( $time ) if $options{cancel};
1203 $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
1204 unless $part_pkg->pkgpart == $real_pkgpart;
1206 $$total_setup += $setup;
1207 $$total_recur += $recur;
1213 #unless ( $discount_show_always ) { # oh, for god's sake
1214 my $error = $self->_handle_taxes(
1219 $options{invoice_time},
1221 \%options # I have serious objections to this
1223 return $error if $error;
1226 $cust_bill_pkg->set_display(
1227 part_pkg => $part_pkg,
1228 real_pkgpart => $real_pkgpart,
1231 push @$cust_bill_pkgs, $cust_bill_pkg;
1233 } #if $setup != 0 || $recur != 0
1241 =item _transfer_balance TO_PKG [ FROM_PKGNUM ]
1243 Takes one argument, a cust_pkg object that is being billed. This will
1244 be called only if the package was created by a package change, and has
1245 not been billed since the package change, and package balance tracking
1246 is enabled. The second argument can be an alternate package number to
1247 transfer the balance from; this should not be used externally.
1249 Transfers the balance from the previous package (now canceled) to
1250 this package, by crediting one package and creating an invoice item for
1251 the other. Inserts the credit and returns the invoice item (so that it
1252 can be added to an invoice that's being built).
1254 If the previous package was never billed, and was also created by a package
1255 change, then this will also transfer the balance from I<its> previous
1256 package, and so on, until reaching a package that either has been billed
1257 or was not created by a package change.
1261 my $balance_transfer_reason;
1263 sub _transfer_balance {
1265 my $cust_pkg = shift;
1266 my $from_pkgnum = shift || $cust_pkg->change_pkgnum;
1267 my $from_pkg = FS::cust_pkg->by_key($from_pkgnum);
1271 # if $from_pkg is not the first package in the chain, and it was never
1273 if ( $from_pkg->change_pkgnum and scalar($from_pkg->cust_bill_pkg) == 0 ) {
1274 @transfers = $self->_transfer_balance($cust_pkg, $from_pkg->change_pkgnum);
1277 my $prev_balance = $self->balance_pkgnum($from_pkgnum);
1278 if ( $prev_balance != 0 ) {
1279 $balance_transfer_reason ||= FS::reason->new_or_existing(
1280 'reason' => 'Package balance transfer',
1281 'type' => 'Internal adjustment',
1285 my $credit = FS::cust_credit->new({
1286 'custnum' => $self->custnum,
1287 'amount' => abs($prev_balance),
1288 'reasonnum' => $balance_transfer_reason->reasonnum,
1289 '_date' => $cust_pkg->change_date,
1292 my $cust_bill_pkg = FS::cust_bill_pkg->new({
1294 'recur' => abs($prev_balance),
1295 #'sdate' => $from_pkg->last_bill, # not sure about this
1296 #'edate' => $cust_pkg->change_date,
1297 'itemdesc' => $self->mt('Previous Balance, [_1]',
1298 $from_pkg->part_pkg->pkg),
1301 if ( $prev_balance > 0 ) {
1302 # credit the old package, charge the new one
1303 $credit->set('pkgnum', $from_pkgnum);
1304 $cust_bill_pkg->set('pkgnum', $cust_pkg->pkgnum);
1307 $credit->set('pkgnum', $cust_pkg->pkgnum);
1308 $cust_bill_pkg->set('pkgnum', $from_pkgnum);
1310 my $error = $credit->insert;
1311 die "error transferring package balance from #".$from_pkgnum.
1312 " to #".$cust_pkg->pkgnum.": $error\n" if $error;
1314 push @transfers, $cust_bill_pkg;
1315 } # $prev_balance != 0
1320 =item _handle_taxes PART_PKG TAXLISTHASH CUST_BILL_PKG CUST_PKG TIME PKGPART [ OPTIONS ]
1322 This is _handle_taxes. It's called once for each cust_bill_pkg generated
1323 from _make_lines, along with the part_pkg, cust_pkg, invoice time, the
1324 non-overridden pkgpart, a flag indicating whether the package is being
1325 canceled, and a partridge in a pear tree.
1327 The most important argument is 'taxlisthash'. This is shared across the
1328 entire invoice. It looks like this:
1330 'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
1331 'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
1334 'cust_main_county' can also be 'tax_rate'. The first object in the array
1335 is always the cust_main_county or tax_rate identified by the key.
1337 That "..." is a list of FS::cust_bill_pkg objects that will be fed to
1338 the 'taxline' method to calculate the amount of the tax. This doesn't
1339 happen until calculate_taxes, though.
1345 my $part_pkg = shift;
1346 my $taxlisthash = shift;
1347 my $cust_bill_pkg = shift;
1348 my $cust_pkg = shift;
1349 my $invoice_time = shift;
1350 my $real_pkgpart = shift;
1351 my $options = shift;
1353 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1355 return if ( $self->payby eq 'COMP' ); #dubious
1357 if ( $conf->exists('enable_taxproducts')
1358 && ( scalar($part_pkg->part_pkg_taxoverride)
1359 || $part_pkg->has_taxproduct
1364 # EXTERNAL TAX RATES (via tax_rate)
1365 my %cust_bill_pkg = ();
1369 #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
1370 push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
1372 push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
1373 push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
1375 my $exempt = $conf->exists('cust_class-tax_exempt')
1376 ? ( $self->cust_class ? $self->cust_class->tax : '' )
1378 # standardize this just to be sure
1379 $exempt = ($exempt eq 'Y') ? 'Y' : '';
1383 foreach my $class (@classes) {
1384 my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
1385 return $err_or_ref unless ref($err_or_ref);
1386 $taxes{$class} = $err_or_ref;
1389 unless (exists $taxes{''}) {
1390 my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
1391 return $err_or_ref unless ref($err_or_ref);
1392 $taxes{''} = $err_or_ref;
1397 my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
1398 foreach my $key (keys %tax_cust_bill_pkg) {
1399 # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
1400 # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of
1402 # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that
1403 # apply to $key-class charges.
1404 my @taxes = @{ $taxes{$key} || [] };
1405 my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
1407 my %localtaxlisthash = ();
1408 foreach my $tax ( @taxes ) {
1410 # this is the tax identifier, not the taxname
1411 my $taxname = ref( $tax ). ' '. $tax->taxnum;
1412 $taxname .= ' billpkgnum'. $cust_bill_pkg->billpkgnum;
1413 # We need to create a separate $taxlisthash entry for each billpkgnum
1414 # on the invoice, so that cust_bill_pkg_tax_location records will
1415 # be linked correctly.
1417 # $taxlisthash: keys are "setup", "recur", and usage classes.
1418 # Values are arrayrefs, first the tax object (cust_main_county
1419 # or tax_rate) and then any cust_bill_pkg objects that the
1421 $taxlisthash->{ $taxname } ||= [ $tax ];
1422 push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
1424 $localtaxlisthash{ $taxname } ||= [ $tax ];
1425 push @{ $localtaxlisthash{ $taxname } }, $tax_cust_bill_pkg;
1429 warn "finding taxed taxes...\n" if $DEBUG > 2;
1430 foreach my $tax ( keys %localtaxlisthash ) {
1431 my $tax_object = shift @{ $localtaxlisthash{$tax} };
1432 warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
1434 next unless $tax_object->can('tax_on_tax');
1436 foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
1437 my $totname = ref( $tot ). ' '. $tot->taxnum;
1439 warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
1441 next unless exists( $localtaxlisthash{ $totname } ); # only increase
1443 warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
1444 # we're calling taxline() right here? wtf?
1445 my $hashref_or_error =
1446 $tax_object->taxline( $localtaxlisthash{$tax},
1447 'custnum' => $self->custnum,
1448 'invoice_time' => $invoice_time,
1450 return $hashref_or_error
1451 unless ref($hashref_or_error);
1453 $taxlisthash->{ $totname } ||= [ $tot ];
1454 push @{ $taxlisthash->{ $totname } }, $hashref_or_error->{amount};
1462 # INTERNAL TAX RATES (cust_main_county)
1464 # We fetch taxes even if the customer is completely exempt,
1465 # because we need to record that fact.
1467 my @loc_keys = qw( district city county state country );
1468 my $location = $cust_pkg->tax_location;
1469 my %taxhash = map { $_ => $location->$_ } @loc_keys;
1471 $taxhash{'taxclass'} = $part_pkg->taxclass;
1473 warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
1475 my @taxes = (); # entries are cust_main_county objects
1476 my %taxhash_elim = %taxhash;
1477 my @elim = qw( district city county state );
1480 #first try a match with taxclass
1481 @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
1483 if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
1484 #then try a match without taxclass
1485 my %no_taxclass = %taxhash_elim;
1486 $no_taxclass{ 'taxclass' } = '';
1487 @taxes = qsearch( 'cust_main_county', \%no_taxclass );
1490 $taxhash_elim{ shift(@elim) } = '';
1492 } while ( !scalar(@taxes) && scalar(@elim) );
1495 my $tax_id = 'cust_main_county '.$_->taxnum;
1496 $taxlisthash->{$tax_id} ||= [ $_ ];
1497 push @{ $taxlisthash->{$tax_id} }, $cust_bill_pkg;
1506 my $part_pkg = shift;
1508 my $cust_pkg = shift;
1510 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1513 if ( $cust_pkg->locationnum && $conf->exists('tax-pkg_address') ) {
1514 $geocode = $cust_pkg->cust_location->geocode('cch');
1516 $geocode = $self->geocode('cch');
1521 my @taxclassnums = map { $_->taxclassnum }
1522 $part_pkg->part_pkg_taxoverride($class);
1524 unless (@taxclassnums) {
1525 @taxclassnums = map { $_->taxclassnum }
1526 grep { $_->taxable eq 'Y' }
1527 $part_pkg->part_pkg_taxrate('cch', $geocode, $class);
1529 warn "Found taxclassnum values of ". join(',', @taxclassnums)
1534 join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
1536 @taxes = qsearch({ 'table' => 'tax_rate',
1537 'hashref' => { 'geocode' => $geocode, },
1538 'extra_sql' => $extra_sql,
1540 if scalar(@taxclassnums);
1542 warn "Found taxes ".
1543 join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n"
1550 =item collect [ HASHREF | OPTION => VALUE ... ]
1552 (Attempt to) collect money for this customer's outstanding invoices (see
1553 L<FS::cust_bill>). Usually used after the bill method.
1555 Actions are now triggered by billing events; see L<FS::part_event> and the
1556 billing events web interface. Old-style invoice events (see
1557 L<FS::part_bill_event>) have been deprecated.
1559 If there is an error, returns the error, otherwise returns false.
1561 Options are passed as name-value pairs.
1563 Currently available options are:
1569 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.
1573 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1577 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1581 set true to surpress email card/ACH decline notices.
1585 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)
1591 # allows for one time override of normal customer billing method
1596 my( $self, %options ) = @_;
1598 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1600 my $invoice_time = $options{'invoice_time'} || time;
1603 local $SIG{HUP} = 'IGNORE';
1604 local $SIG{INT} = 'IGNORE';
1605 local $SIG{QUIT} = 'IGNORE';
1606 local $SIG{TERM} = 'IGNORE';
1607 local $SIG{TSTP} = 'IGNORE';
1608 local $SIG{PIPE} = 'IGNORE';
1610 my $oldAutoCommit = $FS::UID::AutoCommit;
1611 local $FS::UID::AutoCommit = 0;
1614 $self->select_for_update; #mutex
1617 my $balance = $self->balance;
1618 warn "$me collect customer ". $self->custnum. ": balance $balance\n"
1621 if ( exists($options{'retry_card'}) ) {
1622 carp 'retry_card option passed to collect is deprecated; use retry';
1623 $options{'retry'} ||= $options{'retry_card'};
1625 if ( exists($options{'retry'}) && $options{'retry'} ) {
1626 my $error = $self->retry_realtime;
1628 $dbh->rollback if $oldAutoCommit;
1633 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1635 #never want to roll back an event just because it returned an error
1636 local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1638 $self->do_cust_event(
1639 'debug' => ( $options{'debug'} || 0 ),
1640 'time' => $invoice_time,
1641 'check_freq' => $options{'check_freq'},
1642 'stage' => 'collect',
1647 =item retry_realtime
1649 Schedules realtime / batch credit card / electronic check / LEC billing
1650 events for for retry. Useful if card information has changed or manual
1651 retry is desired. The 'collect' method must be called to actually retry
1654 Implementation details: For either this customer, or for each of this
1655 customer's open invoices, changes the status of the first "done" (with
1656 statustext error) realtime processing event to "failed".
1660 sub retry_realtime {
1663 local $SIG{HUP} = 'IGNORE';
1664 local $SIG{INT} = 'IGNORE';
1665 local $SIG{QUIT} = 'IGNORE';
1666 local $SIG{TERM} = 'IGNORE';
1667 local $SIG{TSTP} = 'IGNORE';
1668 local $SIG{PIPE} = 'IGNORE';
1670 my $oldAutoCommit = $FS::UID::AutoCommit;
1671 local $FS::UID::AutoCommit = 0;
1674 #a little false laziness w/due_cust_event (not too bad, really)
1676 my $join = FS::part_event_condition->join_conditions_sql;
1677 my $order = FS::part_event_condition->order_conditions_sql;
1680 . join ( ' OR ' , map {
1681 my $cust_join = FS::part_event->eventtables_cust_join->{$_} || '';
1682 my $custnum = FS::part_event->eventtables_custnum->{$_};
1683 "( part_event.eventtable = " . dbh->quote($_)
1684 . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key
1685 . " from $_ $cust_join"
1686 . " where $custnum = " . dbh->quote( $self->custnum ) . "))" ;
1687 } FS::part_event->eventtables)
1690 #here is the agent virtualization
1691 my $agent_virt = " ( part_event.agentnum IS NULL
1692 OR part_event.agentnum = ". $self->agentnum. ' )';
1694 #XXX this shouldn't be hardcoded, actions should declare it...
1695 my @realtime_events = qw(
1696 cust_bill_realtime_card
1697 cust_bill_realtime_check
1698 cust_bill_realtime_lec
1702 my $is_realtime_event =
1703 ' part_event.action IN ( '.
1704 join(',', map "'$_'", @realtime_events ).
1707 my $batch_or_statustext =
1708 "( part_event.action = 'cust_bill_batch'
1709 OR ( statustext IS NOT NULL AND statustext != '' )
1713 my @cust_event = qsearch({
1714 'table' => 'cust_event',
1715 'select' => 'cust_event.*',
1716 'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
1717 'hashref' => { 'status' => 'done' },
1718 'extra_sql' => " AND $batch_or_statustext ".
1719 " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
1722 my %seen_invnum = ();
1723 foreach my $cust_event (@cust_event) {
1725 #max one for the customer, one for each open invoice
1726 my $cust_X = $cust_event->cust_X;
1727 next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
1731 or $cust_event->part_event->eventtable eq 'cust_bill'
1734 my $error = $cust_event->retry;
1736 $dbh->rollback if $oldAutoCommit;
1737 return "error scheduling event for retry: $error";
1742 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1747 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
1749 Runs billing events; see L<FS::part_event> and the billing events web
1752 If there is an error, returns the error, otherwise returns false.
1754 Options are passed as name-value pairs.
1756 Currently available options are:
1762 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.
1766 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1770 "collect" (the default) or "pre-bill"
1774 set true to surpress email card/ACH decline notices.
1778 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)
1785 # allows for one time override of normal customer billing method
1789 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1792 my( $self, %options ) = @_;
1794 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1796 my $time = $options{'time'} || time;
1799 local $SIG{HUP} = 'IGNORE';
1800 local $SIG{INT} = 'IGNORE';
1801 local $SIG{QUIT} = 'IGNORE';
1802 local $SIG{TERM} = 'IGNORE';
1803 local $SIG{TSTP} = 'IGNORE';
1804 local $SIG{PIPE} = 'IGNORE';
1806 my $oldAutoCommit = $FS::UID::AutoCommit;
1807 local $FS::UID::AutoCommit = 0;
1810 $self->select_for_update; #mutex
1813 my $balance = $self->balance;
1814 warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
1817 # if ( exists($options{'retry_card'}) ) {
1818 # carp 'retry_card option passed to collect is deprecated; use retry';
1819 # $options{'retry'} ||= $options{'retry_card'};
1821 # if ( exists($options{'retry'}) && $options{'retry'} ) {
1822 # my $error = $self->retry_realtime;
1824 # $dbh->rollback if $oldAutoCommit;
1829 # false laziness w/pay_batch::import_results
1831 my $due_cust_event = $self->due_cust_event(
1832 'debug' => ( $options{'debug'} || 0 ),
1834 'check_freq' => $options{'check_freq'},
1835 'stage' => ( $options{'stage'} || 'collect' ),
1837 unless( ref($due_cust_event) ) {
1838 $dbh->rollback if $oldAutoCommit;
1839 return $due_cust_event;
1842 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1843 #never want to roll back an event just because it or a different one
1845 local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1847 foreach my $cust_event ( @$due_cust_event ) {
1851 #re-eval event conditions (a previous event could have changed things)
1852 unless ( $cust_event->test_conditions ) {
1853 #don't leave stray "new/locked" records around
1854 my $error = $cust_event->delete;
1855 return $error if $error;
1860 local $FS::cust_main::Billing_Realtime::realtime_bop_decline_quiet = 1
1861 if $options{'quiet'};
1862 warn " running cust_event ". $cust_event->eventnum. "\n"
1865 #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
1866 if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
1867 #XXX wtf is this? figure out a proper dealio with return value
1879 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
1881 Inserts database records for and returns an ordered listref of new events due
1882 for this customer, as FS::cust_event objects (see L<FS::cust_event>). If no
1883 events are due, an empty listref is returned. If there is an error, returns a
1884 scalar error message.
1886 To actually run the events, call each event's test_condition method, and if
1887 still true, call the event's do_event method.
1889 Options are passed as a hashref or as a list of name-value pairs. Available
1896 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.
1900 "collect" (the default) or "pre-bill"
1904 "Current time" for the events.
1908 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)
1912 Only return events for the specified eventtable (by default, events of all eventtables are returned)
1916 Explicitly pass the objects to be tested (typically used with eventtable).
1920 Set to true to return the objects, but not actually insert them into the
1927 sub due_cust_event {
1929 my %opt = ref($_[0]) ? %{ $_[0] } : @_;
1932 #my $DEBUG = $opt{'debug'}
1933 $opt{'debug'} ||= 0; # silence some warnings
1934 local($DEBUG) = $opt{'debug'}
1935 if $opt{'debug'} > $DEBUG;
1936 $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1938 warn "$me due_cust_event called with options ".
1939 join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
1942 $opt{'time'} ||= time;
1944 local $SIG{HUP} = 'IGNORE';
1945 local $SIG{INT} = 'IGNORE';
1946 local $SIG{QUIT} = 'IGNORE';
1947 local $SIG{TERM} = 'IGNORE';
1948 local $SIG{TSTP} = 'IGNORE';
1949 local $SIG{PIPE} = 'IGNORE';
1951 my $oldAutoCommit = $FS::UID::AutoCommit;
1952 local $FS::UID::AutoCommit = 0;
1955 $self->select_for_update #mutex
1956 unless $opt{testonly};
1959 # find possible events (initial search)
1962 my @cust_event = ();
1964 my @eventtable = $opt{'eventtable'}
1965 ? ( $opt{'eventtable'} )
1966 : FS::part_event->eventtables_runorder;
1968 my $check_freq = $opt{'check_freq'} || '1d';
1970 foreach my $eventtable ( @eventtable ) {
1973 if ( $opt{'objects'} ) {
1975 @objects = @{ $opt{'objects'} };
1977 } elsif ( $eventtable eq 'cust_main' ) {
1979 @objects = ( $self );
1983 my $cm_join = " LEFT JOIN cust_main USING ( custnum )";
1984 # linkage not needed here because FS::cust_main->$eventtable will
1987 #some false laziness w/Cron::bill bill_where
1989 my $join = FS::part_event_condition->join_conditions_sql( $eventtable);
1990 my $where = FS::part_event_condition->where_conditions_sql($eventtable,
1991 'time'=>$opt{'time'},
1993 $where = $where ? "AND $where" : '';
1995 my $are_part_event =
1996 "EXISTS ( SELECT 1 FROM part_event $join
1997 WHERE check_freq = '$check_freq'
1998 AND eventtable = '$eventtable'
1999 AND ( disabled = '' OR disabled IS NULL )
2005 @objects = $self->$eventtable(
2006 'addl_from' => $cm_join,
2007 'extra_sql' => " AND $are_part_event",
2009 } # if ( !$opt{objects} and $eventtable ne 'cust_main' )
2011 my @e_cust_event = ();
2013 my $linkage = FS::part_event->eventtables_cust_join->{$eventtable} || '';
2015 my $cross = "CROSS JOIN $eventtable $linkage";
2016 $cross .= ' LEFT JOIN cust_main USING ( custnum )'
2017 unless $eventtable eq 'cust_main';
2019 foreach my $object ( @objects ) {
2021 #this first search uses the condition_sql magic for optimization.
2022 #the more possible events we can eliminate in this step the better
2024 my $cross_where = '';
2025 my $pkey = $object->primary_key;
2026 $cross_where = "$eventtable.$pkey = ". $object->$pkey();
2028 my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
2030 FS::part_event_condition->where_conditions_sql( $eventtable,
2031 'time'=>$opt{'time'}
2033 my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
2035 $extra_sql = "AND $extra_sql" if $extra_sql;
2037 #here is the agent virtualization
2038 $extra_sql .= " AND ( part_event.agentnum IS NULL
2039 OR part_event.agentnum = ". $self->agentnum. ' )';
2041 $extra_sql .= " $order";
2043 warn "searching for events for $eventtable ". $object->$pkey. "\n"
2044 if $opt{'debug'} > 2;
2045 my @part_event = qsearch( {
2046 'debug' => ( $opt{'debug'} > 3 ? 1 : 0 ),
2047 'select' => 'part_event.*',
2048 'table' => 'part_event',
2049 'addl_from' => "$cross $join",
2050 'hashref' => { 'check_freq' => $check_freq,
2051 'eventtable' => $eventtable,
2054 'extra_sql' => "AND $cross_where $extra_sql",
2058 my $pkey = $object->primary_key;
2059 warn " ". scalar(@part_event).
2060 " possible events found for $eventtable ". $object->$pkey(). "\n";
2063 push @e_cust_event, map {
2064 $_->new_cust_event($object, 'time' => $opt{'time'})
2069 warn " ". scalar(@e_cust_event).
2070 " subtotal possible cust events found for $eventtable\n"
2073 push @cust_event, @e_cust_event;
2077 warn " ". scalar(@cust_event).
2078 " total possible cust events found in initial search\n"
2086 $opt{stage} ||= 'collect';
2088 grep { my $stage = $_->part_event->event_stage;
2089 $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
2099 @cust_event = grep $_->test_conditions( 'stats_hashref' => \%unsat ),
2102 warn " ". scalar(@cust_event). " cust events left satisfying conditions\n"
2105 warn " invalid conditions not eliminated with condition_sql:\n".
2106 join('', map " $_: ".$unsat{$_}."\n", keys %unsat )
2107 if keys %unsat && $DEBUG; # > 1;
2113 unless( $opt{testonly} ) {
2114 foreach my $cust_event ( @cust_event ) {
2116 my $error = $cust_event->insert();
2118 $dbh->rollback if $oldAutoCommit;
2125 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2131 warn " returning events: ". Dumper(@cust_event). "\n"
2138 =item apply_payments_and_credits [ OPTION => VALUE ... ]
2140 Applies unapplied payments and credits.
2142 In most cases, this new method should be used in place of sequential
2143 apply_payments and apply_credits methods.
2145 A hash of optional arguments may be passed. Currently "manual" is supported.
2146 If true, a payment receipt is sent instead of a statement when
2147 'payment_receipt_email' configuration option is set.
2149 If there is an error, returns the error, otherwise returns false.
2153 sub apply_payments_and_credits {
2154 my( $self, %options ) = @_;
2156 local $SIG{HUP} = 'IGNORE';
2157 local $SIG{INT} = 'IGNORE';
2158 local $SIG{QUIT} = 'IGNORE';
2159 local $SIG{TERM} = 'IGNORE';
2160 local $SIG{TSTP} = 'IGNORE';
2161 local $SIG{PIPE} = 'IGNORE';
2163 my $oldAutoCommit = $FS::UID::AutoCommit;
2164 local $FS::UID::AutoCommit = 0;
2167 $self->select_for_update; #mutex
2169 foreach my $cust_bill ( $self->open_cust_bill ) {
2170 my $error = $cust_bill->apply_payments_and_credits(%options);
2172 $dbh->rollback if $oldAutoCommit;
2173 return "Error applying: $error";
2177 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2182 =item apply_credits OPTION => VALUE ...
2184 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2185 to outstanding invoice balances in chronological order (or reverse
2186 chronological order if the I<order> option is set to B<newest>) and returns the
2187 value of any remaining unapplied credits available for refund (see
2188 L<FS::cust_refund>).
2190 Dies if there is an error.
2198 local $SIG{HUP} = 'IGNORE';
2199 local $SIG{INT} = 'IGNORE';
2200 local $SIG{QUIT} = 'IGNORE';
2201 local $SIG{TERM} = 'IGNORE';
2202 local $SIG{TSTP} = 'IGNORE';
2203 local $SIG{PIPE} = 'IGNORE';
2205 my $oldAutoCommit = $FS::UID::AutoCommit;
2206 local $FS::UID::AutoCommit = 0;
2209 $self->select_for_update; #mutex
2211 unless ( $self->total_unapplied_credits ) {
2212 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2216 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2217 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2219 my @invoices = $self->open_cust_bill;
2220 @invoices = sort { $b->_date <=> $a->_date } @invoices
2221 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2223 if ( $conf->exists('pkg-balances') ) {
2224 # limit @credits to those w/ a pkgnum grepped from $self
2226 foreach my $i (@invoices) {
2227 foreach my $li ( $i->cust_bill_pkg ) {
2228 $pkgnums{$li->pkgnum} = 1;
2231 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
2236 foreach my $cust_bill ( @invoices ) {
2238 if ( !defined($credit) || $credit->credited == 0) {
2239 $credit = pop @credits or last;
2243 if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
2244 $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
2246 $owed = $cust_bill->owed;
2248 unless ( $owed > 0 ) {
2249 push @credits, $credit;
2253 my $amount = min( $credit->credited, $owed );
2255 my $cust_credit_bill = new FS::cust_credit_bill ( {
2256 'crednum' => $credit->crednum,
2257 'invnum' => $cust_bill->invnum,
2258 'amount' => $amount,
2260 $cust_credit_bill->pkgnum( $credit->pkgnum )
2261 if $conf->exists('pkg-balances') && $credit->pkgnum;
2262 my $error = $cust_credit_bill->insert;
2264 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2268 redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2272 my $total_unapplied_credits = $self->total_unapplied_credits;
2274 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2276 return $total_unapplied_credits;
2279 =item apply_payments [ OPTION => VALUE ... ]
2281 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2282 to outstanding invoice balances in chronological order.
2284 #and returns the value of any remaining unapplied payments.
2286 A hash of optional arguments may be passed. Currently "manual" is supported.
2287 If true, a payment receipt is sent instead of a statement when
2288 'payment_receipt_email' configuration option is set.
2290 Dies if there is an error.
2294 sub apply_payments {
2295 my( $self, %options ) = @_;
2297 local $SIG{HUP} = 'IGNORE';
2298 local $SIG{INT} = 'IGNORE';
2299 local $SIG{QUIT} = 'IGNORE';
2300 local $SIG{TERM} = 'IGNORE';
2301 local $SIG{TSTP} = 'IGNORE';
2302 local $SIG{PIPE} = 'IGNORE';
2304 my $oldAutoCommit = $FS::UID::AutoCommit;
2305 local $FS::UID::AutoCommit = 0;
2308 $self->select_for_update; #mutex
2312 my @payments = sort { $b->_date <=> $a->_date }
2313 grep { $_->unapplied > 0 }
2316 my @invoices = sort { $a->_date <=> $b->_date}
2317 grep { $_->owed > 0 }
2320 if ( $conf->exists('pkg-balances') ) {
2321 # limit @payments to those w/ a pkgnum grepped from $self
2323 foreach my $i (@invoices) {
2324 foreach my $li ( $i->cust_bill_pkg ) {
2325 $pkgnums{$li->pkgnum} = 1;
2328 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
2333 foreach my $cust_bill ( @invoices ) {
2335 if ( !defined($payment) || $payment->unapplied == 0 ) {
2336 $payment = pop @payments or last;
2340 if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
2341 $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
2343 $owed = $cust_bill->owed;
2345 unless ( $owed > 0 ) {
2346 push @payments, $payment;
2350 my $amount = min( $payment->unapplied, $owed );
2353 'paynum' => $payment->paynum,
2354 'invnum' => $cust_bill->invnum,
2355 'amount' => $amount,
2357 $cbp->{_date} = $payment->_date
2358 if $options{'manual'} && $options{'backdate_application'};
2359 my $cust_bill_pay = new FS::cust_bill_pay($cbp);
2360 $cust_bill_pay->pkgnum( $payment->pkgnum )
2361 if $conf->exists('pkg-balances') && $payment->pkgnum;
2362 my $error = $cust_bill_pay->insert(%options);
2364 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2368 redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2372 my $total_unapplied_payments = $self->total_unapplied_payments;
2374 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2376 return $total_unapplied_payments;
2386 suspend_adjourned_pkgs
2387 unsuspend_resumed_pkgs
2390 (do_cust_event pre-bill)
2393 (vendor-only) _gather_taxes
2394 _omit_zero_value_bundles
2397 apply_payments_and_credits
2406 L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>