package FS::cust_bill; use base qw( FS::cust_bill::Search FS::Template_Mixin FS::cust_main_Mixin FS::Record ); use strict; use vars qw( $DEBUG $me ); # but NOT $conf use Carp; use Fcntl qw(:flock); #for spool_csv use Cwd; use List::Util qw(min max sum); use Date::Format; use DateTime; use File::Temp 0.14; use HTML::Entities; use Storable qw( freeze thaw ); use GD::Barcode; use FS::UID qw( datasrc ); use FS::Misc qw( send_fax do_print ); use FS::Record qw( qsearch qsearchs dbh ); use FS::cust_statement; use FS::cust_bill_pkg; use FS::cust_bill_pkg_display; use FS::cust_bill_pkg_detail; use FS::cust_credit; use FS::cust_pay; use FS::cust_pkg; use FS::cust_credit_bill; use FS::pay_batch; use FS::cust_event; use FS::part_pkg; use FS::cust_bill_pay; use FS::payby; use FS::bill_batch; use FS::cust_bill_batch; use FS::cust_bill_pay_pkg; use FS::cust_credit_bill_pkg; use FS::discount_plan; use FS::cust_bill_void; use FS::reason; use FS::reason_type; use FS::L10N; use FS::Misc::Savepoint; $DEBUG = 0; $me = '[FS::cust_bill]'; =head1 NAME FS::cust_bill - Object methods for cust_bill records =head1 SYNOPSIS use FS::cust_bill; $record = new FS::cust_bill \%hash; $record = new FS::cust_bill { 'column' => 'value' }; $error = $record->insert; $error = $new_record->replace($old_record); $error = $record->delete; $error = $record->check; ( $total_previous_balance, @previous_cust_bill ) = $record->previous; @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg; ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit; @cust_pay_objects = $cust_bill->cust_pay; $tax_amount = $record->tax; @lines = $cust_bill->print_text; @lines = $cust_bill->print_text('time' => $time); =head1 DESCRIPTION An FS::cust_bill object represents an invoice; a declaration that a customer owes you money. The specific charges are itemized as B records (see L). FS::cust_bill inherits from FS::Record. The following fields are currently supported: Regular fields =over 4 =item invnum - primary key (assigned automatically for new invoices) =item custnum - customer (see L) =item _date - specified as a UNIX timestamp; see L. Also see L and L for conversion functions. =item charged - amount of this invoice =item invoice_terms - optional terms override for this specific invoice =back Deprecated fields =over 4 =item billing_balance - the customer's balance immediately before generating this invoice. DEPRECATED. Use the L method to determine the customer's balance at a specific time. =item previous_balance - the customer's balance immediately after generating the invoice before this one. DEPRECATED. =item printed - formerly used to track the number of times an invoice had been printed; no longer used. =back Specific use cases =over 4 =item closed - books closed flag, empty or `Y' =item statementnum - invoice aggregation (see L) =item agent_invid - legacy invoice number =item promised_date - customer promised payment date, for collection =item pending - invoice is still being generated, empty or 'Y' =back =head1 METHODS =over 4 =item new HASHREF Creates a new invoice. To add the invoice to the database, see L<"insert">. Invoices are normally created by calling the bill method of a customer object (see L). =cut sub table { 'cust_bill'; } sub template_conf { 'invoice_'; } # should be the ONLY occurrence of "Invoice" in invoice rendering code. # (except email_subject and invnum_date_pretty) sub notice_name { my $self = shift; $self->conf->config('notice_name') || 'Invoice' } sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum } sub cust_unlinked_msg { my $self = shift; "WARNING: can't find cust_main.custnum ". $self->custnum. ' (cust_bill.invnum '. $self->invnum. ')'; } =item insert Adds this invoice to the database ("Posts" the invoice). If there is an error, returns the error, otherwise returns false. =cut sub insert { my $self = shift; warn "$me insert called\n" if $DEBUG; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; local $SIG{TERM} = 'IGNORE'; local $SIG{TSTP} = 'IGNORE'; local $SIG{PIPE} = 'IGNORE'; my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; my $error = $self->SUPER::insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; } if ( $self->get('cust_bill_pkg') ) { foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) { $cust_bill_pkg->invnum($self->invnum); my $error = $cust_bill_pkg->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; return "can't create invoice line item: $error"; } } } $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } =item void [ REASON [ , REPROCESS_CDRS ] ] Voids this invoice: deletes the invoice and adds a record of the voided invoice to the FS::cust_bill_void table (and related tables starting from FS::cust_bill_pkg_void). =cut sub void { my $self = shift; my $reason = scalar(@_) ? shift : ''; my $reprocess_cdrs = scalar(@_) ? shift : ''; unless (ref($reason) || !$reason) { $reason = FS::reason->new_or_existing( 'class' => 'I', 'type' => 'Invoice void', 'reason' => $reason ); } local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; local $SIG{TERM} = 'IGNORE'; local $SIG{TSTP} = 'IGNORE'; local $SIG{PIPE} = 'IGNORE'; my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; my $cust_bill_void = new FS::cust_bill_void ( { map { $_ => $self->get($_) } $self->fields } ); $cust_bill_void->reasonnum($reason->reasonnum) if $reason; my $error = $cust_bill_void->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; } foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { my $error = $cust_bill_pkg->void($reason, $reprocess_cdrs); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; } } $error = $self->_delete; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; } $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } # removed docs entirely and renamed method to _delete to further indicate it is # internal-only and discourage use # # =item delete # # DO NOT USE THIS METHOD. Instead, apply a credit against the invoice, or use # the B method. # # This is only for internal use by V, which is what you should be using. # # DO NOT USE THIS METHOD. Whatever reason you think you have is almost certainly # wrong. Use B, that's what it is for. Really. This means you. # # =cut sub _delete { my $self = shift; return "Can't delete closed invoice" if $self->closed =~ /^Y/i; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; local $SIG{TERM} = 'IGNORE'; local $SIG{TSTP} = 'IGNORE'; local $SIG{PIPE} = 'IGNORE'; my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; foreach my $table (qw( cust_credit_bill cust_bill_pay_batch cust_bill_pay cust_bill_batch cust_bill_pkg )) { #cust_event # problematic #cust_pay_batch # unnecessary foreach my $linked ( $self->$table() ) { my $error = $linked->delete; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; } } } my $error = $self->SUPER::delete(@_); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; } $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } =item replace [ OLD_RECORD ] You can, but probably shouldn't modify invoices... Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not supplied, replaces this record. If there is an error, returns the error, otherwise returns false. =cut #replace can be inherited from Record.pm # replace_check is now the preferred way to #implement replace data checks # (so $object->replace() works without an argument) sub replace_check { my( $new, $old ) = ( shift, shift ); return "Can't modify closed invoice" if $old->closed =~ /^Y/i; #return "Can't change _date!" unless $old->_date eq $new->_date; return "Can't change _date" unless $old->_date == $new->_date; return "Can't change charged" unless $old->charged == $new->charged || $old->pending eq 'Y' || $old->charged == 0 || $new->{'Hash'}{'cc_surcharge_replace_hack'}; ''; } =item add_cc_surcharge Giant hack =cut sub add_cc_surcharge { my ($self, $pkgnum, $amount) = (shift, shift, shift); my $error; my $cust_bill_pkg = new FS::cust_bill_pkg({ 'invnum' => $self->invnum, 'pkgnum' => $pkgnum, 'setup' => $amount, }); $error = $cust_bill_pkg->insert; return $error if $error; $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1; $self->charged($self->charged+$amount); $error = $self->replace; return $error if $error; $self->apply_payments_and_credits; } =item check Checks all fields to make sure this is a valid invoice. If there is an error, returns the error, otherwise returns false. Called by the insert and replace methods. =cut sub check { my $self = shift; my $error = $self->ut_numbern('invnum') || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' ) || $self->ut_numbern('_date') || $self->ut_money('charged') || $self->ut_numbern('printed') || $self->ut_enum('closed', [ '', 'Y' ]) || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' ) || $self->ut_numbern('agent_invid') #varchar? || $self->ut_flag('pending') ; return $error if $error; $self->_date(time) unless $self->_date; $self->printed(0) if $self->printed eq ''; $self->SUPER::check; } =item display_invnum Returns the displayed invoice number for this invoice: agent_invid if cust_bill-default_agent_invid is set and it has a value, invnum otherwise. =cut sub display_invnum { my $self = shift; if ( $self->agent_invid && FS::Conf->new->exists('cust_bill-default_agent_invid') ) { return $self->agent_invid; } else { return $self->invnum; } } =item previous_bill Returns the customer's last invoice before this one. =cut sub previous_bill { my $self = shift; if ( !$self->get('previous_bill') ) { $self->set('previous_bill', qsearchs({ 'table' => 'cust_bill', 'hashref' => { 'custnum' => $self->custnum, '_date' => { op=>'<', value=>$self->_date } }, 'order_by' => 'ORDER BY _date DESC LIMIT 1', }) ); } $self->get('previous_bill'); } =item following_bill Returns the customer's invoice that follows this one =cut sub following_bill { my $self = shift; if (!$self->get('following_bill')) { $self->set('following_bill', qsearchs({ table => 'cust_bill', hashref => { custnum => $self->custnum, invnum => { op => '>', value => $self->invnum }, }, order_by => 'ORDER BY invnum ASC LIMIT 1', })); } $self->get('following_bill'); } =item previous Returns a list consisting of the total previous balance for this customer, followed by the previous outstanding invoices (as FS::cust_bill objects also). =cut sub previous { my $self = shift; # simple memoize; we use this a lot if (!$self->get('previous')) { my $total = 0; my @cust_bill = sort { $a->_date <=> $b->_date } grep { $_->owed != 0 } qsearch( 'cust_bill', { 'custnum' => $self->custnum, #'_date' => { op=>'<', value=>$self->_date }, 'invnum' => { op=>'<', value=>$self->invnum }, } ) ; foreach ( @cust_bill ) { $total += $_->owed; } $self->set('previous', [$total, @cust_bill]); } return @{ $self->get('previous') }; } =item enable_previous Whether to show the 'Previous Charges' section when printing this invoice. The negation of the 'disable_previous_balance' config setting. =cut sub enable_previous { my $self = shift; my $agentnum = $self->cust_main->agentnum; !$self->conf->exists('disable_previous_balance', $agentnum); } =item cust_bill_pkg Returns the line items (see L) for this invoice. =cut sub cust_bill_pkg { my $self = shift; qsearch( { 'select' => 'cust_bill_pkg.*, pkg_category.categoryname', 'table' => 'cust_bill_pkg', 'addl_from' => ' LEFT JOIN cust_pkg USING ( pkgnum ) '. ' LEFT JOIN part_pkg USING ( pkgpart ) '. ' LEFT JOIN pkg_class USING ( classnum ) '. ' LEFT JOIN pkg_category USING ( categorynum ) ', 'hashref' => { 'invnum' => $self->invnum }, 'order_by' => 'ORDER BY billpkgnum', #important? otherwise we could use # the AUTLOADED FK search. or should # that default to ORDER by the pkey? } ); } =item cust_bill_pkg_pkgnum PKGNUM Returns the line items (see L) for this invoice and specified pkgnum. =cut sub cust_bill_pkg_pkgnum { my( $self, $pkgnum ) = @_; qsearch( { 'table' => 'cust_bill_pkg', 'hashref' => { 'invnum' => $self->invnum, 'pkgnum' => $pkgnum, }, 'order_by' => 'ORDER BY billpkgnum', } ); } =item cust_pkg Returns the packages (see L) corresponding to the line items for this invoice. =cut sub cust_pkg { my $self = shift; my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () } $self->cust_bill_pkg; my %saw = (); grep { ! $saw{$_->pkgnum}++ } @cust_pkg; } =item no_auto Returns true if any of the packages (or their definitions) corresponding to the line items for this invoice have the no_auto flag set. =cut sub no_auto { my $self = shift; grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg; } =item open_cust_bill_pkg Returns the open line items for this invoice. Note that cust_bill_pkg with both setup and recur fees are returned as two separate line items, each with only one fee. =cut # modeled after cust_main::open_cust_bill sub open_cust_bill_pkg { my $self = shift; # grep { $_->owed > 0 } $self->cust_bill_pkg my %other = ( 'recur' => 'setup', 'setup' => 'recur', ); my @open = (); foreach my $field ( qw( recur setup )) { push @open, map { $_->set( $other{$field}, 0 ); $_; } grep { $_->owed($field) > 0 } $self->cust_bill_pkg; } @open; } =item cust_event Returns the new-style customer billing events (see L) for this invoice. =cut #false laziness w/cust_pkg.pm sub cust_event { my $self = shift; qsearch({ 'table' => 'cust_event', 'addl_from' => 'JOIN part_event USING ( eventpart )', 'hashref' => { 'tablenum' => $self->invnum }, 'extra_sql' => " AND eventtable = 'cust_bill' ", }); } =item num_cust_event Returns the number of new-style customer billing events (see L) for this invoice. =cut #false laziness w/cust_pkg.pm sub num_cust_event { my $self = shift; my $sql = "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ". " WHERE tablenum = ? AND eventtable = 'cust_bill'"; my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql"; $sth->execute($self->invnum) or die $sth->errstr. " executing $sql"; $sth->fetchrow_arrayref->[0]; } =item cust_main Returns the customer (see L) for this invoice. =item suspend Suspends all unsuspended packages (see L) for this invoice Returns a list: an empty list on success or a list of errors. =cut sub suspend { my $self = shift; grep { $_->suspend(@_) } grep {! $_->getfield('cancel') } $self->cust_pkg; } =item cust_suspend_if_balance_over AMOUNT Suspends the customer associated with this invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount. Returns a list: an empty list on success or a list of errors. =cut sub cust_suspend_if_balance_over { my( $self, $amount ) = ( shift, shift ); my $cust_main = $self->cust_main; if ( $cust_main->total_owed_date($self->_date) < $amount ) { return (); } else { $cust_main->suspend(@_); } } =item cancel Cancel the packages on this invoice. Largely similar to the cust_main version, but does not bother yet with banned payment options =cut sub cancel { my( $self, %opt ) = @_; warn "$me cancel called on cust_bill ". $self->invnum . " with options ". join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n" if $DEBUG; return ( 'Access denied' ) unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer'); my @pkgs = $self->cust_pkg; if ( !$opt{nobill} && $self->conf->exists('bill_usage_on_cancel') ) { $opt{nobill} = 1; my $error = $self->cust_main->bill( pkg_list => [ @pkgs ], cancel => 1 ); warn "Error billing during cancel, custnum ". $self->custnum. ": $error" if $error; } grep { $_ } map { $_->cancel(%opt) } grep { ! $_->getfield('cancel') } @pkgs; } =item cust_bill_pay Returns all payment applications (see L) for this invoice. =cut sub cust_bill_pay { my $self = shift; map { $_ } #return $self->num_cust_bill_pay unless wantarray; sort { $a->_date <=> $b->_date } qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } ); } =item cust_credited =item cust_credit_bill Returns all applied credits (see L) for this invoice. =cut sub cust_credited { my $self = shift; map { $_ } #return $self->num_cust_credit_bill unless wantarray; sort { $a->_date <=> $b->_date } qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } ) ; } sub cust_credit_bill { shift->cust_credited(@_); } #=item cust_bill_pay_pkgnum PKGNUM # #Returns all payment applications (see L) for this invoice #with matching pkgnum. # #=cut # #sub cust_bill_pay_pkgnum { # my( $self, $pkgnum ) = @_; # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray; # sort { $a->_date <=> $b->_date } # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum, # 'pkgnum' => $pkgnum, # } # ); #} =item cust_bill_pay_pkg PKGNUM Returns all payment applications (see L) for this invoice applied against the matching pkgnum. =cut sub cust_bill_pay_pkg { my( $self, $pkgnum ) = @_; qsearch({ 'select' => 'cust_bill_pay_pkg.*', 'table' => 'cust_bill_pay_pkg', 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '. ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ', 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum. " AND cust_bill_pkg.pkgnum = $pkgnum", }); } #=item cust_credited_pkgnum PKGNUM # #=item cust_credit_bill_pkgnum PKGNUM # #Returns all applied credits (see L) for this invoice #with matching pkgnum. # #=cut # #sub cust_credited_pkgnum { # my( $self, $pkgnum ) = @_; # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray; # sort { $a->_date <=> $b->_date } # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum, # 'pkgnum' => $pkgnum, # } # ); #} # #sub cust_credit_bill_pkgnum { # shift->cust_credited_pkgnum(@_); #} =item cust_credit_bill_pkg PKGNUM Returns all credit applications (see L) for this invoice applied against the matching pkgnum. =cut sub cust_credit_bill_pkg { my( $self, $pkgnum ) = @_; qsearch({ 'select' => 'cust_credit_bill_pkg.*', 'table' => 'cust_credit_bill_pkg', 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '. ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ', 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum. " AND cust_bill_pkg.pkgnum = $pkgnum", }); } =item cust_bill_batch Returns all invoice batch records (L) for this invoice. =cut sub cust_bill_batch { my $self = shift; qsearch('cust_bill_batch', { 'invnum' => $self->invnum }); } =item discount_plans Returns all discount plans (L) for this invoice, as a hash keyed by term length. =cut sub discount_plans { my $self = shift; FS::discount_plan->all($self); } =item tax Returns the tax amount (see L) for this invoice. =cut sub tax { my $self = shift; my $total = 0; my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum , 'pkgnum' => 0 } ); foreach (@taxlines) { $total += $_->setup; } $total; } =item owed Returns the amount owed (still outstanding) on this invoice, which is charged minus all payment applications (see L) and credit applications (see L). =cut sub owed { my $self = shift; my $balance = $self->charged; $balance -= $_->amount foreach ( $self->cust_bill_pay ); $balance -= $_->amount foreach ( $self->cust_credited ); $balance = sprintf( "%.2f", $balance); $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp $balance; } =item owed_on_invoice Returns the amount to be displayed as the "Balance Due" on this invoice. Amount returned depends on conf flags for invoicing See L for the true amount currently owed =cut sub owed_on_invoice { my $self = shift; #return $self->owed() # unless $self->conf->exists('previous_balance-payments_since') # Add charges from this invoice my $owed = $self->charged(); # Add carried balances from previous invoices # If previous items aren't to be displayed on the invoice, # _items_previous() is aware of this and responds appropriately. $owed += $_->{amount} for $self->_items_previous(); # Subtract payments and credits displayed on this invoice $owed -= $_->{amount} for $self->_items_payments(), $self->_items_credits(); return $owed; } sub owed_pkgnum { my( $self, $pkgnum ) = @_; #my $balance = $self->charged; my $balance = 0; $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum); $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum); $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum); $balance = sprintf( "%.2f", $balance); $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp $balance; } =item hide Returns true if this invoice should be hidden. See the selfservice-hide_invoices-taxclass configuraiton setting. =cut sub hide { my $self = shift; my $conf = $self->conf; my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass') or return ''; my @cust_bill_pkg = $self->cust_bill_pkg; my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg; ! grep { $_->taxclass ne $hide_taxclass } @part_pkg; } =item apply_payments_and_credits [ OPTION => VALUE ... ] Applies unapplied payments and credits to this invoice. Payments with the no_auto_apply flag set will not be applied. A hash of optional arguments may be passed. Currently "manual" is supported. If true, a payment receipt is sent instead of a statement when 'payment_receipt_email' configuration option is set. If there is an error, returns the error, otherwise returns false. =cut sub apply_payments_and_credits { my( $self, %options ) = @_; my $conf = $self->conf; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; local $SIG{TERM} = 'IGNORE'; local $SIG{TSTP} = 'IGNORE'; local $SIG{PIPE} = 'IGNORE'; my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; my $savepoint_label = 'cust_bill__apply_payments_and_credits'; savepoint_create( $savepoint_label ); $self->select_for_update; #mutex my @payments = grep { $_->unapplied > 0 } grep { !$_->no_auto_apply } $self->cust_main->cust_pay; my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit; if ( $conf->exists('pkg-balances') ) { # limit @payments & @credits to those w/ a pkgnum grepped from $self my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg; @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments; @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits; } while ( $self->owed > 0 and ( @payments || @credits ) ) { my $app = ''; if ( @payments && @credits ) { #decide which goes first by weight of top (unapplied) line item my @open_lineitems = $self->open_cust_bill_pkg; my $max_pay_weight = max( map { $_->part_pkg->pay_weight || 0 } grep { $_ } map { $_->cust_pkg } @open_lineitems ); my $max_credit_weight = max( map { $_->part_pkg->credit_weight || 0 } grep { $_ } map { $_->cust_pkg } @open_lineitems ); #if both are the same... payments first? it has to be something if ( $max_pay_weight >= $max_credit_weight ) { $app = 'pay'; } else { $app = 'credit'; } } elsif ( @payments ) { $app = 'pay'; } elsif ( @credits ) { $app = 'credit'; } else { die "guru meditation #12 and 35"; } my $unapp_amount; if ( $app eq 'pay' ) { my $payment = shift @payments; $unapp_amount = $payment->unapplied; $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum }; $app->pkgnum( $payment->pkgnum ) if $conf->exists('pkg-balances') && $payment->pkgnum; } elsif ( $app eq 'credit' ) { my $credit = shift @credits; $unapp_amount = $credit->credited; $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum }; $app->pkgnum( $credit->pkgnum ) if $conf->exists('pkg-balances') && $credit->pkgnum; } else { die "guru meditation #12 and 35"; } my $owed; if ( $conf->exists('pkg-balances') && $app->pkgnum ) { warn "owed_pkgnum ". $app->pkgnum; $owed = $self->owed_pkgnum($app->pkgnum); } else { $owed = $self->owed; } next unless $owed > 0; warn "min ( $unapp_amount, $owed )\n" if $DEBUG; $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) ); $app->invnum( $self->invnum ); my $error = $app->insert(%options); if ( $error ) { savepoint_rollback_and_release( $savepoint_label ); $dbh->rollback if $oldAutoCommit; return "Error inserting ". $app->table. " record: $error"; } die $error if $error; } savepoint_release( $savepoint_label ); $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no error } =item send HASHREF Sends this invoice to the destinations configured for this customer: sends email, prints and/or faxes. See L. Options can be passed as a hashref. Positional parameters are no longer allowed. I