X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=eefcc80bc37667e46d2cdc0cfe2b43a7a7257400;hb=947c1f964f1304242f8a6ffabacccf040f1d505e;hp=990a9fc300789ed77e1d64e01d3f78af4e3211da;hpb=affa0392b9ad9ecd49543e30fa096f3be1ece764;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 990a9fc30..eefcc80bc 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -12,11 +12,13 @@ use String::ShellQuote; use HTML::Entities; use Locale::Country; use FS::UID qw( datasrc ); -use FS::Misc qw( send_email send_fax generate_ps do_print ); +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_statement; use FS::cust_bill_pkg; +use FS::cust_bill_pkg_display; use FS::cust_credit; use FS::cust_pay; use FS::cust_pkg; @@ -24,6 +26,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; @@ -80,6 +83,8 @@ owes you money. The specific charges are itemized as B records (see L). FS::cust_bill inherits from FS::Record. The following fields are currently supported: +Regular fields + =over 4 =item invnum - primary key (assigned automatically for new invoices) @@ -91,10 +96,26 @@ L and L for conversion functions. =item charged - amount of this invoice +=back + +Deprecated + +=over 4 + =item printed - deprecated +=back + +Specific use cases + +=over 4 + =item closed - books closed flag, empty or `Y' +=item statementnum - invoice aggregation (see L) + +=item agent_invid - legacy invoice number + =back =head1 METHODS @@ -139,7 +160,50 @@ Really, don't use it. sub delete { my $self = shift; return "Can't delete closed invoice" if $self->closed =~ /^Y/i; - $self->SUPER::delete(@_); + + 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; + + foreach my $table (qw( + cust_bill_event + cust_event + cust_credit_bill + cust_bill_pay + cust_bill_pay + cust_credit_bill + cust_pay_batch + cust_bill_pay_batch + cust_bill_pkg + )) { + + foreach my $linked ( $self->$table() ) { + my $error = $linked->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + } + + my $error = $self->SUPER::delete(@_); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; + } =item replace OLD_RECORD @@ -181,17 +245,16 @@ sub check { my $error = $self->ut_numbern('invnum') - || $self->ut_number('custnum') + || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' ) || $self->ut_numbern('_date') || $self->ut_money('charged') || $self->ut_numbern('printed') || $self->ut_enum('closed', [ '', 'Y' ]) + || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' ) + || $self->ut_numbern('agent_invid') #varchar? ; return $error if $error; - return "Unknown customer" - unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); - $self->_date(time) unless $self->_date; $self->printed(0) if $self->printed eq ''; @@ -199,6 +262,22 @@ sub check { $self->SUPER::check; } +=item display_invnum + +Returns the displayed invoice number for this invoice: agent_invid if +cust_bill-default_agent_invid is set and it has a value, invnum otherwise. + +=cut + +sub display_invnum { + my $self = shift; + if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){ + return $self->agent_invid; + } else { + return $self->invnum; + } +} + =item previous Returns a list consisting of the total previous balance for this customer, @@ -225,7 +304,31 @@ Returns the line items (see L) for this invoice. sub cust_bill_pkg { my $self = shift; - qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } ); + qsearch( + { 'table' => 'cust_bill_pkg', + 'hashref' => { 'invnum' => $self->invnum }, + 'order_by' => 'ORDER BY billpkgnum', + } + ); +} + +=item cust_bill_pkg_pkgnum PKGNUM + +Returns the line items (see L) for this invoice and +specified pkgnum. + +=cut + +sub cust_bill_pkg_pkgnum { + my( $self, $pkgnum ) = @_; + qsearch( + { 'table' => 'cust_bill_pkg', + 'hashref' => { 'invnum' => $self->invnum, + 'pkgnum' => $pkgnum, + }, + 'order_by' => 'ORDER BY billpkgnum', + } + ); } =item cust_pkg @@ -271,8 +374,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 @@ -281,6 +383,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 @@ -353,6 +503,16 @@ sub cust_pay { #; } +sub cust_pay_batch { + my $self = shift; + qsearch('cust_pay_batch', { 'invnum' => $self->invnum } ); +} + +sub cust_bill_pay_batch { + my $self = shift; + qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } ); +} + =item cust_bill_pay Returns all payment applications (see L) for this invoice. @@ -367,6 +527,8 @@ sub cust_bill_pay { =item cust_credited +=item cust_credit_bill + Returns all applied credits (see L) for this invoice. =cut @@ -378,6 +540,42 @@ sub cust_credited { ; } +sub cust_credit_bill { + shift->cust_credited(@_); +} + +=item cust_bill_pay_pkgnum PKGNUM + +Returns all payment applications (see L) for this invoice +with matching pkgnum. + +=cut + +sub cust_bill_pay_pkgnum { + my( $self, $pkgnum ) = @_; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum, + 'pkgnum' => $pkgnum, + } + ); +} + +=item cust_credited_pkgnum PKGNUM + +Returns all applied credits (see L) for this invoice +with matching pkgnum. + +=cut + +sub cust_credited_pkgnum { + my( $self, $pkgnum ) = @_; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum, + 'pkgnum' => $pkgnum, + } + ); +} + =item tax Returns the tax amount (see L) for this invoice. @@ -411,16 +609,59 @@ sub owed { $balance; } -=item apply_payments_and_credits +sub owed_pkgnum { + my( $self, $pkgnum ) = @_; + + #my $balance = $self->charged; + my $balance = 0; + $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum); + + $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum); + $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum); + + $balance = sprintf( "%.2f", $balance); + $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp + $balance; +} + +=item apply_payments_and_credits [ OPTION => VALUE ... ] + +Applies unapplied payments and credits to this invoice. + +A hash of optional arguments may be passed. Currently "manual" is supported. +If true, a payment receipt is sent instead of a statement when +'payment_receipt_email' configuration option is set. + +If there is an error, returns the error, otherwise returns false. =cut sub apply_payments_and_credits { - my $self = shift; + my( $self, %options ) = @_; + + 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; + if ( $conf->exists('pkg-balances') ) { + # limit @payments & @credits to those w/ a pkgnum grepped from $self + my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg; + @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments; + @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits; + } + while ( $self->owed > 0 and ( @payments || @credits ) ) { my $app = ''; @@ -458,50 +699,76 @@ sub apply_payments_and_credits { die "guru meditation #12 and 35"; } + my $unapp_amount; 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 ) ), - }; + $unapp_amount = $payment->unapplied; + $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum }; + $app->pkgnum( $payment->pkgnum ) + if $conf->exists('pkg-balances') && $payment->pkgnum; } 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 ) ), - }; + $unapp_amount = $credit->credited; + $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum }; + $app->pkgnum( $credit->pkgnum ) + if $conf->exists('pkg-balances') && $credit->pkgnum; } else { die "guru meditation #12 and 35"; } + my $owed; + if ( $conf->exists('pkg-balances') && $app->pkgnum ) { + warn "owed_pkgnum ". $app->pkgnum; + $owed = $self->owed_pkgnum($app->pkgnum); + } else { + $owed = $self->owed; + } + next unless $owed > 0; + + warn "min ( $unapp_amount, $owed )\n" if $DEBUG; + $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) ); + $app->invnum( $self->invnum ); - my $error = $app->insert; + my $error = $app->insert(%options); + 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 +=item generate_email OPTION => VALUE ... -PARAMHASH can contain the following: +Options: =over 4 -=item from => sender address, required +=item from + +sender address, required + +=item tempate + +alternate template name, optional + +=item print_text -=item tempate => alternate template name, optional +text attachment arrayref, optional -=item print_text => text attachment arrayref, optional +=item subject -=item subject => email subject, optional +email subject, optional =back @@ -523,6 +790,8 @@ sub generate_email { 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'), ); + my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') ); + if (ref($args{'to'}) eq 'ARRAY') { $return{'to'} = $args{'to'}; } else { @@ -561,7 +830,7 @@ sub generate_email { if ( ref($args{'print_text'}) eq 'ARRAY' ) { $data = $args{'print_text'}; } else { - $data = [ $self->print_text('', $args{'template'}) ]; + $data = [ $self->print_text('', $args{'template'}, %cdrs) ]; } } @@ -578,21 +847,22 @@ sub generate_email { 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; + my $logo; + my $agentnum = $self->cust_main->agentnum; if ( defined($args{'template'}) && length($args{'template'}) - && -e "$path/logo_". $args{'template'}. ".png" + && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum ) ) { - $file = "$path/logo_". $args{'template'}. ".png"; + $logo = 'logo_'. $args{'template'}. '.png'; } else { - $file = "$path/logo.png"; + $logo = "logo.png"; } + my $image_data = $conf->config_binary( $logo, $agentnum); my $image = build MIME::Entity 'Type' => 'image/png', 'Encoding' => 'base64', - 'Path' => $file, + 'Data' => $image_data, 'Filename' => 'logo.png', 'Content-ID' => "<$content_id>", ; @@ -607,7 +877,11 @@ sub generate_email { ' ', ' ', ' ', - $self->print_html('', $args{'template'}, $content_id), + $self->print_html({ time => '', + template => $args{'template'}, + cid => $content_id, + %cdrs, + }), ' ', '', ], @@ -615,6 +889,21 @@ sub generate_email { #'Filename' => 'invoice.pdf', ); + my @otherparts = (); + if ( $self->cust_main->email_csv_cdr ) { + + push @otherparts, build MIME::Entity + 'Type' => 'text/csv', + 'Encoding' => '7bit', + 'Data' => [ map { "$_\n" } + $self->call_details('prepend_billed_number' => 1) + ], + 'Disposition' => 'attachment', + 'Filename' => 'usage-'. $self->invnum. '.csv', + ; + + } + if ( $conf->exists('invoice_email_pdf') ) { #attaching pdf too: @@ -640,9 +929,9 @@ sub generate_email { $related->add_part($image); - my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}); + my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs); - $return{'mimeparts'} = [ $related, $pdf ]; + $return{'mimeparts'} = [ $related, $pdf, @otherparts ]; } else { @@ -654,7 +943,7 @@ sub generate_email { # image/png $return{'content-type'} = 'multipart/related'; - $return{'mimeparts'} = [ $alternative, $image ]; + $return{'mimeparts'} = [ $alternative, $image, @otherparts ]; $return{'type'} = 'multipart/alternative'; #Content-Type of first part... #$return{'disposition'} = 'inline'; @@ -668,7 +957,7 @@ sub generate_email { #mime parts arguments a la MIME::Entity->build(). $return{'mimeparts'} = [ - { $self->mimebuild_pdf('', $args{'template'}) } + { $self->mimebuild_pdf('', $args{'template'}, %cdrs) } ]; } @@ -688,7 +977,7 @@ sub generate_email { if ( ref($args{'print_text'}) eq 'ARRAY' ) { $return{'body'} = $args{'print_text'}; } else { - $return{'body'} = [ $self->print_text('', $args{'template'}) ]; + $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ]; } } @@ -713,7 +1002,7 @@ sub mimebuild_pdf { 'Encoding' => 'base64', 'Data' => [ $self->print_pdf(@_) ], 'Disposition' => 'attachment', - 'Filename' => 'invoice.pdf', + 'Filename' => 'invoice-'. $self->invnum. '.pdf', ); } @@ -730,6 +1019,9 @@ 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 { @@ -758,17 +1050,26 @@ sub send { my $invoice_from = scalar(@_) ? shift - : ( $self->_agent_invoice_from || $conf->config('invoice_from') ); + : ( $self->_agent_invoice_from || #XXX should go away + $conf->config('invoice_from', $self->cust_main->agentnum ) + ); + + 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 ''; @@ -800,13 +1101,17 @@ sub queueable_email { } +#sub email_invoice { sub email { my $self = shift; my $template = scalar(@_) ? shift : ''; my $invoice_from = scalar(@_) ? shift - : ( $self->_agent_invoice_from || $conf->config('invoice_from') ); + : ( $self->_agent_invoice_from || #XXX should go away + $conf->config('invoice_from', $self->cust_main->agentnum ) + ); + my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list; @@ -814,10 +1119,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, ) ); @@ -826,6 +1134,24 @@ sub email { } +sub email_subject { + my $self = shift; + + #my $template = scalar(@_) ? shift : ''; + #per-template? + + my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum) + || '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 as an arrayref. @@ -849,6 +1175,7 @@ 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 : ''; @@ -856,7 +1183,7 @@ sub print { do_print $self->lpr_data($template); } -=item fax [ TEMPLATENAME ] +=item fax_invoice [ TEMPLATENAME ] Faxes this invoice. @@ -864,7 +1191,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 : ''; @@ -881,6 +1208,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 @@ -1279,7 +1646,7 @@ sub print_csv { if ( $cust_bill_pkg->pkgnum ) { ($pkg, $setup, $recur, $sdate, $edate) = ( - $cust_bill_pkg->cust_pkg->part_pkg->pkg, + $cust_bill_pkg->part_pkg->pkg, ( $cust_bill_pkg->setup != 0 ? sprintf("%.2f", $cust_bill_pkg->setup ) : '' ), @@ -1296,11 +1663,9 @@ sub print_csv { } 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), '', '', '' ); + $pkg = $cust_bill_pkg->desc; + $setup = sprintf('%10.2f', $cust_bill_pkg->setup ); + ( $sdate, $edate ) = ( '', '' ); } $csv->combine( @@ -1400,7 +1765,7 @@ sub realtime_bop { $cust_main->agentnum. ")"; my $agent = $agent_obj->agent; my $pkgs = join(', ', - map { $_->cust_pkg->part_pkg->pkg } + map { $_->part_pkg->pkg } grep { $_->pkgnum } $self->cust_bill_pkg ); $description = eval qq("$dtempl"); @@ -1451,586 +1816,893 @@ L and L for conversion functions. =cut -#still some false laziness w/_items stuff (and send_csv) sub print_text { + my( $self, $today, $template, %opt ) = @_; - my( $self, $today, $template ) = @_; - $today ||= time; + my %params = ( 'format' => 'template' ); + $params{'time'} = $today if $today; + $params{'template'} = $template if $template; + $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'}; -# my $invnum = $self->invnum; - my $cust_main = $self->cust_main; - $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') ) - unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/; + $self->print_generic( %params ); +} - my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance -# my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits - #my $balance_due = $self->owed + $pr_total - $cr_total; - my $balance_due = $self->owed + $pr_total; +=item print_latex [ TIME [ , TEMPLATE ] ] - #my @collect = (); - #my($description,$amount); - @buf = (); +Internal method - returns a filename of a filled-in LaTeX template for this +invoice (Note: add ".tex" to get the actual filename), and a filename of +an associated logo (with the .eps extension included). - #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,['','']; - } +See print_ps and print_pdf for methods that return PostScript and PDF output. - #new charges - foreach my $cust_bill_pkg ( - ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first - ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes - ) { +TIME an optional value used to control the printing of overdue messages. The +default is now. It isn't the date of the invoice; that's the `_date' field. +It is specified as a UNIX timestamp; see L. Also see +L and L for conversion functions. - my $desc = $cust_bill_pkg->desc; +=cut - if ( $cust_bill_pkg->pkgnum > 0 ) { +sub print_latex { + my( $self, $today, $template, %opt ) = @_; - if ( $cust_bill_pkg->setup != 0 ) { - 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_bill_pkg->cust_pkg->h_labels($self->_date); - } + my %params = ( 'format' => 'latex' ); + $params{'time'} = $today if $today; + $params{'template'} = $template if $template; + $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'}; - if ( $cust_bill_pkg->recur != 0 ) { - push @buf, [ - $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_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate, - $cust_bill_pkg->sdate ); - } + $template ||= $self->_agent_template; - push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details; + 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"; - } else { #pkgnum tax or one-shot line item + my $agentnum = $self->cust_main->agentnum; - if ( $cust_bill_pkg->setup != 0 ) { - push @buf, [ $desc, - $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ]; - } - if ( $cust_bill_pkg->recur != 0 ) { - push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - " - . time2str("%x", $cust_bill_pkg->edate). ")", - $money_char. sprintf("%10.2f", $cust_bill_pkg->recur) - ]; - } + if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) { + print $lh $conf->config_binary("logo_${template}.eps", $agentnum) + or die "can't write temp file: $!\n"; + } else { + print $lh $conf->config_binary('logo.eps', $agentnum) + or die "can't write temp file: $!\n"; + } + close $lh; + $params{'logo_file'} = $lh->filename; - } + my @filled_in = $self->print_generic( %params ); + + my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX', + DIR => $dir, + SUFFIX => '.tex', + UNLINK => 0, + ) or die "can't open temp file: $!\n"; + print $fh join('', @filled_in ); + close $fh; - } + $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename; + return ($1, $params{'logo_file'}); - push @buf,['','-----------']; - push @buf,['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,['','']; +=item print_generic OPTIONS_HASH - #credits - foreach ( $self->cust_credited ) { +Internal method - returns a filled-in template for this invoice as a scalar. - #something more elaborate if $_->amount ne $_->cust_credit->credited ? +See print_ps and print_pdf for methods that return PostScript and PDF output. - 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) - # ]; - #} +Non optional options include + format - latex, html, template - #get & print payments - foreach ( $self->cust_bill_pay ) { +Optional options include - #something more elaborate if $_->amount ne ->cust_pay->paid ? +template - a value used as a suffix for a configuration template - push @buf,[ - "Payment received ". time2str("%x",$_->cust_pay->_date ), - $money_char. sprintf("%10.2f",$_->amount ) - ]; - } +time - a value used to control the printing of overdue messages. The +default is now. It isn't the date of the invoice; that's the `_date' field. +It is specified as a UNIX timestamp; see L. Also see +L and L for conversion functions. - #balance due - my $balance_due_msg = $self->balance_due_msg; +cid - - push @buf,['','-----------']; - push @buf,[$balance_due_msg, $money_char. - sprintf("%10.2f", $balance_due ) ]; +unsquelch_cdr - overrides any per customer cdr squelching when true - #create the template - $template ||= $self->_agent_template; - my $templatefile = 'invoice_template'; - $templatefile .= "_$template" if length($template); - my @invoice_template = $conf->config($templatefile) - or die "cannot load config file $templatefile"; - $invoice_lines = 0; - my $wasfunc = 0; - foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy - /invoice_lines\((\d*)\)/; - $invoice_lines += $1 || scalar(@buf); - $wasfunc=1; - } - die "no invoice_lines() functions in template?" unless $wasfunc; - my $invoice_template = new Text::Template ( - TYPE => 'ARRAY', - SOURCE => [ map "$_\n", @invoice_template ], - ) or die "can't create new Text::Template object: $Text::Template::ERROR"; - $invoice_template->compile() - or die "can't compile template: $Text::Template::ERROR"; - - #setup template variables - package FS::cust_bill::_template; #! - use vars qw( $custnum $invnum $date $agent @address $overdue - $page $total_pages @buf ); - - $custnum = $self->custnum; - $invnum = $self->invnum; - $date = $self->_date; - $agent = $self->cust_main->agent->agent; - $page = 1; - - if ( $FS::cust_bill::invoice_lines ) { - $total_pages = - int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines ); - $total_pages++ - if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines; - } else { - $total_pages = 1; - } +=cut - #format address (variable for the template) - my $l = 0; - @address = ( '', '', '', '', '', '' ); - package FS::cust_bill; #! - $FS::cust_bill::_template::address[$l++] = - $cust_main->payname. - ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo - ? " (P.O. #". $cust_main->payinfo. ")" - : '' - ) - ; - $FS::cust_bill::_template::address[$l++] = $cust_main->company - if $cust_main->company; - $FS::cust_bill::_template::address[$l++] = $cust_main->address1; - $FS::cust_bill::_template::address[$l++] = $cust_main->address2 - if $cust_main->address2; - $FS::cust_bill::_template::address[$l++] = - $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip; +#what's with all the sprintf('%10.2f')'s in here? will it cause any +# (alignment?) problems to change them all to '%.2f' ? +sub print_generic { - my $countrydefault = $conf->config('countrydefault') || 'US'; - $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country) - unless $cust_main->country eq $countrydefault; - - # #overdue? (variable for the template) - # $FS::cust_bill::_template::overdue = ( - # $balance_due > 0 - # && $today > $self->_date - ## && $self->printed > 1 - # && $self->printed > 0 - # ); - - #and subroutine for the template - sub FS::cust_bill::_template::invoice_lines { - my $lines = shift || scalar(@buf); - map { - scalar(@buf) ? shift @buf : [ '', '' ]; - } - ( 1 .. $lines ); - } - - #and fill it in - $FS::cust_bill::_template::page = 1; - my $lines; - my @collect; - while (@buf) { - push @collect, split("\n", - $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' ) - ); - $FS::cust_bill::_template::page++; - } - - map "$_\n", @collect; - -} - -=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), 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. - -TIME an optional value used to control the printing of overdue messages. The -default is now. It isn't the date of the invoice; that's the `_date' field. -It is specified as a UNIX timestamp; see L. Also see -L and L for conversion functions. - -=cut - -#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 ) = @_; - $today ||= time; - warn "FS::cust_bill::print_latex called on $self with suffix $template\n" + my( $self, %params ) = @_; + my $today = $params{today} ? $params{today} : time; + warn "$me print_generic called on $self with suffix $params{template}\n" if $DEBUG; + my $format = $params{format}; + die "Unknown format: $format" + unless $format =~ /^(latex|html|template)$/; + my $cust_main = $self->cust_main; $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') ) - unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/; + unless $cust_main->payname + && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/; - my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance -# my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits - #my $balance_due = $self->owed + $pr_total - $cr_total; - my $balance_due = $self->owed + $pr_total; + my %delimiters = ( 'latex' => [ '[@--', '--@]' ], + 'html' => [ '<%=', '%>' ], + 'template' => [ '{', '}' ], + ); #create the template - $template ||= $self->_agent_template; - my $templatefile = 'invoice_latex'; - my $suffix = length($template) ? "_$template" : ''; - $templatefile .= $suffix; + my $template = $params{template} ? $params{template} : $self->_agent_template; + my $templatefile = "invoice_$format"; + $templatefile .= "_$template" + if length($template); my @invoice_template = map "$_\n", $conf->config($templatefile) - or die "cannot load config file $templatefile"; + or die "cannot load config data $templatefile"; - my($format, $text_template); - if ( grep { /^%%Detail/ } @invoice_template ) { + my $old_latex = ''; + if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) { #change this to a die when the old code is removed warn "old-style invoice template $templatefile; ". "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n"; - $format = 'old'; - } else { - $format = 'Text::Template'; - $text_template = new Text::Template( - TYPE => 'ARRAY', - SOURCE => \@invoice_template, - DELIMITERS => [ '[@--', '--@]' ], - ); + $old_latex = 'true'; + @invoice_template = _translate_old_latex_format(@invoice_template); + } + + my $text_template = new Text::Template( + TYPE => 'ARRAY', + SOURCE => \@invoice_template, + DELIMITERS => $delimiters{$format}, + ); + + $text_template->compile() + or die "Can't compile $templatefile: $Text::Template::ERROR\n"; + + + # additional substitution could possibly cause breakage in existing templates + my %convert_maps = ( + 'latex' => { + 'notes' => sub { map "$_", @_ }, + 'footer' => sub { map "$_", @_ }, + 'smallfooter' => sub { map "$_", @_ }, + 'returnaddress' => sub { map "$_", @_ }, + 'coupon' => sub { map "$_", @_ }, + 'summary' => sub { map "$_", @_ }, + }, + 'html' => { + 'notes' => + sub { + 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; + $_; + } @_ + }, + 'footer' => + sub { map { s/~/ /g; s/\\\\\*?\s*$/
    /; $_; } @_ }, + 'smallfooter' => + sub { map { s/~/ /g; s/\\\\\*?\s*$/
    /; $_; } @_ }, + 'returnaddress' => + sub { + map { + s/~/ /g; + s/\\\\\*?\s*$/
    /; + s/\\hyphenation\{[\w\s\-]+}//; + s/\\([&])/$1/g; + $_; + } @_ + }, + 'coupon' => sub { "" }, + 'summary' => sub { "" }, + }, + 'template' => { + 'notes' => + sub { + map { + s/%%.*$//g; + s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g; + s/\\begin\{enumerate\}//g; + s/\\item / * /g; + s/\\end\{enumerate\}//g; + s/\\textbf\{(.*)\}/$1/g; + s/\\\\\*/ /; + s/\\dollar ?/\$/g; + $_; + } @_ + }, + 'footer' => + sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ }, + 'smallfooter' => + sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ }, + 'returnaddress' => + sub { + map { + s/~/ /g; + s/\\\\\*?\s*$/\n/; # dubious + s/\\hyphenation\{[\w\s\-]+}//; + $_; + } @_ + }, + 'coupon' => sub { "" }, + 'summary' => sub { "" }, + }, + ); - $text_template->compile() - or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR; - } + # hashes for differing output formats + my %nbsps = ( 'latex' => '~', + 'html' => '', # '&nbps;' would be nice + 'template' => '', # not used + ); + my $nbsp = $nbsps{$format}; + + my %escape_functions = ( 'latex' => \&_latex_escape, + 'html' => \&encode_entities, + 'template' => sub { shift }, + ); + my $escape_function = $escape_functions{$format}; + + my %date_formats = ( 'latex' => '%b %o, %Y', + 'html' => '%b %o, %Y', + 'template' => '%s', + ); + my $date_format = $date_formats{$format}; + + my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}' + }, + 'html' => sub { return ''. shift(). '' + }, + 'template' => sub { shift }, + ); + my $embolden_function = $embolden_functions{$format}; + + + # generate template variables my $returnaddress; - if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) { + if ( + defined( $conf->config_orbase( "invoice_${format}returnaddress", + $template + ) + ) + && length( $conf->config_orbase( "invoice_${format}returnaddress", + $template + ) + ) + ) { + $returnaddress = join("\n", - $conf->config_orbase('invoice_latexreturnaddress', $template) + $conf->config_orbase("invoice_${format}returnaddress", $template) ); + + } elsif ( grep /\S/, + $conf->config_orbase('invoice_latexreturnaddress', $template) ) { + + my $convert_map = $convert_maps{$format}{'returnaddress'}; + $returnaddress = + join( "\n", + &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress", + $template + ) + ) + ); + } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) { + + my $convert_map = $convert_maps{$format}{'returnaddress'}; + $returnaddress = join( "\n", &$convert_map( + map { s/( {2,})/'~' x length($1)/eg; + s/$/\\\\\*/; + $_ + } + ( $conf->config('company_name', $self->cust_main->agentnum), + $conf->config('company_address', $self->cust_main->agentnum), + ) + ) + ); + } else { - $returnaddress = '~'; + + my $warning = "Couldn't find a return address; ". + "do you need to set the company_address configuration value?"; + warn "$warning\n"; + $returnaddress = $nbsp; + #$returnaddress = $warning; + } 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), - '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), - 'zip' => _latex_escape($cust_main->zip), - 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ), - 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ), - 'returnaddress' => $returnaddress, - 'quantity' => 1, - 'terms' => $self->terms, - #'notes' => join("\n", $conf->config('invoice_latexnotes') ), + 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ), + 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n", + 'custnum' => $cust_main->display_custnum, + 'invnum' => $self->invnum, + 'date' => time2str($date_format, $self->_date), + 'today' => time2str('%b %o, %Y', $today), + 'agent' => &$escape_function($cust_main->agent->agent), + 'agent_custid' => &$escape_function($cust_main->agent_custid), + 'payname' => &$escape_function($cust_main->payname), + 'company' => &$escape_function($cust_main->company), + 'address1' => &$escape_function($cust_main->address1), + 'address2' => &$escape_function($cust_main->address2), + 'city' => &$escape_function($cust_main->city), + 'state' => &$escape_function($cust_main->state), + 'zip' => &$escape_function($cust_main->zip), + 'fax' => &$escape_function($cust_main->fax), + 'returnaddress' => $returnaddress, + #'quantity' => 1, + 'terms' => $self->terms, + 'template' => $template, #params{'template'}, + #'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", + 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc", + 'page' => 1, + 'total_pages' => 1, + 'current_charges' => sprintf("%.2f", $self->charged), + 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format? + 'ship_enable' => $conf->exists('invoice-ship_address'), + 'unitprices' => $conf->exists('invoice-unitprice'), ); + $invoice_data{finance_section} = ''; + if ( $conf->config('finance_pkgclass') ) { + my $pkg_class = + qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') }); + $invoice_data{finance_section} = $pkg_class->categoryname; + } + $invoice_data{finance_amount} = '0.00'; + 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 ); + + $invoice_data{'cid'} = $params{'cid'} + if $params{'cid'}; + if ( $cust_main->country eq $countrydefault ) { $invoice_data{'country'} = ''; } else { - $invoice_data{'country'} = _latex_escape(code2country($cust_main->country)); + $invoice_data{'country'} = &$escape_function(code2country($cust_main->country)); } - $invoice_data{'notes'} = - join("\n", -# #do variable substitutions in notes -# map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } - $conf->config_orbase('invoice_latexnotes', $template) - ); - warn "invoice notes: ". $invoice_data{'notes'}. "\n" - if $DEBUG; + my @address = (); + $invoice_data{'address'} = \@address; + push @address, + $cust_main->payname. + ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo + ? " (P.O. #". $cust_main->payinfo. ")" + : '' + ) + ; + push @address, $cust_main->company + if $cust_main->company; + push @address, $cust_main->address1; + push @address, $cust_main->address2 + if $cust_main->address2; + push @address, + $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip; + push @address, $invoice_data{'country'} + if $invoice_data{'country'}; + push @address, '' + while (scalar(@address) < 5); - $invoice_data{'footer'} =~ s/\n+$//; - $invoice_data{'smallfooter'} =~ s/\n+$//; - $invoice_data{'notes'} =~ s/\n+$//; + $invoice_data{'logo_file'} = $params{'logo_file'} + if $params{'logo_file'}; - $invoice_data{'po_line'} = - ( $cust_main->payby eq 'BILL' && $cust_main->payinfo ) - ? _latex_escape("Purchase Order #". $cust_main->payinfo) - : '~'; + my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance +# my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits + #my $balance_due = $self->owed + $pr_total - $cr_total; + my $balance_due = $self->owed + $pr_total; + $invoice_data{'true_previous_balance'} = sprintf("%.2f", $self->previous_balance); + $invoice_data{'balance_adjustments'} = sprintf("%.2f", $self->previous_balance - $self->billing_balance); + $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total); + $invoice_data{'balance'} = sprintf("%.2f", $balance_due); - my @filled_in = (); - if ( $format eq 'old' ) { - - my @line_item = (); - my @total_item = (); - while ( @invoice_template ) { - my $line = shift @invoice_template; - - if ( $line =~ /^%%Detail\s*$/ ) { - - while ( ( my $line_item_line = shift @invoice_template ) - !~ /^%%EndDetail\s*$/ ) { - push @line_item, $line_item_line; - } - foreach my $line_item ( $self->_items ) { - #foreach my $line_item ( $self->_items_pkg ) { - $invoice_data{'ref'} = $line_item->{'pkgnum'}; - $invoice_data{'description'} = - _latex_escape($line_item->{'description'}); - if ( exists $line_item->{'ext_description'} ) { - $invoice_data{'description'} .= - "\\tabularnewline\n~~". - join( "\\tabularnewline\n~~", - map _latex_escape($_), @{$line_item->{'ext_description'}} - ); - } - $invoice_data{'amount'} = $line_item->{'amount'}; - $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; - } - - } elsif ( $line =~ /^%%TotalDetails\s*$/ ) { - - while ( ( my $total_item_line = shift @invoice_template ) - !~ /^%%EndTotalDetails\s*$/ ) { - push @total_item, $total_item_line; - } - - my @total_fill = (); - - my $taxtotal = 0; - foreach my $tax ( $self->_items_tax ) { - $invoice_data{'total_item'} = _latex_escape($tax->{'description'}); - $taxtotal += $tax->{'amount'}; - $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'}; - push @total_fill, - map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } - @total_item; - } + my $agentnum = $self->cust_main->agentnum; + + my $summarypage = ''; + if ( $conf->exists('invoice_usesummary', $agentnum) ) { + $summarypage = 1; + } + $invoice_data{'summarypage'} = $summarypage; + + #do variable substitution in notes, footer, smallfooter + foreach my $include (qw( notes footer smallfooter coupon )) { + + my $inc_file = $conf->key_orbase("invoice_${format}$include", $template); + my @inc_src; + + if ( $conf->exists($inc_file, $agentnum) + && length( $conf->config($inc_file, $agentnum) ) ) { + + @inc_src = $conf->config($inc_file, $agentnum); + + } else { + + $inc_file = $conf->key_orbase("invoice_latex$include", $template); + + my $convert_map = $convert_maps{$format}{$include}; + + @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g; + s/--\@\]/$delimiters{$format}[1]/g; + $_; + } + &$convert_map( $conf->config($inc_file, $agentnum) ); - if ( $taxtotal ) { - $invoice_data{'total_item'} = 'Sub-total'; - $invoice_data{'total_amount'} = - '\dollar '. sprintf('%.2f', $self->charged - $taxtotal ); - unshift @total_fill, - map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } - @total_item; - } - - $invoice_data{'total_item'} = '\textbf{Total}'; - $invoice_data{'total_amount'} = - '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}'; - push @total_fill, - map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } - @total_item; - - #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments - - # credits - foreach my $credit ( $self->_items_credits ) { - $invoice_data{'total_item'} = _latex_escape($credit->{'description'}); - #$credittotal - $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'}; - push @total_fill, - map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } - @total_item; - } - - # payments - foreach my $payment ( $self->_items_payments ) { - $invoice_data{'total_item'} = _latex_escape($payment->{'description'}); - #$paymenttotal - $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'}; - push @total_fill, - map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } - @total_item; - } - - $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}'; - $invoice_data{'total_amount'} = - '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}'; - push @total_fill, - map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } - @total_item; - - push @filled_in, @total_fill; - - } else { - #$line =~ s/\$(\w+)/$invoice_data{$1}/eg; - $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg; - push @filled_in, $line; - } - } - sub nounder { - my $var = $1; - $var =~ s/_/\-/g; - $var; + my $inc_tt = new Text::Template ( + TYPE => 'ARRAY', + SOURCE => [ map "$_\n", @inc_src ], + DELIMITERS => $delimiters{$format}, + ) or die "Can't create new Text::Template object: $Text::Template::ERROR"; + + unless ( $inc_tt->compile() ) { + my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n"; + warn $error. "Template:\n". join('', map "$_\n", @inc_src); + die $error; } - } elsif ( $format eq 'Text::Template' ) { + $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data ); - my @detail_items = (); - my @total_items = (); + $invoice_data{$include} =~ s/\n+$// + if ($format eq 'latex'); + } + + $invoice_data{'po_line'} = + ( $cust_main->payby eq 'BILL' && $cust_main->payinfo ) + ? &$escape_function("Purchase Order #". $cust_main->payinfo) + : $nbsp; + + my %money_chars = ( 'latex' => '', + 'html' => $conf->config('money_char') || '$', + 'template' => '', + ); + my $money_char = $money_chars{$format}; + + my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too + 'html' => $conf->config('money_char') || '$', + 'template' => '', + ); + my $other_money_char = $other_money_chars{$format}; + $invoice_data{'dollar'} = $other_money_char; + + my @detail_items = (); + my @total_items = (); + my @buf = (); + my @sections = (); + + $invoice_data{'detail_items'} = \@detail_items; + $invoice_data{'total_items'} = \@total_items; + $invoice_data{'buf'} = \@buf; + $invoice_data{'sections'} = \@sections; + + my $previous_section = { 'description' => 'Previous Charges', + 'subtotal' => $other_money_char. + sprintf('%.2f', $pr_total), + 'summarized' => $summarypage ? 'Y' : '', + }; + + my $taxtotal = 0; + my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees', + 'subtotal' => $taxtotal, # adjusted below + 'summarized' => $summarypage ? 'Y' : '', + }; + + my $adjusttotal = 0; + my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments', + 'subtotal' => 0, # adjusted below + 'summarized' => $summarypage ? 'Y' : '', + }; + + my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y'; + my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum); + my $late_sections = []; + if ( $multisection ) { + push @sections, + $self->_items_sections( $late_sections, $summarypage, $escape_function ); + }else{ + push @sections, { 'description' => '', 'subtotal' => '' }; + } + + unless ( $conf->exists('disable_previous_balance') + || $conf->exists('previous_balance-summary_only') + ) + { + + foreach my $line_item ( $self->_items_previous ) { - $invoice_data{'detail_items'} = \@detail_items; - $invoice_data{'total_items'} = \@total_items; - - foreach my $line_item ( $self->_items ) { my $detail = { ext_description => [], }; $detail->{'ref'} = $line_item->{'pkgnum'}; $detail->{'quantity'} = 1; - $detail->{'description'} = _latex_escape($line_item->{'description'}); + $detail->{'section'} = $previous_section; + $detail->{'description'} = &$escape_function($line_item->{'description'}); if ( exists $line_item->{'ext_description'} ) { @{$detail->{'ext_description'}} = map { - _latex_escape($_); + &$escape_function($_); } @{$line_item->{'ext_description'}}; } - $detail->{'amount'} = $line_item->{'amount'}; + $detail->{'amount'} = ( $old_latex ? '' : $money_char). + $line_item->{'amount'}; + $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; + + push @detail_items, $detail; + push @buf, [ $detail->{'description'}, + $money_char. sprintf("%10.2f", $line_item->{'amount'}), + ]; + } + + } + + if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) { + push @buf, ['','-----------']; + push @buf, [ 'Total Previous Balance', + $money_char. sprintf("%10.2f", $pr_total) ]; + push @buf, ['','']; + } + + foreach my $section (@sections, @$late_sections) { + + $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} ) + if ( $invoice_data{finance_section} && + $section->{'description'} eq $invoice_data{finance_section} ); + + $section->{'subtotal'} = $other_money_char. + sprintf('%.2f', $section->{'subtotal'}) + if $multisection; + + if ( $section->{'description'} ) { + push @buf, ( [ &$escape_function($section->{'description'}), '' ], + [ '', '' ], + ); + } + + my %options = (); + $options{'section'} = $section if $multisection; + $options{'format'} = $format; + $options{'escape_function'} = $escape_function; + $options{'format_function'} = sub { () } unless $unsquelched; + $options{'unsquelched'} = $unsquelched; + $options{'summary_page'} = $summarypage; + + foreach my $line_item ( $self->_items_pkg(%options) ) { + my $detail = { + ext_description => [], + }; + $detail->{'ref'} = $line_item->{'pkgnum'}; + $detail->{'quantity'} = $line_item->{'quantity'}; + $detail->{'section'} = $section; + $detail->{'description'} = &$escape_function($line_item->{'description'}); + if ( exists $line_item->{'ext_description'} ) { + @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}}; + } + $detail->{'amount'} = ( $old_latex ? '' : $money_char ). + $line_item->{'amount'}; + $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ). + $line_item->{'unit_amount'}; $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; push @detail_items, $detail; + push @buf, ( [ $detail->{'description'}, + $money_char. sprintf("%10.2f", $line_item->{'amount'}), + ], + map { [ " ". $_, '' ] } @{$detail->{'ext_description'}}, + ); + } + + if ( $section->{'description'} ) { + push @buf, ( ['','-----------'], + [ $section->{'description'}. ' sub-total', + $money_char. sprintf("%10.2f", $section->{'subtotal'}) + ], + [ '', '' ], + [ '', '' ], + ); } + } - my $taxtotal = 0; - foreach my $tax ( $self->_items_tax ) { - my $total = {}; - $total->{'total_item'} = _latex_escape($tax->{'description'}); - $taxtotal += $tax->{'amount'}; - $total->{'total_amount'} = '\dollar '. $tax->{'amount'}; - push @total_items, $total; + $invoice_data{current_less_finance} = + sprintf('%.2f', $self->charged - $invoice_data{finance_amount} ); + + if ( $multisection && !$conf->exists('disable_previous_balance') ) { + unshift @sections, $previous_section if $pr_total; + } + + foreach my $tax ( $self->_items_tax ) { + + $taxtotal += $tax->{'amount'}; + + my $description = &$escape_function( $tax->{'description'} ); + my $amount = sprintf( '%.2f', $tax->{'amount'} ); + + if ( $multisection ) { + + my $money = $old_latex ? '' : $money_char; + push @detail_items, { + ext_description => [], + ref => '', + quantity => '', + description => $description, + amount => $money. $amount, + product_code => '', + section => $tax_section, + }; + + } else { + + push @total_items, { + 'total_item' => $description, + 'total_amount' => $other_money_char. $amount, + }; + } + + push @buf,[ $description, + $money_char. $amount, + ]; + + } - if ( $taxtotal ) { - my $total = {}; - $total->{'total_item'} = 'Sub-total'; - $total->{'total_amount'} = - '\dollar '. sprintf('%.2f', $self->charged - $taxtotal ); + if ( $taxtotal ) { + my $total = {}; + $total->{'total_item'} = 'Sub-total'; + $total->{'total_amount'} = + $other_money_char. sprintf('%.2f', $self->charged - $taxtotal ); + + if ( $multisection ) { + $tax_section->{'subtotal'} = $other_money_char. + sprintf('%.2f', $taxtotal); + $tax_section->{'pretotal'} = 'New charges sub-total '. + $total->{'total_amount'}; + push @sections, $tax_section if $taxtotal; + }else{ unshift @total_items, $total; } - - { - my $total = {}; - $total->{'total_item'} = '\textbf{Total}'; - $total->{'total_amount'} = - '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}'; + } + $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal); + + push @buf,['','-----------']; + push @buf,[( $conf->exists('disable_previous_balance') + ? 'Total Charges' + : 'Total New Charges' + ), + $money_char. sprintf("%10.2f",$self->charged) ]; + push @buf,['','']; + + { + my $total = {}; + $total->{'total_item'} = &$embolden_function('Total'); + $total->{'total_amount'} = + &$embolden_function( + $other_money_char. + sprintf( '%.2f', + $self->charged + ( $conf->exists('disable_previous_balance') + ? 0 + : $pr_total + ) + ) + ); + if ( $multisection ) { + $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char. + sprintf('%.2f', $self->charged ); + }else{ push @total_items, $total; } + push @buf,['','-----------']; + push @buf,['Total Charges', + $money_char. + sprintf( '%10.2f', $self->charged + + ( $conf->exists('disable_previous_balance') + ? 0 + : $pr_total + ) + ) + ]; + push @buf,['','']; + } + 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 $credittotal = 0; + foreach my $credit ( $self->_items_credits('trim_len'=>60) ) { + my $total; - $total->{'total_item'} = _latex_escape($credit->{'description'}); - #$credittotal - $total->{'total_amount'} = '-\dollar '. $credit->{'amount'}; - push @total_items, $total; + $total->{'total_item'} = &$escape_function($credit->{'description'}); + $credittotal += $credit->{'amount'}; + $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'}; + $adjusttotal += $credit->{'amount'}; + if ( $multisection ) { + my $money = $old_latex ? '' : $money_char; + push @detail_items, { + ext_description => [], + ref => '', + quantity => '', + description => &$escape_function($credit->{'description'}), + amount => $money. $credit->{'amount'}, + product_code => '', + section => $adjust_section, + }; + } else { + push @total_items, $total; + } + } - + $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal); + + #credits (again) + foreach my $credit ( $self->_items_credits('trim_len'=>32) ) { + push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ]; + } + # payments + my $paymenttotal = 0; 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; + $total->{'total_item'} = &$escape_function($payment->{'description'}); + $paymenttotal += $payment->{'amount'}; + $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'}; + $adjusttotal += $payment->{'amount'}; + if ( $multisection ) { + my $money = $old_latex ? '' : $money_char; + push @detail_items, { + ext_description => [], + ref => '', + quantity => '', + description => &$escape_function($payment->{'description'}), + amount => $money. $payment->{'amount'}, + product_code => '', + section => $adjust_section, + }; + }else{ + push @total_items, $total; + } + push @buf, [ $payment->{'description'}, + $money_char. sprintf("%10.2f", $payment->{'amount'}), + ]; } + $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal); + if ( $multisection ) { + $adjust_section->{'subtotal'} = $other_money_char. + sprintf('%.2f', $adjusttotal); + push @sections, $adjust_section; + } + { my $total; - $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}'; + $total->{'total_item'} = &$embolden_function($self->balance_due_msg); $total->{'total_amount'} = - '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}'; - push @total_items, $total; + &$embolden_function( + $other_money_char. sprintf('%.2f', $summarypage + ? $self->charged + + $self->billing_balance + : $self->owed + $pr_total + ) + ); + if ( $multisection ) { + $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '. + $total->{'total_amount'}; + }else{ + push @total_items, $total; + } + push @buf,['','-----------']; + push @buf,[$self->balance_due_msg, $money_char. + sprintf("%10.2f", $balance_due ) ]; } + } - } else { - die "guru meditation #54"; + if ( $multisection ) { + push @sections, @$late_sections + if $unsquelched; } - 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"; + my @includelist = (); + push @includelist, 'summary' if $summarypage; + foreach my $include ( @includelist ) { - 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"; + my $inc_file = $conf->key_orbase("invoice_${format}$include", $template); + my @inc_src; + + if ( length( $conf->config($inc_file, $agentnum) ) ) { + + @inc_src = $conf->config($inc_file, $agentnum); + + } else { + + $inc_file = $conf->key_orbase("invoice_latex$include", $template); + + my $convert_map = $convert_maps{$format}{$include}; + + @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g; + s/--\@\]/$delimiters{$format}[1]/g; + $_; + } + &$convert_map( $conf->config($inc_file, $agentnum) ); + + } + + my $inc_tt = new Text::Template ( + TYPE => 'ARRAY', + SOURCE => [ map "$_\n", @inc_src ], + DELIMITERS => $delimiters{$format}, + ) or die "Can't create new Text::Template object: $Text::Template::ERROR"; + + unless ( $inc_tt->compile() ) { + my $error = "Can't compile $inc_file 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+$// + if ($format eq 'latex'); } - close $lh; - $invoice_data{'logo_file'} = $lh->filename; - my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX', - DIR => $dir, - SUFFIX => '.tex', - UNLINK => 0, - ) or die "can't open temp file: $!\n"; - if ( $format eq 'old' ) { - print $fh join('', @filled_in ); - } elsif ( $format eq 'Text::Template' ) { - $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data); - } else { - die "guru meditation #32"; + $invoice_lines = 0; + my $wasfunc = 0; + foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy + /invoice_lines\((\d*)\)/; + $invoice_lines += $1 || scalar(@buf); + $wasfunc=1; } - close $fh; + die "no invoice_lines() functions in template?" + if ( $format eq 'template' && !$wasfunc ); - $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename; - return ($1, $invoice_data{'logo_file'}); + if ($format eq 'template') { + + if ( $invoice_lines ) { + $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines ); + $invoice_data{'total_pages'}++ + if scalar(@buf) % $invoice_lines; + } + + #setup subroutine for the template + sub FS::cust_bill::_template::invoice_lines { + my $lines = shift || scalar(@FS::cust_bill::_template::buf); + map { + scalar(@FS::cust_bill::_template::buf) + ? shift @FS::cust_bill::_template::buf + : [ '', '' ]; + } + ( 1 .. $lines ); + } + + my $lines; + my @collect; + while (@buf) { + push @collect, split("\n", + $text_template->fill_in( HASH => \%invoice_data, + PACKAGE => 'FS::cust_bill::_template' + ) + ); + $FS::cust_bill::_template::page++; + } + map "$_\n", @collect; + }else{ + warn "filling in template for invoice ". $self->invnum. "\n" + if $DEBUG; + warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n" + if $DEBUG > 1; + $text_template->fill_in(HASH => \%invoice_data); + } } =item print_ps [ TIME [ , TEMPLATE ] ] @@ -2050,8 +2722,8 @@ sub print_ps { my ($file, $lfile) = $self->print_latex(@_); my $ps = generate_ps($file); unlink($lfile); - $ps; + $ps; } =item print_pdf [ TIME [ , TEMPLATE ] ] @@ -2069,254 +2741,40 @@ sub print_pdf { my $self = shift; my ($file, $lfile) = $self->print_latex(@_); + my $pdf = generate_pdf($file); + unlink($lfile); - 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"); - unlink("$lfile"); - - my $pdf = ''; - while () { - $pdf .= $_; - } - - close PDF; - - return $pdf; - + $pdf; } -=item print_html [ TIME [ , TEMPLATE [ , CID ] ] ] - -Returns an HTML invoice, as a scalar. - -TIME an optional value used to control the printing of overdue messages. The -default is now. It isn't the date of the invoice; that's the `_date' field. -It is specified as a UNIX timestamp; see L. Also see -L and L for conversion functions. - -CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used -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; - - my $cust_main = $self->cust_main; - $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') ) - unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/; - - $template ||= $self->_agent_template; - my $templatefile = 'invoice_html'; - my $suffix = length($template) ? "_$template" : ''; - $templatefile .= $suffix; - my @html_template = map "$_\n", $conf->config($templatefile) - or die "cannot load config file $templatefile"; - - my $html_template = new Text::Template( - TYPE => 'ARRAY', - SOURCE => \@html_template, - DELIMITERS => [ '<%=', '%>' ], - ); - - $html_template->compile() - 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), - 'payname' => encode_entities($cust_main->payname), - 'company' => encode_entities($cust_main->company), - 'address1' => encode_entities($cust_main->address1), - 'address2' => encode_entities($cust_main->address2), - 'city' => encode_entities($cust_main->city), - 'state' => encode_entities($cust_main->state), - 'zip' => encode_entities($cust_main->zip), - 'terms' => $self->terms, - 'cid' => $cid, - 'template' => $template, -# 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc", - ); - - if ( - defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) ) - && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) ) - ) { - $invoice_data{'returnaddress'} = - join("\n", $conf->config('invoice_htmlreturnaddress', $template) ); - } else { - $invoice_data{'returnaddress'} = - join("\n", map { - s/~/ /g; - s/\\\\\*?\s*$/
    /; - s/\\hyphenation\{[\w\s\-]+\}//; - $_; - } - $conf->config_orbase( 'invoice_latexreturnaddress', - $template - ) - ); - } - - my $countrydefault = $conf->config('countrydefault') || 'US'; - if ( $cust_main->country eq $countrydefault ) { - $invoice_data{'country'} = ''; - } else { - $invoice_data{'country'} = - encode_entities(code2country($cust_main->country)); - } - - 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/%%(.*)$//; - s/\\section\*\{\\textsc\{(.)(.*)\}\}/

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

      /; - s/\\item /
    1. /; - s/\\end\{enumerate\}/<\/ol>/; - s/\\textbf\{(.*)\}/$1<\/b>/; - s/\\\\\*/ /; - $_; - } - $conf->config_orbase('invoice_latexnotes', $template) - ); - } - -# #do variable substitutions in notes -# $invoice_data{'notes'} = -# join("\n", -# map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } -# $conf->config_orbase('invoice_latexnotes', $suffix) -# ); - - 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 ) - ? encode_entities("Purchase Order #". $cust_main->payinfo) - : ''; - - my $money_char = $conf->config('money_char') || '$'; - - foreach my $line_item ( $self->_items ) { - 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->{'amount'} = $money_char. $line_item->{'amount'}; - $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; - - push @{$invoice_data{'detail_items'}}, $detail; - } - - - my $taxtotal = 0; - foreach my $tax ( $self->_items_tax ) { - my $total = {}; - $total->{'total_item'} = encode_entities($tax->{'description'}); - $taxtotal += $tax->{'amount'}; - $total->{'total_amount'} = $money_char. $tax->{'amount'}; - push @{$invoice_data{'total_items'}}, $total; - } - - if ( $taxtotal ) { - my $total = {}; - $total->{'total_item'} = 'Sub-total'; - $total->{'total_amount'} = - $money_char. sprintf('%.2f', $self->charged - $taxtotal ); - unshift @{$invoice_data{'total_items'}}, $total; - } +=item print_html [ TIME [ , TEMPLATE [ , CID ] ] ] - my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance - { - my $total = {}; - $total->{'total_item'} = 'Total'; - $total->{'total_amount'} = - "$money_char". sprintf('%.2f', $self->charged + $pr_total ). ''; - push @{$invoice_data{'total_items'}}, $total; - } +Returns an HTML invoice, as a scalar. + +TIME an optional value used to control the printing of overdue messages. The +default is now. It isn't the date of the invoice; that's the `_date' field. +It is specified as a UNIX timestamp; see L. Also see +L and L for conversion functions. - #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments +CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used +when emailing the invoice as part of a multipart/related MIME email. - # 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; - } +=cut - # 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; +sub print_html { + my $self = shift; + my %params; + if ( ref $_[0] ) { + %params = %{ shift() }; + }else{ + $params{'time'} = shift; + $params{'template'} = shift; + $params{'cid'} = shift; } - { - 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; - } + $params{'format'} = 'html'; - $html_template->fill_in( HASH => \%invoice_data); + $self->print_generic( %params ); } # quick subroutine for print_latex @@ -2337,6 +2795,67 @@ sub _latex_escape { #utility methods for print_* +sub _translate_old_latex_format { + warn "_translate_old_latex_format called\n" + if $DEBUG; + + my @template = (); + while ( @_ ) { + my $line = shift; + + if ( $line =~ /^%%Detail\s*$/ ) { + + push @template, q![@--!, + q! foreach my $_tr_line (@detail_items) {!, + q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!, + q! $_tr_line->{'description'} .= !, + q! "\\tabularnewline\n~~".!, + q! join( "\\tabularnewline\n~~",!, + q! @{$_tr_line->{'ext_description'}}!, + q! );!, + q! }!; + + while ( ( my $line_item_line = shift ) + !~ /^%%EndDetail\s*$/ ) { + $line_item_line =~ s/'/\\'/g; # nice LTS + $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes + $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g; + push @template, " \$OUT .= '$line_item_line';"; + } + + push @template, '}', + '--@]'; + + } elsif ( $line =~ /^%%TotalDetails\s*$/ ) { + + push @template, '[@--', + ' foreach my $_tr_line (@total_items) {'; + + while ( ( my $total_item_line = shift ) + !~ /^%%EndTotalDetails\s*$/ ) { + $total_item_line =~ s/'/\\'/g; # nice LTS + $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes + $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g; + push @template, " \$OUT .= '$total_item_line';"; + } + + push @template, '}', + '--@]'; + + } else { + $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g; + push @template, $line; + } + + } + + if ($DEBUG) { + warn "$_\n" foreach @template; + } + + (@template); +} + sub terms { my $self = shift; @@ -2346,8 +2865,8 @@ sub terms { 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'; + #use configured default + $conf->config('invoice_default_terms') || ''; } sub due_date { @@ -2376,13 +2895,145 @@ 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_sections { + my $self = shift; + my $late = shift; + my $summarypage = shift; + my $escape = shift; + + my %s = (); + my %l = (); + my %not_tax = (); + + foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) + { + + + my $usage = $cust_bill_pkg->usage; + + foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) { + next if ( $display->summary && $summarypage ); + + my $desc = $display->section; + my $type = $display->type; + + if ( $cust_bill_pkg->pkgnum > 0 ) { + $not_tax{$desc} = 1; + } + + if ( $display->post_total && !$summarypage ) { + if (! $type || $type eq 'S') { + $l{$desc} += $cust_bill_pkg->setup + if ( $cust_bill_pkg->setup != 0 ); + } + + if (! $type) { + $l{$desc} += $cust_bill_pkg->recur + if ( $cust_bill_pkg->recur != 0 ); + } + + if ($type && $type eq 'R') { + $l{$desc} += $cust_bill_pkg->recur - $usage + if ( $cust_bill_pkg->recur != 0 ); + } + + if ($type && $type eq 'U') { + $l{$desc} += $usage; + } + + } else { + if (! $type || $type eq 'S') { + $s{$desc} += $cust_bill_pkg->setup + if ( $cust_bill_pkg->setup != 0 ); + } + + if (! $type) { + $s{$desc} += $cust_bill_pkg->recur + if ( $cust_bill_pkg->recur != 0 ); + } + + if ($type && $type eq 'R') { + $s{$desc} += $cust_bill_pkg->recur - $usage + if ( $cust_bill_pkg->recur != 0 ); + } + + if ($type && $type eq 'U') { + $s{$desc} += $usage; + } + + } + + } + + } + + my %cache = map { $_->categoryname => $_ } + qsearch( 'pkg_category', {disabled => 'Y'} ); + $cache{$_->categoryname} = $_ + foreach qsearch( 'pkg_category', {disabled => ''} ); + + push @$late, map { { 'description' => &{$escape}($_), + 'subtotal' => $l{$_}, + 'post_total' => 1, + } } + sort { $cache{$a}->weight <=> $cache{$b}->weight } keys %l; + + map { { 'description' => &{$escape}($_), + 'subtotal' => $s{$_}, + 'summarized' => $not_tax{$_} ? '' : 'Y', + 'tax_section' => $not_tax{$_} ? '' : 'Y', + } } + sort { $cache{$a}->weight <=> $cache{$b}->weight } + ( $summarypage + ? ( grep { exists($s{$_}) || !$cache{$_}->disabled } keys %cache ) + : ( keys %s ) + ); + +} + 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(@_); @@ -2426,83 +3077,222 @@ sub _items_pkg { $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_); } +sub _taxsort { + return 0 unless $a cmp $b; + return -1 if $b eq 'Tax'; + return 1 if $a eq 'Tax'; + return -1 if $b eq 'Other surcharges'; + return 1 if $a eq 'Other surcharges'; + $a cmp $b; +} + sub _items_tax { my $self = shift; - my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg; + my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg; $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_); } 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 $format_function = $opt{format_function} || ''; + my $unsquelched = $opt{unsquelched} || ''; + my $section = $opt{section}->{description} if $opt{section}; + my $summary_page = $opt{summary_page} || ''; my @b = (); - foreach my $cust_bill_pkg ( @$cust_bill_pkg ) { - - my $desc = $cust_bill_pkg->desc; - - if ( $cust_bill_pkg->pkgnum > 0 ) { - - if ( $cust_bill_pkg->setup != 0 ) { - my $description = $desc; - $description .= ' Setup' if $cust_bill_pkg->recur != 0; - my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date); - push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0; - push @b, { - description => $description, - #pkgpart => $part_pkg->pkgpart, - pkgnum => $cust_bill_pkg->pkgnum, - amount => sprintf("%.2f", $cust_bill_pkg->setup), - ext_description => \@d, - }; - } + my ($s, $r, $u) = ( undef, undef, undef ); + foreach my $cust_bill_pkg ( @$cust_bill_pkg ) + { - if ( $cust_bill_pkg->recur != 0 ) { - push @b, { - 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), - ext_description => - [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate, - $cust_bill_pkg->sdate), - $cust_bill_pkg->details, - ], - }; + foreach ( $s, $r, $u ) { + if ( $_ && !$cust_bill_pkg->hidden ) { + $_->{amount} = sprintf( "%.2f", $_->{amount} ), + $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ), + push @b, { %$_ }; + $_ = undef; } + } - } else { #pkgnum tax or one-shot line item (??) + foreach my $display ( grep { defined($section) + ? $_->section eq $section + : 1 + } + grep { $_->summary || !$summary_page } + $cust_bill_pkg->cust_bill_pkg_display + ) + { + + my $type = $display->type; + + my $desc = $cust_bill_pkg->desc; + $desc = substr($desc, 0, 50). '...' + if $format eq 'latex' && length($desc) > 50; + + my %details_opt = ( 'format' => $format, + 'escape_function' => $escape_function, + 'format_function' => $format_function, + ); + + if ( $cust_bill_pkg->pkgnum > 0 ) { + + my $cust_pkg = $cust_bill_pkg->cust_pkg; + + if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) { + + my $description = $desc; + $description .= ' Setup' 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 + || $cust_bill_pkg->hidden; + push @d, $cust_bill_pkg->details(%details_opt) + if $cust_bill_pkg->recur == 0; + + if ( $cust_bill_pkg->hidden ) { + $s->{amount} += $cust_bill_pkg->setup; + $s->{unit_amount} += $cust_bill_pkg->unitsetup; + push @{ $s->{ext_description} }, @d; + } else { + $s = { + description => $description, + #pkgpart => $part_pkg->pkgpart, + pkgnum => $cust_bill_pkg->pkgnum, + amount => $cust_bill_pkg->setup, + unit_amount => $cust_bill_pkg->unitsetup, + quantity => $cust_bill_pkg->quantity, + ext_description => \@d, + }; + }; + + } + + if ( $cust_bill_pkg->recur != 0 && + ( !$type || $type eq 'R' || $type eq 'U' ) + ) + { + + my $is_summary = $display->summary; + my $description = $is_summary ? "Usage charges" : $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 + my @dates = ( $self->_date ); + my $prev = $cust_bill_pkg->previous_cust_bill_pkg; + push @dates, $prev->sdate if $prev; + + push @d, map &{$escape_function}($_), + $cust_pkg->h_labels_short(@dates) + #$cust_bill_pkg->edate, + #$cust_bill_pkg->sdate) + unless $cust_pkg->part_pkg->hide_svc_detail + || $cust_bill_pkg->itemdesc + || $cust_bill_pkg->hidden + || $is_summary; + + push @d, $cust_bill_pkg->details(%details_opt) + unless ($is_summary || $type && $type eq 'R'); + + my $amount = 0; + if (!$type) { + $amount = $cust_bill_pkg->recur; + }elsif($type eq 'R') { + $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage; + }elsif($type eq 'U') { + $amount = $cust_bill_pkg->usage; + } + + if ( !$type || $type eq 'R' ) { + + if ( $cust_bill_pkg->hidden ) { + $r->{amount} += $amount; + $r->{unit_amount} += $cust_bill_pkg->unitrecur; + push @{ $r->{ext_description} }, @d; + } else { + $r = { + description => $description, + #pkgpart => $part_pkg->pkgpart, + pkgnum => $cust_bill_pkg->pkgnum, + amount => $amount, + unit_amount => $cust_bill_pkg->unitrecur, + quantity => $cust_bill_pkg->quantity, + ext_description => \@d, + }; + } + + } elsif ( $amount ) { # && $type eq 'U' + + if ( $cust_bill_pkg->hidden ) { + $u->{amount} += $amount; + $u->{unit_amount} += $cust_bill_pkg->unitrecur; + push @{ $u->{ext_description} }, @d; + } else { + $u = { + description => $description, + #pkgpart => $part_pkg->pkgpart, + pkgnum => $cust_bill_pkg->pkgnum, + amount => $amount, + unit_amount => $cust_bill_pkg->unitrecur, + quantity => $cust_bill_pkg->quantity, + ext_description => \@d, + }; + } + + } + + } # recurring or usage with recurring charge + + } else { #pkgnum tax or one-shot line item (??) + + if ( $cust_bill_pkg->setup != 0 ) { + push @b, { + 'description' => $desc, + 'amount' => sprintf("%.2f", $cust_bill_pkg->setup), + }; + } + if ( $cust_bill_pkg->recur != 0 ) { + push @b, { + 'description' => "$desc (". + time2str("%x", $cust_bill_pkg->sdate). ' - '. + time2str("%x", $cust_bill_pkg->edate). ')', + 'amount' => sprintf("%.2f", $cust_bill_pkg->recur), + }; + } - if ( $cust_bill_pkg->setup != 0 ) { - push @b, { - 'description' => $desc, - 'amount' => sprintf("%.2f", $cust_bill_pkg->setup), - }; - } - if ( $cust_bill_pkg->recur != 0 ) { - push @b, { - 'description' => "$desc (". - time2str("%x", $cust_bill_pkg->sdate). ' - '. - time2str("%x", $cust_bill_pkg->edate). ')', - 'amount' => sprintf("%.2f", $cust_bill_pkg->recur), - }; } } } + foreach ( $s, $r, $u ) { + if ( $_ ) { + $_->{amount} = sprintf( "%.2f", $_->{amount} ), + $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ), + push @b, { %$_ }; + } + } + @b; } sub _items_credits { - my $self = shift; + my( $self, %opt ) = @_; + my $trim_len = $opt{'trim_len'} || 60; my @b; #credits @@ -2510,10 +3300,10 @@ sub _items_credits { #something more elaborate if $_->amount ne $_->cust_credit->credited ? - my $reason = $_->cust_credit->reason; - #my $reason = substr($_->cust_credit->reason,0,32); - #$reason .= '...' if length($reason) < length($_->cust_credit->reason); + my $reason = substr($_->cust_credit->reason, 0, $trim_len); + $reason .= '...' if length($reason) < length($_->cust_credit->reason); $reason = " ($reason) " if $reason; + push @b, { #'description' => 'Credit ref\#'. $_->crednum. # " (". time2str("%x",$_->cust_credit->_date) .")". @@ -2523,12 +3313,6 @@ sub _items_credits { 'amount' => sprintf("%.2f",$_->amount), }; } - #foreach ( @cr_cust_credit ) { - # push @buf,[ - # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")", - # $money_char. sprintf("%10.2f",$_->credited) - # ]; - #} @b; @@ -2554,6 +3338,38 @@ sub _items_payments { } +=item call_details [ OPTION => VALUE ... ] + +Returns an array of CSV strings representing the call details for this invoice +The only option available is the boolean prepend_billed_number + +=cut + +sub call_details { + my ($self, %opt) = @_; + + my $format_function = sub { shift }; + + if ($opt{prepend_billed_number}) { + $format_function = sub { + my $detail = shift; + my $row = shift; + + $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail; + + }; + } + + my @details = map { $_->details( 'format_function' => $format_function, + 'escape_function' => sub{ return() }, + ) + } + grep { $_->pkgnum } + $self->cust_bill_pkg; + my $header = $details[0]; + ( $header, grep { $_ ne $header } @details ); +} + =back @@ -2561,7 +3377,7 @@ sub _items_payments { =over 4 -=item reprint +=item process_reprint =cut @@ -2569,7 +3385,7 @@ sub process_reprint { process_re_X('print', @_); } -=item reemail +=item process_reemail =cut @@ -2577,7 +3393,7 @@ sub process_reemail { process_re_X('email', @_); } -=item refax +=item process_refax =cut @@ -2585,12 +3401,28 @@ 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 "process_re_X $method for job $job\n" if $DEBUG; + warn "$me process_re_X $method for job $job\n" if $DEBUG; my $param = thaw(decode_base64(shift)); warn Dumper($param) if $DEBUG; @@ -2605,7 +3437,6 @@ sub process_re_X { sub re_X { my($method, $job, %param ) = @_; -# [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ], if ( $DEBUG ) { warn "re_X $method for job $job with param:\n". join( '', map { " $_ => ". $param{$_}. "\n" } keys %param ); @@ -2615,47 +3446,24 @@ sub re_X { my $distinct = ''; my $orderby = 'ORDER BY cust_bill._date'; - my @where; - - if ( $param{'begin'} =~ /^(\d+)$/ ) { - push @where, "cust_bill._date >= $1"; - } - if ( $param{'end'} =~ /^(\d+)$/ ) { - push @where, "cust_bill._date < $1"; - } - if ( $param{'agentnum'} =~ /^(\d+)$/ ) { - push @where, "cust_main.agentnum = $1"; - } - - my $owed = - "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay - WHERE cust_bill_pay.invnum = cust_bill.invnum ) - - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill - WHERE cust_credit_bill.invnum = cust_bill.invnum )"; - - push @where, "0 != $owed" - if $param{'open'}; + my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param); - push @where, "cust_bill._date < ". (time-86400*$param{'days'}) - if $param{'days'}; - - my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : ''; + 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, + } ); - my $addl_from = 'left join cust_main using ( custnum )'; + $method .= '_invoice' unless $method eq 'email' || $method eq 'print'; - if ( $param{'newest_percust'} ) { - $distinct = 'DISTINCT ON ( cust_bill.custnum )'; - $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC'; - #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'"; - } - - my @cust_bill = qsearch( 'cust_bill', - {}, - "$distinct cust_bill.*", - $extra_sql, - '', - $addl_from - ); + 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 ) { @@ -2678,13 +3486,175 @@ sub re_X { =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,