1 package FS::cust_main::Billing;
4 use vars qw( $conf $DEBUG $me );
7 use FS::Record qw( qsearch qsearchs );
10 use FS::cust_bill_pkg_display;
11 use FS::cust_bill_pay;
12 use FS::cust_credit_bill;
14 use FS::cust_tax_adjustment;
16 use FS::tax_rate_location;
17 use FS::cust_bill_pkg_tax_location;
18 use FS::cust_bill_pkg_tax_rate_location;
20 # 1 is mostly method/subroutine entry and options
21 # 2 traces progress of some operations
22 # 3 is even more information including possibly sensitive data
24 $me = '[FS::cust_main::Billing]';
26 install_callback FS::UID sub {
28 #yes, need it for stuff below (prolly should be cached)
33 FS::cust_main::Billing - Billing mixin for cust_main
39 These methods are available on FS::cust_main objects.
45 =item bill_and_collect
47 Cancels and suspends any packages due, generates bills, applies payments and
48 credits, and applies collection events to run cards, send bills and notices,
51 By default, warns on errors and continues with the next operation (but see the
54 Options are passed as name-value pairs. Currently available options are:
60 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:
64 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
68 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.
72 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
76 If set true, re-charges setup fees.
80 If set any errors prevent subsequent operations from continusing. If set
81 specifically to "return", returns the error (or false, if there is no error).
82 Any other true value causes errors to die.
86 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)
90 Optional FS::queue entry to receive status updates.
94 Options are passed to the B<bill> and B<collect> methods verbatim, so all
95 options of those methods are also available.
99 sub bill_and_collect {
100 my( $self, %options ) = @_;
104 #$options{actual_time} not $options{time} because freeside-daily -d is for
105 #pre-printing invoices
107 $options{'actual_time'} ||= time;
108 my $job = $options{'job'};
110 $job->update_statustext('0,cleaning expired packages') if $job;
111 $error = $self->cancel_expired_pkgs( $options{actual_time} );
113 $error = "Error expiring custnum ". $self->custnum. ": $error";
114 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
115 elsif ( $options{fatal} ) { die $error; }
116 else { warn $error; }
119 $error = $self->suspend_adjourned_pkgs( $options{actual_time} );
121 $error = "Error adjourning custnum ". $self->custnum. ": $error";
122 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
123 elsif ( $options{fatal} ) { die $error; }
124 else { warn $error; }
127 $job->update_statustext('20,billing packages') if $job;
128 $error = $self->bill( %options );
130 $error = "Error billing custnum ". $self->custnum. ": $error";
131 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
132 elsif ( $options{fatal} ) { die $error; }
133 else { warn $error; }
136 $job->update_statustext('50,applying payments and credits') if $job;
137 $error = $self->apply_payments_and_credits;
139 $error = "Error applying custnum ". $self->custnum. ": $error";
140 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
141 elsif ( $options{fatal} ) { die $error; }
142 else { warn $error; }
145 $job->update_statustext('70,running collection events') if $job;
146 unless ( $conf->exists('cancelled_cust-noevents')
147 && ! $self->num_ncancelled_pkgs
149 $error = $self->collect( %options );
151 $error = "Error collecting custnum ". $self->custnum. ": $error";
152 if ($options{fatal} && $options{fatal} eq 'return') { return $error; }
153 elsif ($options{fatal} ) { die $error; }
154 else { warn $error; }
157 $job->update_statustext('100,finished') if $job;
163 sub cancel_expired_pkgs {
164 my ( $self, $time, %options ) = @_;
166 my @cancel_pkgs = $self->ncancelled_pkgs( {
167 'extra_sql' => " AND expire IS NOT NULL AND expire > 0 AND expire <= $time "
172 foreach my $cust_pkg ( @cancel_pkgs ) {
173 my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
174 my $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum,
175 'reason_otaker' => $cpr->otaker
179 push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
182 scalar(@errors) ? join(' / ', @errors) : '';
186 sub suspend_adjourned_pkgs {
187 my ( $self, $time, %options ) = @_;
189 my @susp_pkgs = $self->ncancelled_pkgs( {
191 " AND ( susp IS NULL OR susp = 0 )
192 AND ( ( bill IS NOT NULL AND bill != 0 AND bill < $time )
193 OR ( adjourn IS NOT NULL AND adjourn != 0 AND adjourn <= $time )
198 #only because there's no SQL test for is_prepaid :/
200 grep { ( $_->part_pkg->is_prepaid
205 && $_->adjourn <= $time
213 foreach my $cust_pkg ( @susp_pkgs ) {
214 my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
215 if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
216 my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
217 'reason_otaker' => $cpr->otaker
221 push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
224 scalar(@errors) ? join(' / ', @errors) : '';
230 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
231 conjunction with the collect method by calling B<bill_and_collect>.
233 If there is an error, returns the error, otherwise returns false.
235 Options are passed as name-value pairs. Currently available options are:
241 If set true, re-charges setup fees.
245 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:
249 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
253 An array ref of specific packages (objects) to attempt billing, instead trying all of them.
255 $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
259 A hashref of pkgparts to exclude from this billing run (can also be specified as a comma-separated scalar).
263 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.
267 This boolean value informs the us that the package is being cancelled. This
268 typically might mean not charging the normal recurring fee but only usage
269 fees since the last billing. Setup charges may be charged. Not all package
270 plans support this feature (they tend to charge 0).
274 Optional terms to be printed on this invoice. Otherwise, customer-specific
275 terms or the default terms are used.
282 my( $self, %options ) = @_;
283 return '' if $self->payby eq 'COMP';
284 warn "$me bill customer ". $self->custnum. "\n"
287 my $time = $options{'time'} || time;
288 my $invoice_time = $options{'invoice_time'} || $time;
290 $options{'not_pkgpart'} ||= {};
291 $options{'not_pkgpart'} = { map { $_ => 1 }
292 split(/\s*,\s*/, $options{'not_pkgpart'})
294 unless ref($options{'not_pkgpart'});
296 local $SIG{HUP} = 'IGNORE';
297 local $SIG{INT} = 'IGNORE';
298 local $SIG{QUIT} = 'IGNORE';
299 local $SIG{TERM} = 'IGNORE';
300 local $SIG{TSTP} = 'IGNORE';
301 local $SIG{PIPE} = 'IGNORE';
303 my $oldAutoCommit = $FS::UID::AutoCommit;
304 local $FS::UID::AutoCommit = 0;
307 warn "$me acquiring lock on customer ". $self->custnum. "\n"
310 $self->select_for_update; #mutex
312 warn "$me running pre-bill events for customer ". $self->custnum. "\n"
315 my $error = $self->do_cust_event(
316 'debug' => ( $options{'debug'} || 0 ),
317 'time' => $invoice_time,
318 'check_freq' => $options{'check_freq'},
319 'stage' => 'pre-bill',
322 $dbh->rollback if $oldAutoCommit;
326 warn "$me done running pre-bill events for customer ". $self->custnum. "\n"
329 #keep auto-charge and non-auto-charge line items separate
330 my @passes = ( '', 'no_auto' );
332 my %cust_bill_pkg = map { $_ => [] } @passes;
335 # find the packages which are due for billing, find out how much they are
336 # & generate invoice database.
339 my %total_setup = map { my $z = 0; $_ => \$z; } @passes;
340 my %total_recur = map { my $z = 0; $_ => \$z; } @passes;
342 my %taxlisthash = map { $_ => {} } @passes;
344 my @precommit_hooks = ();
346 $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ]; #param checks?
347 foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
349 next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
351 warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
353 #? to avoid use of uninitialized value errors... ?
354 $cust_pkg->setfield('bill', '')
355 unless defined($cust_pkg->bill);
357 #my $part_pkg = $cust_pkg->part_pkg;
359 my $real_pkgpart = $cust_pkg->pkgpart;
360 my %hash = $cust_pkg->hash;
362 # we could implement this bit as FS::part_pkg::has_hidden, but we already
363 # suffer from performance issues
364 $options{has_hidden} = 0;
365 my @part_pkg = $cust_pkg->part_pkg->self_and_bill_linked;
366 $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
368 foreach my $part_pkg ( @part_pkg ) {
370 $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
372 my $pass = ($cust_pkg->no_auto || $part_pkg->no_auto) ? 'no_auto' : '';
375 $self->_make_lines( 'part_pkg' => $part_pkg,
376 'cust_pkg' => $cust_pkg,
377 'precommit_hooks' => \@precommit_hooks,
378 'line_items' => $cust_bill_pkg{$pass},
379 'setup' => $total_setup{$pass},
380 'recur' => $total_recur{$pass},
381 'tax_matrix' => $taxlisthash{$pass},
383 'real_pkgpart' => $real_pkgpart,
384 'options' => \%options,
387 $dbh->rollback if $oldAutoCommit;
391 } #foreach my $part_pkg
393 } #foreach my $cust_pkg
395 #if the customer isn't on an automatic payby, everything can go on a single
397 #if ( $cust_main->payby !~ /^(CARD|CHEK)$/ ) {
398 #merge everything into one list
401 foreach my $pass (@passes) { # keys %cust_bill_pkg ) {
403 my @cust_bill_pkg = _omit_zero_value_bundles(@{ $cust_bill_pkg{$pass} });
405 next unless @cust_bill_pkg; #don't create an invoice w/o line items
407 if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
408 !$conf->exists('postal_invoice-recurring_only')
412 my $postal_pkg = $self->charge_postal_fee();
413 if ( $postal_pkg && !ref( $postal_pkg ) ) {
415 $dbh->rollback if $oldAutoCommit;
416 return "can't charge postal invoice fee for customer ".
417 $self->custnum. ": $postal_pkg";
419 } elsif ( $postal_pkg ) {
421 my $real_pkgpart = $postal_pkg->pkgpart;
422 # we could implement this bit as FS::part_pkg::has_hidden, but we already
423 # suffer from performance issues
424 $options{has_hidden} = 0;
425 my @part_pkg = $postal_pkg->part_pkg->self_and_bill_linked;
426 $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
428 foreach my $part_pkg ( @part_pkg ) {
429 my %postal_options = %options;
430 delete $postal_options{cancel};
432 $self->_make_lines( 'part_pkg' => $part_pkg,
433 'cust_pkg' => $postal_pkg,
434 'precommit_hooks' => \@precommit_hooks,
435 'line_items' => \@cust_bill_pkg,
436 'setup' => $total_setup{$pass},
437 'recur' => $total_recur{$pass},
438 'tax_matrix' => $taxlisthash{$pass},
440 'real_pkgpart' => $real_pkgpart,
441 'options' => \%postal_options,
444 $dbh->rollback if $oldAutoCommit;
449 # it's silly to have a zero value postal_pkg, but....
450 @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
456 my $listref_or_error =
457 $self->calculate_taxes( \@cust_bill_pkg, $taxlisthash{$pass}, $invoice_time);
459 unless ( ref( $listref_or_error ) ) {
460 $dbh->rollback if $oldAutoCommit;
461 return $listref_or_error;
464 foreach my $taxline ( @$listref_or_error ) {
465 ${ $total_setup{$pass} } =
466 sprintf('%.2f', ${ $total_setup{$pass} } + $taxline->setup );
467 push @cust_bill_pkg, $taxline;
471 warn "adding tax adjustments...\n" if $DEBUG > 2;
472 foreach my $cust_tax_adjustment (
473 qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum,
479 my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
481 my $itemdesc = $cust_tax_adjustment->taxname;
482 $itemdesc = '' if $itemdesc eq 'Tax';
484 push @cust_bill_pkg, new FS::cust_bill_pkg {
490 'itemdesc' => $itemdesc,
491 'itemcomment' => $cust_tax_adjustment->comment,
492 'cust_tax_adjustment' => $cust_tax_adjustment,
493 #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
498 my $charged = sprintf('%.2f', ${ $total_setup{$pass} } + ${ $total_recur{$pass} } );
500 my @cust_bill = $self->cust_bill;
501 my $balance = $self->balance;
502 my $previous_balance = scalar(@cust_bill)
503 ? ( $cust_bill[$#cust_bill]->billing_balance || 0 )
506 $previous_balance += $cust_bill[$#cust_bill]->charged
507 if scalar(@cust_bill);
508 #my $balance_adjustments =
509 # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged);
511 #create the new invoice
512 my $cust_bill = new FS::cust_bill ( {
513 'custnum' => $self->custnum,
514 '_date' => ( $invoice_time ),
515 'charged' => $charged,
516 'billing_balance' => $balance,
517 'previous_balance' => $previous_balance,
518 'invoice_terms' => $options{'invoice_terms'},
520 $error = $cust_bill->insert;
522 $dbh->rollback if $oldAutoCommit;
523 return "can't create invoice for customer #". $self->custnum. ": $error";
526 foreach my $cust_bill_pkg ( @cust_bill_pkg ) {
527 $cust_bill_pkg->invnum($cust_bill->invnum);
528 my $error = $cust_bill_pkg->insert;
530 $dbh->rollback if $oldAutoCommit;
531 return "can't create invoice line item: $error";
535 } #foreach my $pass ( keys %cust_bill_pkg )
537 foreach my $hook ( @precommit_hooks ) {
542 $dbh->rollback if $oldAutoCommit;
543 return "$@ running precommit hook $hook\n";
547 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
551 #discard bundled packages of 0 value
552 sub _omit_zero_value_bundles {
554 my @cust_bill_pkg = ();
555 my @cust_bill_pkg_bundle = ();
558 foreach my $cust_bill_pkg ( @_ ) {
559 if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) {
560 push @cust_bill_pkg, @cust_bill_pkg_bundle if $sum > 0;
561 @cust_bill_pkg_bundle = ();
564 $sum += $cust_bill_pkg->setup + $cust_bill_pkg->recur;
565 push @cust_bill_pkg_bundle, $cust_bill_pkg;
567 push @cust_bill_pkg, @cust_bill_pkg_bundle if $sum > 0;
573 =item calculate_taxes LINEITEMREF TAXHASHREF INVOICE_TIME
575 This is a weird one. Perhaps it should not even be exposed.
577 Generates tax line items (see L<FS::cust_bill_pkg>) for this customer.
578 Usually used internally by bill method B<bill>.
580 If there is an error, returns the error, otherwise returns reference to a
581 list of line items suitable for insertion.
587 An array ref of the line items being billed.
591 A strange beast. The keys to this hash are internal identifiers consisting
592 of the name of the tax object type, a space, and its unique identifier ( e.g.
593 'cust_main_county 23' ). The values of the hash are listrefs. The first
594 item in the list is the tax object. The remaining items are either line
595 items or floating point values (currency amounts).
597 The taxes are calculated on this entity. Calculated exemption records are
598 transferred to the LINEITEMREF items on the assumption that they are related.
604 This specifies the date appearing on the associated invoice. Some
605 jurisdictions (i.e. Texas) have tax exemptions which are date sensitive.
610 sub calculate_taxes {
611 my ($self, $cust_bill_pkg, $taxlisthash, $invoice_time) = @_;
613 my @tax_line_items = ();
615 warn "having a look at the taxes we found...\n" if $DEBUG > 2;
617 # keys are tax names (as printed on invoices / itemdesc )
618 # values are listrefs of taxlisthash keys (internal identifiers)
621 # keys are taxlisthash keys (internal identifiers)
622 # values are (cumulative) amounts
625 # keys are taxlisthash keys (internal identifiers)
626 # values are listrefs of cust_bill_pkg_tax_location hashrefs
627 my %tax_location = ();
629 # keys are taxlisthash keys (internal identifiers)
630 # values are listrefs of cust_bill_pkg_tax_rate_location hashrefs
631 my %tax_rate_location = ();
633 foreach my $tax ( keys %$taxlisthash ) {
634 my $tax_object = shift @{ $taxlisthash->{$tax} };
635 warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
636 warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2;
637 my $hashref_or_error =
638 $tax_object->taxline( $taxlisthash->{$tax},
639 'custnum' => $self->custnum,
640 'invoice_time' => $invoice_time
642 return $hashref_or_error unless ref($hashref_or_error);
644 unshift @{ $taxlisthash->{$tax} }, $tax_object;
646 my $name = $hashref_or_error->{'name'};
647 my $amount = $hashref_or_error->{'amount'};
649 #warn "adding $amount as $name\n";
650 $taxname{ $name } ||= [];
651 push @{ $taxname{ $name } }, $tax;
653 $tax{ $tax } += $amount;
655 $tax_location{ $tax } ||= [];
656 if ( $tax_object->get('pkgnum') || $tax_object->get('locationnum') ) {
657 push @{ $tax_location{ $tax } },
659 'taxnum' => $tax_object->taxnum,
660 'taxtype' => ref($tax_object),
661 'pkgnum' => $tax_object->get('pkgnum'),
662 'locationnum' => $tax_object->get('locationnum'),
663 'amount' => sprintf('%.2f', $amount ),
667 $tax_rate_location{ $tax } ||= [];
668 if ( ref($tax_object) eq 'FS::tax_rate' ) {
669 my $taxratelocationnum =
670 $tax_object->tax_rate_location->taxratelocationnum;
671 push @{ $tax_rate_location{ $tax } },
673 'taxnum' => $tax_object->taxnum,
674 'taxtype' => ref($tax_object),
675 'amount' => sprintf('%.2f', $amount ),
676 'locationtaxid' => $tax_object->location,
677 'taxratelocationnum' => $taxratelocationnum,
683 #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
684 my %packagemap = map { $_->pkgnum => $_ } @$cust_bill_pkg;
685 foreach my $tax ( keys %$taxlisthash ) {
686 foreach ( @{ $taxlisthash->{$tax} }[1 ... scalar(@{ $taxlisthash->{$tax} })] ) {
687 next unless ref($_) eq 'FS::cust_bill_pkg';
689 push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
690 splice( @{ $_->_cust_tax_exempt_pkg } );
694 #consolidate and create tax line items
695 warn "consolidating and generating...\n" if $DEBUG > 2;
696 foreach my $taxname ( keys %taxname ) {
699 my @cust_bill_pkg_tax_location = ();
700 my @cust_bill_pkg_tax_rate_location = ();
701 warn "adding $taxname\n" if $DEBUG > 1;
702 foreach my $taxitem ( @{ $taxname{$taxname} } ) {
703 next if $seen{$taxitem}++;
704 warn "adding $tax{$taxitem}\n" if $DEBUG > 1;
705 $tax += $tax{$taxitem};
706 push @cust_bill_pkg_tax_location,
707 map { new FS::cust_bill_pkg_tax_location $_ }
708 @{ $tax_location{ $taxitem } };
709 push @cust_bill_pkg_tax_rate_location,
710 map { new FS::cust_bill_pkg_tax_rate_location $_ }
711 @{ $tax_rate_location{ $taxitem } };
715 $tax = sprintf('%.2f', $tax );
717 my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
723 if ( $pkg_category and
724 $conf->config('invoice_latexsummary') ||
725 $conf->config('invoice_htmlsummary')
729 my %hash = ( 'section' => $pkg_category->categoryname );
730 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
734 push @tax_line_items, new FS::cust_bill_pkg {
740 'itemdesc' => $taxname,
741 'display' => \@display,
742 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
743 'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
752 my ($self, %params) = @_;
754 my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
755 my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
756 my $precommit_hooks = $params{precommit_hooks} or die "no package specified";
757 my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
758 my $total_setup = $params{setup} or die "no setup accumulator specified";
759 my $total_recur = $params{recur} or die "no recur accumulator specified";
760 my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
761 my $time = $params{'time'} or die "no time specified";
762 my (%options) = %{$params{options}};
765 my $real_pkgpart = $params{real_pkgpart};
766 my %hash = $cust_pkg->hash;
767 my $old_cust_pkg = new FS::cust_pkg \%hash;
773 $cust_pkg->pkgpart($part_pkg->pkgpart);
781 if ( $options{'resetup'}
782 || ( ! $cust_pkg->setup
783 && ( ! $cust_pkg->start_date
784 || $cust_pkg->start_date <= $time
786 && ( ! $conf->exists('disable_setup_suspended_pkgs')
787 || ( $conf->exists('disable_setup_suspended_pkgs') &&
788 ! $cust_pkg->getfield('susp')
795 warn " bill setup\n" if $DEBUG > 1;
798 $setup = eval { $cust_pkg->calc_setup( $time, \@details ) };
799 return "$@ running calc_setup for $cust_pkg\n"
802 $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
804 $cust_pkg->setfield('setup', $time)
805 unless $cust_pkg->setup;
806 #do need it, but it won't get written to the db
807 #|| $cust_pkg->pkgpart != $real_pkgpart;
809 $cust_pkg->setfield('start_date', '')
810 if $cust_pkg->start_date;
818 #XXX unit stuff here too
822 if ( ! $cust_pkg->get('susp')
823 and ! $cust_pkg->get('start_date')
824 and ( $part_pkg->getfield('freq') ne '0'
825 && ( $cust_pkg->getfield('bill') || 0 ) <= $time
827 || ( $part_pkg->plan eq 'voip_cdr'
828 && $part_pkg->option('bill_every_call')
830 || ( $options{cancel} )
833 # XXX should this be a package event? probably. events are called
834 # at collection time at the moment, though...
835 $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
836 if $part_pkg->can('reset_usage');
837 #don't want to reset usage just cause we want a line item??
838 #&& $part_pkg->pkgpart == $real_pkgpart;
840 warn " bill recur\n" if $DEBUG > 1;
843 # XXX shared with $recur_prog
844 $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
848 #over two params! lets at least switch to a hashref for the rest...
849 my $increment_next_bill = ( $part_pkg->freq ne '0'
850 && ( $cust_pkg->getfield('bill') || 0 ) <= $time
853 my %param = ( 'precommit_hooks' => $precommit_hooks,
854 'increment_next_bill' => $increment_next_bill,
855 'discounts' => \@discounts,
856 'real_pkgpart' => $real_pkgpart,
859 my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
860 $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
861 return "$@ running $method for $cust_pkg\n"
864 if ( $increment_next_bill ) {
866 my $next_bill = $part_pkg->add_freq($sdate);
867 return "unparsable frequency: ". $part_pkg->freq
870 #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
871 # only for figuring next bill date, nothing else, so, reset $sdate again
873 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
874 #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
875 $cust_pkg->last_bill($sdate);
877 $cust_pkg->setfield('bill', $next_bill );
883 warn "\$setup is undefined" unless defined($setup);
884 warn "\$recur is undefined" unless defined($recur);
885 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
888 # If there's line items, create em cust_bill_pkg records
889 # If $cust_pkg has been modified, update it (if we're a real pkgpart)
892 if ( $lineitems || $options{has_hidden} ) {
894 if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
895 # hmm.. and if just the options are modified in some weird price plan?
897 warn " package ". $cust_pkg->pkgnum. " modified; updating\n"
900 my $error = $cust_pkg->replace( $old_cust_pkg,
901 'options' => { $cust_pkg->options },
903 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
904 if $error; #just in case
907 $setup = sprintf( "%.2f", $setup );
908 $recur = sprintf( "%.2f", $recur );
909 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
910 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
912 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
913 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
918 !$part_pkg->hidden && $options{has_hidden} ) #include some $0 lines
921 warn " charges (setup=$setup, recur=$recur); adding line items\n"
924 my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
926 warn " adding customer package invoice detail: $_\n"
927 foreach @cust_pkg_detail;
929 push @details, @cust_pkg_detail;
931 my $cust_bill_pkg = new FS::cust_bill_pkg {
932 'pkgnum' => $cust_pkg->pkgnum,
934 'unitsetup' => $unitsetup,
936 'unitrecur' => $unitrecur,
937 'quantity' => $cust_pkg->quantity,
938 'details' => \@details,
939 'discounts' => \@discounts,
940 'hidden' => $part_pkg->hidden,
943 if ( $part_pkg->option('recur_temporality', 1) eq 'preceding' ) {
944 $cust_bill_pkg->sdate( $hash{last_bill} );
945 $cust_bill_pkg->edate( $sdate - 86399 ); #60s*60m*24h-1
946 $cust_bill_pkg->edate( $time ) if $options{cancel};
947 } else { #if ( $part_pkg->option('recur_temporality', 1) eq 'upcoming' ) {
948 $cust_bill_pkg->sdate( $sdate );
949 $cust_bill_pkg->edate( $cust_pkg->bill );
950 #$cust_bill_pkg->edate( $time ) if $options{cancel};
953 $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
954 unless $part_pkg->pkgpart == $real_pkgpart;
956 $$total_setup += $setup;
957 $$total_recur += $recur;
964 $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart, \%options);
965 return $error if $error;
967 push @$cust_bill_pkgs, $cust_bill_pkg;
969 } #if $setup != 0 || $recur != 0
979 my $part_pkg = shift;
980 my $taxlisthash = shift;
981 my $cust_bill_pkg = shift;
982 my $cust_pkg = shift;
983 my $invoice_time = shift;
984 my $real_pkgpart = shift;
987 my %cust_bill_pkg = ();
991 #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
992 push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
993 push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
994 push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
996 if ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) {
998 if ( $conf->exists('enable_taxproducts')
999 && ( scalar($part_pkg->part_pkg_taxoverride)
1000 || $part_pkg->has_taxproduct
1005 if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
1006 return "fatal: Can't (yet) use tax-pkg_address with taxproducts";
1009 foreach my $class (@classes) {
1010 my $err_or_ref = $self->_gather_taxes( $part_pkg, $class );
1011 return $err_or_ref unless ref($err_or_ref);
1012 $taxes{$class} = $err_or_ref;
1015 unless (exists $taxes{''}) {
1016 my $err_or_ref = $self->_gather_taxes( $part_pkg, '' );
1017 return $err_or_ref unless ref($err_or_ref);
1018 $taxes{''} = $err_or_ref;
1023 my @loc_keys = qw( city county state country );
1025 if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
1026 my $cust_location = $cust_pkg->cust_location;
1027 %taxhash = map { $_ => $cust_location->$_() } @loc_keys;
1030 ( $conf->exists('tax-ship_address') && length($self->ship_last) )
1033 %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys;
1036 $taxhash{'taxclass'} = $part_pkg->taxclass;
1039 my %taxhash_elim = %taxhash;
1040 my @elim = qw( city county state );
1043 #first try a match with taxclass
1044 @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
1046 if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
1047 #then try a match without taxclass
1048 my %no_taxclass = %taxhash_elim;
1049 $no_taxclass{ 'taxclass' } = '';
1050 @taxes = qsearch( 'cust_main_county', \%no_taxclass );
1053 $taxhash_elim{ shift(@elim) } = '';
1055 } while ( !scalar(@taxes) && scalar(@elim) );
1057 @taxes = grep { ! $_->taxname or ! $self->tax_exemption($_->taxname) }
1059 if $self->cust_main_exemption; #just to be safe
1061 if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
1063 $_->set('pkgnum', $cust_pkg->pkgnum );
1064 $_->set('locationnum', $cust_pkg->locationnum );
1068 $taxes{''} = [ @taxes ];
1069 $taxes{'setup'} = [ @taxes ];
1070 $taxes{'recur'} = [ @taxes ];
1071 $taxes{$_} = [ @taxes ] foreach (@classes);
1073 # # maybe eliminate this entirely, along with all the 0% records
1074 # unless ( @taxes ) {
1076 # "fatal: can't find tax rate for state/county/country/taxclass ".
1077 # join('/', map $taxhash{$_}, qw(state county country taxclass) );
1080 } #if $conf->exists('enable_taxproducts') ...
1085 my $separate = $conf->exists('separate_usage');
1086 my $temp_pkg = new FS::cust_pkg { pkgpart => $real_pkgpart };
1087 my $usage_mandate = $temp_pkg->part_pkg->option('usage_mandate', 'Hush!');
1088 my $section = $temp_pkg->part_pkg->categoryname;
1089 if ( $separate || $section || $usage_mandate ) {
1091 my %hash = ( 'section' => $section );
1093 $section = $temp_pkg->part_pkg->option('usage_section', 'Hush!');
1094 my $summary = $temp_pkg->part_pkg->option('summarize_usage', 'Hush!');
1096 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
1097 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
1099 push @display, new FS::cust_bill_pkg_display
1102 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
1106 if ($separate && $section && $summary) {
1107 push @display, new FS::cust_bill_pkg_display { type => 'U',
1112 if ($usage_mandate || $section && $summary) {
1113 $hash{post_total} = 'Y';
1116 if ($separate || $usage_mandate) {
1117 $hash{section} = $section if ($separate || $usage_mandate);
1118 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
1122 $cust_bill_pkg->set('display', \@display);
1124 my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
1125 foreach my $key (keys %tax_cust_bill_pkg) {
1126 my @taxes = @{ $taxes{$key} || [] };
1127 my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
1129 my %localtaxlisthash = ();
1130 foreach my $tax ( @taxes ) {
1132 my $taxname = ref( $tax ). ' '. $tax->taxnum;
1133 # $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
1134 # ' locationnum'. $cust_pkg->locationnum
1135 # if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
1137 $taxlisthash->{ $taxname } ||= [ $tax ];
1138 push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
1140 $localtaxlisthash{ $taxname } ||= [ $tax ];
1141 push @{ $localtaxlisthash{ $taxname } }, $tax_cust_bill_pkg;
1145 warn "finding taxed taxes...\n" if $DEBUG > 2;
1146 foreach my $tax ( keys %localtaxlisthash ) {
1147 my $tax_object = shift @{ $localtaxlisthash{$tax} };
1148 warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
1150 next unless $tax_object->can('tax_on_tax');
1152 foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
1153 my $totname = ref( $tot ). ' '. $tot->taxnum;
1155 warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
1157 next unless exists( $localtaxlisthash{ $totname } ); # only increase
1159 warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
1160 my $hashref_or_error =
1161 $tax_object->taxline( $localtaxlisthash{$tax},
1162 'custnum' => $self->custnum,
1163 'invoice_time' => $invoice_time,
1165 return $hashref_or_error
1166 unless ref($hashref_or_error);
1168 $taxlisthash->{ $totname } ||= [ $tot ];
1169 push @{ $taxlisthash->{ $totname } }, $hashref_or_error->{amount};
1181 my $part_pkg = shift;
1185 my $geocode = $self->geocode('cch');
1187 my @taxclassnums = map { $_->taxclassnum }
1188 $part_pkg->part_pkg_taxoverride($class);
1190 unless (@taxclassnums) {
1191 @taxclassnums = map { $_->taxclassnum }
1192 grep { $_->taxable eq 'Y' }
1193 $part_pkg->part_pkg_taxrate('cch', $geocode, $class);
1195 warn "Found taxclassnum values of ". join(',', @taxclassnums)
1200 join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
1202 @taxes = qsearch({ 'table' => 'tax_rate',
1203 'hashref' => { 'geocode' => $geocode, },
1204 'extra_sql' => $extra_sql,
1206 if scalar(@taxclassnums);
1208 warn "Found taxes ".
1209 join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n"
1216 =item collect [ HASHREF | OPTION => VALUE ... ]
1218 (Attempt to) collect money for this customer's outstanding invoices (see
1219 L<FS::cust_bill>). Usually used after the bill method.
1221 Actions are now triggered by billing events; see L<FS::part_event> and the
1222 billing events web interface. Old-style invoice events (see
1223 L<FS::part_bill_event>) have been deprecated.
1225 If there is an error, returns the error, otherwise returns false.
1227 Options are passed as name-value pairs.
1229 Currently available options are:
1235 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.
1239 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1243 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1247 set true to surpress email card/ACH decline notices.
1251 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)
1257 # allows for one time override of normal customer billing method
1262 my( $self, %options ) = @_;
1263 my $invoice_time = $options{'invoice_time'} || time;
1266 local $SIG{HUP} = 'IGNORE';
1267 local $SIG{INT} = 'IGNORE';
1268 local $SIG{QUIT} = 'IGNORE';
1269 local $SIG{TERM} = 'IGNORE';
1270 local $SIG{TSTP} = 'IGNORE';
1271 local $SIG{PIPE} = 'IGNORE';
1273 my $oldAutoCommit = $FS::UID::AutoCommit;
1274 local $FS::UID::AutoCommit = 0;
1277 $self->select_for_update; #mutex
1280 my $balance = $self->balance;
1281 warn "$me collect customer ". $self->custnum. ": balance $balance\n"
1284 if ( exists($options{'retry_card'}) ) {
1285 carp 'retry_card option passed to collect is deprecated; use retry';
1286 $options{'retry'} ||= $options{'retry_card'};
1288 if ( exists($options{'retry'}) && $options{'retry'} ) {
1289 my $error = $self->retry_realtime;
1291 $dbh->rollback if $oldAutoCommit;
1296 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1298 #never want to roll back an event just because it returned an error
1299 local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1301 $self->do_cust_event(
1302 'debug' => ( $options{'debug'} || 0 ),
1303 'time' => $invoice_time,
1304 'check_freq' => $options{'check_freq'},
1305 'stage' => 'collect',
1311 =item apply_payments_and_credits [ OPTION => VALUE ... ]
1313 Applies unapplied payments and credits.
1315 In most cases, this new method should be used in place of sequential
1316 apply_payments and apply_credits methods.
1318 A hash of optional arguments may be passed. Currently "manual" is supported.
1319 If true, a payment receipt is sent instead of a statement when
1320 'payment_receipt_email' configuration option is set.
1322 If there is an error, returns the error, otherwise returns false.
1326 sub apply_payments_and_credits {
1327 my( $self, %options ) = @_;
1329 local $SIG{HUP} = 'IGNORE';
1330 local $SIG{INT} = 'IGNORE';
1331 local $SIG{QUIT} = 'IGNORE';
1332 local $SIG{TERM} = 'IGNORE';
1333 local $SIG{TSTP} = 'IGNORE';
1334 local $SIG{PIPE} = 'IGNORE';
1336 my $oldAutoCommit = $FS::UID::AutoCommit;
1337 local $FS::UID::AutoCommit = 0;
1340 $self->select_for_update; #mutex
1342 foreach my $cust_bill ( $self->open_cust_bill ) {
1343 my $error = $cust_bill->apply_payments_and_credits(%options);
1345 $dbh->rollback if $oldAutoCommit;
1346 return "Error applying: $error";
1350 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1355 =item apply_credits OPTION => VALUE ...
1357 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1358 to outstanding invoice balances in chronological order (or reverse
1359 chronological order if the I<order> option is set to B<newest>) and returns the
1360 value of any remaining unapplied credits available for refund (see
1361 L<FS::cust_refund>).
1363 Dies if there is an error.
1371 local $SIG{HUP} = 'IGNORE';
1372 local $SIG{INT} = 'IGNORE';
1373 local $SIG{QUIT} = 'IGNORE';
1374 local $SIG{TERM} = 'IGNORE';
1375 local $SIG{TSTP} = 'IGNORE';
1376 local $SIG{PIPE} = 'IGNORE';
1378 my $oldAutoCommit = $FS::UID::AutoCommit;
1379 local $FS::UID::AutoCommit = 0;
1382 $self->select_for_update; #mutex
1384 unless ( $self->total_unapplied_credits ) {
1385 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1389 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1390 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1392 my @invoices = $self->open_cust_bill;
1393 @invoices = sort { $b->_date <=> $a->_date } @invoices
1394 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
1396 if ( $conf->exists('pkg-balances') ) {
1397 # limit @credits to those w/ a pkgnum grepped from $self
1399 foreach my $i (@invoices) {
1400 foreach my $li ( $i->cust_bill_pkg ) {
1401 $pkgnums{$li->pkgnum} = 1;
1404 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
1409 foreach my $cust_bill ( @invoices ) {
1411 if ( !defined($credit) || $credit->credited == 0) {
1412 $credit = pop @credits or last;
1416 if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
1417 $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
1419 $owed = $cust_bill->owed;
1421 unless ( $owed > 0 ) {
1422 push @credits, $credit;
1426 my $amount = min( $credit->credited, $owed );
1428 my $cust_credit_bill = new FS::cust_credit_bill ( {
1429 'crednum' => $credit->crednum,
1430 'invnum' => $cust_bill->invnum,
1431 'amount' => $amount,
1433 $cust_credit_bill->pkgnum( $credit->pkgnum )
1434 if $conf->exists('pkg-balances') && $credit->pkgnum;
1435 my $error = $cust_credit_bill->insert;
1437 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
1441 redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
1445 my $total_unapplied_credits = $self->total_unapplied_credits;
1447 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1449 return $total_unapplied_credits;
1452 =item apply_payments [ OPTION => VALUE ... ]
1454 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1455 to outstanding invoice balances in chronological order.
1457 #and returns the value of any remaining unapplied payments.
1459 A hash of optional arguments may be passed. Currently "manual" is supported.
1460 If true, a payment receipt is sent instead of a statement when
1461 'payment_receipt_email' configuration option is set.
1463 Dies if there is an error.
1467 sub apply_payments {
1468 my( $self, %options ) = @_;
1470 local $SIG{HUP} = 'IGNORE';
1471 local $SIG{INT} = 'IGNORE';
1472 local $SIG{QUIT} = 'IGNORE';
1473 local $SIG{TERM} = 'IGNORE';
1474 local $SIG{TSTP} = 'IGNORE';
1475 local $SIG{PIPE} = 'IGNORE';
1477 my $oldAutoCommit = $FS::UID::AutoCommit;
1478 local $FS::UID::AutoCommit = 0;
1481 $self->select_for_update; #mutex
1485 my @payments = sort { $b->_date <=> $a->_date }
1486 grep { $_->unapplied > 0 }
1489 my @invoices = sort { $a->_date <=> $b->_date}
1490 grep { $_->owed > 0 }
1493 if ( $conf->exists('pkg-balances') ) {
1494 # limit @payments to those w/ a pkgnum grepped from $self
1496 foreach my $i (@invoices) {
1497 foreach my $li ( $i->cust_bill_pkg ) {
1498 $pkgnums{$li->pkgnum} = 1;
1501 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
1506 foreach my $cust_bill ( @invoices ) {
1508 if ( !defined($payment) || $payment->unapplied == 0 ) {
1509 $payment = pop @payments or last;
1513 if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
1514 $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
1516 $owed = $cust_bill->owed;
1518 unless ( $owed > 0 ) {
1519 push @payments, $payment;
1523 my $amount = min( $payment->unapplied, $owed );
1525 my $cust_bill_pay = new FS::cust_bill_pay ( {
1526 'paynum' => $payment->paynum,
1527 'invnum' => $cust_bill->invnum,
1528 'amount' => $amount,
1530 $cust_bill_pay->pkgnum( $payment->pkgnum )
1531 if $conf->exists('pkg-balances') && $payment->pkgnum;
1532 my $error = $cust_bill_pay->insert(%options);
1534 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
1538 redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
1542 my $total_unapplied_payments = $self->total_unapplied_payments;
1544 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1546 return $total_unapplied_payments;