X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=84487d2ce843d4a92d118eefd82aa00cd92ffb21;hb=94f0030bae0ce3e493b99860901158e30e9651fd;hp=6ded57facfa8078d10bdeda12dedb5874b60a732;hpb=2e4fa975e054554beac71883436b143267d7aa12;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 6ded57fac..84487d2ce 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -6,10 +6,12 @@ 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); use Date::Format; +use DateTime; use File::Temp 0.14; use HTML::Entities; use Storable qw( freeze thaw ); @@ -26,11 +28,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,7 +38,10 @@ 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]'; @@ -144,6 +147,7 @@ Invoices are normally created by calling the bill method of a customer object =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) @@ -203,7 +207,7 @@ sub insert { } -=item void +=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 @@ -214,6 +218,15 @@ FS::cust_bill_pkg_void). 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'; @@ -229,7 +242,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; @@ -237,7 +250,7 @@ sub void { } foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { - my $error = $cust_bill_pkg->void($reason); + my $error = $cust_bill_pkg->void($reason, $reprocess_cdrs); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -258,14 +271,13 @@ 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. +DO NOT USE THIS METHOD. Instead, apply a credit against the invoice, or use +the B 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. +This is only for internal use by V, which is what you should be using. -Really, don't use it. +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 @@ -285,15 +297,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; @@ -440,6 +451,27 @@ sub previous_bill { $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, @@ -449,16 +481,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 @@ -483,7 +519,13 @@ Returns the line items (see L) for this invoice. sub cust_bill_pkg { my $self = shift; qsearch( - { 'table' => 'cust_bill_pkg', + { + '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 @@ -565,32 +607,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. @@ -867,6 +883,35 @@ sub owed { $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 ) = @_; @@ -902,6 +947,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 @@ -926,9 +972,14 @@ sub apply_payments_and_credits { 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 } $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') ) { @@ -1012,6 +1063,7 @@ sub apply_payments_and_credits { my $error = $app->insert(%options); if ( $error ) { + savepoint_rollback_and_release( $savepoint_label ); $dbh->rollback if $oldAutoCommit; return "Error inserting ". $app->table. " record: $error"; } @@ -1019,6 +1071,7 @@ sub apply_payments_and_credits { } + savepoint_release( $savepoint_label ); $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no error @@ -1085,10 +1138,7 @@ sub email { # this is where we set the From: address $from ||= $self->_agent_invoice_from || #XXX should go away - $conf->config('invoice_from_name', $self->cust_main->agentnum ) ? - $conf->config('invoice_from_name', $self->cust_main->agentnum ) . ' <' . - $conf->config('invoice_from', $self->cust_main->agentnum ) . '>' : - $conf->config('invoice_from', $self->cust_main->agentnum ); + $conf->invoice_from_full( $self->cust_main->agentnum ); my @invoicing_list = $self->cust_main->invoicing_list_emailonly; @@ -1117,6 +1167,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 ); @@ -1145,6 +1198,11 @@ sub email_subject { eval qq("$subject"); } +sub pdf_filename { + my $self = shift; + 'Invoice-'. $self->invnum. '.pdf'; +} + =item lpr_data HASHREF Returns the postscript or plaintext for this invoice as an arrayref. @@ -1256,6 +1314,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); } @@ -1346,6 +1408,11 @@ See L for a description of the output format. sub send_csv { my($self, %opt) = @_; + if ( $FS::Misc::DISABLE_ALL_NOTICES ) { + warn 'send_csv() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG; + return; + } + #create file(s) my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill"; @@ -1422,6 +1489,11 @@ in the ICS format. sub spool_csv { my($self, %opt) = @_; + if ( $FS::Misc::DISABLE_ALL_NOTICES ) { + warn 'spool_csv() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG; + return; + } + my $time = $opt{'time'} || time; my $cust_main = $self->cust_main; @@ -1645,6 +1717,9 @@ sub print_csv { my $time = $opt{'time'} || time; + $self->set('_template', $opt{template}) + if exists $opt{template}; + my $tracctnum = ''; #leaking out from billco-specific sections :/ if ( $format eq 'billco' ) { @@ -1903,7 +1978,19 @@ sub print_csv { if ( lc($opt{'format'}) eq 'billco' ) { my $lineseq = 0; - foreach my $item ( $self->_items_pkg ) { + my %items_opt = ( format => 'template', + escape_function => sub { shift } ); + # I don't know what characters billco actually tolerates in spool entries. + # Text::CSV will take care of delimiters, though. + + my @items = ( $self->_items_pkg(%items_opt), + $self->_items_fee(%items_opt) ); + foreach my $item (@items) { + + my $description = $item->{'description'}; + if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) { + $description .= ': ' . $item->{ext_description}[0]; + } $csv->combine( '', # 1 | N/A-Leave Empty CHAR 2 @@ -1911,7 +1998,7 @@ sub print_csv { $tracctnum, # 3 | Account Number CHAR 15 $self->invnum, # 4 | Invoice Number CHAR 15 $lineseq++, # 5 | Line Sequence (sort order) NUM 6 - $item->{'description'}, # 6 | Transaction Detail CHAR 100 + $description, # 6 | Transaction Detail CHAR 100 $item->{'amount'}, # 7 | Amount NUM* 9 '', # 8 | Line Format Control** CHAR 2 '', # 9 | Grouping Code CHAR 2 @@ -1973,24 +2060,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 @@ -2196,7 +2267,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 ) { @@ -2437,7 +2508,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 }; @@ -2658,7 +2729,7 @@ sub _items_svc_phone_sections { } -=sub _items_usage_class_summary OPTIONS +=item _items_usage_class_summary OPTIONS Returns a list of detail items summarizing the usage charges on this invoice. Each one will have 'amount', 'description' (the usage charge name), @@ -2673,10 +2744,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". @@ -2687,150 +2760,812 @@ 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; } +=item _items_previous() + + Returns an array of hashrefs, each hashref representing a line-item on + the current bill for previous unpaid invoices. + + keys for each previous_item: + - amount (see notes) + - pkgnum + - description + - invnum + - _date + + Payments and credits shown on this invoice may vary based on configuraiton. + + when conf flag previous_balance-payments_since is set: + This method works backwards to rebuild the invoice as a snapshot in time. + The invoice displayed will have the balances owed, and payments made, + reflecting the state of the account at the time of invoice generation. + +=cut + sub _items_previous { + my $self = shift; - my $conf = $self->conf; - my $cust_main = $self->cust_main; - my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance - my @b = (); - foreach ( @pr_cust_bill ) { - my $date = $conf->exists('invoice_show_prior_due_date') - ? 'due '. $_->due_date2str('short') - : $self->time2str_local('short', $_->_date); - push @b, { - 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)", - #'pkgpart' => 'N/A', - 'pkgnum' => 'N/A', - 'amount' => sprintf("%.2f", $_->owed), - }; + + # simple memoize + if ($self->get('_items_previous')) { + return sort { $a->{_date} <=> $b->{_date} } + values %{ $self->get('_items_previous') }; } - @b; - #{ - # 'description' => 'Previous Balance', - # #'pkgpart' => 'N/A', - # 'pkgnum' => 'N/A', - # 'amount' => sprintf("%10.2f", $pr_total ), - # 'ext_description' => [ map { - # "Invoice ". $_->invnum. - # " (". time2str("%x",$_->_date). ") ". - # sprintf("%10.2f", $_->owed) - # } @pr_cust_bill ], + # Gets the customer's current balance and outstanding invoices. + my ($prev_balance, @open_invoices) = $self->previous; + + my %invoices = map { + $_->invnum => $self->__items_previous_map_invoice($_) + } @open_invoices; + + # Which credits and payments displayed on the bill will vary based on + # conf flag previous_balance-payments_since. + my @credits = $self->_items_credits(); + my @payments = $self->_items_payments(); + + + if ($self->conf->exists('previous_balance-payments_since')) { + # For each credit or payment, determine which invoices it was applied to. + # Manipulate data displayed so the invoice displayed appears as a + # snapshot in time... with previous balances and balance owed displayed + # as they were at the time of invoice creation. + + my @credits_postbill = $self->_items_credits_postbill(); + my @payments_postbill = $self->_items_payments_postbill(); + + my %pmnt_dupechk; + my %cred_dupechk; + + # Each section below follows this pattern on a payment/credit + # + # - Dupe check, avoid adjusting for the same item twice + # - If invoice being adjusted for isn't in our list, add it + # - Adjust the invoice balance to refelct balnace without the + # credit or payment applied + # + + # Working with payments displayed on this bill + for my $pmt_hash (@payments) { + my $pmt_obj = qsearchs('cust_pay', {paynum => $pmt_hash->{paynum}}); + for my $cust_bill_pay ($pmt_obj->cust_bill_pay) { + next if exists $pmnt_dupechk{$cust_bill_pay->billpaynum}; + $pmnt_dupechk{$cust_bill_pay->billpaynum} = 1; + + my $invnum = $cust_bill_pay->invnum; + + $invoices{$invnum} = $self->__items_previous_get_invoice($invnum) + unless exists $invoices{$invnum}; + + $invoices{$invnum}->{amount} += $cust_bill_pay->amount; + } + } + + # Working with credits displayed on this bill + for my $cred_hash (@credits) { + my $cred_obj = qsearchs('cust_credit', {crednum => $cred_hash->{crednum}}); + for my $cust_credit_bill ($cred_obj->cust_credit_bill) { + next if exists $cred_dupechk{$cust_credit_bill->creditbillnum}; + $cred_dupechk{$cust_credit_bill->creditbillnum} = 1; + + my $invnum = $cust_credit_bill->invnum; + + $invoices{$invnum} = $self->__items_previous_get_invoice($invnum) + unless exists $invoices{$invnum}; + + $invoices{$invnum}->{amount} += $cust_credit_bill->amount; + } + } + + # Working with both credits and payments which are not displayed + # on this bill, but which have affected this bill's balances + for my $postbill (@payments_postbill, @credits_postbill) { + + if ($postbill->{billpaynum}) { + next if exists $pmnt_dupechk{$postbill->{billpaynum}}; + $pmnt_dupechk{$postbill->{billpaynum}} = 1; + } elsif ($postbill->{creditbillnum}) { + next if exists $cred_dupechk{$postbill->{creditbillnum}}; + $cred_dupechk{$postbill->{creditbillnum}} = 1; + } else { + die "Missing creditbillnum or billpaynum"; + } + + my $invnum = $postbill->{invnum}; + + $invoices{$invnum} = $self->__items_previous_get_invoice($invnum) + unless exists $invoices{$invnum}; + + $invoices{$invnum}->{amount} += $postbill->{amount}; + } + + # Make sure current invoice doesn't appear in previous items + delete $invoices{$self->invnum} + if exists $invoices{$self->invnum}; + + } + + # Make sure amount is formatted as a dollar string + # (Formatting should happen on the template side, but is not?) + $invoices{$_}->{amount} = sprintf('%.2f',$invoices{$_}->{amount}) + for keys %invoices; + + $self->set('_items_previous', \%invoices); + return sort { $a->{_date} <=> $b->{_date} } values %invoices; - #}; } +=item _items_previous_total + + Return sum of amounts from all items returned by _items_previous + Results will vary based on invoicing conf flags + +=cut + +sub _items_previous_total { + my $self = shift; + my $tot = 0; + $tot += $_->{amount} for $self->_items_previous(); + return $tot; +} + +sub __items_previous_get_invoice { + # Helper function for _items_previous + # + # Read a record from cust_bill, return a hash of it's information + my ($self, $invnum) = @_; + die "Incorrect usage of __items_previous_get_invoice()" unless $invnum; + + my $cust_bill = qsearchs('cust_bill', {invnum => $invnum}); + return $self->__items_previous_map_invoice($cust_bill); +} + +sub __items_previous_map_invoice { + # Helper function for _items_previous + # + # Transform a cust_bill object into a simple hash reference of the type + # required by _items_previous + my ($self, $cust_bill) = @_; + die "Incorrect usage of __items_previous_map_invoice" unless ref $cust_bill; + + my $date = $self->conf->exists('invoice_show_prior_due_date') + ? 'due '.$cust_bill->due_date2str('short') + : $self->time2str_local('short', $cust_bill->_date); + + return { + invnum => $cust_bill->invnum, + amount => $cust_bill->owed, + pkgnum => 'N/A', + _date => $cust_bill->_date, + description => join(' ', + $self->mt('Previous Balance, Invoice #'), + $cust_bill->invnum, + "($date)" + ), + } +} + +=item _items_credits() + + Return array of hashrefs containing credits to be shown as line-items + when rendering this bill. + + keys for each credit item: + - crednum: id of payment + - amount: payment amount + - description: line item to be displayed on the bill + + This method has three ways it selects which credits to display on + this bill: + + 1) Default Case: No Conf flag for 'previous_balance-payments_since' + + Returns credits that have been applied to this bill only + + 2) Case: + Conf flag set for 'previous_balance-payments_since' + + List all credits that have been recorded during the time period + between the timestamps of the last invoice and this invoice + + 3) Case: + Conf flag set for 'previous_balance-payments_since' + $opt{'template'} eq 'statement' + + List all payments that have been recorded between the timestamps + of the previous invoice and the following invoice. + + This is used to give the customer a receipt for a payment + in the form of their last bill with the payment amended. + + I am concerned with this implementation, but leaving in place as is + If this option is selected, while viewing an older bill, the old bill + will show ALL future credits for future bills, but no charges for + future bills. Somebody could be misled into believing they have a + large account credit when they don't. Also, interrupts the chain of + invoices as an account history... the customer could have two invoices + in their fileing cabinet, for two different dates, both with a line item + for the same duplicate credit. The accounting is technically accurate, + but somebody could easily become confused and think two credits were + made, when really those two line items on two different bills represent + only a single credit + +=cut + sub _items_credits { - my( $self, %opt ) = @_; - my $trim_len = $opt{'trim_len'} || 60; - my @b; - #credits - my @objects; + my $self= shift; + + # Simple memoize + return @{$self->get('_items_credits')} if $self->get('_items_credits'); + + my %opt = @_; + my $template = $opt{template} || $self->get('_template'); + my $trim_len = $opt{template} || $self->get('trim_len') || 40; + + my @return; + my @cust_credit_objs; + if ( $self->conf->exists('previous_balance-payments_since') ) { - if ( $opt{'template'} eq 'statement' ) { - # then the current bill is a "statement" (i.e. an invoice sent as - # a payment receipt) - # and in that case we want to see payments on or after THIS invoice - @objects = qsearch('cust_credit', { - 'custnum' => $self->custnum, - '_date' => {op => '>=', value => $self->_date}, - }); + if ($template eq 'statement') { + # Case 3 (see above) + # Return credits timestamped between the previous and following bills + + my $previous_bill = $self->previous_bill; + my $following_bill = $self->following_bill; + + my $date_start = ref $previous_bill ? $previous_bill->_date : 0; + my $date_end = ref $following_bill ? $following_bill->_date : undef; + + my %query = ( + table => 'cust_credit', + hashref => { + custnum => $self->custnum, + _date => { op => '>=', value => $date_start }, + }, + ); + $query{extra_sql} = " AND _date <= $date_end " if $date_end; + + @cust_credit_objs = qsearch(\%query); + } else { - my $date = 0; - $date = $self->previous_bill->_date if $self->previous_bill; - @objects = qsearch('cust_credit', { - 'custnum' => $self->custnum, - '_date' => {op => '>=', value => $date}, + # Case 2 (see above) + # Return credits timestamps between this and the previous bills + + my $date_start = 0; + my $date_end = $self->_date; + + my $previous_bill = $self->previous_bill; + if (ref $previous_bill) { + $date_start = $previous_bill->_date; + } + + @cust_credit_objs = qsearch({ + table => 'cust_credit', + hashref => { + custnum => $self->custnum, + _date => {op => '>=', value => $date_start}, + }, + extra_sql => " AND _date <= $date_end ", }); } + } else { - @objects = $self->cust_credited; + # Case 1 (see above) + # Return only credits that have been applied to this bill + + @cust_credit_objs = $self->cust_credited; + } - foreach my $obj ( @objects ) { + # Translate objects into hashrefs + foreach my $obj ( @cust_credit_objs ) { my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit; + my %r_obj = ( + amount => sprintf('%.2f',$cust_credit->amount), + crednum => $cust_credit->crednum, + _date => $cust_credit->_date, + creditreason => $cust_credit->reason, + ); my $reason = substr($cust_credit->reason, 0, $trim_len); $reason .= '...' if length($reason) < length($cust_credit->reason); $reason = " ($reason) " if $reason; - push @b, { - #'description' => 'Credit ref\#'. $_->crednum. - # " (". time2str("%x",$_->cust_credit->_date) .")". - # $reason, - 'description' => $self->mt('Credit applied').' '. - $self->time2str_local('short', $obj->_date). $reason, - 'amount' => sprintf("%.2f",$obj->amount), - }; + $r_obj{description} = join(' ', + $self->mt('Credit applied'), + $self->time2str_local('short', $cust_credit->_date), + $reason, + ); + + push @return, \%r_obj; + } + $self->set('_items_credits',\@return); + @return; } - @b; +=item _items_credits_total + + Return the total of al items from _items_credits + Will vary based on invoice display conf flag + +=cut +sub _items_credits_total { + my $self = shift; + my $tot = 0; + $tot += $_->{amount} for $self->_items_credits(); + return $tot; } + + +=item _items_credits_postbill() + + Returns an array of hashrefs for credits where + - Credit issued after this invoice + - Credit applied to an invoice before this invoice + + Returned hashrefs are of the format returned by _items_credits() + +=cut + +sub _items_credits_postbill { + my $self = shift; + + my @cust_credit_bill = qsearch({ + table => 'cust_credit_bill', + select => join(', ',qw( + cust_credit_bill.creditbillnum + cust_credit_bill._date + cust_credit_bill.invnum + cust_credit_bill.amount + )), + addl_from => ' LEFT JOIN cust_credit'. + ' ON (cust_credit_bill.crednum = cust_credit.crednum) ', + extra_sql => ' WHERE cust_credit.custnum = '.$self->custnum. + ' AND cust_credit_bill._date > '.$self->_date. + ' AND cust_credit_bill.invnum < '.$self->invnum.' ', +#! did not investigate why hashref doesn't work for this join query +# hashref => { +# 'cust_credit.custnum' => {op => '=', value => $self->custnum}, +# 'cust_credit_bill._date' => {op => '>', value => $self->_date}, +# 'cust_credit_bill.invnum' => {op => '<', value => $self->invnum}, +# }, + }); + + return map {{ + _date => $_->_date, + invnum => $_->invnum, + amount => $_->amount, + creditbillnum => $_->creditbillnum, + }} @cust_credit_bill; +} + +=item _items_payments_postbill() + + Returns an array of hashrefs for payments where + - Payment occured after this invoice + - Payment applied to an invoice before this invoice + + Returned hashrefs are of the format returned by _items_payments() + +=cut + +sub _items_payments_postbill { + my $self = shift; + + my @cust_bill_pay = qsearch({ + table => 'cust_bill_pay', + select => join(', ',qw( + cust_bill_pay.billpaynum + cust_bill_pay._date + cust_bill_pay.invnum + cust_bill_pay.amount + )), + addl_from => ' LEFT JOIN cust_bill'. + ' ON (cust_bill_pay.invnum = cust_bill.invnum) ', + extra_sql => ' WHERE cust_bill.custnum = '.$self->custnum. + ' AND cust_bill_pay._date > '.$self->_date. + ' AND cust_bill_pay.invnum < '.$self->invnum.' ', + }); + + return map {{ + _date => $_->_date, + invnum => $_->invnum, + amount => $_->amount, + billpaynum => $_->billpaynum, + }} @cust_bill_pay; +} + +=item _items_payments() + + Return array of hashrefs containing payments to be shown as line-items + when rendering this bill. + + keys for each payment item: + - paynum: id of payment + - amount: payment amount + - description: line item to be displayed on the bill + + This method has three ways it selects which payments to display on + this bill: + + 1) Default Case: No Conf flag for 'previous_balance-payments_since' + + Returns payments that have been applied to this bill only + + 2) Case: + Conf flag set for 'previous_balance-payments_since' + + List all payments that have been recorded between the timestamps + of the previous invoice and this invoice + + 3) Case: + Conf flag set for 'previous_balance-payments_since' + $opt{'template'} eq 'statement' + + List all payments that have been recorded between the timestamps + of the previous invoice and the following invoice. + + I am concerned with this implementation, but leaving in place as is + If this option is selected, while viewing an older bill, the old bill + will show ALL future payments for future bills, but no charges for + future bills. Somebody could be misled into believing they have a + large account credit when they don't. Also, interrupts the chain of + invoices as an account history... the customer could have two invoices + in their fileing cabinet, for two different dates, both with a line item + for the same duplicate payment. The accounting is technically accurate, + but somebody could easily become confused and think two payments were + made, when really those two line items on two different bills represent + only a single payment. + +=cut + sub _items_payments { + my $self = shift; + + # Simple memoize + return @{$self->get('_items_payments')} if $self->get('_items_payments'); + my %opt = @_; + my $template = $opt{template} || $self->get('_template'); + + my @return; + my @cust_pay_objs; + + my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details'); - my @b; - my $detailed = $self->conf->exists('invoice_payment_details'); - my @objects; if ( $self->conf->exists('previous_balance-payments_since') ) { - # then show payments dated on/after the previous bill... - if ( $opt{'template'} eq 'statement' ) { - # then the current bill is a "statement" (i.e. an invoice sent as - # a payment receipt) - # and in that case we want to see payments on or after THIS invoice - @objects = qsearch('cust_pay', { - 'custnum' => $self->custnum, - '_date' => {op => '>=', value => $self->_date}, - }); + if ($template eq 'statement') { + # Case 3 (see above) + # Return payments timestamped between the previous and following bills + + my $previous_bill = $self->previous_bill; + my $following_bill = $self->following_bill; + + my $date_start = ref $previous_bill ? $previous_bill->_date : 0; + my $date_end = ref $following_bill ? $following_bill->_date : undef; + + my %query = ( + table => 'cust_pay', + hashref => { + custnum => $self->custnum, + _date => { op => '>=', value => $date_start }, + }, + ); + $query{extra_sql} = " AND _date <= $date_end " if $date_end; + + @cust_pay_objs = qsearch(\%query); + } else { - # the normal case: payments on or after the previous invoice - my $date = 0; - $date = $self->previous_bill->_date if $self->previous_bill; - @objects = qsearch('cust_pay', { - 'custnum' => $self->custnum, - '_date' => {op => '>=', value => $date}, + # Case 2 (see above) + # Return payments timestamped between this and the previous bill + + my $date_start = 0; + my $date_end = $self->_date; + + my $previous_bill = $self->previous_bill; + if (ref $previous_bill) { + $date_start = $previous_bill->_date; + } + + @cust_pay_objs = qsearch({ + table => 'cust_pay', + hashref => { + custnum => $self->custnum, + _date => {op => '>=', value => $date_start}, + }, + extra_sql => " AND _date <= $date_end ", }); - # and before the current bill... - @objects = grep { $_->_date < $self->_date } @objects; } + } else { - @objects = $self->cust_bill_pay; + # Case 1 (see above) + # Return payments applied only to this bill + + @cust_pay_objs = $self->cust_bill_pay; + + } + + $self->set( + '_items_payments', + [ $self->__items_payments_make_hashref(@cust_pay_objs) ] + ); + return @{ $self->get('_items_payments') }; } - foreach my $obj (@objects) { +=item _items_payments_total + + Return a total of all records returned by _items_payments + Results vary based on invoicing conf flags + +=cut + +sub _items_payments_total { + my $self = shift; + my $tot = 0; + $tot += $_->{amount} for $self->_items_payments(); + return $tot; +} + +sub __items_payments_make_hashref { + # Transform a FS::cust_pay object into a simple hashref for invoice + my ($self, @cust_pay_objs) = @_; + my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details'); + my @return; + + for my $obj (@cust_pay_objs) { + + # In case we're passed FS::cust_bill_pay (or something else?) + # Below, we use $obj to render amount rather than $cust_apy. + # If we were passed cust_bill_pay objs, then: + # $obj->amount represents the amount applied to THIS invoice + # $cust_pay->amount represents the total payment, which may have + # been applied accross several invoices. + # If we were passed cust_bill_pay objects, then the conf flag + # previous_balance-payments_since is NOT set, so we should not + # present any payments not applied to this invoice. my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay; - my $desc = $self->mt('Payment received').' '. - $self->time2str_local('short', $cust_pay->_date ); - $desc .= $self->mt(' via ') . - $cust_pay->payby_payinfo_pretty( $self->cust_main->locale ) - if $detailed; - - push @b, { - 'description' => $desc, - 'amount' => sprintf("%.2f", $obj->amount ) + + my %r_obj = ( + _date => $cust_pay->_date, + amount => sprintf("%.2f", $obj->amount), + paynum => $cust_pay->paynum, + payinfo => $cust_pay->payby_payinfo_pretty(), + description => join(' ', + $self->mt('Payment received'), + $self->time2str_local('short', $cust_pay->_date), + ), + ); + + if ($c_invoice_payment_details) { + $r_obj{description} = join(' ', + $r_obj{description}, + $self->mt('via'), + $cust_pay->payby_payinfo_pretty($self->cust_main->locale), + ); + } + + push @return, \%r_obj; + } + return @return; + } + +=item _items_total() + + Generate the line-items to be shown on the bill in the "Totals" section + + Returns a list of hashrefs, each with the keys: + - total_item: description field + - total_amount: dollar-formatted number amount + + Information presented by this method varies based on Conf + + Conf previous_balance-payments_due + - default, flag not set + Only transactions that were applied to this bill bill be + displayed and calculated intothe total. If items exist in + the past-due section, those items will disappear from this + invoice if they have been paid off. + + - previous_balance-payments_due flag is set + Transactions occuring after the timestsamp of this + invoice are not reflected on invoice line items + + Only payments/credits applied between the previous invoice + and this one are displayed and calculated into the total + + - previous_balance-payments_due && $opt{template} eq 'statement' + Same as above, except payments/credits occuring before the date + of the following invoice are also displayed and calculated into + the total + + Conf previous_balance-exclude_from_total + - default, flag not set + The "Totals" section contains a single line item. + The dollar amount of this line items is a sum of old and new charges + - previous_balance-exclude_from_total flag is set + The "Totals" section contains two line items. + One for previous balance, one for new charges + !NOTE: Avent virtualization flag 'disable_previous_balance' can + override the global conf flag previous_balance-exclude_from_total + + Conf invoice_show_prior_due_date + - default, flag not set + Total line item in the "Totals" section does not mention due date + - invoice_show_prior_due_date flag is set + Total line item in the "Totals" section includes either the due + date of the invoice, or the specified invoice terms + ? Not sure why this is called "Prior" due date, since we seem to be + displaying THIS due date... +=cut + +sub _items_total { + my $self = shift; + my $conf = $self->conf; + + my $c_multi_line_total = 0; + $c_multi_line_total = 1 + if $conf->exists('previous_balance-exclude_from_total') + && $self->enable_previous(); + + my @line_items; + my $invoice_charges = $self->charged(); + + # _items_previous() is aware of conf flags + my $previous_balance = 0; + $previous_balance += $_->{amount} for $self->_items_previous(); + + my $total_charges; + my $total_descr; + + if ( $previous_balance && $c_multi_line_total ) { + # previous balance, new charges on separate lines + + push @line_items, { + total_amount => sprintf('%.2f',$previous_balance), + total_item => $self->mt( + $conf->config('previous_balance-text') || 'Previous Balance' + ), + }; + + $total_charges = $invoice_charges; + $total_descr = $self->mt( + $conf->config('previous_balance-text-total_new_charges') + || 'Total New Charges' + ); + + } else { + # previous balance and new charges combined into a single total line + $total_charges = $invoice_charges + $previous_balance; + $total_descr = $self->mt('Total Charges'); + } + + if ( $conf->exists('invoice_show_prior_due_date') && !$conf->exists('invoice_omit_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 ) { + $total_descr .= $self->invoice_pay_by_msg; + } elsif ( $self->terms ) { + $total_descr = join(' ', + $total_descr, + '-', + $self->mt($self->terms) + ); + } + } + + push @line_items, { + total_amount => sprintf('%.2f', $total_charges), + total_item => $total_descr, }; + + return @line_items; +} + +=item _items_aging_balances + + Returns an array of aged balance amounts from a given epoch timestamp. + + The time of day is ignored for this calculation, so that slight differences + on the generation time of an invoice doesn't determine which column an + aged balance falls into. + + Will not include any balances dated after the given timestamp in + the calculated totals + + usage: + @aged_balances = $b->_items_aging_balances( $b->_date ) + + @aged_balances = ( + under30d, + 30d-60d, + 60d-90d, + over90d + ) + +=cut + +sub _items_aging_balances { + my ($self, $basetime) = @_; + die "Incorrect usage of _items_aging_balances()" unless ref $self; + + $basetime = $self->_date unless $basetime; + my @aging_balances = (0, 0, 0, 0); + my @open_invoices = $self->_items_previous(); + my $d30 = 2592000; # 60 * 60 * 24 * 30, + my $d60 = 5184000; # 60 * 60 * 24 * 60, + my $d90 = 7776000; # 60 * 60 * 24 * 90 + + # Move the clock back on our given day to 12:00:01 AM + my $dt_basetime = DateTime->from_epoch(epoch => $basetime); + my $dt_12am = DateTime->new( + year => $dt_basetime->year, + month => $dt_basetime->month, + day => $dt_basetime->day, + hour => 0, + minute => 0, + second => 1, + )->epoch(); + + # set our epoch breakpoints + $_ = $dt_12am - $_ for $d30, $d60, $d90; + + # grep the aged balances + for my $oinv (@open_invoices) { + if ($oinv->{_date} <= $basetime && $oinv->{_date} > $d30) { + # If post invoice dated less than 30days ago + $aging_balances[0] += $oinv->{amount}; + } elsif ($oinv->{_date} <= $d30 && $oinv->{_date} > $d60) { + # If past invoice dated between 30-60 days ago + $aging_balances[1] += $oinv->{amount}; + } elsif ($oinv->{_date} <= $d60 && $oinv->{_date} > $d90) { + # If past invoice dated between 60-90 days ago + $aging_balances[2] += $oinv->{amount}; + } else { + # If past invoice dated 90+ days ago + $aging_balances[3] += $oinv->{amount}; + } } - @b; + return map{ sprintf('%.2f',$_) } @aging_balances; +} + +=item has_call_details + +Returns true if this invoice has call details. + +=cut +sub has_call_details { + my $self = shift; + $self->scalar_sql(" + SELECT 1 FROM cust_bill_pkg_detail + LEFT JOIN cust_bill_pkg USING (billpkgnum) + WHERE cust_bill_pkg_detail.format = 'C' + AND cust_bill_pkg.invnum = ? + LIMIT 1 + ", $self->invnum); } =item call_details [ OPTION => VALUE ... ] @@ -2865,6 +3600,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 @@ -2928,6 +3675,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 ) = @_; @@ -2937,22 +3687,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'; @@ -3090,4 +3833,3 @@ documentation. =cut 1; -