X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=162d6ed8d5c9020ddb732a409cce6b955e7b1af1;hb=3ee89324c4a7baa589dd74bed4a88e0074462002;hp=e4215bf2b42b7e7ec17c83d87824441afeaba6d9;hpb=c8cd96b69c9c1ede44c06c04f2703079d1afdf2b;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index e4215bf2b..162d6ed8d 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1,9 +1,11 @@ package FS::cust_bill; use strict; -use vars qw( @ISA $DEBUG $me $conf $money_char $date_format $rdate_format ); +use vars qw( @ISA $DEBUG $me $conf + $money_char $date_format $rdate_format $date_format_long ); use vars qw( $invoice_lines @buf ); #yuck use Fcntl qw(:flock); #for spool_csv +use Cwd; use List::Util qw(min max); use Date::Format; use Text::Template 1.20; @@ -12,6 +14,7 @@ use String::ShellQuote; use HTML::Entities; use Locale::Country; use Storable qw( freeze thaw ); +use GD::Barcode; use FS::UID qw( datasrc ); use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print ); use FS::Record qw( qsearch qsearchs dbh ); @@ -36,6 +39,8 @@ use FS::part_bill_event; use FS::payby; use FS::bill_batch; use FS::cust_bill_batch; +use FS::cust_bill_pay_pkg; +use FS::cust_credit_bill_pkg; @ISA = qw( FS::cust_main_Mixin FS::Record ); @@ -45,9 +50,10 @@ $me = '[FS::cust_bill]'; #ask FS::UID to run this stuff for us later FS::UID->install_callback( sub { $conf = new FS::Conf; - $money_char = $conf->config('money_char') || '$'; - $date_format = $conf->config('date_format') || '%x'; - $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; + $money_char = $conf->config('money_char') || '$'; + $date_format = $conf->config('date_format') || '%x'; #/YY + $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY + $date_format_long = $conf->config('date_format_long') || '%b %o, %Y'; } ); =head1 NAME @@ -162,6 +168,45 @@ sub cust_unlinked_msg { Adds this invoice to the database ("Posts" the invoice). If there is an error, returns the error, otherwise returns false. +=cut + +sub insert { + my $self = shift; + warn "$me insert called\n" if $DEBUG; + + 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; + + my $error = $self->SUPER::insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + if ( $self->get('cust_bill_pkg') ) { + foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) { + $cust_bill_pkg->invnum($self->invnum); + my $error = $cust_bill_pkg->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "can't create invoice line item: $error"; + } + } + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; + +} + =item delete This method now works but you probably shouldn't use it. Instead, apply a @@ -200,6 +245,7 @@ sub delete { cust_pay_batch cust_bill_pay_batch cust_bill_pkg + cust_bill_batch )) { foreach my $linked ( $self->$table() ) { @@ -224,13 +270,13 @@ sub delete { } -=item replace OLD_RECORD +=item replace [ OLD_RECORD ] -Replaces the OLD_RECORD with this one in the database. If there is an error, -returns the error, otherwise returns false. +You can, but probably shouldn't modify invoices... -Only printed may be changed. printed is normally updated by calling the -collect method of a customer object (see L). +Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not +supplied, replaces this record. If there is an error, returns the error, +otherwise returns false. =cut @@ -241,11 +287,11 @@ collect method of a customer object (see L). sub replace_check { my( $new, $old ) = ( shift, shift ); - return "Can't change custnum!" unless $old->custnum == $new->custnum; + return "Can't modify closed invoice" if $old->closed =~ /^Y/i; #return "Can't change _date!" unless $old->_date eq $new->_date; - return "Can't change _date!" unless $old->_date == $new->_date; - return "Can't change charged!" unless $old->charged == $new->charged - || $old->charged == 0; + return "Can't change _date" unless $old->_date == $new->_date; + return "Can't change charged" unless $old->charged == $new->charged + || $old->charged == 0; ''; } @@ -577,44 +623,97 @@ sub cust_credit_bill { shift->cust_credited(@_); } -=item cust_bill_pay_pkgnum PKGNUM +#=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 ) = @_; +# map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray; +# sort { $a->_date <=> $b->_date } +# qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum, +# 'pkgnum' => $pkgnum, +# } +# ); +#} + +=item cust_bill_pay_pkg PKGNUM Returns all payment applications (see L) for this invoice -with matching pkgnum. +applied against the matching pkgnum. =cut -sub cust_bill_pay_pkgnum { +sub cust_bill_pay_pkg { my( $self, $pkgnum ) = @_; - map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray; - sort { $a->_date <=> $b->_date } - qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum, - 'pkgnum' => $pkgnum, - } - ); + + qsearch({ + 'select' => 'cust_bill_pay_pkg.*', + 'table' => 'cust_bill_pay_pkg', + 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '. + ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ', + 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum. + " AND cust_bill_pkg.pkgnum = $pkgnum", + }); + } -=item cust_credited_pkgnum PKGNUM +#=item cust_credited_pkgnum PKGNUM +# +#=item cust_credit_bill_pkgnum PKGNUM +# +#Returns all applied credits (see L) for this invoice +#with matching pkgnum. +# +#=cut +# +#sub cust_credited_pkgnum { +# my( $self, $pkgnum ) = @_; +# map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray; +# sort { $a->_date <=> $b->_date } +# qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum, +# 'pkgnum' => $pkgnum, +# } +# ); +#} +# +#sub cust_credit_bill_pkgnum { +# shift->cust_credited_pkgnum(@_); +#} -=item cust_credit_bill_pkgnum PKGNUM +=item cust_credit_bill_pkg PKGNUM -Returns all applied credits (see L) for this invoice -with matching pkgnum. +Returns all credit applications (see L) for this invoice +applied against the matching pkgnum. =cut -sub cust_credited_pkgnum { +sub cust_credit_bill_pkg { my( $self, $pkgnum ) = @_; - map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray; - sort { $a->_date <=> $b->_date } - qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum, - 'pkgnum' => $pkgnum, - } - ); + + qsearch({ + 'select' => 'cust_credit_bill_pkg.*', + 'table' => 'cust_credit_bill_pkg', + 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '. + ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ', + 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum. + " AND cust_bill_pkg.pkgnum = $pkgnum", + }); + } -sub cust_credit_bill_pkgnum { - shift->cust_credited_pkgnum(@_); +=item cust_bill_batch + +Returns all invoice batch records (L) for this invoice. + +=cut + +sub cust_bill_batch { + my $self = shift; + qsearch('cust_bill_batch', { 'invnum' => $self->invnum }); } =item tax @@ -657,8 +756,8 @@ sub owed_pkgnum { 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 -= $_->amount for $self->cust_bill_pay_pkg($pkgnum); + $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum); $balance = sprintf( "%.2f", $balance); $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp @@ -917,6 +1016,19 @@ sub generate_email { 'Filename' => 'logo.png', 'Content-ID' => "<$content_id>", ; + + my $barcode; + if($conf->exists('invoice-barcode')){ + my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from"; + $barcode = build MIME::Entity + 'Type' => 'image/png', + 'Encoding' => 'base64', + 'Data' => $self->invoice_barcode(0), + 'Filename' => 'barcode.png', + 'Content-ID' => "<$barcode_content_id>", + ; + $opt{'barcode_cid'} = $barcode_content_id; + } $alternative->attach( 'Type' => 'text/html', @@ -990,7 +1102,12 @@ sub generate_email { # image/png $return{'content-type'} = 'multipart/related'; - $return{'mimeparts'} = [ $alternative, $image, @otherparts ]; + if($conf->exists('invoice-barcode')){ + $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ]; + } + else { + $return{'mimeparts'} = [ $alternative, $image, @otherparts ]; + } $return{'type'} = 'multipart/alternative'; #Content-Type of first part... #$return{'disposition'} = 'inline'; @@ -1200,8 +1317,14 @@ sub email { my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list; - #better to notify this person than silence - @invoicing_list = ($invoice_from) unless @invoicing_list; + if ( ! @invoicing_list ) { #no recipients + if ( $conf->exists('cust_bill-no_recipients-error') ) { + die 'No recipients for customer #'. $self->custnum; + } else { + #default: better to notify this person than silence + @invoicing_list = ($invoice_from); + } + } my $subject = $self->email_subject($template); @@ -2009,7 +2132,7 @@ sub print_text { $params{'time'} = $today if $today; $params{'template'} = $template if $template; $params{$_} = $opt{$_} - foreach grep $opt{$_}, qw( unsquealch_cdr notice_name ); + foreach grep $opt{$_}, qw( unsquelch_cdr notice_name ); $self->print_generic( %params ); } @@ -2051,7 +2174,7 @@ sub print_latex { $params{'time'} = $today if $today; $params{'template'} = $template if $template; $params{$_} = $opt{$_} - foreach grep $opt{$_}, qw( unsquealch_cdr notice_name ); + foreach grep $opt{$_}, qw( unsquelch_cdr notice_name ); $template ||= $self->_agent_template; @@ -2074,6 +2197,28 @@ sub print_latex { close $lh; $params{'logo_file'} = $lh->filename; + if($conf->exists('invoice-barcode')){ + my $png_file = $self->invoice_barcode($dir); + my $eps_file = $png_file; + $eps_file =~ s/\.png$/.eps/g; + $png_file =~ /(barcode.*png)/; + $png_file = $1; + $eps_file =~ /(barcode.*eps)/; + $eps_file = $1; + + my $curr_dir = cwd(); + chdir($dir); + # after painfuly long experimentation, it was determined that sam2p won't + # accept : and other chars in the path, no matter how hard I tried to + # escape them, hence the chdir (and chdir back, just to be safe) + system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0 + or die "sam2p failed: $!\n"; + unlink($png_file); + chdir($curr_dir); + + $params{'barcode_file'} = $eps_file; + } + my @filled_in = $self->print_generic( %params ); my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX', @@ -2085,10 +2230,39 @@ sub print_latex { close $fh; $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename; - return ($1, $params{'logo_file'}); + return ($1, $params{'logo_file'}, $params{'barcode_file'}); } +=item invoice_barcode DIR_OR_FALSE + +Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value, +it is taken as the temp directory where the PNG file will be generated and the +PNG file name is returned. Otherwise, the PNG image itself is returned. + +=cut + +sub invoice_barcode { + my ($self, $dir) = (shift,shift); + + my $gdbar = new GD::Barcode('Code39',$self->invnum); + die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar; + my $gd = $gdbar->plot(Height => 30); + + if($dir) { + my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX', + DIR => $dir, + SUFFIX => '.png', + UNLINK => 0, + ) or die "can't open temp file: $!\n"; + print $bh $gd->png or die "cannot write barcode to file: $!\n"; + my $png_file = $bh->filename; + close $bh; + return $png_file; + } + return $gd->png; +} + =item print_generic OPTION => VALUE ... Internal method - returns a filled-in template for this invoice as a scalar. @@ -2139,6 +2313,9 @@ sub print_generic { 'template' => [ '{', '}' ], ); + warn "$me print_generic creating template\n" + if $DEBUG > 1; + #create the template my $template = $params{template} ? $params{template} : $self->_agent_template; my $templatefile = "invoice_$format"; @@ -2156,12 +2333,18 @@ sub print_generic { @invoice_template = _translate_old_latex_format(@invoice_template); } + warn "$me print_generic creating T:T object\n" + if $DEBUG > 1; + my $text_template = new Text::Template( TYPE => 'ARRAY', SOURCE => \@invoice_template, DELIMITERS => $delimiters{$format}, ); + warn "$me print_generic compiling T:T object\n" + if $DEBUG > 1; + $text_template->compile() or die "Can't compile $templatefile: $Text::Template::ERROR\n"; @@ -2252,15 +2435,19 @@ sub print_generic { my $nbsp = $nbsps{$format}; my %escape_functions = ( 'latex' => \&_latex_escape, - 'html' => \&encode_entities, + 'html' => \&_html_escape_nbsp,#\&encode_entities, 'template' => sub { shift }, ); my $escape_function = $escape_functions{$format}; + my $escape_function_nonbsp = ($format eq 'html') + ? \&_html_escape : $escape_function; - my %date_formats = ( 'latex' => '%b %o, %Y', - 'html' => '%b %o, %Y', + my %date_formats = ( 'latex' => $date_format_long, + 'html' => $date_format_long, 'template' => '%s', ); + $date_formats{'html'} =~ s/ / /g; + my $date_format = $date_formats{$format}; my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}' @@ -2271,6 +2458,14 @@ sub print_generic { ); my $embolden_function = $embolden_functions{$format}; + my %newline_tokens = ( 'latex' => '\\\\', + 'html' => '
', + 'template' => "\n", + ); + my $newline_token = $newline_tokens{$format}; + + warn "$me generating template variables\n" + if $DEBUG > 1; # generate template variables my $returnaddress; @@ -2324,6 +2519,9 @@ sub print_generic { } + warn "$me generating invoice data\n" + if $DEBUG > 1; + my $agentnum = $self->cust_main->agentnum; my %invoice_data = ( @@ -2331,13 +2529,14 @@ sub print_generic { #invoice from info 'company_name' => scalar( $conf->config('company_name', $agentnum) ), 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n", + 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ), 'returnaddress' => $returnaddress, 'agent' => &$escape_function($cust_main->agent->agent), #invoice info 'invnum' => $self->invnum, 'date' => time2str($date_format, $self->_date), - 'today' => time2str('%b %o, %Y', $today), + 'today' => time2str($date_format_long, $today), 'terms' => $self->terms, 'template' => $template, #params{'template'}, 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function? @@ -2379,6 +2578,21 @@ sub print_generic { 'total_pages' => 1, ); + + my $min_sdate = 999999999999; + my $max_edate = 0; + foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { + next unless $cust_bill_pkg->pkgnum > 0; + $min_sdate = $cust_bill_pkg->sdate + if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate; + $max_edate = $cust_bill_pkg->edate + if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate; + } + + $invoice_data{'bill_period'} = ''; + $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate) + . " to " . time2str('%e %h', $max_edate) + if ($max_edate != 0 && $min_sdate != 999999999999); $invoice_data{finance_section} = ''; if ( $conf->config('finance_pkgclass') ) { @@ -2430,6 +2644,12 @@ sub print_generic { $invoice_data{'logo_file'} = $params{'logo_file'} if $params{'logo_file'}; + $invoice_data{'barcode_file'} = $params{'barcode_file'} + if $params{'barcode_file'}; + $invoice_data{'barcode_img'} = $params{'barcode_img'} + if $params{'barcode_img'}; + $invoice_data{'barcode_cid'} = $params{'barcode_cid'} + if $params{'barcode_cid'}; my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits @@ -2446,7 +2666,9 @@ sub print_generic { } $invoice_data{'summarypage'} = $summarypage; - #do variable substitution in notes, footer, smallfooter + warn "$me substituting variables in notes, footer, smallfooter\n" + if $DEBUG > 1; + foreach my $include (qw( notes footer smallfooter coupon )) { my $inc_file = $conf->key_orbase("invoice_${format}$include", $template); @@ -2517,12 +2739,15 @@ sub print_generic { $invoice_data{'buf'} = \@buf; $invoice_data{'sections'} = \@sections; + warn "$me generating sections\n" + if $DEBUG > 1; + my $previous_section = { 'description' => 'Previous Charges', 'subtotal' => $other_money_char. sprintf('%.2f', $pr_total), 'summarized' => $summarypage ? 'Y' : '', }; - $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '. + $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. join(' / ', map { $cust_main->balance_date_range(@$_) } $self->_prior_month30s ) @@ -2559,7 +2784,7 @@ sub print_generic { my $extra_lines = (); if ( $multisection ) { ($extra_sections, $extra_lines) = - $self->_items_extra_usage_sections($escape_function, $format) + $self->_items_extra_usage_sections($escape_function_nonbsp, $format) if $conf->exists('usage_class_as_a_section', $cust_main->agentnum); push @$extra_sections, $adjust_section if $adjust_section->{sort_weight}; @@ -2568,13 +2793,13 @@ sub print_generic { push @sections, $self->_items_sections( $late_sections, # this could stand a refactor $summarypage, - $escape_function, + $escape_function_nonbsp, $extra_sections, $format, #bah ); if ($conf->exists('svc_phone_sections')) { my ($phone_sections, $phone_lines) = - $self->_items_svc_phone_sections($escape_function, $format); + $self->_items_svc_phone_sections($escape_function_nonbsp, $format); push @{$late_sections}, @$phone_sections; push @detail_items, @$phone_lines; } @@ -2587,6 +2812,9 @@ sub print_generic { ) { + warn "$me adding previous balances\n" + if $DEBUG > 1; + foreach my $line_item ( $self->_items_previous ) { my $detail = { @@ -2612,16 +2840,31 @@ sub print_generic { } } - + 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, ['','']; } + + if ( $conf->exists('svc_phone-did-summary') ) { + warn "$me adding DID summary\n" + if $DEBUG > 1; + + my ($didsummary,$minutes) = $self->_did_summary; + my $didsummary_desc = 'DID Activity Summary (since last invoice)'; + push @detail_items, + { 'description' => $didsummary_desc, + 'ext_description' => [ $didsummary, $minutes ], + }; + } foreach my $section (@sections, @$late_sections) { + warn "$me adding section \n". Dumper($section) + if $DEBUG > 1; + # begin some normalization $section->{'subtotal'} = $section->{'amount'} if $multisection @@ -2647,6 +2890,9 @@ sub print_generic { ); } + warn "$me setting options\n" + if $DEBUG > 1; + my $multilocation = scalar($cust_main->cust_location); #too expensive? my %options = (); $options{'section'} = $section if $multisection; @@ -2660,7 +2906,14 @@ sub print_generic { $options{'multilocation'} = $multilocation; $options{'multisection'} = $multisection; + warn "$me searching for line items\n" + if $DEBUG > 1; + foreach my $line_item ( $self->_items_pkg(%options) ) { + + warn "$me adding line item $line_item\n" + if $DEBUG > 1; + my $detail = { ext_description => [], }; @@ -2706,6 +2959,9 @@ sub print_generic { unshift @sections, $previous_section if $pr_total; } + warn "$me adding taxes\n" + if $DEBUG > 1; + foreach my $tax ( $self->_items_tax ) { $taxtotal += $tax->{'amount'}; @@ -2892,6 +3148,26 @@ sub print_generic { push @buf,[$self->balance_due_msg, $money_char. sprintf("%10.2f", $balance_due ) ]; } + + if ( $conf->exists('previous_balance-show_credit') + and $cust_main->balance < 0 ) { + my $credit_total = { + 'total_item' => &$embolden_function($self->credit_balance_msg), + 'total_amount' => &$embolden_function( + $other_money_char. sprintf('%.2f', -$cust_main->balance) + ), + }; + if ( $multisection ) { + $adjust_section->{'posttotal'} .= $newline_token . + $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'}; + } + else { + push @total_items, $credit_total; + } + push @buf,['','-----------']; + push @buf,[$self->credit_balance_msg, $money_char. + sprintf("%10.2f", -$cust_main->balance ) ]; + } } if ( $multisection ) { @@ -3039,10 +3315,10 @@ I, if specified, overrides "Invoice" as the name of the sent docume sub print_ps { my $self = shift; - my ($file, $lfile) = $self->print_latex(@_); + my ($file, $logofile, $barcodefile) = $self->print_latex(@_); my $ps = generate_ps($file); - unlink($file.'.tex'); - unlink($lfile); + unlink($logofile); + unlink($barcodefile) if $barcodefile; $ps; } @@ -3068,10 +3344,10 @@ I, if specified, overrides "Invoice" as the name of the sent docume sub print_pdf { my $self = shift; - my ($file, $lfile) = $self->print_latex(@_); + my ($file, $logofile, $barcodefile) = $self->print_latex(@_); my $pdf = generate_pdf($file); - unlink($file.'.tex'); - unlink($lfile); + unlink($logofile); + unlink($barcodefile) if $barcodefile; $pdf; } @@ -3106,7 +3382,7 @@ sub print_html { } $params{'format'} = 'html'; - + $self->print_generic( %params ); } @@ -3126,6 +3402,18 @@ sub _latex_escape { $value; } +sub _html_escape { + my $value = shift; + encode_entities($value); + $value; +} + +sub _html_escape_nbsp { + my $value = _html_escape(shift); + $value =~ s/ +/ /g; + $value; +} + #utility methods for print_* sub _translate_old_latex_format { @@ -3239,6 +3527,8 @@ sub balance_due_date { $duedate; } +sub credit_balance_msg { 'Credit Balance Remaining' } + =item invnum_date_pretty Returns a string with the invoice number and date, for example: @@ -3380,7 +3670,7 @@ sub _items_sections { } } @sections; push @early, @$extra_sections if $extra_sections; - + sort { $a->{sort_weight} <=> $b->{sort_weight} } @early; } @@ -3742,6 +4032,80 @@ sub _items_extra_usage_sections { } +sub _did_summary { + my $self = shift; + my $end = $self->_date; + + # start at date of previous invoice + 1 second or 0 if no previous invoice + my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum); + $start = 0 if !$start; + $start++; + + my $cust_main = $self->cust_main; + my @pkgs = $cust_main->all_pkgs; + my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes) + = (0,0,0,0,0); + my @seen = (); + foreach my $pkg ( @pkgs ) { + my @h_cust_svc = $pkg->h_cust_svc($end); + foreach my $h_cust_svc ( @h_cust_svc ) { + next if grep {$_ eq $h_cust_svc->svcnum} @seen; + next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone'; + + my $inserted = $h_cust_svc->date_inserted; + my $deleted = $h_cust_svc->date_deleted; + my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5); + my $phone_deleted; + $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted; + +# DID either activated or ported in; cannot be both for same DID simultaneously + if ($inserted >= $start && $inserted <= $end && $phone_inserted + && (!$phone_inserted->lnp_status + || $phone_inserted->lnp_status eq '' + || $phone_inserted->lnp_status eq 'native')) { + $num_activated++; + } + else { # this one not so clean, should probably move to (h_)svc_phone + my $phone_portedin = qsearchs( 'h_svc_phone', + { 'svcnum' => $h_cust_svc->svcnum, + 'lnp_status' => 'portedin' }, + FS::h_svc_phone->sql_h_searchs($end), + ); + $num_portedin++ if $phone_portedin; + } + +# DID either deactivated or ported out; cannot be both for same DID simultaneously + if($deleted >= $start && $deleted <= $end && $phone_deleted + && (!$phone_deleted->lnp_status + || $phone_deleted->lnp_status ne 'portingout')) { + $num_deactivated++; + } + elsif($deleted >= $start && $deleted <= $end && $phone_deleted + && $phone_deleted->lnp_status + && $phone_deleted->lnp_status eq 'portingout') { + $num_portedout++; + } + + # increment usage minutes + if ( $phone_inserted ) { + my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1); + $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1; + } + else { + warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum; + } + + # don't look at this service again + push @seen, $h_cust_svc->svcnum; + } + } + + $minutes = sprintf("%d", $minutes); + ("Activated: $num_activated Ported-In: $num_portedin Deactivated: " + . "$num_deactivated Ported-Out: $num_portedout ", + "Total Minutes: $minutes"); +} + sub _items_svc_phone_sections { my $self = shift; my $escape = shift; @@ -3886,6 +4250,85 @@ sub _items_svc_phone_sections { push @lines, $l; } } + + if($conf->exists('phone_usage_class_summary')) { + # this only works with Latex + my @newlines; + my @newsections; + + # after this, we'll have only two sections per DID: + # Calls Summary and Calls Detail + foreach my $section ( @sections ) { + if($section->{'post_total'}) { + $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'}; + $section->{'total_line_generator'} = sub { '' }; + $section->{'total_generator'} = sub { '' }; + $section->{'header_generator'} = sub { '' }; + $section->{'description_generator'} = ''; + push @newsections, $section; + my %calls_detail = %$section; + $calls_detail{'post_total'} = ''; + $calls_detail{'sort_weight'} = ''; + $calls_detail{'description_generator'} = sub { '' }; + $calls_detail{'header_generator'} = sub { + return ' & Date/Time & Called Number & Duration & Price' + if $format eq 'latex'; + ''; + }; + $calls_detail{'description'} = 'Calls Detail: ' + . $section->{'phonenum'}; + push @newsections, \%calls_detail; + } + } + + # after this, each usage class is collapsed/summarized into a single + # line under the Calls Summary section + foreach my $newsection ( @newsections ) { + if($newsection->{'post_total'}) { # this means Calls Summary + foreach my $section ( @sections ) { + next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} + && !$section->{'post_total'}); + my $newdesc = $section->{'description'}; + my $tn = $section->{'phonenum'}; + $newdesc =~ s/$tn//g; + my $line = { ext_description => [], + pkgnum => '', + ref => '', + quantity => '', + calls => $section->{'calls'}, + section => $newsection, + duration => $section->{'duration'}, + description => $newdesc, + amount => sprintf("%.2f",$section->{'amount'}), + product_code => 'N/A', + }; + push @newlines, $line; + } + } + } + + # after this, Calls Details is populated with all CDRs + foreach my $newsection ( @newsections ) { + if(!$newsection->{'post_total'}) { # this means Calls Details + foreach my $line ( @lines ) { + next unless (scalar(@{$line->{'ext_description'}}) && + $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'} + ); + my @extdesc = @{$line->{'ext_description'}}; + my @newextdesc; + foreach my $extdesc ( @extdesc ) { + $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex'; + push @newextdesc, $extdesc; + } + $line->{'ext_description'} = \@newextdesc; + $line->{'section'} = $newsection; + push @newlines, $line; + } + } + } + + return(\@newsections, \@newlines); + } return(\@sections, \@lines); @@ -3943,9 +4386,21 @@ sub _items_previous { sub _items_pkg { my $self = shift; my %options = @_; + + warn "$me _items_pkg searching for all package line items\n" + if $DEBUG > 1; + my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg; + + warn "$me _items_pkg filtering line items\n" + if $DEBUG > 1; my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_); + if ($options{section} && $options{section}->{condensed}) { + + warn "$me _items_pkg condensing section\n" + if $DEBUG > 1; + my %itemshash = (); local $Storable::canonical = 1; foreach ( @items ) { @@ -3965,6 +4420,10 @@ sub _items_pkg { } keys %itemshash; } + + warn "$me _items_pkg returning ". scalar(@items). " items\n" + if $DEBUG > 1; + @items; } @@ -3985,7 +4444,7 @@ sub _items_tax { sub _items_cust_bill_pkg { my $self = shift; - my $cust_bill_pkg = shift; + my $cust_bill_pkgs = shift; my %opt = @_; my $format = $opt{format} || ''; @@ -3996,22 +4455,16 @@ sub _items_cust_bill_pkg { my $summary_page = $opt{summary_page} || ''; my $multilocation = $opt{multilocation} || ''; my $multisection = $opt{multisection} || ''; + my $discount_show_always = 0; my @b = (); my ($s, $r, $u) = ( undef, undef, undef ); - foreach my $cust_bill_pkg ( @$cust_bill_pkg ) + foreach my $cust_bill_pkg ( @$cust_bill_pkgs ) { - foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) { - if ( $_ && !$cust_bill_pkg->hidden ) { - $_->{amount} = sprintf( "%.2f", $_->{amount} ), - $_->{amount} =~ s/^\-0\.00$/0.00/; - $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ), - push @b, { %$_ } - unless $_->{amount} == 0; - $_ = undef; - } - } + warn "$me _items_cust_bill_pkg considering cust_bill_pkg ". + $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n" + if $DEBUG > 1; foreach my $display ( grep { defined($section) ? $_->section eq $section @@ -4023,6 +4476,9 @@ sub _items_cust_bill_pkg { ) { + warn "$me _items_cust_bill_pkg considering display item $display\n" + if $DEBUG > 1; + my $type = $display->type; my $desc = $cust_bill_pkg->desc; @@ -4036,26 +4492,45 @@ sub _items_cust_bill_pkg { if ( $cust_bill_pkg->pkgnum > 0 ) { + warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n" + if $DEBUG > 1; + my $cust_pkg = $cust_bill_pkg->cust_pkg; - if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) { + if ( (!$type || $type eq 'S') + && ( $cust_bill_pkg->setup != 0 + || $cust_bill_pkg->setup_show_zero + ) + ) + { + + warn "$me _items_cust_bill_pkg adding setup\n" + if $DEBUG > 1; my $description = $desc; - $description .= ' Setup' if $cust_bill_pkg->recur != 0; + $description .= ' Setup' + if $cust_bill_pkg->recur != 0 + || $discount_show_always + || $cust_bill_pkg->recur_show_zero; my @d = (); unless ( $cust_pkg->part_pkg->hide_svc_detail || $cust_bill_pkg->hidden ) { + push @d, map &{$escape_function}($_), - $cust_pkg->h_labels_short($self->_date); + $cust_pkg->h_labels_short($self->_date, undef, 'I') + unless $cust_bill_pkg->pkgpart_override; #don't redisplay services + if ( $multilocation ) { my $loc = $cust_pkg->location_label; - $loc = substr($desc, 0, 50). '...' + $loc = substr($loc, 0, 50). '...' if $format eq 'latex' && length($loc) > 50; push @d, &{$escape_function}($loc); } + } + push @d, $cust_bill_pkg->details(%details_opt) if $cust_bill_pkg->recur == 0; @@ -4065,6 +4540,7 @@ sub _items_cust_bill_pkg { push @{ $s->{ext_description} }, @d; } else { $s = { + _is_setup => 1, description => $description, #pkgpart => $part_pkg->pkgpart, pkgnum => $cust_bill_pkg->pkgnum, @@ -4077,19 +4553,28 @@ sub _items_cust_bill_pkg { } - if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ) && - ( !$type || $type eq 'R' || $type eq 'U' ) + if ( ( !$type || $type eq 'R' || $type eq 'U' ) + && ( + $cust_bill_pkg->recur != 0 + || $cust_bill_pkg->setup == 0 + || $discount_show_always + || $cust_bill_pkg->recur_show_zero + ) ) { + warn "$me _items_cust_bill_pkg adding recur/usage\n" + if $DEBUG > 1; + my $is_summary = $display->summary; my $description = ($is_summary && $type && $type eq 'U') ? "Usage charges" : $desc; - unless ( $conf->exists('disable_line_item_date_ranges') ) { - $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate). - " - ". time2str($date_format, $cust_bill_pkg->edate). ")"; - } + $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate). + " - ". time2str($date_format, $cust_bill_pkg->edate). + ")" + unless $conf->exists('disable_line_item_date_ranges') + || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1); my @d = (); @@ -4098,39 +4583,64 @@ sub _items_cust_bill_pkg { my @dates = ( $self->_date ); my $prev = $cust_bill_pkg->previous_cust_bill_pkg; push @dates, $prev->sdate if $prev; + push @dates, undef if !$prev; unless ( $cust_pkg->part_pkg->hide_svc_detail || $cust_bill_pkg->itemdesc || $cust_bill_pkg->hidden || $is_summary && $type && $type eq 'U' ) { + + warn "$me _items_cust_bill_pkg adding service details\n" + if $DEBUG > 1; + push @d, map &{$escape_function}($_), - $cust_pkg->h_labels_short(@dates) + $cust_pkg->h_labels_short(@dates, 'I') #$cust_bill_pkg->edate, #$cust_bill_pkg->sdate) - ; + unless $cust_bill_pkg->pkgpart_override; #don't redisplay services + + warn "$me _items_cust_bill_pkg done adding service details\n" + if $DEBUG > 1; + if ( $multilocation ) { my $loc = $cust_pkg->location_label; - $loc = substr($desc, 0, 50). '...' + $loc = substr($loc, 0, 50). '...' if $format eq 'latex' && length($loc) > 50; push @d, &{$escape_function}($loc); } + } - push @d, $cust_bill_pkg->details(%details_opt) - unless ($is_summary || $type && $type eq 'R'); + unless ( $is_summary ) { + warn "$me _items_cust_bill_pkg adding details\n" + if $DEBUG > 1; + + #instead of omitting details entirely in this case (unwanted side + # effects), just omit CDRs + $details_opt{'format_function'} = sub { () } + if $type && $type eq 'R'; + + push @d, $cust_bill_pkg->details(%details_opt); + } + + warn "$me _items_cust_bill_pkg calculating amount\n" + if $DEBUG > 1; my $amount = 0; if (!$type) { $amount = $cust_bill_pkg->recur; - }elsif($type eq 'R') { + } elsif ($type eq 'R') { $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage; - }elsif($type eq 'U') { + } elsif ($type eq 'U') { $amount = $cust_bill_pkg->usage; } if ( !$type || $type eq 'R' ) { + warn "$me _items_cust_bill_pkg adding recur\n" + if $DEBUG > 1; + if ( $cust_bill_pkg->hidden ) { $r->{amount} += $amount; $r->{unit_amount} += $cust_bill_pkg->unitrecur; @@ -4149,6 +4659,9 @@ sub _items_cust_bill_pkg { } else { # $type eq 'U' + warn "$me _items_cust_bill_pkg adding usage\n" + if $DEBUG > 1; + if ( $cust_bill_pkg->hidden ) { $u->{amount} += $amount; $u->{unit_amount} += $cust_bill_pkg->unitrecur; @@ -4171,6 +4684,9 @@ sub _items_cust_bill_pkg { } else { #pkgnum tax or one-shot line item (??) + warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n" + if $DEBUG > 1; + if ( $cust_bill_pkg->setup != 0 ) { push @b, { 'description' => $desc, @@ -4190,18 +4706,40 @@ sub _items_cust_bill_pkg { } - } + $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount + && $conf->exists('discount-show-always')); - foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) { - if ( $_ ) { - $_->{amount} = sprintf( "%.2f", $_->{amount} ), - $_->{amount} =~ s/^\-0\.00$/0.00/; - $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ), - push @b, { %$_ } - unless $_->{amount} == 0; + foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) { + if ( $_ && !$cust_bill_pkg->hidden ) { + $_->{amount} = sprintf( "%.2f", $_->{amount} ), + $_->{amount} =~ s/^\-0\.00$/0.00/; + $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ), + push @b, { %$_ } + if $_->{amount} != 0 + || $discount_show_always + || ( ! $_->{_is_setup} && $cust_bill_pkg->recur_show_zero ) + || ( $_->{_is_setup} && $cust_bill_pkg->setup_show_zero ) + ; + $_ = undef; + } } + } + #foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) { + # if ( $_ ) { + # $_->{amount} = sprintf( "%.2f", $_->{amount} ), + # $_->{amount} =~ s/^\-0\.00$/0.00/; + # $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ), + # push @b, { %$_ } + # if $_->{amount} != 0 + # || $discount_show_always + # } + #} + + warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n" + if $DEBUG > 1; + @b; } @@ -4462,6 +5000,25 @@ sub credited_sql { WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )"; } +=item due_date_sql + +Returns an SQL fragment to retrieve the due date of an invoice. +Currently only supported on PostgreSQL. + +=cut + +sub due_date_sql { +'COALESCE( + SUBSTRING( + COALESCE( + cust_bill.invoice_terms, + cust_main.invoice_terms, + \''.($conf->config('invoice_default_terms') || '').'\' + ), E\'Net (\\\\d+)\' + )::INTEGER, 0 +) * 86400 + cust_bill._date' +} + =item search_sql_where HASHREF Class method which returns an SQL WHERE fragment to search for parameters