X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=472dc731822a02f76839af56e5ccdb7dde7cd3f1;hb=3ae50113ba75f6e1feb56106ff94b1417d40fb5e;hp=c3ad158dbc77f7ea98a63a2c665388181af1f1ad;hpb=bbff687b94d49c5348ce51005433840ec7d51bea;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index c3ad158db..472dc7318 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1,8 +1,10 @@ package FS::cust_bill; use strict; -use vars qw( @ISA $DEBUG $conf $money_char ); +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 Date::Format; use Text::Template 1.20; use File::Temp 0.14; @@ -10,23 +12,28 @@ use String::ShellQuote; use HTML::Entities; use Locale::Country; use FS::UID qw( datasrc ); -use FS::Record qw( qsearch qsearchs ); -use FS::Misc qw( send_email send_fax ); +use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print ); +use FS::Record qw( qsearch qsearchs dbh ); +use FS::cust_main_Mixin; use FS::cust_main; use FS::cust_bill_pkg; use FS::cust_credit; 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::part_pkg; use FS::cust_bill_pay; +use FS::cust_bill_pay_batch; use FS::part_bill_event; +use FS::payby; -@ISA = qw( FS::Record ); +@ISA = qw( FS::cust_main_Mixin FS::Record ); $DEBUG = 0; +$me = '[FS::cust_bill]'; #ask FS::UID to run this stuff for us later FS::UID->install_callback( sub { @@ -104,6 +111,13 @@ Invoices are normally created by calling the bill method of a customer object sub table { 'cust_bill'; } +sub cust_linked { $_[0]->cust_main_custnum; } +sub cust_unlinked_msg { + my $self = shift; + "WARNING: can't find cust_main.custnum ". $self->custnum. + ' (cust_bill.invnum '. $self->invnum. ')'; +} + =item insert Adds this invoice to the database ("Posts" the invoice). If there is an error, @@ -111,8 +125,14 @@ returns the error, otherwise returns false. =item delete -Currently unimplemented. I don't remove invoices because there would then be -no record you ever posted this invoice (which is bad, no?) +This method now works but you probably shouldn't use it. Instead, apply a +credit against the invoice. + +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 @@ -132,14 +152,20 @@ collect method of a customer object (see L). =cut -sub replace { +#replace can be inherited from Record.pm + +# replace_check is now the preferred way to #implement replace data checks +# (so $object->replace() works without an argument) + +sub replace_check { my( $new, $old ) = ( shift, shift ); return "Can't change custnum!" unless $old->custnum == $new->custnum; #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; + return "Can't change charged!" unless $old->charged == $new->charged + || $old->charged == 0; - $new->SUPER::replace($old); + ''; } =item check @@ -202,6 +228,47 @@ 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. + +Note that cust_bill_pkg with both setup and recur fees are returned as two +separate line items, each with only one fee. + +=cut + +# modeled after cust_main::open_cust_bill +sub open_cust_bill_pkg { + my $self = shift; + + # grep { $_->owed > 0 } $self->cust_bill_pkg + + my %other = ( 'recur' => 'setup', + 'setup' => 'recur', ); + my @open = (); + foreach my $field ( qw( recur setup )) { + push @open, map { $_->set( $other{$field}, 0 ); $_; } + grep { $_->owed($field) > 0 } + $self->cust_bill_pkg; + } + + @open; +} + =item cust_bill_event Returns the completed invoice events (see L) for this @@ -226,6 +293,25 @@ sub cust_main { qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); } +=item cust_suspend_if_balance_over AMOUNT + +Suspends the customer associated with this invoice if the total amount owed on +this invoice and all older invoices is greater than the specified amount. + +Returns a list: an empty list on success or a list of errors. + +=cut + +sub cust_suspend_if_balance_over { + my( $self, $amount ) = ( shift, shift ); + my $cust_main = $self->cust_main; + if ( $cust_main->total_owed_date($self->_date) < $amount ) { + return (); + } else { + $cust_main->suspend(@_); + } +} + =item cust_credit Depreciated. See the cust_credited method. @@ -325,6 +411,103 @@ sub owed { $balance; } +=item apply_payments_and_credits + +=cut + +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; + + 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; + 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 @@ -360,7 +543,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)$/ } @@ -411,13 +594,25 @@ 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" + ) + { + $file = "$path/logo_". $args{'template'}. ".png"; + } else { + $file = "$path/logo.png"; + } my $image = build MIME::Entity 'Type' => 'image/png', 'Encoding' => 'base64', - 'Path' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc/logo.png", + 'Path' => $file, 'Filename' => 'logo.png', 'Content-ID' => "<$content_id>", ; @@ -538,14 +733,14 @@ sub mimebuild_pdf { 'Encoding' => 'base64', 'Data' => [ $self->print_pdf(@_) ], 'Disposition' => 'attachment', - 'Filename' => 'invoice.pdf', + 'Filename' => 'invoice-'. $self->invnum. '.pdf', ); } =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ] -Sends this invoice to the destinations configured for this customer: send -emails or print. See L. +Sends this invoice to the destinations configured for this customer: sends +email, prints and/or faxes. See L. TEMPLATENAME, if specified, is the name of a suffix for alternate invoices. @@ -555,8 +750,26 @@ single agent) or an arrayref of agentnums. INVOICE_FROM, if specified, overrides the default email invoice From: address. +AMOUNT, if specified, only sends the invoice if the total amount owed on this +invoice and all older invoices is greater than the specified amount. + =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 : ''; @@ -570,15 +783,22 @@ sub send { ? shift : ( $self->_agent_invoice_from || $conf->config('invoice_from') ); + my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0; + + return '' + unless $self->cust_main->total_owed_date($self->_date) > $balance_over; + my @invoicing_list = $self->cust_main->invoicing_list; + #$self->email_invoice($template, $invoice_from) $self->email($template, $invoice_from) if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list; + #$self->print_invoice($template) $self->print($template) if grep { $_ eq 'POST' } @invoicing_list; #postal - $self->fax($template) + $self->fax_invoice($template) if grep { $_ eq 'FAX' } @invoicing_list; #fax ''; @@ -595,6 +815,22 @@ 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_invoice { sub email { my $self = shift; my $template = scalar(@_) ? shift : ''; @@ -609,10 +845,13 @@ sub email { #better to notify this person than silence @invoicing_list = ($invoice_from) unless @invoicing_list; + my $subject = $self->email_subject($template); + my $error = send_email( $self->generate_email( 'from' => $invoice_from, 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ], + 'subject' => $subject, 'template' => $template, ) ); @@ -621,9 +860,26 @@ sub email { } +sub email_subject { + my $self = shift; + + #my $template = scalar(@_) ? shift : ''; + #per-template? + + my $subject = $conf->config('invoice_subject') || 'Invoice'; + + my $cust_main = $self->cust_main; + my $name = $cust_main->name; + my $name_short = $cust_main->name_short; + my $invoice_number = $self->invnum; + my $invoice_date = $self->_date_pretty; + + eval qq("$subject"); +} + =item lpr_data [ TEMPLATENAME ] -Returns the postscript or plaintext for this invoice. +Returns the postscript or plaintext for this invoice as an arrayref. TEMPLATENAME, if specified, is the name of a suffix for alternate invoices. @@ -644,20 +900,15 @@ TEMPLATENAME, if specified, is the name of a suffix for alternate invoices. =cut +#sub print_invoice { sub print { my $self = shift; my $template = scalar(@_) ? shift : ''; - my $lpr = $conf->config('lpr'); - open(LPR, "|$lpr") - or die "Can't open pipe to $lpr: $!\n"; - print LPR @{ $self->lpr_data($template) }; - close LPR - or die $! ? "Error closing $lpr: $!\n" - : "Exit status $? from $lpr\n"; + do_print $self->lpr_data($template); } -=item fax [ TEMPLATENAME ] +=item fax_invoice [ TEMPLATENAME ] Faxes this invoice. @@ -665,7 +916,7 @@ TEMPLATENAME, if specified, is the name of a suffix for alternate invoices. =cut -sub fax { +sub fax_invoice { my $self = shift; my $template = scalar(@_) ? shift : ''; @@ -682,6 +933,46 @@ sub fax { } +=item ftp_invoice [ TEMPLATENAME ] + +Sends this invoice data via FTP. + +TEMPLATENAME is unused? + +=cut + +sub ftp_invoice { + my $self = shift; + my $template = scalar(@_) ? shift : ''; + + $self->send_csv( + 'protocol' => 'ftp', + 'server' => $conf->config('cust_bill-ftpserver'), + 'username' => $conf->config('cust_bill-ftpusername'), + 'password' => $conf->config('cust_bill-ftppassword'), + 'dir' => $conf->config('cust_bill-ftpdir'), + 'format' => $conf->config('cust_bill-ftpformat'), + ); +} + +=item spool_invoice [ TEMPLATENAME ] + +Spools this invoice data (see L) + +TEMPLATENAME is unused? + +=cut + +sub spool_invoice { + my $self = shift; + my $template = scalar(@_) ? shift : ''; + + $self->spool_csv( + 'format' => $conf->config('cust_bill-spoolformat'), + 'agent_spools' => $conf->exists('cust_bill-spoolagent'), + ); +} + =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ] Like B, but only sends the invoice if it is the newest open invoice for @@ -705,7 +996,7 @@ sub send_if_newest { $self->send(@_); } -=item send_csv OPTIONS +=item send_csv OPTION => VALUE, ... Sends invoice as a CSV data-file to a remote host with the specified protocol. @@ -720,7 +1011,148 @@ dir The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number and YYMMDDHHMMSS is a timestamp. -The fields of the CSV file is as follows: +See L for a description of the output format. + +=cut + +sub send_csv { + my($self, %opt) = @_; + + #create file(s) + + my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill"; + mkdir $spooldir, 0700 unless -d $spooldir; + + my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time); + my $file = "$spooldir/$tracctnum.csv"; + + my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum ); + + open(CSV, ">$file") or die "can't open $file: $!"; + print CSV $header; + + print CSV $detail; + + close CSV; + + my $net; + if ( $opt{protocol} eq 'ftp' ) { + eval "use Net::FTP;"; + die $@ if $@; + $net = Net::FTP->new($opt{server}) or die @$; + } else { + die "unknown protocol: $opt{protocol}"; + } + + $net->login( $opt{username}, $opt{password} ) + or die "can't FTP to $opt{username}\@$opt{server}: login error: $@"; + + $net->binary or die "can't set binary mode"; + + $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}"; + + $net->put($file) or die "can't put $file: $!"; + + $net->quit; + + unlink $file; + +} + +=item spool_csv + +Spools CSV invoice data. + +Options are: + +=over 4 + +=item format - 'default' or 'billco' + +=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L). + +=item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file + +=item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount. + +=back + +=cut + +sub spool_csv { + my($self, %opt) = @_; + + my $cust_main = $self->cust_main; + + if ( $opt{'dest'} ) { + my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 } + $cust_main->invoicing_list; + return 'N/A' unless $invoicing_list{$opt{'dest'}} + || ! keys %invoicing_list; + } + + if ( $opt{'balanceover'} ) { + return 'N/A' + if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'}; + } + + my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill"; + mkdir $spooldir, 0700 unless -d $spooldir; + + my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time); + + my $file = + "$spooldir/". + ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ). + ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) . + '.csv'; + + my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum ); + + open(CSV, ">>$file") or die "can't open $file: $!"; + flock(CSV, LOCK_EX); + seek(CSV, 0, 2); + + print CSV $header; + + if ( lc($opt{'format'}) eq 'billco' ) { + + flock(CSV, LOCK_UN); + close CSV; + + $file = + "$spooldir/". + ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ). + '-detail.csv'; + + open(CSV,">>$file") or die "can't open $file: $!"; + flock(CSV, LOCK_EX); + seek(CSV, 0, 2); + } + + print CSV $detail; + + flock(CSV, LOCK_UN); + close CSV; + + return ''; + +} + +=item print_csv OPTION => VALUE, ... + +Returns CSV data for this invoice. + +Options are: + +format - 'default' or 'billco' + +Returns a list consisting of two scalars. The first is a single line of CSV +header information for this invoice. The second is one or more lines of CSV +detail information for this invoice. + +If I is not specified or "default", the fields of the CSV file are as +follows: record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate @@ -728,13 +1160,13 @@ record_type, invnum, custnum, _date, charged, first, last, company, address1, ad =item record type - B is either C or C -If B is C, this is a primary invoice record. The +B is C for the initial header line only. The last five fields (B through B) are irrelevant, and all other fields are filled in. -If B is C, this is a line item record. Only the -first two fields (B and B) and the last five fields -(B through B) are filled in. +B is C for detail lines. Only the first two fields +(B and B) and the last five fields (B through B) +are filled in. =item invnum - invoice number @@ -774,101 +1206,209 @@ first two fields (B and B) and the last five fields =back +If I is "billco", the fields of the header CSV file are as follows: + + +-------------------------------------------------------------------+ + | FORMAT HEADER FILE | + |-------------------------------------------------------------------| + | Field | Description | Name | Type | Width | + | 1 | N/A-Leave Empty | RC | CHAR | 2 | + | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 | + | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 | + | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 | + | 5 | Transaction Zip Code | TRZIP | CHAR | 5 | + | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 | + | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 | + | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 | + | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 | + | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 | + | 11 | Transaction City Bill To | TRCITY | CHAR | 20 | + | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 | + | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 | + | 14 | Bill Due Date | DUEDATE | CHAR | 10 | + | 15 | Previous Balance | BALFWD | NUM* | 9 | + | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 | + | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 | + | 18 | Total Amt Due | TOTALDUE | NUM* | 9 | + | 19 | Total Amt Due | AMTDUE | NUM* | 9 | + | 20 | 30 Day Aging | AMT30 | NUM* | 9 | + | 21 | 60 Day Aging | AMT60 | NUM* | 9 | + | 22 | 90 Day Aging | AMT90 | NUM* | 9 | + | 23 | Y/N | AGESWITCH | CHAR | 1 | + | 24 | Remittance automation | SCANLINE | CHAR | 100 | + | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 | + | 26 | Customer Reference Number | CUSTREF | CHAR | 15 | + | 27 | Federal Tax*** | FEDTAX | NUM* | 9 | + | 28 | State Tax*** | STATETAX | NUM* | 9 | + | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 | + +-------+-------------------------------+------------+------+-------+ + +If I is "billco", the fields of the detail CSV file are as follows: + + FORMAT FOR DETAIL FILE + | | | | + Field | Description | Name | Type | Width + 1 | N/A-Leave Empty | RC | CHAR | 2 + 2 | N/A-Leave Empty | CUSTID | CHAR | 15 + 3 | Account Number | TRACCTNUM | CHAR | 15 + 4 | Invoice Number | TRINVOICE | CHAR | 15 + 5 | Line Sequence (sort order) | LINESEQ | NUM | 6 + 6 | Transaction Detail | DETAILS | CHAR | 100 + 7 | Amount | AMT | NUM* | 9 + 8 | Line Format Control** | LNCTRL | CHAR | 2 + 9 | Grouping Code | GROUP | CHAR | 2 + 10 | User Defined | ACCT CODE | CHAR | 15 + =cut -sub send_csv { +sub print_csv { my($self, %opt) = @_; + + eval "use Text::CSV_XS"; + die $@ if $@; - #part one: create file + my $cust_main = $self->cust_main; - my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill"; - mkdir $spooldir, 0700 unless -d $spooldir; + my $csv = Text::CSV_XS->new({'always_quote'=>1}); - my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time); + if ( lc($opt{'format'}) eq 'billco' ) { - open(CSV, ">$file") or die "can't open $file: $!"; + my $taxtotal = 0; + $taxtotal += $_->{'amount'} foreach $self->_items_tax; - eval "use Text::CSV_XS"; - die $@ if $@; + my $duedate = $self->balance_due_date; - my $csv = Text::CSV_XS->new({'always_quote'=>1}); + my( $previous_balance, @unused ) = $self->previous; #previous balance - my $cust_main = $self->cust_main; + my $pmt_cr_applied = 0; + $pmt_cr_applied += $_->{'amount'} + foreach ( $self->_items_payments, $self->_items_credits ) ; - $csv->combine( - 'cust_bill', - $self->invnum, - $self->custnum, - time2str("%x", $self->_date), - sprintf("%.2f", $self->charged), - ( map { $cust_main->getfield($_) } - qw( first last company address1 address2 city state zip country ) ), - map { '' } (1..5), - ) or die "can't create csv"; - print CSV $csv->string. "\n"; - - #new charges (false laziness w/print_text) - foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { - - my($pkg, $setup, $recur, $sdate, $edate); - if ( $cust_bill_pkg->pkgnum ) { - - ($pkg, $setup, $recur, $sdate, $edate) = ( - $cust_bill_pkg->cust_pkg->part_pkg->pkg, - ( $cust_bill_pkg->setup != 0 - ? sprintf("%.2f", $cust_bill_pkg->setup ) - : '' ), - ( $cust_bill_pkg->recur != 0 - ? sprintf("%.2f", $cust_bill_pkg->recur ) - : '' ), - time2str("%x", $cust_bill_pkg->sdate), - time2str("%x", $cust_bill_pkg->edate), - ); + my $totaldue = sprintf('%.2f', $self->owed + $previous_balance); - } else { #pkgnum tax - next unless $cust_bill_pkg->setup != 0; - my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') - ? ( $cust_bill_pkg->itemdesc || 'Tax' ) - : 'Tax'; - ($pkg, $setup, $recur, $sdate, $edate) = - ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' ); - } + $csv->combine( + '', # 1 | N/A-Leave Empty CHAR 2 + '', # 2 | N/A-Leave Empty CHAR 15 + $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15 + $self->invnum, # 4 | Transaction Invoice No CHAR 15 + $cust_main->zip, # 5 | Transaction Zip Code CHAR 5 + $cust_main->company, # 6 | Transaction Company Bill To CHAR 30 + #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30 + $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30 + $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30 + $cust_main->address1, # 9 | Bill To Street Address CHAR 30 + '', # 10 | Ancillary Billing Information CHAR 30 + $cust_main->city, # 11 | Transaction City Bill To CHAR 20 + $cust_main->state, # 12 | Transaction State Bill To CHAR 2 + + # XXX ? + time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10 + + # XXX ? + $duedate, # 14 | Bill Due Date CHAR 10 + + $previous_balance, # 15 | Previous Balance NUM* 9 + $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9 + sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9 + $totaldue, # 18 | Total Amt Due NUM* 9 + $totaldue, # 19 | Total Amt Due NUM* 9 + '', # 20 | 30 Day Aging NUM* 9 + '', # 21 | 60 Day Aging NUM* 9 + '', # 22 | 90 Day Aging NUM* 9 + 'N', # 23 | Y/N CHAR 1 + '', # 24 | Remittance automation CHAR 100 + $taxtotal, # 25 | Total Taxes & Fees NUM* 9 + $self->custnum, # 26 | Customer Reference Number CHAR 15 + '0', # 27 | Federal Tax*** NUM* 9 + sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9 + '0', # 29 | Other Taxes & Fees*** NUM* 9 + ); + } else { + $csv->combine( - 'cust_bill_pkg', + 'cust_bill', $self->invnum, - ( map { '' } (1..11) ), - ($pkg, $setup, $recur, $sdate, $edate) + $self->custnum, + time2str("%x", $self->_date), + sprintf("%.2f", $self->charged), + ( map { $cust_main->getfield($_) } + qw( first last company address1 address2 city state zip country ) ), + map { '' } (1..5), ) or die "can't create csv"; - print CSV $csv->string. "\n"; - } - close CSV or die "can't close CSV: $!"; + my $header = $csv->string. "\n"; + + my $detail = ''; + if ( lc($opt{'format'}) eq 'billco' ) { + + my $lineseq = 0; + foreach my $item ( $self->_items_pkg ) { + + $csv->combine( + '', # 1 | N/A-Leave Empty CHAR 2 + '', # 2 | N/A-Leave Empty CHAR 15 + $opt{'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 + $item->{'amount'}, # 7 | Amount NUM* 9 + '', # 8 | Line Format Control** CHAR 2 + '', # 9 | Grouping Code CHAR 2 + '', # 10 | User Defined CHAR 15 + ); - #part two: upload it + $detail .= $csv->string. "\n"; + + } - my $net; - if ( $opt{protocol} eq 'ftp' ) { - eval "use Net::FTP;"; - die $@ if $@; - $net = Net::FTP->new($opt{server}) or die @$; } else { - die "unknown protocol: $opt{protocol}"; - } - $net->login( $opt{username}, $opt{password} ) - or die "can't FTP to $opt{username}\@$opt{server}: login error: $@"; + foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { + + my($pkg, $setup, $recur, $sdate, $edate); + if ( $cust_bill_pkg->pkgnum ) { + + ($pkg, $setup, $recur, $sdate, $edate) = ( + $cust_bill_pkg->cust_pkg->part_pkg->pkg, + ( $cust_bill_pkg->setup != 0 + ? sprintf("%.2f", $cust_bill_pkg->setup ) + : '' ), + ( $cust_bill_pkg->recur != 0 + ? sprintf("%.2f", $cust_bill_pkg->recur ) + : '' ), + ( $cust_bill_pkg->sdate + ? time2str("%x", $cust_bill_pkg->sdate) + : '' ), + ($cust_bill_pkg->edate + ?time2str("%x", $cust_bill_pkg->edate) + : '' ), + ); + + } else { #pkgnum tax + next unless $cust_bill_pkg->setup != 0; + my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') + ? ( $cust_bill_pkg->itemdesc || 'Tax' ) + : 'Tax'; + ($pkg, $setup, $recur, $sdate, $edate) = + ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' ); + } + + $csv->combine( + 'cust_bill_pkg', + $self->invnum, + ( map { '' } (1..11) ), + ($pkg, $setup, $recur, $sdate, $edate) + ) or die "can't create csv"; - $net->binary or die "can't set binary mode"; + $detail .= $csv->string. "\n"; - $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}"; + } - $net->put($file) or die "can't put $file: $!"; - - $net->quit; + } - unlink $file; + ( $header, $detail ); } @@ -965,78 +1505,31 @@ 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 sub batch_card { - my $self = shift; + my ($self, %options) = @_; my $cust_main = $self->cust_main; - my $cust_pay_batch = new FS::cust_pay_batch ( { - 'invnum' => $self->getfield('invnum'), - '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'), - 'cardnum' => $cust_main->payinfo, - 'exp' => $cust_main->getfield('paydate'), - 'payname' => $cust_main->getfield('payname'), - 'amount' => $self->owed, - } ); - my $error = $cust_pay_batch->insert; - die $error if $error; - - ''; + $options{invnum} = $self->invnum; + + $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 ] ] @@ -1050,7 +1543,7 @@ L and L for conversion functions. =cut -#still some false laziness w/print_text +#still some false laziness w/_items stuff (and send_csv) sub print_text { my( $self, $today, $template ) = @_; @@ -1071,18 +1564,20 @@ sub print_text { @buf = (); #previous balance - foreach ( @pr_cust_bill ) { - push @buf, [ - "Previous Balance, Invoice #". $_->invnum. - " (". time2str("%x",$_->_date). ")", - $money_char. sprintf("%10.2f",$_->owed) - ]; - } - if (@pr_cust_bill) { - push @buf,['','-----------']; - push @buf,[ 'Total Previous Balance', - $money_char. sprintf("%10.2f",$pr_total ) ]; - push @buf,['','']; + unless ($conf->exists('disable_previous_balance')) { + foreach ( @pr_cust_bill ) { + push @buf, [ + "Previous Balance, Invoice #". $_->invnum. + " (". time2str("%x",$_->_date). ")", + $money_char. sprintf("%10.2f",$_->owed) + ]; + } + if (@pr_cust_bill) { + push @buf,['','-----------']; + push @buf,[ 'Total Previous Balance', + $money_char. sprintf("%10.2f",$pr_total ) ]; + push @buf,['','']; + } } #new charges @@ -1091,100 +1586,107 @@ sub print_text { ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes ) { - if ( $cust_bill_pkg->pkgnum > 0 ) { + my $desc = $cust_bill_pkg->desc; - my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } ); - my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } ); - my $pkg = $part_pkg->pkg; + if ( $cust_bill_pkg->pkgnum > 0 ) { if ( $cust_bill_pkg->setup != 0 ) { - my $description = $pkg; + my $description = $desc; $description .= ' Setup' if $cust_bill_pkg->recur != 0; push @buf, [ $description, $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ]; push @buf, map { [ " ". $_->[0]. ": ". $_->[1], '' ] } - $cust_pkg->h_labels($self->_date); + $cust_bill_pkg->cust_pkg->h_labels($self->_date); } if ( $cust_bill_pkg->recur != 0 ) { push @buf, [ - "$pkg (" . 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, map { [ " ". $_->[0]. ": ". $_->[1], '' ] } - $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate); + $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate, + $cust_bill_pkg->sdate ); } push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details; } else { #pkgnum tax or one-shot line item - my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') - ? ( $cust_bill_pkg->itemdesc || 'Tax' ) - : 'Tax'; + if ( $cust_bill_pkg->setup != 0 ) { - push @buf, [ $itemdesc, + push @buf, [ $desc, $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ]; } if ( $cust_bill_pkg->recur != 0 ) { - push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - " - . time2str("%x", $cust_bill_pkg->edate). ")", + push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - " + . time2str("%x", $cust_bill_pkg->edate). ")", $money_char. sprintf("%10.2f", $cust_bill_pkg->recur) ]; } + } + } push @buf,['','-----------']; - push @buf,['Total New Charges', + push @buf,[ ( $conf->exists('disable_previous_balance') + ? 'Total Charges' + : 'Total New Charges'), $money_char. sprintf("%10.2f",$self->charged) ]; push @buf,['','']; - push @buf,['','-----------']; - push @buf,['Total Charges', - $money_char. sprintf("%10.2f",$self->charged + $pr_total) ]; - push @buf,['','']; + unless ($conf->exists('disable_previous_balance')) { + push @buf,['','-----------']; + push @buf,['Total Charges', + $money_char. sprintf("%10.2f",$self->charged + $pr_total) ]; + push @buf,['','']; - #credits - foreach ( $self->cust_credited ) { + #credits + foreach ( $self->cust_credited ) { - #something more elaborate if $_->amount ne $_->cust_credit->credited ? + #something more elaborate if $_->amount ne $_->cust_credit->credited ? - my $reason = substr($_->cust_credit->reason,0,32); - $reason .= '...' if length($reason) < length($_->cust_credit->reason); - $reason = " ($reason) " if $reason; - push @buf,[ - "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". - $reason, - $money_char. sprintf("%10.2f",$_->amount) - ]; - } - #foreach ( @cr_cust_credit ) { - # push @buf,[ - # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")", - # $money_char. sprintf("%10.2f",$_->credited) - # ]; - #} + my $reason = substr($_->cust_credit->reason,0,32); + $reason .= '...' if length($reason) < length($_->cust_credit->reason); + $reason = " ($reason) " if $reason; + push @buf,[ + "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". + $reason, + $money_char. sprintf("%10.2f",$_->amount) + ]; + } + #foreach ( @cr_cust_credit ) { + # push @buf,[ + # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")", + # $money_char. sprintf("%10.2f",$_->credited) + # ]; + #} - #get & print payments - foreach ( $self->cust_bill_pay ) { + #get & print payments + foreach ( $self->cust_bill_pay ) { - #something more elaborate if $_->amount ne ->cust_pay->paid ? + #something more elaborate if $_->amount ne ->cust_pay->paid ? - push @buf,[ - "Payment received ". time2str("%x",$_->cust_pay->_date ), - $money_char. sprintf("%10.2f",$_->amount ) - ]; - } + push @buf,[ + "Payment received ". time2str("%x",$_->cust_pay->_date ), + $money_char. sprintf("%10.2f",$_->amount ) + ]; + } - #balance due - my $balance_due_msg = $self->balance_due_msg; + #balance due + my $balance_due_msg = $self->balance_due_msg; - push @buf,['','-----------']; - push @buf,[$balance_due_msg, $money_char. - sprintf("%10.2f", $balance_due ) ]; + push @buf,['','-----------']; + push @buf,[$balance_due_msg, $money_char. + sprintf("%10.2f", $balance_due ) ]; + } #create the template $template ||= $self->_agent_template; @@ -1209,12 +1711,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 = @@ -1294,7 +1798,7 @@ L and L for conversion functions. =cut -#still some false laziness w/print_text +#still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though) sub print_latex { my( $self, $today, $template ) = @_; @@ -1347,27 +1851,45 @@ 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), 'agent' => _latex_escape($cust_main->agent->agent), + 'agent_custid' => _latex_escape($cust_main->agent_custid), 'payname' => _latex_escape($cust_main->payname), 'company' => _latex_escape($cust_main->company), 'address1' => _latex_escape($cust_main->address1), 'address2' => _latex_escape($cust_main->address2), 'city' => _latex_escape($cust_main->city), 'state' => _latex_escape($cust_main->state), + #'quantity' => 1, 'zip' => _latex_escape($cust_main->zip), - 'footer' => join("\n", $conf->config('invoice_latexfooter') ), - 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ), + 'fax' => _latex_escape($cust_main->fax), + 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ), + 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ), 'returnaddress' => $returnaddress, 'quantity' => 1, 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt', #'notes' => join("\n", $conf->config('invoice_latexnotes') ), 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc", + 'current_charges' => sprintf('%.2f', $self->charged ), + 'previous_balance' => sprintf("%.2f", $pr_total), + 'balance' => sprintf("%.2f", $balance_due), + 'duedate' => $self->balance_due_date, + 'ship_enable' => $conf->exists('invoice-ship_address'), + 'unitprices' => $conf->exists('invoice-unitprice'), ); my $countrydefault = $conf->config('countrydefault') || 'US'; + my $prefix = $cust_main->has_ship_address ? 'ship_' : ''; + foreach ( qw( contact company address1 address2 city state zip country fax) ){ + my $method = $prefix.$_; + $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method); + } + $invoice_data{'ship_country'} = '' + if ( $invoice_data{'ship_country'} eq $countrydefault ); + if ( $cust_main->country eq $countrydefault ) { $invoice_data{'country'} = ''; } else { @@ -1383,6 +1905,28 @@ sub print_latex { warn "invoice notes: ". $invoice_data{'notes'}. "\n" if $DEBUG; + #do variable substitution in coupon + foreach my $include (qw( coupon )) { + + my @inc_src = $conf->config_orbase("invoice_latex$include", $template); + + my $inc_tt = new Text::Template ( + TYPE => 'ARRAY', + SOURCE => [ map "$_\n", @inc_src ], + DELIMITERS => [ '[@--', '--@]' ], + ) or die "Can't create new Text::Template object: $Text::Template::ERROR"; + + unless ( $inc_tt->compile() ) { + my $error = "Can't compile $include template: $Text::Template::ERROR\n"; + warn $error. "Template:\n". join('', map "$_\n", @inc_src); + die $error; + } + + $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data ); + + $invoice_data{$include} =~ s/\n+$// + } + $invoice_data{'footer'} =~ s/\n+$//; $invoice_data{'smallfooter'} =~ s/\n+$//; $invoice_data{'notes'} =~ s/\n+$//; @@ -1406,7 +1950,7 @@ sub print_latex { !~ /^%%EndDetail\s*$/ ) { push @line_item, $line_item_line; } - foreach my $line_item ( $self->_items ) { + foreach my $line_item ( $self->_items ) { #( 'format'=>'latex' ) ) { #foreach my $line_item ( $self->_items_pkg ) { $invoice_data{'ref'} = $line_item->{'pkgnum'}; $invoice_data{'description'} = @@ -1418,7 +1962,9 @@ sub print_latex { map _latex_escape($_), @{$line_item->{'ext_description'}} ); } - $invoice_data{'amount'} = $line_item->{'amount'}; + $invoice_data{'amount'} = $line_item->{'amount'}; + $invoice_data{'unit_amount'} = $line_item->{'unit_amount'}; + $invoice_data{'quantity'} = $line_item->{'quantity'}; $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; push @filled_in, map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item; @@ -1512,7 +2058,8 @@ sub print_latex { $invoice_data{'detail_items'} = \@detail_items; $invoice_data{'total_items'} = \@total_items; - foreach my $line_item ( $self->_items ) { + my %options = ( 'format' => 'latex', 'escape_function' => \&_latex_escape ); + foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) { my $detail = { ext_description => [], }; @@ -1520,11 +2067,10 @@ sub print_latex { $detail->{'quantity'} = 1; $detail->{'description'} = _latex_escape($line_item->{'description'}); if ( exists $line_item->{'ext_description'} ) { - @{$detail->{'ext_description'}} = map { - _latex_escape($_); - } @{$line_item->{'ext_description'}}; + @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}}; } $detail->{'amount'} = $line_item->{'amount'}; + $detail->{'unit_amount'} = $line_item->{'unit_amount'}; $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; push @detail_items, $detail; @@ -1541,47 +2087,63 @@ sub print_latex { } if ( $taxtotal ) { + $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal); my $total = {}; $total->{'total_item'} = 'Sub-total'; $total->{'total_amount'} = '\dollar '. sprintf('%.2f', $self->charged - $taxtotal ); unshift @total_items, $total; + }else{ + $invoice_data{'taxtotal'} = '0.00'; } { my $total = {}; $total->{'total_item'} = '\textbf{Total}'; $total->{'total_amount'} = - '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}'; + '\textbf{\dollar '. + sprintf( '%.2f', + $self->charged + ( $conf->exists('disable_previous_balance') + ? 0 + : $pr_total + ) + ). + '}'; push @total_items, $total; } - #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments + unless ($conf->exists('disable_previous_balance')) { + #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments - # credits - foreach my $credit ( $self->_items_credits ) { - my $total; - $total->{'total_item'} = _latex_escape($credit->{'description'}); - #$credittotal - $total->{'total_amount'} = '-\dollar '. $credit->{'amount'}; - push @total_items, $total; - } + # credits + my $credittotal = 0; + foreach my $credit ( $self->_items_credits ) { + my $total; + $total->{'total_item'} = _latex_escape($credit->{'description'}); + $credittotal += $credit->{'amount'}; + $total->{'total_amount'} = '-\dollar '. $credit->{'amount'}; + push @total_items, $total; + } + $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal); - # payments - foreach my $payment ( $self->_items_payments ) { - my $total = {}; - $total->{'total_item'} = _latex_escape($payment->{'description'}); - #$paymenttotal - $total->{'total_amount'} = '-\dollar '. $payment->{'amount'}; - push @total_items, $total; - } + # payments + my $paymenttotal = 0; + foreach my $payment ( $self->_items_payments ) { + my $total = {}; + $total->{'total_item'} = _latex_escape($payment->{'description'}); + $paymenttotal += $payment->{'amount'}; + $total->{'total_amount'} = '-\dollar '. $payment->{'amount'}; + push @total_items, $total; + } + $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal); - { - my $total; - $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}'; - $total->{'total_amount'} = - '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}'; - push @total_items, $total; + { + my $total; + $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}'; + $total->{'total_amount'} = + '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}'; + push @total_items, $total; + } } } else { @@ -1623,34 +2185,9 @@ 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 $ps = generate_ps($file); + + $ps; } =item print_pdf [ TIME [ , TEMPLATE ] ] @@ -1668,43 +2205,9 @@ sub print_pdf { my $self = shift; my $file = $self->print_latex(@_); - - my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc; - chdir($dir); - - #system('pdflatex', "$file.tex"); - #system('pdflatex', "$file.tex"); - #! LaTeX Error: Unknown graphics extension: .eps. - - 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('dvipdf', "$file.dvi", "$file.pdf" ); - system( - "dvips -q -t letter -f $sfile.dvi ". - "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ". - " -c save pop -" - ) == 0 - or die "dvips | gs failed: $!"; - - open(PDF, "<$file.pdf") - or die "can't open $file.pdf: $! (error in LaTeX template?)\n"; - - unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex"); - - my $pdf = ''; - while () { - $pdf .= $_; - } - - close PDF; - - return $pdf; - + my $pdf = generate_pdf($file); + + $pdf; } =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ] @@ -1721,6 +2224,7 @@ when emailing the invoice as part of a multipart/related MIME email. =cut +#some falze laziness w/print_text and print_latex (and send_csv) sub print_html { my( $self, $today, $template, $cid ) = @_; $today ||= time; @@ -1746,10 +2250,12 @@ 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), 'agent' => encode_entities($cust_main->agent->agent), + 'agent_custid' => encode_entities($cust_main->agent_custid), 'payname' => encode_entities($cust_main->payname), 'company' => encode_entities($cust_main->company), 'address1' => encode_entities($cust_main->address1), @@ -1757,23 +2263,42 @@ sub print_html { 'city' => encode_entities($cust_main->city), 'state' => encode_entities($cust_main->state), 'zip' => encode_entities($cust_main->zip), + 'fax' => encode_entities($cust_main->fax), 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt', 'cid' => $cid, + 'template' => $template, + 'ship_enable' => $conf->exists('invoice-ship_address'), + 'unitprices' => $conf->exists('invoice-unitprice'), # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc", ); - $invoice_data{'returnaddress'} = - length( $conf->config_orbase('invoice_htmlreturnaddress', $template) ) - ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) ) - : join("\n", map { - s/~/ /g; - s/\\\\\*?\s*$/
/; - s/\\hyphenation\{[\w\s\-]+\}//; - $_; - } - $conf->config_orbase('invoice_latexreturnaddress', $template) - ); + my $prefix = $cust_main->has_ship_address ? 'ship_' : ''; + foreach ( qw( contact company address1 address2 city state zip country fax) ){ + my $method = $prefix.$_; + $invoice_data{"ship_$_"} = encode_entities($cust_main->$method); + } + + if ( + defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) ) + && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) ) + ) { + $invoice_data{'returnaddress'} = + join("\n", $conf->config_orbase('invoice_htmlreturnaddress', $template) ); + } else { + $invoice_data{'returnaddress'} = + join("\n", map { + s/~/ /g; + s/\\\\\*?\s*$/
/; + s/\\hyphenation\{[\w\s\-]+\}//; + s/\\([&])/$1/g; + $_; + } + $conf->config_orbase( 'invoice_latexreturnaddress', + $template + ) + ); + } my $countrydefault = $conf->config('countrydefault') || 'US'; if ( $cust_main->country eq $countrydefault ) { @@ -1783,20 +2308,30 @@ sub print_html { encode_entities(code2country($cust_main->country)); } - $invoice_data{'notes'} = - length($conf->config_orbase('invoice_htmlnotes', $template)) - ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) ) - : join("\n", map { - s/%%(.*)$//; - s/\\section\*\{\\textsc\{(.)(.*)\}\}/

