X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=68571f77ebe5a2d7b63746dd93602d5ff4c742cb;hb=000f20dff74059f44950c7191beaab951aeae316;hp=9b7752a6db662a2b95a663c3513759bd6767da0f;hpb=e1e374c37cafcb4e9e26a9ec30c8234b26dc0dad;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 9b7752a6d..68571f77e 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -12,11 +12,12 @@ 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_bill_pkg; +use FS::cust_bill_pkg_display; use FS::cust_credit; use FS::cust_pay; use FS::cust_pkg; @@ -226,7 +227,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 @@ -426,6 +451,38 @@ sub 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. @@ -459,6 +516,21 @@ sub owed { $balance; } +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 =cut @@ -482,6 +554,13 @@ sub apply_payments_and_credits { 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 = ''; @@ -519,28 +598,39 @@ 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"; + $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) ); + $app->invnum( $self->invnum ); my $error = $app->insert; @@ -557,19 +647,27 @@ sub apply_payments_and_credits { } -=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 @@ -591,6 +689,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 { @@ -629,7 +729,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) ]; } } @@ -646,21 +746,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>", ; @@ -675,7 +776,11 @@ sub generate_email { ' ', ' ', ' ', - $self->print_html('', $args{'template'}, $content_id), + $self->print_html({ time => '', + template => $args{'template'}, + cid => $content_id, + %cdrs, + }), ' ', '', ], @@ -683,6 +788,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: @@ -708,9 +828,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 { @@ -722,7 +842,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'; @@ -736,7 +856,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) } ]; } @@ -756,7 +876,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) ]; } } @@ -781,7 +901,7 @@ sub mimebuild_pdf { 'Encoding' => 'base64', 'Data' => [ $self->print_pdf(@_) ], 'Disposition' => 'attachment', - 'Filename' => 'invoice.pdf', + 'Filename' => 'invoice-'. $self->invnum. '.pdf', ); } @@ -798,6 +918,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 { @@ -826,17 +949,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 ''; @@ -868,13 +1000,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; @@ -882,10 +1018,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, ) ); @@ -894,6 +1033,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. @@ -917,6 +1074,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 : ''; @@ -924,7 +1082,7 @@ sub print { do_print $self->lpr_data($template); } -=item fax [ TEMPLATENAME ] +=item fax_invoice [ TEMPLATENAME ] Faxes this invoice. @@ -932,7 +1090,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 : ''; @@ -949,6 +1107,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 @@ -1347,7 +1545,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 ) : '' ), @@ -1364,11 +1562,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( @@ -1468,7 +1664,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"); @@ -1520,11 +1716,12 @@ L and L for conversion functions. =cut sub print_text { - my( $self, $today, $template ) = @_; + my( $self, $today, $template, %opt ) = @_; my %params = ( 'format' => 'template' ); $params{'time'} = $today if $today; $params{'template'} = $template if $template; + $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'}; $self->print_generic( %params ); } @@ -1545,12 +1742,12 @@ L and L for conversion functions. =cut sub print_latex { - - my( $self, $today, $template ) = @_; + my( $self, $today, $template, %opt ) = @_; my %params = ( 'format' => 'latex' ); $params{'time'} = $today if $today; $params{'template'} = $template if $template; + $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'}; $template ||= $self->_agent_template; @@ -1561,11 +1758,13 @@ sub print_latex { UNLINK => 0, ) or die "can't open temp file: $!\n"; - if ($template && $conf->exists("logo_${template}.eps")) { - print $lh $conf->config_binary("logo_${template}.eps") + my $agentnum = $self->cust_main->agentnum; + + 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') + } else { + print $lh $conf->config_binary('logo.eps', $agentnum) or die "can't write temp file: $!\n"; } close $lh; @@ -1606,8 +1805,12 @@ L and L for conversion functions. cid - +unsquelch_cdr - overrides any per customer cdr squelching when true + =cut +#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( $self, %params ) = @_; @@ -1621,8 +1824,8 @@ sub print_generic { 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 %delimiters = ( 'latex' => [ '[@--', '--@]' ], 'html' => [ '<%=', '%>' ], @@ -1635,7 +1838,7 @@ sub print_generic { $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 $old_latex = ''; if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) { @@ -1653,7 +1856,7 @@ sub print_generic { ); $text_template->compile() - or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR; + or die "Can't compile $templatefile: $Text::Template::ERROR\n"; # additional substitution could possibly cause breakage in existing templates @@ -1663,6 +1866,7 @@ sub print_generic { 'footer' => sub { map "$_", @_ }, 'smallfooter' => sub { map "$_", @_ }, 'returnaddress' => sub { map "$_", @_ }, + 'coupon' => sub { map "$_", @_ }, }, 'html' => { 'notes' => @@ -1676,6 +1880,8 @@ sub print_generic { s/\\textbf\{(.*)\}/$1<\/b>/g; s/\\\\\*/
/g; s/\\dollar ?/\$/g; + s/\\#/#/g; + s/~/ /g; $_; } @_ }, @@ -1689,9 +1895,11 @@ sub print_generic { s/~/ /g; s/\\\\\*?\s*$/
/; s/\\hyphenation\{[\w\s\-]+}//; + s/\\([&])/$1/g; $_; } @_ }, + 'coupon' => sub { "" }, }, 'template' => { 'notes' => @@ -1721,6 +1929,7 @@ sub print_generic { $_; } @_ }, + 'coupon' => sub { "" }, }, ); @@ -1738,7 +1947,7 @@ sub print_generic { ); my $escape_function = $escape_functions{$format}; - my %date_formats = ( 'latex' => '%b, %o, %Y', + my %date_formats = ( 'latex' => '%b %o, %Y', 'html' => '%b %o, %Y', 'template' => '%s', ); @@ -1781,15 +1990,19 @@ sub print_generic { ) ) ); - } elsif ( grep /\S/, $conf->config('company_address') ) { + } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) { - $returnaddress = join( "\n", $conf->config('company_address') ); - - $returnaddress = - join( '\\*'."\n", map s/( {2,})/'~' x length($1)/eg, - $conf->config('company_address') - ) - if $format eq 'latex'; + 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 { @@ -1802,13 +2015,14 @@ sub print_generic { } my %invoice_data = ( - 'company_name' => scalar( $conf->config('company_name') ), - 'company_address' => join("\n", $conf->config('company_address') ). "\n", - 'custnum' => $self->custnum, + '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), @@ -1816,21 +2030,34 @@ sub print_generic { '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, + #'quantity' => 1, 'terms' => $self->terms, - 'template' => $params{'template'}, + '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", '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'), ); + 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'}; - my $countrydefault = $conf->config('countrydefault') || 'US'; if ( $cust_main->country eq $countrydefault ) { $invoice_data{'country'} = ''; } else { @@ -1858,36 +2085,54 @@ sub print_generic { push @address, '' while (scalar(@address) < 5); + $invoice_data{'logo_file'} = $params{'logo_file'} + if $params{'logo_file'}; + + 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{'previous_balance'} = sprintf("%.2f", $pr_total); + $invoice_data{'balance'} = sprintf("%.2f", $balance_due); + + my $agentnum = $self->cust_main->agentnum; + #do variable substitution in notes, footer, smallfooter - foreach my $include (qw( notes footer smallfooter )) { + foreach my $include (qw( notes footer smallfooter coupon )) { + + my $inc_file = $conf->key_orbase("invoice_${format}$include", $template); + my @inc_src; - my @inc_src = $conf->config_orbase("invoice_latex$include", $template ); - my $convert_map = $convert_maps{$format}{$include}; + if ( $conf->exists($inc_file, $agentnum) + && length( $conf->config($inc_file, $agentnum) ) ) { + + @inc_src = $conf->config($inc_file, $agentnum); - if ( - defined( $conf->config_orbase("invoice_${format}$include", $template) ) - && length( $conf->config_orbase('invoice_${format}$include', $template) ) - ) { - @inc_src = $conf->config_orbase("invoice_${format}$include", $template ); } else { - @inc_src = - map { s/\[@--/$delimiters{$format}[0]/g; - s/--@]/$delimiters{$format}[1]/g; - $_; - } - &$convert_map( - $conf->config_orbase("invoice_latex$include", $template ) - ); + + $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"; + ) or die "Can't create new Text::Template object: $Text::Template::ERROR"; - $inc_tt->compile() - or die "can't compile template: $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 ); @@ -1900,18 +2145,13 @@ sub print_generic { ? &$escape_function("Purchase Order #". $cust_main->payinfo) : $nbsp; - 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 %money_chars = ( 'latex' => '', 'html' => $conf->config('money_char') || '$', 'template' => '', ); my $money_char = $money_chars{$format}; - my %other_money_chars = ( 'latex' => '\dollar ', + my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too 'html' => $conf->config('money_char') || '$', 'template' => '', ); @@ -1932,46 +2172,62 @@ sub print_generic { sprintf('%.2f', $pr_total), }; + my $taxtotal = 0; + my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees', + 'subtotal' => $taxtotal }; # adjusted below + + my $adjusttotal = 0; + my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments', + 'subtotal' => 0 }; # adjusted below + + 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; + push @sections, $self->_items_sections( $late_sections ); }else{ push @sections, { 'description' => '', 'subtotal' => '' }; } - foreach my $line_item ( $self->_items_previous ) { - my $detail = { - ext_description => [], - }; - $detail->{'ref'} = $line_item->{'pkgnum'}; - $detail->{'quantity'} = 1; - $detail->{'section'} = $previous_section; - $detail->{'description'} = &$escape_function($line_item->{'description'}); - if ( exists $line_item->{'ext_description'} ) { - @{$detail->{'ext_description'}} = map { - &$escape_function($_); - } @{$line_item->{'ext_description'}}; - } - { - my $money = $old_latex ? '' : $money_char; - $detail->{'amount'} = $money. $line_item->{'amount'}; + unless ( $conf->exists('disable_previous_balance') + || $conf->exists('previous_balance-summary_only') + ) + { + + foreach my $line_item ( $self->_items_previous ) { + + my $detail = { + ext_description => [], + }; + $detail->{'ref'} = $line_item->{'pkgnum'}; + $detail->{'quantity'} = 1; + $detail->{'section'} = $previous_section; + $detail->{'description'} = &$escape_function($line_item->{'description'}); + if ( exists $line_item->{'ext_description'} ) { + @{$detail->{'ext_description'}} = map { + &$escape_function($_); + } @{$line_item->{'ext_description'}}; + } + $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'}), + ]; } - $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) { + + 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) { + foreach my $section (@sections, @$late_sections) { $section->{'subtotal'} = $other_money_char. sprintf('%.2f', $section->{'subtotal'}) @@ -1985,24 +2241,26 @@ sub print_generic { my %options = (); $options{'section'} = $section if $multisection; + $options{'format'} = $format; + $options{'escape_function'} = $escape_function; + $options{'format_function'} = sub { () } unless $unsquelched; + $options{'unsquelched'} = $unsquelched; foreach my $line_item ( $self->_items_pkg(%options) ) { my $detail = { ext_description => [], }; $detail->{'ref'} = $line_item->{'pkgnum'}; - $detail->{'quantity'} = 1; + $detail->{'quantity'} = $line_item->{'quantity'}; $detail->{'section'} = $section; $detail->{'description'} = &$escape_function($line_item->{'description'}); if ( exists $line_item->{'ext_description'} ) { - @{$detail->{'ext_description'}} = map { - &$escape_function($_); - } @{$line_item->{'ext_description'}}; - } - { - my $money = $old_latex ? '' : $money_char; - $detail->{'amount'} = $money. $line_item->{'amount'}; + @{$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; @@ -2025,37 +2283,68 @@ sub print_generic { } - if ( $multisection ) { - unshift @sections, $previous_section; + if ( $multisection && !$conf->exists('disable_previous_balance') ) { + unshift @sections, $previous_section if $pr_total; } - my $taxtotal = 0; foreach my $tax ( $self->_items_tax ) { - my $total = {}; - $total->{'total_item'} = &$escape_function($tax->{'description'}); + $taxtotal += $tax->{'amount'}; - $total->{'total_amount'} = $other_money_char. $tax->{'amount'}; - push @total_items, $total; - push @buf,[ $total->{'total_item'}, - $money_char. sprintf("%10.2f", $total->{'total_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'} = + $other_money_char. sprintf('%.2f', $self->charged - $taxtotal ); + if ( $multisection ) { - $total->{'total_item'} = 'New charges sub-total'; + $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{ - $total->{'total_item'} = 'Sub-total'; + unshift @total_items, $total; } - $total->{'total_amount'} = - $other_money_char. sprintf('%.2f', $self->charged - $taxtotal ); - unshift @total_items, $total; } + $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal); 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,['','']; @@ -2063,71 +2352,127 @@ sub print_generic { my $total = {}; $total->{'total_item'} = &$embolden_function('Total'); $total->{'total_amount'} = - $total->{'total_amount'} = &$embolden_function( - $other_money_char. sprintf('%.2f', $self->charged + $pr_total ) + $other_money_char. + sprintf( '%.2f', + $self->charged + ( $conf->exists('disable_previous_balance') + ? 0 + : $pr_total + ) + ) ); - push @total_items, $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 + $pr_total) ]; + $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 + my $credittotal = 0; + foreach my $credit ( $self->_items_credits('trim_len'=>60) ) { + + my $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); - #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments + #credits (again) + foreach my $credit ( $self->_items_credits('trim_len'=>32) ) { + push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ]; + } - # credits - foreach my $credit ( $self->_items_credits ) { - my $total; - $total->{'total_item'} = &$escape_function($credit->{'description'}); - #$credittotal - $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'}; - push @total_items, $total; - } + # payments + my $paymenttotal = 0; + foreach my $payment ( $self->_items_payments ) { + my $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); - # credits (again) - foreach ( $self->cust_credited ) { - - #something more elaborate if $_->amount ne $_->cust_credit->credited ? + if ( $multisection ) { + $adjust_section->{'subtotal'} = $other_money_char. + sprintf('%.2f', $adjusttotal); + push @sections, $adjust_section; + } - 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) - ]; + { + my $total; + $total->{'total_item'} = &$embolden_function($self->balance_due_msg); + $total->{'total_amount'} = + &$embolden_function( + $other_money_char. sprintf('%.2f', $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 ) ]; + } } - # payments - foreach my $payment ( $self->_items_payments ) { - my $total = {}; - $total->{'total_item'} = &$escape_function($payment->{'description'}); - #$paymenttotal - $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'}; - push @total_items, $total; - push @buf, [ $payment->{'description'}, - $money_char. sprintf("%10.2f", $payment->{'amount'}), - ]; - } - - { - my $total; - $total->{'total_item'} = &$embolden_function($self->balance_due_msg); - $total->{'total_amount'} = - &$embolden_function( - $other_money_char. sprintf('%.2f', $self->owed + $pr_total ) - ); - push @total_items, $total; - push @buf,['','-----------']; - push @buf,[$self->balance_due_msg, $money_char. - sprintf("%10.2f", $balance_due ) ]; + if ( $multisection ) { + push @sections, @$late_sections + if $unsquelched; } - $invoice_data{'logo_file'} = $params{'logo_file'} - if $params{'logo_file'}; - $invoice_lines = 0; my $wasfunc = 0; foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy @@ -2195,8 +2540,8 @@ sub print_ps { my ($file, $lfile) = $self->print_latex(@_); my $ps = generate_ps($file); unlink($lfile); - $ps; + $ps; } =item print_pdf [ TIME [ , TEMPLATE ] ] @@ -2214,44 +2559,10 @@ 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 ] ] ] @@ -2269,12 +2580,17 @@ when emailing the invoice as part of a multipart/related MIME email. =cut sub print_html { - my( $self, $today, $template, $cid ) = @_; + my $self = shift; + my %params; + if ( ref $_[0] ) { + %params = %{ shift() }; + }else{ + $params{'time'} = shift; + $params{'template'} = shift; + $params{'cid'} = shift; + } - my %params = ( 'format' => 'html' ); - $params{'time'} = $today if $today; - $params{'template'} = $template if $template; - $params{'cid'} = $cid if $cid; + $params{'format'} = 'html'; $self->print_generic( %params ); } @@ -2367,8 +2683,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 { @@ -2397,37 +2713,123 @@ 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 %s = (); - foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { + my %l = (); - if ( $cust_bill_pkg->pkgnum > 0 ) { - - my $desc = $cust_bill_pkg->cust_pkg->part_pkg->classname; + foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) + { - $s{$desc} += $cust_bill_pkg->setup - if ( $cust_bill_pkg->setup != 0 ); + if ( $cust_bill_pkg->pkgnum > 0 ) { + my $usage = $cust_bill_pkg->usage; + + foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) { + my $desc = $display->section; + my $type = $display->type; + + if ( $display->post_total ) { + 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; + } + + } - $s{$desc} += $cust_bill_pkg->recur - if ( $cust_bill_pkg->recur != 0 ); + } } } + push @$late, map { { 'description' => $_, + 'subtotal' => $l{$_}, + 'post_total' => 1, + } } sort keys %l; + map { {'description' => $_, 'subtotal' => $s{$_}} } sort 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(@_); @@ -2467,95 +2869,224 @@ sub _items_previous { sub _items_pkg { my $self = shift; - my %options = @_; - my $section = delete $options{'section'}; - my @cust_bill_pkg = - grep { $_->pkgnum && - ( defined($section) - ? $_->cust_pkg->part_pkg->classname eq $section->{'description'} - : 1 - ) - } $self->cust_bill_pkg; - $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options); + my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_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 @b = (); - foreach my $cust_bill_pkg ( @$cust_bill_pkg ) { - - my $desc = $cust_bill_pkg->desc; + 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}; - if ( $cust_bill_pkg->pkgnum > 0 ) { + my @b = (); + my ($s, $r, $u) = ( undef, undef, undef ); + foreach my $cust_bill_pkg ( @$cust_bill_pkg ) + { - 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, - }; + foreach ( $s, $r, $u ) { + if ( $_ && !$cust_bill_pkg->hidden ) { + $_->{amount} = sprintf( "%.2f", $_->{amount} ), + $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ), + push @b, { %$_ }; + $_ = undef; } + } - 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 my $display ( grep { defined($section) + ? $_->section eq $section + : 1 + } + $cust_bill_pkg->cust_bill_pkg_display + ) + { - } else { #pkgnum tax or one-shot line item (??) + my $type = $display->type; + + 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 %details_opt = ( 'format' => $format, + 'escape_function' => $escape_function, + 'format_function' => $format_function, + ); + + if ( $cust_bill_pkg->pkgnum > 0 ) { + + 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 @@ -2563,10 +3094,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) .")". @@ -2576,12 +3107,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; @@ -2607,6 +3132,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 @@ -2614,7 +3171,7 @@ sub _items_payments { =over 4 -=item reprint +=item process_reprint =cut @@ -2622,7 +3179,7 @@ sub process_reprint { process_re_X('print', @_); } -=item reemail +=item process_reemail =cut @@ -2630,7 +3187,7 @@ sub process_reemail { process_re_X('email', @_); } -=item refax +=item process_refax =cut @@ -2638,12 +3195,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; @@ -2669,16 +3242,22 @@ sub re_X { my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param); - my $addl_from = 'left join cust_main using ( custnum )'; + my $addl_from = 'LEFT JOIN cust_main USING ( custnum )'; - my @cust_bill = qsearch( 'cust_bill', - {}, - #"$distinct cust_bill.*", - "cust_bill.*", - $extra_sql, - '', - $addl_from - ); + 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 ) { @@ -2788,6 +3367,11 @@ Note: validates all passed-in data; i.e. safe to use with unchecked CGI params. 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+)$/ ) { @@ -2838,7 +3422,22 @@ sub search_sql { } - push @search, $FS::CurrentUser::CurrentUser->agentnums_sql; + 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 );