X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=4acdd85a3e0a1ef3a8a4ba878178fae2008cab9e;hb=4396080ed2829ae0595f1fd777f39d090c9bcd7c;hp=6fac0a946257dcae0c8f16ef1b402ece1cf4367b;hpb=4445165860112be241b1a51c50436ad0828a297a;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 6fac0a946..4acdd85a3 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -235,6 +235,25 @@ sub cust_bill_pkg { ); } +=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 Returns the packages (see L) corresponding to the line items for @@ -432,6 +451,38 @@ sub cust_credited { ; } +=item cust_bill_pay_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 + +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. @@ -465,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 @@ -488,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 = ''; @@ -525,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; @@ -605,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 { @@ -643,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) ]; } } @@ -690,7 +776,11 @@ sub generate_email { ' ', ' ', ' ', - $self->print_html('', $args{'template'}, $content_id), + $self->print_html({ time => '', + template => $args{'template'}, + cid => $content_id, + %cdrs, + }), ' ', '', ], @@ -698,6 +788,18 @@ 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 ], + 'Disposition' => 'attachment', + ; + + } + if ( $conf->exists('invoice_email_pdf') ) { #attaching pdf too: @@ -723,9 +825,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 { @@ -737,7 +839,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'; @@ -751,7 +853,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) } ]; } @@ -771,7 +873,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) ]; } } @@ -796,7 +898,7 @@ sub mimebuild_pdf { 'Encoding' => 'base64', 'Data' => [ $self->print_pdf(@_) ], 'Disposition' => 'attachment', - 'Filename' => 'invoice.pdf', + 'Filename' => 'invoice-'. $self->invnum. '.pdf', ); } @@ -1457,11 +1559,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( @@ -1613,11 +1713,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 ); } @@ -1638,11 +1739,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; @@ -2280,7 +2382,8 @@ sub print_generic { # credits my $credittotal = 0; - foreach my $credit ( $self->_items_credits ) { + foreach my $credit ( $self->_items_credits('trim_len'=>60) ) { + my $total; $total->{'total_item'} = &$escape_function($credit->{'description'}); $credittotal += $credit->{'amount'}; @@ -2297,26 +2400,18 @@ sub print_generic { product_code => '', section => $adjust_section, }; - }else{ + } else { push @total_items, $total; } + } $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal); - - # credits (again) - foreach ( $self->cust_credited ) { - - #something more elaborate if $_->amount ne $_->cust_credit->credited ? - - 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) - ]; - } + #credits (again) + foreach my $credit ( $self->_items_credits('trim_len'=>32) ) { + push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ]; + } + # payments my $paymenttotal = 0; foreach my $payment ( $self->_items_payments ) { @@ -2585,8 +2680,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 { @@ -2802,8 +2897,19 @@ sub _items_cust_bill_pkg { my $section = $opt{section}->{description} if $opt{section}; my @b = (); + my ($s, $r, $u) = ( undef, undef, undef ); foreach my $cust_bill_pkg ( @$cust_bill_pkg ) { + + foreach ( $s, $r, $u ) { + if ( $_ && !$cust_bill_pkg->hidden ) { + $_->{amount} = sprintf( "%.2f", $_->{amount} ), + $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ), + push @b, { %$_ }; + $_ = undef; + } + } + foreach my $display ( grep { defined($section) ? $_->section eq $section : 1 @@ -2832,19 +2938,28 @@ sub _items_cust_bill_pkg { my $description = $desc; $description .= ' Setup' if $cust_bill_pkg->recur != 0; - my @d = map &{$escape_function}($_), - $cust_pkg->h_labels_short($self->_date); + 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; - push @b, { - description => $description, - #pkgpart => $part_pkg->pkgpart, - pkgnum => $cust_bill_pkg->pkgnum, - amount => sprintf("%.2f", $cust_bill_pkg->setup), - unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup), - quantity => $cust_bill_pkg->quantity, - ext_description => \@d, + 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, + }; }; } @@ -2862,16 +2977,23 @@ sub _items_cust_bill_pkg { " - ". 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 @d = (); + 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($self->_date) - #$cust_bill_pkg->edate, - #$cust_bill_pkg->sdate), - ; - - @d = () if ($cust_bill_pkg->itemdesc || $is_summary); + $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'); @@ -2884,17 +3006,45 @@ sub _items_cust_bill_pkg { $amount = $cust_bill_pkg->usage; } - push @b, { - description => $description, - #pkgpart => $part_pkg->pkgpart, - pkgnum => $cust_bill_pkg->pkgnum, - amount => sprintf("%.2f", $amount), - unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur), - quantity => $cust_bill_pkg->quantity, - ext_description => \@d, - } unless ( $type eq 'U' && ! $amount ); + 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 (??) @@ -2919,12 +3069,21 @@ sub _items_cust_bill_pkg { } + 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 @@ -2932,10 +3091,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) .")". @@ -2945,12 +3104,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; @@ -2976,6 +3129,22 @@ sub _items_payments { } +=item call_details + +Returns an array of CSV strings representing the call details for this invoice + +=cut + +sub call_details { + my $self = shift; + map { $_->details( 'format_function' => sub{ shift }, + 'escape_function' => sub{ return() }, + ) + } + grep { $_->pkgnum } + $self->cust_bill_pkg; +} + =back