$1<\/font>\U$2<\/b>/; - s/\\begin\{enumerate\}/

    /; - s/\\item /
  1. /; - s/\\end\{enumerate\}/<\/ol>/; - s/\\textbf\{(.*)\}/$1<\/b>/; - $_; - } - $conf->config_orbase('invoice_latexnotes', $template) - ); + if ( + defined( $conf->config_orbase('invoice_htmlnotes', $template) ) + && length( $conf->config_orbase('invoice_htmlnotes', $template) ) + ) { + $invoice_data{'notes'} = + join("\n", $conf->config_orbase('invoice_htmlnotes', $template) ); + } else { + $invoice_data{'notes'} = + join("\n", map { + s/%%(.*)$//g; + s/\\section\*\{\\textsc\{(.)(.*)\}\}/

    $1<\/font>\U$2<\/b>/g; + s/\\begin\{enumerate\}/

      /g; + s/\\item /
    1. /g; + s/\\end\{enumerate\}/<\/ol>/g; + s/\\textbf\{(.*)\}/$1<\/b>/g; + s/\\\\\*/
      /g; + s/\\dollar ?/\$/g; + s/\\#/#/g; + s/~/ /g; + $_; + } + $conf->config_orbase('invoice_latexnotes', $template) + ); + } # #do variable substitutions in notes # $invoice_data{'notes'} = @@ -1805,11 +2340,18 @@ sub print_html { # $conf->config_orbase('invoice_latexnotes', $suffix) # ); - $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter') - ? join("\n", $conf->config('invoice_htmlfooter') ) - : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/
      /; $_; } - $conf->config('invoice_latexfooter') + if ( + defined( $conf->config_orbase('invoice_htmlfooter', $template) ) + && length( $conf->config_orbase('invoice_htmlfooter', $template) ) + ) { + $invoice_data{'footer'} = + join("\n", $conf->config_orbase('invoice_htmlfooter', $template) ); + } else { + $invoice_data{'footer'} = + join("\n", map { s/~/ /g; s/\\\\\*?\s*$/
      /; $_; } + $conf->config_orbase('invoice_latexfooter', $template) ); + } $invoice_data{'po_line'} = ( $cust_main->payby eq 'BILL' && $cust_main->payinfo ) @@ -1818,16 +2360,15 @@ sub print_html { my $money_char = $conf->config('money_char') || '$'; - foreach my $line_item ( $self->_items ) { + my %options = ( 'format' => 'html', 'escape_function' => \&encode_entities ); + foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) { my $detail = { ext_description => [], }; $detail->{'ref'} = $line_item->{'pkgnum'}; $detail->{'description'} = encode_entities($line_item->{'description'}); if ( exists $line_item->{'ext_description'} ) { - @{$detail->{'ext_description'}} = map { - encode_entities($_); - } @{$line_item->{'ext_description'}}; + @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}}; } $detail->{'amount'} = $money_char. $line_item->{'amount'}; $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; @@ -1858,36 +2399,45 @@ sub print_html { my $total = {}; $total->{'total_item'} = 'Total'; $total->{'total_amount'} = - "$money_char". sprintf('%.2f', $self->charged + $pr_total ). ''; + "$money_char". + sprintf( '%.2f', + $self->charged + ( $conf->exists('disable_previous_balance') + ? 0 + : $pr_total + ) + ). + ''; push @{$invoice_data{'total_items'}}, $total; } - #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments + unless ($conf->exists('disable_previous_balance')) { + #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments - # credits - foreach my $credit ( $self->_items_credits ) { - my $total; - $total->{'total_item'} = encode_entities($credit->{'description'}); - #$credittotal - $total->{'total_amount'} = "-$money_char". $credit->{'amount'}; - push @{$invoice_data{'total_items'}}, $total; - } + # credits + foreach my $credit ( $self->_items_credits ) { + my $total; + $total->{'total_item'} = encode_entities($credit->{'description'}); + #$credittotal + $total->{'total_amount'} = "-$money_char". $credit->{'amount'}; + push @{$invoice_data{'total_items'}}, $total; + } - # payments - foreach my $payment ( $self->_items_payments ) { - my $total = {}; - $total->{'total_item'} = encode_entities($payment->{'description'}); - #$paymenttotal - $total->{'total_amount'} = "-$money_char". $payment->{'amount'}; - push @{$invoice_data{'total_items'}}, $total; - } + # payments + foreach my $payment ( $self->_items_payments ) { + my $total = {}; + $total->{'total_item'} = encode_entities($payment->{'description'}); + #$paymenttotal + $total->{'total_amount'} = "-$money_char". $payment->{'amount'}; + push @{$invoice_data{'total_items'}}, $total; + } - { - my $total; - $total->{'total_item'} = ''. $self->balance_due_msg. ''; - $total->{'total_amount'} = - "$money_char". sprintf('%.2f', $self->owed + $pr_total ). ''; - push @{$invoice_data{'total_items'}}, $total; + { + my $total; + $total->{'total_item'} = ''. $self->balance_due_msg. ''; + $total->{'total_amount'} = + "$money_char". sprintf('%.2f', $self->owed + $pr_total ). ''; + push @{$invoice_data{'total_items'}}, $total; + } } $html_template->fill_in( HASH => \%invoice_data); @@ -1923,13 +2473,49 @@ sub balance_due_msg { $msg; } +sub balance_due_date { + my $self = shift; + 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) ); + } + $duedate; +} + +=item invnum_date_pretty + +Returns a string with the invoice number and date, for example: +"Invoice #54 (3/20/2008)" + +=cut + +sub invnum_date_pretty { + my $self = shift; + 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')'; +} + +=item _date_pretty + +Returns a string with the date, for example: "3/20/2008" + +=cut + +sub _date_pretty { + my $self = shift; + time2str('%x', $self->_date); +} + sub _items { my $self = shift; - my @display = scalar(@_) - ? @_ - : qw( _items_previous _items_pkg ); - #: qw( _items_pkg ); - #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments ); + + #my @display = scalar(@_) + # ? @_ + # : qw( _items_previous _items_pkg ); + # #: qw( _items_pkg ); + # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments ); + my @display = qw( _items_previous _items_pkg ); + my @b = (); foreach my $display ( @display ) { push @b, $self->$display(@_); @@ -1982,59 +2568,94 @@ sub _items_tax { sub _items_cust_bill_pkg { my $self = shift; my $cust_bill_pkg = shift; + my %opt = @_; + + my $format = $opt{format} || ''; + my $escape_function = $opt{escape_function} || sub { shift }; my @b = (); foreach my $cust_bill_pkg ( @$cust_bill_pkg ) { - if ( $cust_bill_pkg->pkgnum > 0 ) { + my $cust_pkg = $cust_bill_pkg->cust_pkg; + + my $desc = $cust_bill_pkg->desc; + $desc = substr($desc, 0, 50). '...' + if $format eq 'latex' && length($desc) > 50; - my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } ); - my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } ); - my $pkg = $part_pkg->pkg; + my %details_opt = ( 'format' => $format, + 'escape_function' => $escape_function, + ); + + if ( $cust_bill_pkg->pkgnum > 0 ) { if ( $cust_bill_pkg->setup != 0 ) { - my $description = $pkg; + + my $description = $desc; $description .= ' Setup' if $cust_bill_pkg->recur != 0; - my @d = $cust_pkg->h_labels_short($self->_date); - push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0; + + my @d = (); + push @d, map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date) + unless $cust_pkg->part_pkg->hide_svc_detail; + + push @d, $cust_bill_pkg->details(%details_opt) + if $cust_bill_pkg->recur == 0; + push @b, { description => $description, #pkgpart => $part_pkg->pkgpart, - pkgnum => $cust_pkg->pkgnum, + pkgnum => $cust_bill_pkg->pkgnum, amount => sprintf("%.2f", $cust_bill_pkg->setup), + unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup), + quantity => $cust_bill_pkg->quantity, ext_description => \@d, }; } if ( $cust_bill_pkg->recur != 0 ) { + + my $description = $desc; + unless ( $conf->exists('disable_line_item_date_ranges') ) { + $description .= " (" . time2str("%x", $cust_bill_pkg->sdate). + " - ". time2str("%x", $cust_bill_pkg->edate). ")"; + } + + my @d = (); + + #at least until cust_bill_pkg has "past" ranges in addition to + #the "future" sdate/edate ones... see #3032 + push @d, map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date) + #$cust_bill_pkg->edate, + #$cust_bill_pkg->sdate), + unless $cust_pkg->part_pkg->hide_svc_detail + || $cust_bill_pkg->itemdesc; + + push @d, $cust_bill_pkg->details(%details_opt); + push @b, { - description => "$pkg (" . - time2str('%x', $cust_bill_pkg->sdate). ' - '. - time2str('%x', $cust_bill_pkg->edate). ')', + description => $description, #pkgpart => $part_pkg->pkgpart, - pkgnum => $cust_pkg->pkgnum, + pkgnum => $cust_bill_pkg->pkgnum, amount => sprintf("%.2f", $cust_bill_pkg->recur), - ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate, - $cust_bill_pkg->sdate), - $cust_bill_pkg->details, - ], + unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur), + quantity => $cust_bill_pkg->quantity, + ext_description => \@d, }; + } } else { #pkgnum tax or one-shot line item (??) - my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') - ? ( $cust_bill_pkg->itemdesc || 'Tax' ) - : 'Tax'; if ( $cust_bill_pkg->setup != 0 ) { push @b, { - 'description' => $itemdesc, + 'description' => $desc, 'amount' => sprintf("%.2f", $cust_bill_pkg->setup), }; } if ( $cust_bill_pkg->recur != 0 ) { push @b, { - 'description' => "$itemdesc (". + 'description' => "$desc (". time2str("%x", $cust_bill_pkg->sdate). ' - '. time2str("%x", $cust_bill_pkg->edate). ')', 'amount' => sprintf("%.2f", $cust_bill_pkg->recur), @@ -2102,15 +2723,291 @@ sub _items_payments { } + +=back + +=head1 SUBROUTINES + +=over 4 + +=item process_reprint + +=cut + +sub process_reprint { + process_re_X('print', @_); +} + +=item process_reemail + +=cut + +sub process_reemail { + process_re_X('email', @_); +} + +=item process_refax + +=cut + +sub process_refax { + process_re_X('fax', @_); +} + +=item process_reftp + +=cut + +sub process_reftp { + process_re_X('ftp', @_); +} + +=item respool + +=cut + +sub process_respool { + process_re_X('spool', @_); +} + +use Storable qw(thaw); +use Data::Dumper; +use MIME::Base64; +sub process_re_X { + my( $method, $job ) = ( shift, shift ); + warn "$me process_re_X $method for job $job\n" if $DEBUG; + + my $param = thaw(decode_base64(shift)); + warn Dumper($param) if $DEBUG; + + re_X( + $method, + $job, + %$param, + ); + +} + +sub re_X { + my($method, $job, %param ) = @_; + if ( $DEBUG ) { + warn "re_X $method for job $job with param:\n". + join( '', map { " $_ => ". $param{$_}. "\n" } keys %param ); + } + + #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(\%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, + } ); + + $method .= '_invoice' unless $method eq 'email' || $method eq 'print'; + + warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n" + if $DEBUG; + + my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo + foreach my $cust_bill ( @cust_bill ) { + $cust_bill->$method(); + + if ( $job ) { #progressbar foo + $num++; + if ( time - $min_sec > $last ) { + my $error = $job->update_statustext( + int( 100 * $num / scalar(@cust_bill) ) + ); + die $error if $error; + $last = time; + } + } + + } + +} + +=back + +=head1 CLASS METHODS + +=over 4 + +=item owed_sql + +Returns an SQL fragment to retreive the amount owed (charged minus credited and paid). + +=cut + +sub owed_sql { + my $class = shift; + 'charged - '. $class->paid_sql. ' - '. $class->credited_sql; +} + +=item net_sql + +Returns an SQL fragment to retreive the net amount (charged minus credited). + +=cut + +sub net_sql { + my $class = shift; + 'charged - '. $class->credited_sql; +} + +=item paid_sql + +Returns an SQL fragment to retreive the amount paid against this invoice. + +=cut + +sub paid_sql { + #my $class = shift; + "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay + WHERE cust_bill.invnum = cust_bill_pay.invnum )"; +} + +=item credited_sql + +Returns an SQL fragment to retreive the amount credited against this invoice. + +=cut + +sub credited_sql { + #my $class = shift; + "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill + WHERE cust_bill.invnum = cust_credit_bill.invnum )"; +} + +=item search_sql HASHREF + +Class method which returns an SQL WHERE fragment to search for parameters +specified in HASHREF. Valid parameters are + +=over 4 + +=item begin + +Epoch date (UNIX timestamp) setting a lower bound for _date values + +=item end + +Epoch date (UNIX timestamp) setting an upper bound for _date values + +=item invnum_min + +=item invnum_max + +=item agentnum + +=item owed + +=item net + +=item days + +=item newest_percust + +=back + +Note: validates all passed-in data; i.e. safe to use with unchecked CGI params. + +=cut + +sub search_sql { + my($class, $param) = @_; + if ( $DEBUG ) { + warn "$me search_sql called with params: \n". + join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n"; + } + + my @search = (); + + if ( $param->{'begin'} =~ /^(\d+)$/ ) { + push @search, "cust_bill._date >= $1"; + } + if ( $param->{'end'} =~ /^(\d+)$/ ) { + push @search, "cust_bill._date < $1"; + } + if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) { + push @search, "cust_bill.invnum >= $1"; + } + if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) { + push @search, "cust_bill.invnum <= $1"; + } + if ( $param->{'agentnum'} =~ /^(\d+)$/ ) { + push @search, "cust_main.agentnum = $1"; + } + + push @search, '0 != '. FS::cust_bill->owed_sql + if $param->{'open'}; + + push @search, '0 != '. FS::cust_bill->net_sql + if $param->{'net'}; + + push @search, "cust_bill._date < ". (time-86400*$param->{'days'}) + if $param->{'days'}; + + if ( $param->{'newest_percust'} ) { + + #$distinct = 'DISTINCT ON ( cust_bill.custnum )'; + #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC'; + + my @newest_where = map { my $x = $_; + $x =~ s/\bcust_bill\./newest_cust_bill./g; + $x; + } + grep ! /^cust_main./, @search; + my $newest_where = scalar(@newest_where) + ? ' AND '. join(' AND ', @newest_where) + : ''; + + + push @search, "cust_bill._date = ( + SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill + WHERE newest_cust_bill.custnum = cust_bill.custnum + $newest_where + )"; + + } + + my $curuser = $FS::CurrentUser::CurrentUser; + if ( $curuser->username eq 'fs_queue' + && $param->{'CurrentUser'} =~ /^(\w+)$/ ) { + my $username = $1; + my $newuser = qsearchs('access_user', { + 'username' => $username, + 'disabled' => '', + } ); + if ( $newuser ) { + $curuser = $newuser; + } else { + warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n"; + } + } + + push @search, $curuser->agentnums_sql; + + join(' AND ', @search ); + +} + =back =head1 BUGS The delete method. -print_text formatting (and some logic :/) is in source, but needs to be -slurped in from a file. Also number of lines ($=). - =head1 SEE ALSO L, L, L, L,