X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=ca35a443aab909645fcf98a0eba036c16aa77c22;hp=5013211872471b8beab3c366883a70bcb59b8c09;hb=f9a181e4c2e505df84de16190ee3b75011326f3f;hpb=70811e8e9f3b5e5aeb06abebeb099dc0af44a0e9 diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 501321187..ca35a443a 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -13,6 +13,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 ); @@ -37,6 +38,7 @@ use FS::part_bill_event; use FS::payby; use FS::bill_batch; use FS::cust_bill_batch; +use Cwd; @ISA = qw( FS::cust_main_Mixin FS::Record ); @@ -987,6 +989,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', @@ -1060,7 +1075,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'; @@ -2152,6 +2172,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', @@ -2163,8 +2205,37 @@ 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 ... @@ -2362,6 +2433,12 @@ 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; @@ -2475,6 +2552,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') ) { @@ -2526,6 +2618,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 @@ -2623,7 +2721,7 @@ sub print_generic { 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 ) @@ -3025,6 +3123,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 ) { @@ -3172,9 +3290,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($lfile); + unlink($logofile); + unlink($barcodefile); $ps; } @@ -3200,9 +3319,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($lfile); + unlink($logofile); + unlink($barcodefile); $pdf; } @@ -3237,7 +3357,7 @@ sub print_html { } $params{'format'} = 'html'; - + $self->print_generic( %params ); } @@ -3382,6 +3502,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: @@ -4095,6 +4217,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); @@ -4210,7 +4411,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} || ''; @@ -4225,9 +4426,12 @@ sub _items_cust_bill_pkg { 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 ) { + warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n" + if $DEBUG > 1; + $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount && $conf->exists('discount-show-always')); @@ -4252,6 +4456,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; @@ -4265,10 +4472,16 @@ 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') ) { + warn "$me _items_cust_bill_pkg adding setup\n" + if $DEBUG > 1; + my $description = $desc; $description .= ' Setup' if $cust_bill_pkg->recur != 0; @@ -4317,14 +4530,17 @@ sub _items_cust_bill_pkg { ) { + 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'); my @d = (); @@ -4341,12 +4557,18 @@ sub _items_cust_bill_pkg { || $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, '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($loc, 0, 50). '...' @@ -4356,20 +4578,35 @@ sub _items_cust_bill_pkg { } - 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; @@ -4388,6 +4625,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; @@ -4410,6 +4650,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, @@ -4431,6 +4674,9 @@ sub _items_cust_bill_pkg { } + warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n" + if $DEBUG > 1; + foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) { if ( $_ ) { $_->{amount} = sprintf( "%.2f", $_->{amount} ),