X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=79dbbba67995a05f87e289931864b9a3956735a5;hb=674cb2d9d7105f4cc2871539b2e9f7088cdaa750;hp=a6a782a0c70c4751f47b026375ed4e4f2414058f;hpb=c52f1b6999b57cc3cb52fe8e4304d0bbfbbbb2f2;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index a6a782a0c..79dbbba67 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1,25 +1,23 @@ 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( @ISA $DEBUG $me $conf - $money_char $date_format $rdate_format $date_format_long ); -use vars qw( $invoice_lines @buf ); #yuck +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); +use List::Util qw(min max sum); use Date::Format; -use Text::Template 1.20; use File::Temp 0.14; -use String::ShellQuote; use HTML::Entities; -use Locale::Country; use Storable qw( freeze thaw ); use GD::Barcode; use FS::UID qw( datasrc ); -use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print ); +use FS::Misc qw( send_fax do_print ); use FS::Record qw( qsearch qsearchs dbh ); -use FS::cust_main_Mixin; -use FS::cust_main; use FS::cust_statement; use FS::cust_bill_pkg; use FS::cust_bill_pkg_display; @@ -29,33 +27,23 @@ use FS::cust_pay; use FS::cust_pkg; use FS::cust_credit_bill; use FS::pay_batch; -use FS::cust_pay_batch; -use FS::cust_bill_event; use FS::cust_event; use FS::part_pkg; use FS::cust_bill_pay; -use FS::cust_bill_pay_batch; -use FS::part_bill_event; use FS::payby; use FS::bill_batch; use FS::cust_bill_batch; use FS::cust_bill_pay_pkg; use FS::cust_credit_bill_pkg; - -@ISA = qw( FS::cust_main_Mixin FS::Record ); +use FS::discount_plan; +use FS::cust_bill_void; +use FS::reason; +use FS::reason_type; +use FS::L10N; $DEBUG = 0; $me = '[FS::cust_bill]'; -#ask FS::UID to run this stuff for us later -FS::UID->install_callback( sub { - $conf = new FS::Conf; - $money_char = $conf->config('money_char') || '$'; - $date_format = $conf->config('date_format') || '%x'; #/YY - $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY - $date_format_long = $conf->config('date_format_long') || '%b %o, %Y'; -} ); - =head1 NAME FS::cust_bill - Object methods for cust_bill records @@ -86,7 +74,7 @@ FS::cust_bill - Object methods for cust_bill records $tax_amount = $record->tax; @lines = $cust_bill->print_text; - @lines = $cust_bill->print_text $time; + @lines = $cust_bill->print_text('time' => $time); =head1 DESCRIPTION @@ -112,21 +100,19 @@ L and L for conversion functions. =back -Customer info at invoice generation time +Deprecated fields =over 4 -=item previous_balance - -=item billing_balance +=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. -=back - -Deprecated - -=over 4 +=item previous_balance - the customer's balance immediately after generating +the invoice before this one. DEPRECATED. -=item printed - deprecated +=item printed - formerly used to track the number of times an invoice had +been printed; no longer used. =back @@ -140,6 +126,10 @@ Specific use cases =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 @@ -155,8 +145,25 @@ Invoices are normally created by calling the bill method of a customer object =cut sub table { 'cust_bill'; } +sub template_conf { 'invoice_'; } + +sub has_sections { + my $self = shift; + my $agentnum = $self->cust_main->agentnum; + my $tc = $self->template_conf; + + $self->conf->exists($tc.'sections', $agentnum) || + $self->conf->exists($tc.'sections_by_location', $agentnum); +} + +# 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; } +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. @@ -207,20 +214,83 @@ sub insert { } -=item delete +=item void [ REASON ] -This method now works but you probably shouldn't use it. Instead, apply a -credit against the invoice. +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 -Using this method to delete invoices outright is really, really bad. There -would be no record you ever posted this invoice, and there are no check to -make sure charged = 0 or that there are no associated cust_bill_pkg records. +sub void { + my $self = shift; + my $reason = scalar(@_) ? shift : ''; -Really, don't use it. + unless (ref($reason) || !$reason) { + $reason = FS::reason->new_or_existing( + 'class' => 'I', + 'type' => 'Invoice void', + 'reason' => $reason + ); + } -=cut + 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); + 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; + + ''; + +} -sub delete { +# 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; @@ -236,17 +306,14 @@ sub delete { my $dbh = dbh; foreach my $table (qw( - cust_bill_event - cust_event - cust_credit_bill - cust_bill_pay - cust_bill_pay cust_credit_bill - cust_pay_batch cust_bill_pay_batch - cust_bill_pkg + 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; @@ -291,6 +358,7 @@ sub replace_check { #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'}; @@ -345,6 +413,7 @@ sub check { || $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; @@ -364,13 +433,33 @@ cust_bill-default_agent_invid is set and it has a value, invnum otherwise. sub display_invnum { my $self = shift; - if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){ + 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 previous Returns a list consisting of the total previous balance for this customer, @@ -380,13 +469,33 @@ followed by the previous outstanding invoices (as FS::cust_bill objects also). sub previous { my $self = shift; - my $total = 0; - my @cust_bill = sort { $a->_date <=> $b->_date } - grep { $_->owed != 0 && $_->_date < $self->_date } - qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) - ; - foreach ( @cust_bill ) { $total += $_->owed; } - $total, @cust_bill; + # 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 @@ -400,7 +509,9 @@ sub cust_bill_pkg { qsearch( { 'table' => 'cust_bill_pkg', 'hashref' => { 'invnum' => $self->invnum }, - 'order_by' => 'ORDER BY billpkgnum', + 'order_by' => 'ORDER BY billpkgnum', #important? otherwise we could use + # the AUTLOADED FK search. or should + # that default to ORDER by the pkey? } ); } @@ -478,32 +589,6 @@ sub open_cust_bill_pkg { @open; } -=item cust_bill_event - -Returns the completed invoice events (deprecated, old-style events - see L) for this invoice. - -=cut - -sub cust_bill_event { - my $self = shift; - qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } ); -} - -=item num_cust_bill_event - -Returns the number of completed invoice events (deprecated, old-style events - see L) for this invoice. - -=cut - -sub num_cust_bill_event { - my $self = shift; - my $sql = - "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?"; - 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_event Returns the new-style customer billing events (see L) for this invoice. @@ -542,11 +627,21 @@ sub num_cust_event { 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 cust_main { +sub suspend { my $self = shift; - qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); + + grep { $_->suspend(@_) } + grep {! $_->getfield('cancel') } + $self->cust_pkg; + } =item cust_suspend_if_balance_over AMOUNT @@ -568,55 +663,35 @@ sub cust_suspend_if_balance_over { } } -=item cust_credit - -Depreciated. See the cust_credited method. +=item cancel - #Returns a list consisting of the total previous credited (see - #L) and unapplied for this customer, followed by the previous - #outstanding credits (FS::cust_credit objects). +Cancel the packages on this invoice. Largely similar to the cust_main version, but does not bother yet with banned payment options =cut -sub cust_credit { - use Carp; - croak "FS::cust_bill->cust_credit depreciated; see ". - "FS::cust_bill->cust_credit_bill"; - #my $self = shift; - #my $total = 0; - #my @cust_credit = sort { $a->_date <=> $b->_date } - # grep { $_->credited != 0 && $_->_date < $self->_date } - # qsearch('cust_credit', { 'custnum' => $self->custnum } ) - #; - #foreach (@cust_credit) { $total += $_->credited; } - #$total, @cust_credit; -} - -=item cust_pay - -Depreciated. See the cust_bill_pay method. +sub cancel { + my( $self, %opt ) = @_; -#Returns all payments (see L) for this invoice. + warn "$me cancel called on cust_bill ". $self->invnum . " with options ". + join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n" + if $DEBUG; -=cut + return ( 'Access denied' ) + unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer'); -sub cust_pay { - use Carp; - croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay"; - #my $self = shift; - #sort { $a->_date <=> $b->_date } - # qsearch( 'cust_pay', { 'invnum' => $self->invnum } ) - #; -} + my @pkgs = $self->cust_pkg; -sub cust_pay_batch { - my $self = shift; - qsearch('cust_pay_batch', { 'invnum' => $self->invnum } ); -} + 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; + } -sub cust_bill_pay_batch { - my $self = shift; - qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } ); + grep { $_ } + map { $_->cancel(%opt) } + grep { ! $_->getfield('cancel') } + @pkgs; } =item cust_bill_pay @@ -745,6 +820,18 @@ sub cust_bill_batch { 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. @@ -793,9 +880,27 @@ sub owed_pkgnum { $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 @@ -807,6 +912,7 @@ If there is an error, returns the error, otherwise returns false. sub apply_payments_and_credits { my( $self, %options ) = @_; + my $conf = $self->conf; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -821,7 +927,9 @@ sub apply_payments_and_credits { $self->select_for_update; #mutex - my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay; + 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') ) { @@ -917,596 +1025,218 @@ sub apply_payments_and_credits { } -=item generate_email OPTION => VALUE ... +=item send HASHREF -Options: - -=over 4 - -=item from - -sender address, required - -=item tempate - -alternate template name, optional - -=item print_text +Sends this invoice to the destinations configured for this customer: sends +email, prints and/or faxes. See L. -text attachment arrayref, optional +Options can be passed as a hashref. Positional parameters are no longer +allowed. -=item subject +I