X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=3af0c54a80a1abf0654edd86a20117ed097f5904;hb=7409f698686642a83b15a6f51f9034d2b8086fbc;hp=e1281e37206793e1ca4c4acefab4f9628430777e;hpb=f43c7c5765db18b35a3d3bec837681725bf0c245;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index e1281e372..3af0c54a8 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -4,6 +4,7 @@ use strict; use vars qw( @ISA $DEBUG $me $conf $money_char ); use vars qw( $invoice_lines @buf ); #yuck use Fcntl qw(:flock); #for spool_csv +use List::Util qw(min max); use IPC::Run3; use Date::Format; use Text::Template 1.20; @@ -228,6 +229,20 @@ sub cust_bill_pkg { qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } ); } +=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 { $_->cust_pkg } $self->cust_bill_pkg; + my %saw = (); + grep { ! $saw{$_->pkgnum}++ } @cust_pkg; +} + =item open_cust_bill_pkg Returns the open line items for this invoice. @@ -294,7 +309,7 @@ sub cust_suspend_if_balance_over { if ( $cust_main->total_owed_date($self->_date) < $amount ) { return (); } else { - $cust_main->suspend; + $cust_main->suspend(@_); } } @@ -397,6 +412,83 @@ sub owed { $balance; } +=item apply_payments_and_credits + +=cut + +sub apply_payments_and_credits { + my $self = shift; + + my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay; + my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit; + + 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"; + } + + if ( $app eq 'pay' ) { + + my $payment = shift @payments; + + $app = new FS::cust_bill_pay { + 'paynum' => $payment->paynum, + 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ), + }; + + } elsif ( $app eq 'credit' ) { + + my $credit = shift @credits; + + $app = new FS::cust_credit_bill { + 'crednum' => $credit->crednum, + 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ), + }; + + } else { + die "guru meditation #12 and 35"; + } + + $app->invnum( $self->invnum ); + + my $error = $app->insert; + die $error if $error; + + } + +} =item generate_email PARAMHASH @@ -432,7 +524,7 @@ sub generate_email { 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'), ); - if (ref($args{'to'} eq 'ARRAY')) { + if (ref($args{'to'}) eq 'ARRAY') { $return{'to'} = $args{'to'}; } else { $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ } @@ -483,16 +575,17 @@ sub generate_email { 'Disposition' => 'inline', ); - $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com'; - my $content_id = join('.', rand()*(2**32), $$, time). "\@$1"; + $args{'from'} =~ /\@([\w\.\-]+)/; + my $from = $1 || 'example.com'; + my $content_id = join('.', rand()*(2**32), $$, time). "\@$from"; my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc"; my $file; - if ( defined($args{'_template'}) && length($args{'_template'}) - && -e "$path/logo_". $args{'_template'}. ".png" + if ( defined($args{'template'}) && length($args{'template'}) + && -e "$path/logo_". $args{'template'}. ".png" ) { - $file = "$path/logo_". $args{'_template'}. ".png"; + $file = "$path/logo_". $args{'template'}. ".png"; } else { $file = "$path/logo.png"; } @@ -640,6 +733,21 @@ INVOICE_FROM, if specified, overrides the default email invoice From: address. =cut +sub queueable_send { + my %opt = @_; + + my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } ) + or die "invalid invoice number: " . $opt{invnum}; + + my @args = ( $opt{template}, $opt{agentnum} ); + push @args, $opt{invoice_from} + if exists($opt{invoice_from}) && $opt{invoice_from}; + + my $error = $self->send( @args ); + die $error if $error; + +} + sub send { my $self = shift; my $template = scalar(@_) ? shift : ''; @@ -678,6 +786,21 @@ INVOICE_FROM, if specified, overrides the default email invoice From: address. =cut +sub queueable_email { + my %opt = @_; + + my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } ) + or die "invalid invoice number: " . $opt{invnum}; + + my @args = ( $opt{template} ); + push @args, $opt{invoice_from} + if exists($opt{invoice_from}) && $opt{invoice_from}; + + my $error = $self->email( @args ); + die $error if $error; + +} + sub email { my $self = shift; my $template = scalar(@_) ? shift : ''; @@ -1070,11 +1193,7 @@ sub print_csv { my $taxtotal = 0; $taxtotal += $_->{'amount'} foreach $self->_items_tax; - my $duedate = ''; - if ( $conf->exists('invoice_default_terms') - && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) { - $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) ); - } + my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format? my( $previous_balance, @unused ) = $self->previous; #previous balance @@ -1303,10 +1422,11 @@ sub realtime_bop { } -=item batch_card +=item batch_card OPTION => VALUE... Adds a payment for this invoice to the pending credit card batch (see -L). +L), or, if the B option is set to a true value, +runs the payment using a realtime gateway. =cut @@ -1314,99 +1434,9 @@ sub batch_card { my ($self, %options) = @_; my $cust_main = $self->cust_main; - my $amount = sprintf("%.2f", $cust_main->balance - $cust_main->in_transit_payments); - return '' unless $amount > 0; + $options{invnum} = $self->invnum; - if ($options{'realtime'}) { - return $cust_main->realtime_bop ( $FS::payby::payby2bop{$cust_main->payby}, $amount, - %options, - ); - } - - my $oldAutoCommit = $FS::UID::AutoCommit; - local $FS::UID::AutoCommit = 0; - my $dbh = dbh; - - $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE") - or return "Cannot lock pay_batch: " . $dbh->errstr; - - my $pay_batch = qsearchs('pay_batch', {'status' => 'O'}); - - unless ( $pay_batch ) { - $pay_batch = new FS::pay_batch; - $pay_batch->setfield('status' => 'O'); - my $error = $pay_batch->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - die "error creating new batch: $error\n"; - } - } - - my $old_cust_pay_batch = qsearchs('cust_pay_batch', { - 'batchnum' => $pay_batch->getfield('batchnum'), - 'custnum' => $cust_main->getfield('custnum'), - } ); - - my $cust_pay_batch = new FS::cust_pay_batch ( { - 'batchnum' => $pay_batch->getfield('batchnum'), - 'invnum' => $self->getfield('invnum'), # is there a better value? - 'custnum' => $cust_main->getfield('custnum'), - 'last' => $cust_main->getfield('last'), - 'first' => $cust_main->getfield('first'), - 'address1' => $cust_main->getfield('address1'), - 'address2' => $cust_main->getfield('address2'), - 'city' => $cust_main->getfield('city'), - 'state' => $cust_main->getfield('state'), - 'zip' => $cust_main->getfield('zip'), - 'country' => $cust_main->getfield('country'), - 'payby' => $cust_main->payby, - 'payinfo' => $cust_main->payinfo, - 'exp' => $cust_main->getfield('paydate'), - 'payname' => $cust_main->getfield('payname'), - 'amount' => $amount, # consolidating - } ); - - $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum) - if $old_cust_pay_batch; - - my $error; - if ($old_cust_pay_batch) { - $error = $cust_pay_batch->replace($old_cust_pay_batch) - } else { - $error = $cust_pay_batch->insert; - } - - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - die $error; - } - - my $unapplied = $cust_main->total_credited + $cust_main->total_unapplied_payments + $cust_main->in_transit_payments; - foreach my $cust_bill ($cust_main->open_cust_bill) { - $dbh->commit or die $dbh->errstr if $oldAutoCommit; - my $cust_bill_pay_batch = new FS::cust_bill_pay_batch { - 'invnum' => $cust_bill->invnum, - 'paybatchnum' => $cust_pay_batch->paybatchnum, - 'amount' => $cust_bill->owed, - '_date' => time, - }; - if ($unapplied >= $cust_bill_pay_batch->amount){ - $unapplied -= $cust_bill_pay_batch->amount; - next; - }else{ - $cust_bill_pay_batch->amount(sprintf ( "%.2f", - $cust_bill_pay_batch->amount - $unapplied )); - $unapplied = 0; - } - $error = $cust_bill_pay_batch->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - die $error; - } - } - - $dbh->commit or die $dbh->errstr if $oldAutoCommit; - ''; + $cust_main->batch_card(%options); } sub _agent_template { @@ -1619,12 +1649,14 @@ sub print_text { #setup template variables package FS::cust_bill::_template; #! - use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent ); + use vars qw( $custnum $invnum $date $agent @address $overdue + $page $total_pages @buf ); + $custnum = $self->custnum; $invnum = $self->invnum; $date = $self->_date; - $page = 1; $agent = $self->cust_main->agent->agent; + $page = 1; if ( $FS::cust_bill::invoice_lines ) { $total_pages = @@ -1693,7 +1725,8 @@ sub print_text { =item print_latex [ TIME [ , TEMPLATE ] ] Internal method - returns a filename of a filled-in LaTeX template for this -invoice (Note: add ".tex" to get the actual filename). +invoice (Note: add ".tex" to get the actual filename), and a filename of +an associated logo (with the .eps extension included). See print_ps and print_pdf for methods that return PostScript and PDF output. @@ -1757,6 +1790,7 @@ sub print_latex { } my %invoice_data = ( + 'custnum' => $self->custnum, 'invnum' => $self->invnum, 'date' => time2str('%b %o, %Y', $self->_date), 'today' => time2str('%b %o, %Y', $today), @@ -1772,8 +1806,9 @@ sub print_latex { 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ), 'returnaddress' => $returnaddress, 'quantity' => 1, - 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt', + 'terms' => $self->terms, #'notes' => join("\n", $conf->config('invoice_latexnotes') ), + # better hang on to conf_dir for a while 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc", ); @@ -1999,6 +2034,22 @@ sub print_latex { } my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc; + my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX', + DIR => $dir, + SUFFIX => '.eps', + UNLINK => 0, + ) or die "can't open temp file: $!\n"; + + if ($template && $conf->exists("logo_${template}.eps")) { + print $lh $conf->config_binary("logo_${template}.eps") + or die "can't write temp file: $!\n"; + }else{ + print $lh $conf->config_binary('logo.eps') + or die "can't write temp file: $!\n"; + } + close $lh; + $invoice_data{'logo_file'} = $lh->filename; + my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX', DIR => $dir, SUFFIX => '.tex', @@ -2014,7 +2065,7 @@ sub print_latex { close $fh; $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename; - return $1; + return ($1, $invoice_data{'logo_file'}); } @@ -2032,7 +2083,7 @@ L and L for conversion functions. sub print_ps { my $self = shift; - my $file = $self->print_latex(@_); + my ($file, $lfile) = $self->print_latex(@_); my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc; chdir($dir); @@ -2051,12 +2102,24 @@ sub print_ps { or die "can't open $file.ps: $! (error in LaTeX template?)\n"; unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex"); + unlink("$lfile"); my $ps = ''; + + if ( $conf->exists('lpr-postscript_prefix') ) { + my $prefix = $conf->config('lpr-postscript_prefix'); + $ps .= eval qq("$prefix"); + } + while () { $ps .= $_; } + if ( $conf->exists('lpr-postscript_suffix') ) { + my $suffix = $conf->config('lpr-postscript_suffix'); + $ps .= eval qq("$suffix"); + } + close POSTSCRIPT; return $ps; @@ -2077,7 +2140,7 @@ L and L for conversion functions. sub print_pdf { my $self = shift; - my $file = $self->print_latex(@_); + my ($file, $lfile) = $self->print_latex(@_); my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc; chdir($dir); @@ -2105,6 +2168,7 @@ sub print_pdf { or die "can't open $file.pdf: $! (error in LaTeX template?)\n"; unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex"); + unlink("$lfile"); my $pdf = ''; while () { @@ -2157,6 +2221,7 @@ sub print_html { or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR; my %invoice_data = ( + 'custnum' => $self->custnum, 'invnum' => $self->invnum, 'date' => time2str('%b %o, %Y', $self->_date), 'today' => time2str('%b %o, %Y', $today), @@ -2168,8 +2233,7 @@ sub print_html { 'city' => encode_entities($cust_main->city), 'state' => encode_entities($cust_main->state), 'zip' => encode_entities($cust_main->zip), - 'terms' => $conf->config('invoice_default_terms') - || 'Payable upon receipt', + 'terms' => $self->terms, 'cid' => $cid, 'template' => $template, # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc", @@ -2218,6 +2282,7 @@ sub print_html { s/\\item /
  • /; s/\\end\{enumerate\}/<\/ol>/; s/\\textbf\{(.*)\}/$1<\/b>/; + s/\\\\\*/ /; $_; } $conf->config_orbase('invoice_latexnotes', $template) @@ -2344,14 +2409,41 @@ sub _latex_escape { #utility methods for print_* +sub terms { + my $self = shift; + + #check for an invoice- specific override (eventually) + + #check for a customer- specific override + return $self->cust_main->invoice_terms + if $self->cust_main->invoice_terms; + + #use configured default or default default + $conf->config('invoice_default_terms') || 'Payable upon receipt'; +} + +sub due_date { + my $self = shift; + my $duedate = ''; + if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) { + $duedate = $self->_date() + ( $1 * 86400 ); + } + $duedate; +} + +sub due_date2str { + my $self = shift; + $self->due_date ? time2str(shift, $self->due_date) : ''; +} + sub balance_due_msg { my $self = shift; my $msg = 'Balance Due'; - return $msg unless $conf->exists('invoice_default_terms'); - if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) { - $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) ); - } elsif ( $conf->config('invoice_default_terms') ) { - $msg .= ' - '. $conf->config('invoice_default_terms'); + return $msg unless $self->terms; + if ( $self->due_date ) { + $msg .= ' - Please pay by '. $self->due_date2str('%x'); + } elsif ( $self->terms ) { + $msg .= ' - '. $self->terms; } $msg; }