X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=79dbbba67995a05f87e289931864b9a3956735a5;hp=068d0d1d3e96bf76f56bb3fbf79e3af7fb453b4e;hb=f822e27a1e00594332ffa487a1c284234c5580a6;hpb=5310efb6d40b32ae08ae78553fc8721693925da7 diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 068d0d1d3..79dbbba67 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -6,6 +6,7 @@ use base qw( FS::cust_bill::Search FS::Template_Mixin 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); @@ -26,11 +27,9 @@ use FS::cust_pay; use FS::cust_pkg; use FS::cust_credit_bill; use FS::pay_batch; -use FS::cust_bill_event; use FS::cust_event; use FS::part_pkg; use FS::cust_bill_pay; -use FS::part_bill_event; use FS::payby; use FS::bill_batch; use FS::cust_bill_batch; @@ -38,6 +37,8 @@ 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; $DEBUG = 0; @@ -144,6 +145,16 @@ 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) @@ -203,7 +214,7 @@ sub insert { } -=item void +=item void [ REASON ] 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 @@ -215,6 +226,14 @@ sub void { my $self = shift; my $reason = 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'; @@ -229,7 +248,7 @@ sub void { my $cust_bill_void = new FS::cust_bill_void ( { map { $_ => $self->get($_) } $self->fields } ); - $cust_bill_void->reason($reason); + $cust_bill_void->reasonnum($reason->reasonnum) if $reason; my $error = $cust_bill_void->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -244,7 +263,7 @@ sub void { } } - $error = $self->delete; + $error = $self->_delete; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -256,20 +275,22 @@ sub void { } -=item delete - -This method now works but you probably shouldn't use it. Instead, apply a -credit against the invoice, or use the new void method. - -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. - -Really, don't use it. - -=cut - -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; @@ -285,15 +306,14 @@ sub delete { my $dbh = dbh; foreach my $table (qw( - cust_bill_event - cust_event cust_credit_bill - cust_bill_pay - cust_pay_batch 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; @@ -449,16 +469,20 @@ 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 } - qsearch( 'cust_bill', { 'custnum' => $self->custnum, - #'_date' => { op=>'<', value=>$self->_date }, - 'invnum' => { op=>'<', value=>$self->invnum }, - } ) - ; - 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 @@ -565,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. @@ -902,6 +900,7 @@ sub hide { =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 @@ -928,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') ) { @@ -1114,6 +1115,9 @@ sub queueable_email { my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } ) or die "invalid invoice number: " . $opt{invnum}; + $self->set('mode', $opt{mode}) + if $opt{mode}; + my %args = map {$_ => $opt{$_}} grep { $opt{$_} } qw( from notice_name no_coupon template ); @@ -1253,6 +1257,10 @@ sub batch_invoice { batchnum => $bill_batch->batchnum, invnum => $self->invnum, }); + if ( $self->mode ) { + $opt->{mode} ||= $self->mode; + $opt->{mode} = $opt->{mode}->modenum if ref $opt->{mode}; + } return $cust_bill_batch->insert($opt); } @@ -1982,24 +1990,8 @@ sub print_csv { } -=item comp - -Pays this invoice with a compliemntary payment. If there is an error, -returns the error, otherwise returns false. - -=cut - sub comp { - my $self = shift; - my $cust_pay = new FS::cust_pay ( { - 'invnum' => $self->invnum, - 'paid' => $self->owed, - '_date' => '', - 'payby' => 'COMP', - 'payinfo' => $self->cust_main->payinfo, - 'paybatch' => '', - } ); - $cust_pay->insert; + croak 'cust_bill->comp is deprecated (COMP payments are deprecated)'; } =item realtime_card @@ -2205,7 +2197,7 @@ sub _items_extra_usage_sections { my %classnums = (); my %lines = (); - my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50; + my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40; my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} ); foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { @@ -2446,7 +2438,7 @@ sub _items_svc_phone_sections { my %classnums = (); my %lines = (); - my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50; + my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40; my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} ); $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 }; @@ -2682,10 +2674,12 @@ sub _items_usage_class_summary { my %opt = @_; my $escape = $opt{escape} || sub { $_[0] }; + my $money_char = $opt{money_char}; my $invnum = $self->invnum; my @classes = qsearch({ 'table' => 'usage_class', - 'select' => 'classnum, classname, SUM(amount) AS amount', + 'select' => 'classnum, classname, SUM(amount) AS amount,'. + ' COUNT(*) AS calls, SUM(duration) AS duration', 'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' . ' LEFT JOIN cust_bill_pkg USING (billpkgnum)', 'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum". @@ -2696,17 +2690,21 @@ sub _items_usage_class_summary { my @l; my $section = { description => &{$escape}($self->mt('Usage Summary')), - no_subtotal => 1, usage_section => 1, + subtotal => 0, }; foreach my $class (@classes) { + $section->{subtotal} += $class->get('amount'); push @l, { 'description' => &{$escape}($class->classname), - 'amount' => sprintf('%.2f', $class->amount), + 'amount' => $money_char.sprintf('%.2f', $class->get('amount')), + 'quantity' => $class->get('calls'), + 'duration' => $class->get('duration'), 'usage_classnum' => $class->classnum, 'section' => $section, }; } + $section->{subtotal} = $money_char.sprintf('%.2f', $section->{subtotal}); return @l; } @@ -2745,7 +2743,7 @@ sub _items_previous { sub _items_credits { my( $self, %opt ) = @_; - my $trim_len = $opt{'trim_len'} || 60; + my $trim_len = $opt{'trim_len'} || 40; my @b; #credits @@ -2842,6 +2840,76 @@ sub _items_payments { } +sub _items_total { + my $self = shift; + my $conf = $self->conf; + + my @items; + my ($pr_total) = $self->previous; + my ($previous_charges_desc, $new_charges_desc, $new_charges_amount); + + if ( $conf->exists('previous_balance-exclude_from_total') ) { + # if enabled, specifically add a line for the previous balance total + $previous_charges_desc = $self->mt( + $conf->config('previous_balance-text') || 'Previous Balance' + ); + + # then return separate lines for previous balance and total new charges + if ( $pr_total ) { + push @items, + { total_item => $previous_charges_desc, + total_amount => sprintf('%.2f',$pr_total) + }; + } + } + + if ( $conf->exists('previous_balance-exclude_from_total') + or !$self->enable_previous ) { + # show new charges only + + $new_charges_desc = $self->mt( + $conf->config('previous_balance-text-total_new_charges') + || 'Total New Charges' + ); + + $new_charges_amount = $self->charged; + + } else { + # show new charges + previous invoice total + + $new_charges_desc = $self->mt('Total Charges'); + if ( $self->enable_previous ) { + $new_charges_amount = sprintf('%.2f', $self->charged + $pr_total); + } else { + $new_charges_amount = sprintf('%.2f', $self->charged); + } + + } + + if ( $conf->exists('invoice_show_prior_due_date') ) { + # then the due date should be shown with Total New Charges, + # and should NOT be shown with the Balance Due message. + if ( $self->due_date ) { + # localize the "Please pay by" message and the date itself + # (grammar issues with this, yeah) + $new_charges_desc .= ' - ' . $self->mt('Please pay by') . ' ' . + $self->due_date2str('short'); + } elsif ( $self->terms ) { + # phrases like "due on receipt" should be localized + $new_charges_desc .= ' - ' . $self->mt($self->terms); + } + } + + push @items, + { total_item => $new_charges_desc, + total_amount => $new_charges_amount, + }; + + @items; +} + + + =item call_details [ OPTION => VALUE ... ] Returns an array of CSV strings representing the call details for this invoice @@ -2874,6 +2942,18 @@ sub call_details { ( $header, grep { $_ ne $header } @details ); } +=item cust_pay_batch + +Returns all L records linked to this invoice. Deprecated, +will be removed. + +=cut + +sub cust_pay_batch { + carp "FS::cust_bill->cust_pay_batch is deprecated"; + my $self = shift; + qsearch('cust_pay_batch', { 'invnum' => $self->invnum }); +} =back @@ -2937,6 +3017,9 @@ sub process_re_X { } +# this is called from search/cust_bill.html and given all its search +# parameters, so it needs to perform the same search. + sub re_X { # spool_invoice ftp_invoice fax_invoice print_invoice my($method, $job, %param ) = @_; @@ -2946,22 +3029,15 @@ sub re_X { } #some false laziness w/search/cust_bill.html - my $distinct = ''; - my $orderby = 'ORDER BY cust_bill._date'; - - my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param); - - my $addl_from = 'LEFT JOIN cust_main USING ( custnum )'; - - my @cust_bill = qsearch( { - #'select' => "cust_bill.*", - 'table' => 'cust_bill', - 'addl_from' => $addl_from, - 'hashref' => {}, - 'extra_sql' => $extra_sql, - 'order_by' => $orderby, - 'debug' => 1, - } ); + $param{'order_by'} = 'cust_bill._date'; + + my $query = FS::cust_bill->search(\%param); + delete $query->{'count_query'}; + delete $query->{'count_addl'}; + + $query->{debug} = 1; # was in here before, is obviously useful + + my @cust_bill = qsearch( $query ); $method .= '_invoice' unless $method eq 'email' || $method eq 'print';