X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=f6dbc3df032adf01ef46e39b5881f9eb1a8e4f5c;hb=eb4ff7f73c5d4bdf74a3472448b5a195598ff4cd;hp=dc45dc32b71aa770e4fec69dbd8b78519961d634;hpb=811f2540b22045c137b4b8f2b5ff17bf6cbe957c;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index dc45dc32b..f6dbc3df0 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -5,7 +5,6 @@ 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; use File::Temp 0.14; @@ -13,7 +12,7 @@ use String::ShellQuote; use HTML::Entities; use Locale::Country; use FS::UID qw( datasrc ); -use FS::Misc qw( send_email send_fax ); +use FS::Misc qw( send_email send_fax generate_ps do_print ); use FS::Record qw( qsearch qsearchs dbh ); use FS::cust_main_Mixin; use FS::cust_main; @@ -25,6 +24,7 @@ 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; @@ -272,8 +272,7 @@ sub open_cust_bill_pkg { =item cust_bill_event -Returns the completed invoice events (see L) for this -invoice. +Returns the completed invoice events (deprecated, old-style events - see L) for this invoice. =cut @@ -282,6 +281,54 @@ sub cust_bill_event { 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. + +=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 @@ -419,6 +466,19 @@ sub owed { sub apply_payments_and_credits { my $self = shift; + 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; + + $self->select_for_update; #mutex + my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay; my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit; @@ -432,12 +492,16 @@ sub apply_payments_and_credits { my @open_lineitems = $self->open_cust_bill_pkg; my $max_pay_weight = - max( map { $_->cust_pkg->part_pkg->pay_weight || 0 } - @open_lineitems + max( map { $_->part_pkg->pay_weight || 0 } + grep { $_ } + map { $_->cust_pkg } + @open_lineitems ); my $max_credit_weight = - max( map { $_->cust_pkg->part_pkg->credit_weight || 0 } - @open_lineitems + 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 @@ -480,10 +544,17 @@ sub apply_payments_and_credits { $app->invnum( $self->invnum ); my $error = $app->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error inserting ". $app->table. " record: $error"; + } die $error if $error; } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; #no error + } =item generate_email PARAMHASH @@ -520,7 +591,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)$/ } @@ -571,8 +642,9 @@ 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; @@ -728,6 +800,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 : ''; @@ -766,6 +853,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 : ''; @@ -819,15 +921,7 @@ sub print { my $self = shift; my $template = scalar(@_) ? shift : ''; - my $lpr = $conf->config('lpr'); - - my $outerr = ''; - run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr; - if ( $? ) { - $outerr = ": $outerr" if length($outerr); - die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n"; - } - + do_print $self->lpr_data($template); } =item fax [ TEMPLATENAME ] @@ -1158,11 +1252,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 @@ -1403,148 +1493,19 @@ 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 = ( - 'status' => 'O', - 'payby' => FS::payby->payby2payment($cust_main->payby), - ); - - my $pay_batch = qsearchs( 'pay_batch', \%pay_batch ); - - unless ( $pay_batch ) { - $pay_batch = new FS::pay_batch \%pay_batch; - 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->batchnum, - 'custnum' => $cust_main->custnum, - } ); - - my $cust_pay_batch = new FS::cust_pay_batch ( { - 'batchnum' => $pay_batch->batchnum, - 'invnum' => $self->getfield('invnum'), # is there a better value? - # this field should be - # removed... - # cust_bill_pay_batch now - 'custnum' => $cust_main->custnum, - 'last' => $cust_main->getfield('last'), - 'first' => $cust_main->getfield('first'), - 'address1' => $cust_main->address1, - 'address2' => $cust_main->address2, - 'city' => $cust_main->city, - 'state' => $cust_main->state, - 'zip' => $cust_main->zip, - 'country' => $cust_main->country, - 'payby' => $cust_main->payby, - 'payinfo' => $cust_main->payinfo, - 'exp' => $cust_main->paydate, - 'payname' => $cust_main->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 { my $self = shift; - $self->_agent_plandata('agent_templatename'); + $self->cust_main->agent_template; } sub _agent_invoice_from { my $self = shift; - $self->_agent_plandata('agent_invoice_from'); -} - -sub _agent_plandata { - my( $self, $option ) = @_; - - my $part_bill_event = qsearchs( 'part_bill_event', - { - 'payby' => $self->cust_main->payby, - 'plan' => 'send_agent', - 'plandata' => { 'op' => '~', - 'value' => "(^|\n)agentnum ". - '([0-9]*, )*'. - $self->cust_main->agentnum. - '(, [0-9]*)*'. - "(\n|\$)", - }, - }, - '', - 'ORDER BY seconds LIMIT 1' - ); - - return '' unless $part_bill_event; - - if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) { - return $1; - } else { - warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart. - " plandata for $option"; - return ''; - } - + $self->cust_main->agent_invoice_from; } =item print_text [ TIME [ , TEMPLATE ] ] @@ -1615,8 +1576,12 @@ sub print_text { if ( $cust_bill_pkg->recur != 0 ) { push @buf, [ - "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " . - time2str("%x", $cust_bill_pkg->edate) . ")", + $desc . + ( $conf->exists('disable_line_item_date_ranges') + ? '' + : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " . + time2str("%x", $cust_bill_pkg->edate) . ")" + ), $money_char. sprintf("%10.2f", $cust_bill_pkg->recur) ]; push @buf, @@ -1792,7 +1757,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. @@ -1872,8 +1838,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", ); @@ -2098,7 +2065,23 @@ sub print_latex { die "guru meditation #54"; } - my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc; + 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', @@ -2114,7 +2097,7 @@ sub print_latex { close $fh; $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename; - return $1; + return ($1, $invoice_data{'logo_file'}); } @@ -2132,34 +2115,10 @@ L and L for conversion functions. sub print_ps { my $self = shift; - my $file = $self->print_latex(@_); - - my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc; - chdir($dir); - - my $sfile = shell_quote $file; - - system("pslatex $sfile.tex >/dev/null 2>&1") == 0 - or die "pslatex $file.tex failed; see $file.log for details?\n"; - system("pslatex $sfile.tex >/dev/null 2>&1") == 0 - or die "pslatex $file.tex failed; see $file.log for details?\n"; - - system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0 - or die "dvips failed"; - - open(POSTSCRIPT, "<$file.ps") - or die "can't open $file.ps: $! (error in LaTeX template?)\n"; - - unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex"); - - my $ps = ''; - while () { - $ps .= $_; - } - - close POSTSCRIPT; - - return $ps; + my ($file, $lfile) = $self->print_latex(@_); + my $ps = generate_ps($file); + unlink($lfile); + $ps; } @@ -2177,9 +2136,9 @@ 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; + my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; chdir($dir); #system('pdflatex', "$file.tex"); @@ -2205,6 +2164,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 () { @@ -2269,8 +2229,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", @@ -2319,6 +2278,7 @@ sub print_html { s/\\item /
  • /; s/\\end\{enumerate\}/<\/ol>/; s/\\textbf\{(.*)\}/$1<\/b>/; + s/\\\\\*/ /; $_; } $conf->config_orbase('invoice_latexnotes', $template) @@ -2445,14 +2405,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; } @@ -2540,9 +2527,12 @@ sub _items_cust_bill_pkg { if ( $cust_bill_pkg->recur != 0 ) { push @b, { - description => "$desc (" . - time2str('%x', $cust_bill_pkg->sdate). ' - '. - time2str('%x', $cust_bill_pkg->edate). ')', + description => $desc . + ( $conf->exists('disable_line_item_date_ranges') + ? '' + : " (" .time2str("%x", $cust_bill_pkg->sdate). + " - ".time2str("%x", $cust_bill_pkg->edate).")" + ), #pkgpart => $part_pkg->pkgpart, pkgnum => $cust_bill_pkg->pkgnum, amount => sprintf("%.2f", $cust_bill_pkg->recur), @@ -2635,6 +2625,8 @@ sub _items_payments { =back + + =head1 SUBROUTINES =over 4 @@ -2756,6 +2748,34 @@ sub re_X { =back +=head1 CLASS METHODS + +=over 4 + +=item owed_sql + +Returns an SQL fragment to retreived the amount owed. + +=cut + +sub owed_sql { + #my $class = shift; + + "charged + - COALESCE( + ( SELECT SUM(amount) FROM cust_bill_pay + WHERE cust_bill.invnum = cust_bill_pay.invnum ) + ,0 + ) + - COALESCE( + ( SELECT SUM(amount) FROM cust_credit_bill + WHERE cust_bill.invnum = cust_credit_bill.invnum ) + ,0 + ) + "; + +} + =head1 BUGS The delete method.