X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=62ab87f0fae5f8ab33c53abb9e34fac224b4cb05;hp=28a7257cd1995bf6ea3202a215953b5a7585c8a9;hb=0f7643c1af2d909e0c3172e5bec0c01855fca1b9;hpb=06a85a88bfdb0d3fc79ee055eb8327658dfe63ab diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 28a7257cd..62ab87f0f 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1,7 +1,8 @@ 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 List::Util qw(min max); @@ -34,6 +35,8 @@ use FS::cust_bill_pay; use FS::cust_bill_pay_batch; use FS::part_bill_event; use FS::payby; +use FS::bill_batch; +use FS::cust_bill_batch; @ISA = qw( FS::cust_main_Mixin FS::Record ); @@ -43,9 +46,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 @@ -160,6 +164,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 @@ -222,13 +265,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 @@ -239,11 +282,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; ''; } @@ -356,11 +399,24 @@ this invoice. sub cust_pkg { my $self = shift; - my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg; + my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () } + $self->cust_bill_pkg; my %saw = (); grep { ! $saw{$_->pkgnum}++ } @cust_pkg; } +=item no_auto + +Returns true if any of the packages (or their definitions) corresponding to the +line items for this invoice have the no_auto flag set. + +=cut + +sub no_auto { + my $self = shift; + grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg; +} + =item open_cust_bill_pkg Returns the open line items for this invoice. @@ -1185,8 +1241,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); @@ -1287,7 +1349,13 @@ sub print { 'notice_name' => $notice_name, ); - do_print $self->lpr_data(\%opt); + if($conf->exists('invoice_print_pdf')) { + # Add the invoice to the current batch. + $self->batch_invoice(\%opt); + } + else { + do_print $self->lpr_data(\%opt); + } } =item fax_invoice HASHREF | [ TEMPLATE ] @@ -1333,6 +1401,23 @@ sub fax_invoice { } +=item batch_invoice [ HASHREF ] + +Place this invoice into the open batch (see C). If there +isn't an open batch, one will be created. + +=cut + +sub batch_invoice { + my ($self, $opt) = @_; + my $batch = FS::bill_batch->get_open_batch; + my $cust_bill_batch = FS::cust_bill_batch->new({ + batchnum => $batch->batchnum, + invnum => $self->invnum, + }); + return $cust_bill_batch->insert($opt); +} + =item ftp_invoice [ TEMPLATENAME ] Sends this invoice data via FTP. @@ -1873,7 +1958,8 @@ sub realtime_lec { } sub realtime_bop { - my( $self, $method ) = @_; + my( $self, $method ) = (shift,shift); + my %opt = @_; my $cust_main = $self->cust_main; my $balance = $cust_main->balance; @@ -1899,6 +1985,15 @@ sub realtime_bop { $cust_main->realtime_bop($method, $amount, 'description' => $description, 'invnum' => $self->invnum, +#this didn't do what we want, it just calls apply_payments_and_credits +# 'apply' => 1, + 'apply_to_invoice' => 1, + %opt, + #what we want: + #this changes application behavior: auto payments + #triggered against a specific invoice are now applied + #to that invoice instead of oldest open. + #seem okay to me... ); } @@ -2206,15 +2301,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(). '}' @@ -2278,18 +2377,20 @@ sub print_generic { } + my $agentnum = $self->cust_main->agentnum; + my %invoice_data = ( #invoice from info - 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ), - 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n", + 'company_name' => scalar( $conf->config('company_name', $agentnum) ), + 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n", '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? @@ -2308,7 +2409,21 @@ sub print_generic { 'unitprices' => $conf->exists('invoice-unitprice'), 'smallernotes' => $conf->exists('invoice-smallernotes'), 'smallerfooter' => $conf->exists('invoice-smallerfooter'), + 'balance_due_below_line' => $conf->exists('balance_due_below_line'), + #layout info -- would be fancy to calc some of this and bury the template + # here in the code + 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)), + 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)), + 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)), + 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)), + 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)), + 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum), + 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)), + 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)), + 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)), + 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum), + # better hang on to conf_dir for a while (for old templates) 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc", @@ -2324,7 +2439,8 @@ sub print_generic { qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') }); $invoice_data{finance_section} = $pkg_class->categoryname; } - $invoice_data{finance_amount} = '0.00'; + $invoice_data{finance_amount} = '0.00'; + $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion my $countrydefault = $conf->config('countrydefault') || 'US'; my $prefix = $cust_main->has_ship_address ? 'ship_' : ''; @@ -2377,8 +2493,6 @@ sub print_generic { $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total); $invoice_data{'balance'} = sprintf("%.2f", $balance_due); - my $agentnum = $self->cust_main->agentnum; - my $summarypage = ''; if ( $conf->exists('invoice_usesummary', $agentnum) ) { $summarypage = 1; @@ -2492,12 +2606,13 @@ sub print_generic { my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y'; my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum); + $invoice_data{'multisection'} = $multisection; my $late_sections = []; my $extra_sections = []; 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}; @@ -2506,13 +2621,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; } @@ -2557,9 +2672,25 @@ sub print_generic { $money_char. sprintf("%10.2f", $pr_total) ]; push @buf, ['','']; } + + if ( $conf->exists('svc_phone-did-summary') ) { + my ($didsummary,$minutes) = $self->_did_summary; + my $didsummary_desc = 'DID Activity Summary (Past 30 days)'; + push @detail_items, + { 'description' => $didsummary_desc, + 'ext_description' => [ $didsummary, $minutes ], + } + if !$multisection; + } foreach my $section (@sections, @$late_sections) { + # begin some normalization + $section->{'subtotal'} = $section->{'amount'} + if $multisection + && !exists($section->{subtotal}) + && exists($section->{amount}); + $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} ) if ( $invoice_data{finance_section} && $section->{'description'} eq $invoice_data{finance_section} ); @@ -2568,7 +2699,7 @@ sub print_generic { sprintf('%.2f', $section->{'subtotal'}) if $multisection; - # begin some normalization + # continue some normalization $section->{'amount'} = $section->{'subtotal'} if $multisection; @@ -2590,6 +2721,7 @@ sub print_generic { $options{'skip_usage'} = scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections; $options{'multilocation'} = $multilocation; + $options{'multisection'} = $multisection; foreach my $line_item ( $self->_items_pkg(%options) ) { my $detail = { @@ -2700,17 +2832,19 @@ sub print_generic { { my $total = {}; - $total->{'total_item'} = &$embolden_function('Total'); + my $item = 'Total'; + $item = $conf->config('previous_balance-exclude_from_total') + || 'Total New Charges' + if $conf->exists('previous_balance-exclude_from_total'); + my $amount = $self->charged + + ( $conf->exists('disable_previous_balance') || + $conf->exists('previous_balance-exclude_from_total') + ? 0 + : $pr_total + ); + $total->{'total_item'} = &$embolden_function($item); $total->{'total_amount'} = - &$embolden_function( - $other_money_char. - sprintf( '%.2f', - $self->charged + ( $conf->exists('disable_previous_balance') - ? 0 - : $pr_total - ) - ) - ); + &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) ); if ( $multisection ) { if ( $adjust_section->{'sort_weight'} ) { $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char. @@ -2723,14 +2857,9 @@ sub print_generic { push @total_items, $total; } push @buf,['','-----------']; - push @buf,['Total Charges', + push @buf,[$item, $money_char. - sprintf( '%10.2f', $self->charged + - ( $conf->exists('disable_previous_balance') - ? 0 - : $pr_total - ) - ) + sprintf( '%10.2f', $amount ) ]; push @buf,['','']; } @@ -3058,6 +3187,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 { @@ -3292,6 +3433,7 @@ sub _items_sections { if ( $summarypage ) { @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled } map { $_->categoryname } qsearch('pkg_category', {}); + push @sections, '' if exists($subtotal{''}); } else { @sections = keys %subtotal; } @@ -3333,7 +3475,9 @@ my %condensed_format = ( 'fields' => [ sub { shift->{description} }, sub { shift->{quantity} }, - sub { shift->{amount} }, + sub { my($href, %opt) = @_; + ($opt{dollar} || ''). $href->{amount}; + }, ], 'align' => [ qw( l r r ) ], 'span' => [ qw( 5 1 1 ) ], # unitprices? @@ -3407,6 +3551,7 @@ sub _condensed_description_generator { my ( $f, $prefix, $suffix, $separator, $column ) = _condensed_generator_defaults($format); + my $money_char = '$'; if ($format eq 'latex') { $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n"; $suffix = '\\\\'; @@ -3415,6 +3560,7 @@ sub _condensed_description_generator { sub { my ($d,$a,$s,$w) = @_; return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}"; }; + $money_char = '\\dollar'; }elsif ( $format eq 'html' ) { $prefix = '">'; $suffix = ''; @@ -3423,16 +3569,22 @@ sub _condensed_description_generator { sub { my ($d,$a,$s,$w) = @_; return qq!$d!; }; + #$money_char = $conf->config('money_char') || '$'; + $money_char = ''; # this is madness } sub { - my @args = @_; + #my @args = @_; + my $href = shift; my @result = (); foreach (my $i = 0; $f->{label}->[$i]; $i++) { - push @result, &{$column}( &{$f->{fields}->[$i]}(@args), - map { $f->{$_}->[$i] } qw(align span width) - ); + my $dollar = ''; + $dollar = $money_char if $i == scalar(@{$f->{label}})-1; + push @result, + &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar), + map { $f->{$_}->[$i] } qw(align span width) + ); } $prefix. join( $separator, @result ). $suffix; @@ -3663,6 +3815,72 @@ sub _items_extra_usage_sections { } +sub _did_summary { + my $self = shift; + my $end = $self->_date; + my $start = $end - 2592000; # 30 days + 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); + 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 + my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end); + foreach my $cdr ( @cdrs ) { + $minutes += $cdr->billsec/60; + } + + # 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; @@ -3673,10 +3891,14 @@ sub _items_svc_phone_sections { my %lines = (); my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} ); + $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 }; foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { next unless $cust_bill_pkg->pkgnum > 0; + my @header = $cust_bill_pkg->details_header; + next unless scalar(@header); + foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) { my $phonenum = $detail->phonenum; @@ -3725,6 +3947,7 @@ sub _items_svc_phone_sections { 'duration' => 0, 'sort_weight' => $usage_class{$detail->classnum}->weight, 'phonenum' => $phonenum, + 'header' => [ @header ], }; $sections{"$phonenum $line"}{amount} += $amount; #subtotal $sections{"$phonenum $line"}{calls}++; @@ -3755,11 +3978,17 @@ sub _items_svc_phone_sections { my %sectionmap = (); my $simple = new FS::usage_class { format => 'simple' }; #bleh - my $usage_simple = new FS::usage_class { format => 'usage_simple' }; #bleh foreach ( keys %sections ) { + my @header = @{ $sections{$_}{header} || [] }; + my $usage_simple = + new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' }; my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0; my $usage_class = $summary ? $simple : $usage_simple; my $ending = $summary ? ' usage charges' : ''; + my %gen_opt = (); + unless ($summary) { + $gen_opt{label} = [ map{ &{$escape}($_) } @header ]; + } $sectionmap{$_} = { 'description' => &{$escape}($_. $ending), 'amount' => $sections{$_}{amount}, #subtotal 'calls' => $sections{$_}{calls}, @@ -3770,7 +3999,7 @@ sub _items_svc_phone_sections { 'sort_weight' => $sections{$_}{sort_weight}, 'post_total' => $summary, #inspire pagebreak ( - ( map { $_ => $usage_class->$_($format) } + ( map { $_ => $usage_class->$_($format, %gen_opt) } qw( description_generator header_generator total_generator @@ -3879,12 +4108,12 @@ sub _items_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; + return 0 unless $a->itemdesc cmp $b->itemdesc; + return -1 if $b->itemdesc eq 'Tax'; + return 1 if $a->itemdesc eq 'Tax'; + return -1 if $b->itemdesc eq 'Other surcharges'; + return 1 if $a->itemdesc eq 'Other surcharges'; + $a->itemdesc cmp $b->itemdesc; } sub _items_tax { @@ -3905,19 +4134,24 @@ sub _items_cust_bill_pkg { my $section = $opt{section}->{description} if $opt{section}; 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 ) { + $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount + && $conf->exists('discount-show-always')); + 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; + unless ( $_->{amount} == 0 && !$discount_show_always ); $_ = undef; } } @@ -3926,7 +4160,8 @@ sub _items_cust_bill_pkg { ? $_->section eq $section : 1 } - grep { !$_->summary || !$summary_page } + #grep { !$_->summary || !$summary_page } # bunk! + grep { !$_->summary || $multisection } $cust_bill_pkg->cust_bill_pkg_display ) { @@ -3955,15 +4190,20 @@ sub _items_cust_bill_pkg { 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; @@ -3985,7 +4225,8 @@ sub _items_cust_bill_pkg { } - if ( $cust_bill_pkg->recur != 0 && + if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 || + ($discount_show_always && $cust_bill_pkg->recur == 0) ) && ( !$type || $type eq 'R' || $type eq 'U' ) ) { @@ -4006,23 +4247,27 @@ 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' ) { + 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 + 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) @@ -4055,7 +4300,7 @@ sub _items_cust_bill_pkg { }; } - } elsif ( $amount ) { # && $type eq 'U' + } else { # $type eq 'U' if ( $cust_bill_pkg->hidden ) { $u->{amount} += $amount; @@ -4106,7 +4351,7 @@ sub _items_cust_bill_pkg { $_->{amount} =~ s/^\-0\.00$/0.00/; $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ), push @b, { %$_ } - unless $_->{amount} == 0; + unless ( $_->{amount} == 0 && !$discount_show_always ); } } @@ -4321,8 +4566,10 @@ Returns an SQL fragment to retreive the amount owed (charged minus credited and =cut sub owed_sql { - my $class = shift; - 'charged - '. $class->paid_sql. ' - '. $class->credited_sql; + my ($class, $start, $end) = @_; + 'charged - '. + $class->paid_sql($start, $end). ' - '. + $class->credited_sql($start, $end); } =item net_sql @@ -4332,8 +4579,8 @@ Returns an SQL fragment to retreive the net amount (charged minus credited). =cut sub net_sql { - my $class = shift; - 'charged - '. $class->credited_sql; + my ($class, $start, $end) = @_; + 'charged - '. $class->credited_sql($start, $end); } =item paid_sql @@ -4343,9 +4590,13 @@ Returns an SQL fragment to retreive the amount paid against this invoice. =cut sub paid_sql { - #my $class = shift; + my ($class, $start, $end) = @_; + $start &&= "AND cust_bill_pay._date <= $start"; + $end &&= "AND cust_bill_pay._date > $end"; + $start = '' unless defined($start); + $end = '' unless defined($end); "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay - WHERE cust_bill.invnum = cust_bill_pay.invnum )"; + WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )"; } =item credited_sql @@ -4355,9 +4606,32 @@ Returns an SQL fragment to retreive the amount credited against this invoice. =cut sub credited_sql { - #my $class = shift; + my ($class, $start, $end) = @_; + $start &&= "AND cust_credit_bill._date <= $start"; + $end &&= "AND cust_credit_bill._date > $end"; + $start = '' unless defined($start); + $end = '' unless defined($end); "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill - WHERE cust_bill.invnum = cust_credit_bill.invnum )"; + 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