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; ''; } =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