X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=35ce48c35ebfe665d4a2414f9528c61e7aae090f;hp=0a1577f63ad02bb51a4f02d88806896c493ec600;hb=395cc72629d31c8dcd138acf423e66d2d73d89d2;hpb=f306f5bf02319e5d89fb603ae34a900916e8cba5 diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 0a1577f63..35ce48c35 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -43,6 +43,7 @@ use FS::bill_batch; use FS::cust_bill_batch; use FS::cust_bill_pay_pkg; use FS::cust_credit_bill_pkg; +use FS::discount_plan; use FS::L10N; @ISA = qw( FS::cust_main_Mixin FS::Record ); @@ -143,6 +144,8 @@ Specific use cases =item agent_invid - legacy invoice number +=item promised_date - customer promised payment date, for collection + =back =head1 METHODS @@ -748,6 +751,18 @@ sub cust_bill_batch { qsearch('cust_bill_batch', { 'invnum' => $self->invnum }); } +=item discount_plans + +Returns all discount plans (L) for this invoice, as a +hash keyed by term length. + +=cut + +sub discount_plans { + my $self = shift; + FS::discount_plan->all($self); +} + =item tax Returns the tax amount (see L) for this invoice. @@ -796,6 +811,23 @@ sub owed_pkgnum { $balance; } +=item hide + +Returns true if this invoice should be hidden. See the +selfservice-hide_invoices-taxclass configuraiton setting. + +=cut + +sub hide { + my $self = shift; + my $conf = $self->conf; + my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass') + or return ''; + my @cust_bill_pkg = $self->cust_bill_pkg; + my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg; + ! grep { $_->taxclass ne $hide_taxclass } @part_pkg; +} + =item apply_payments_and_credits [ OPTION => VALUE ... ] Applies unapplied payments and credits to this invoice. @@ -1028,41 +1060,54 @@ sub generate_email { 'Disposition' => 'inline', ); - $args{'from'} =~ /\@([\w\.\-]+)/; - my $from = $1 || 'example.com'; - my $content_id = join('.', rand()*(2**32), $$, time). "\@$from"; - my $logo; - my $agentnum = $cust_main->agentnum; - if ( defined($args{'template'}) && length($args{'template'}) - && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum ) - ) - { - $logo = 'logo_'. $args{'template'}. '.png'; + my $htmldata; + my $image = ''; + my $barcode = ''; + if ( $conf->exists('invoice_email_pdf') + and scalar($conf->config('invoice_email_pdf_note')) ) { + + $htmldata = join('
', $conf->config('invoice_email_pdf_note') ); + } else { - $logo = "logo.png"; - } - my $image_data = $conf->config_binary( $logo, $agentnum); - - my $image = build MIME::Entity - 'Type' => 'image/png', - 'Encoding' => 'base64', - 'Data' => $image_data, - 'Filename' => 'logo.png', - 'Content-ID' => "<$content_id>", - ; + + $args{'from'} =~ /\@([\w\.\-]+)/; + my $from = $1 || 'example.com'; + my $content_id = join('.', rand()*(2**32), $$, time). "\@$from"; + + my $logo; + my $agentnum = $cust_main->agentnum; + if ( defined($args{'template'}) && length($args{'template'}) + && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum ) + ) + { + $logo = 'logo_'. $args{'template'}. '.png'; + } else { + $logo = "logo.png"; + } + my $image_data = $conf->config_binary( $logo, $agentnum); + + $image = build MIME::Entity + 'Type' => 'image/png', + 'Encoding' => 'base64', + 'Data' => $image_data, + '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; + 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; + } + + $htmldata = $self->print_html({ 'cid'=>$content_id, %opt }); } $alternative->attach( @@ -1075,7 +1120,7 @@ sub generate_email { ' ', ' ', ' ', - $self->print_html({ 'cid'=>$content_id, %opt }), + $htmldata, ' ', '', ], @@ -1083,6 +1128,7 @@ sub generate_email { #'Filename' => 'invoice.pdf', ); + my @otherparts = (); if ( $cust_main->email_csv_cdr ) { @@ -1121,7 +1167,7 @@ sub generate_email { $related->add_part($alternative); - $related->add_part($image); + $related->add_part($image) if $image; my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt); @@ -1137,11 +1183,10 @@ sub generate_email { # image/png $return{'content-type'} = 'multipart/related'; - if($conf->exists('invoice-barcode')){ - $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ]; - } - else { - $return{'mimeparts'} = [ $alternative, $image, @otherparts ]; + if ($conf->exists('invoice-barcode') && $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'; @@ -1335,6 +1380,7 @@ sub queueable_email { #sub email_invoice { sub email { my $self = shift; + return if $self->hide; my $conf = $self->conf; my( $template, $invoice_from, $notice_name, $no_coupon ); @@ -1453,7 +1499,9 @@ I, if specified, overrides "Invoice" as the name of the sent docume #sub print_invoice { sub print { my $self = shift; + return if $self->hide; my $conf = $self->conf; + my( $template, $notice_name ); if ( ref($_[0]) ) { my $opt = shift; @@ -1493,7 +1541,9 @@ I, if specified, overrides "Invoice" as the name of the sent docume sub fax_invoice { my $self = shift; + return if $self->hide; my $conf = $self->conf; + my( $template, $notice_name ); if ( ref($_[0]) ) { my $opt = shift; @@ -2362,11 +2412,13 @@ unsquelch_cdr - overrides any per customer cdr squelching when true notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required) +locale - override customer's locale + =cut #what's with all the sprintf('%10.2f')'s in here? will it cause any # (alignment in text invoice?) problems to change them all to '%.2f' ? -# yes: fixed width (dot matrix) text printing will be borked +# yes: fixed width/plain text printing will be borked sub print_generic { my( $self, %params ) = @_; my $conf = $self->conf; @@ -2655,7 +2707,7 @@ sub print_generic { ); #localization - my $lh = FS::L10N->get_handle($cust_main->locale); + my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale ); $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) }; my %info = FS::Locales->locale_info($cust_main->locale || 'en_US'); # eval to avoid death for unimplemented languages @@ -2941,7 +2993,7 @@ sub print_generic { } } - unless ( $conf->exists('disable_previous_balance') + unless ( $conf->exists('disable_previous_balance', $agentnum) || $conf->exists('previous_balance-summary_only') ) { @@ -2975,7 +3027,8 @@ sub print_generic { } - if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) { + if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) ) + { push @buf, ['','-----------']; push @buf, [ $self->mt('Total Previous Balance'), $money_char. sprintf("%10.2f", $pr_total) ]; @@ -3032,7 +3085,7 @@ sub print_generic { $options{'section'} = $section if $multisection; $options{'format'} = $format; $options{'escape_function'} = $escape_function; - $options{'format_function'} = sub { () } unless $unsquelched; + $options{'no_usage'} = 1 unless $unsquelched; $options{'unsquelched'} = $unsquelched; $options{'summary_page'} = $summarypage; $options{'skip_usage'} = @@ -3091,7 +3144,7 @@ sub print_generic { $invoice_data{current_less_finance} = sprintf('%.2f', $self->charged - $invoice_data{finance_amount} ); - if ( $multisection && !$conf->exists('disable_previous_balance') + if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum) || $conf->exists('previous_balance-summary_only') ) { unshift @sections, $previous_section if $pr_total; @@ -3155,7 +3208,7 @@ sub print_generic { push @buf,['','-----------']; push @buf,[$self->mt( - $conf->exists('disable_previous_balance') + $conf->exists('disable_previous_balance', $agentnum) ? 'Total Charges' : 'Total New Charges' ), @@ -3169,7 +3222,7 @@ sub print_generic { || 'Total New Charges' if $conf->exists('previous_balance-exclude_from_total'); my $amount = $self->charged + - ( $conf->exists('disable_previous_balance') || + ( $conf->exists('disable_previous_balance', $agentnum) || $conf->exists('previous_balance-exclude_from_total') ? 0 : $pr_total @@ -3196,7 +3249,7 @@ sub print_generic { push @buf,['','']; } - unless ( $conf->exists('disable_previous_balance') ) { + unless ( $conf->exists('disable_previous_balance', $agentnum) ) { #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments # credits @@ -4760,6 +4813,7 @@ format: the invoice format. escape_function: the function used to escape strings. +DEPRECATED? (expensive, mostly unused?) format_function: the function used to format CDRs. section: a hashref containing 'description'; if this is present, @@ -4788,6 +4842,7 @@ sub _items_cust_bill_pkg { my $format = $opt{format} || ''; my $escape_function = $opt{escape_function} || sub { shift }; my $format_function = $opt{format_function} || ''; + my $no_usage = $opt{no_usage} || ''; my $unsquelched = $opt{unsquelched} || ''; #unused my $section = $opt{section}->{description} if $opt{section}; my $summary_page = $opt{summary_page} || ''; #unused @@ -4844,6 +4899,7 @@ sub _items_cust_bill_pkg { my %details_opt = ( 'format' => $format, 'escape_function' => $escape_function, 'format_function' => $format_function, + 'no_usage' => $opt{'no_usage'}, ); if ( $cust_bill_pkg->pkgnum > 0 ) { @@ -4931,11 +4987,20 @@ sub _items_cust_bill_pkg { my $description = ($is_summary && $type && $type eq 'U') ? "Usage charges" : $desc; - $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); + unless ( + $conf->exists('disable_line_item_date_ranges') + || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1) + ) { + my $time_period; + my $date_style = $conf->config('cust_bill-line_item-date_style'); + if ( $date_style eq 'month_of' ) { + $time_period = time2str('The month of %B', $cust_bill_pkg->sdate); + } else { + $time_period = time2str($date_format, $cust_bill_pkg->sdate). + " - ". time2str($date_format, $cust_bill_pkg->edate); + } + $description .= " ($time_period)"; + } my @d = (); my @seconds = (); # for display of usage info @@ -5001,7 +5066,7 @@ sub _items_cust_bill_pkg { #instead of omitting details entirely in this case (unwanted side # effects), just omit CDRs - $details_opt{'format_function'} = sub { () } + $details_opt{'no_usage'} = 1 if $type && $type eq 'R'; push @d, $cust_bill_pkg->details(%details_opt); @@ -5178,100 +5243,34 @@ a setup fee if the discount is allowed to apply to setup fees. sub _items_discounts_avail { my $self = shift; - my %terms; my $list_pkgnums = 0; # if any packages are not eligible for all discounts - - my ($previous_balance) = $self->previous; - - foreach (qsearch('discount',{ 'months' => { op => '>', value => 1} })) { - $terms{$_->months} = { - pkgnums => [], - base => $previous_balance || 0, # pre-discount sum of charges - discounted => $previous_balance || 0, # post-discount sum - list_pkgnums => 0, # whether any packages are not discounted - } - } - foreach my $months (keys %terms) { - my $hash = $terms{$months}; - - # tricky, because packages may not all be eligible for the same discounts - foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { - my $cust_pkg = $cust_bill_pkg->cust_pkg or next; - my $part_pkg = $cust_pkg->part_pkg or next; - - next if $part_pkg->freq ne '1'; - my $setup = $cust_bill_pkg->setup || 0; - my $recur = $cust_bill_pkg->recur || 0; - my $permonth = $part_pkg->base_recur_permonth || 0; - - my ($discount) = grep { $_->months == $months } - map { $_->discount } $part_pkg->part_pkg_discount; - - $hash->{base} += $setup + $recur + ($months - 1) * $permonth; - if ($discount) { - - my $discountable; - if ( $discount->setup ) { - $discountable += $setup; - } - else { - $hash->{discounted} += $setup; - } - - if ( $discount->percent ) { - $discountable += $months * $permonth; - $discountable -= ($discountable * $discount->percent / 100); - $discountable -= ($permonth - $recur); # correct for prorate - $hash->{discounted} += $discountable; - } - else { - $discountable += $recur; - $discountable -= $discount->amount * $recur/$permonth; - - $discountable += ($months - 1) * max($permonth - $discount->amount,0); - } - - $hash->{discounted} += $discountable; - push @{ $hash->{pkgnums} }, $cust_pkg->pkgnum; - } - else { #no discount - $hash->{discounted} += $setup + $recur + ($months - 1) * $permonth; - $hash->{list_pkgnums} = 1; - } - } #foreach $cust_bill_pkg - # don't show this line if no packages have discounts at this term - # or if there are no new charges to apply the discount to - delete $terms{$months} if $hash->{base} == $hash->{discounted} - or $hash->{base} == 0; + my %plans = $self->discount_plans; - } + $list_pkgnums = grep { $_->list_pkgnums } values %plans; - $list_pkgnums = grep { $_->{list_pkgnums} > 0 } values %terms; + map { + my $months = $_; + my $plan = $plans{$months}; - foreach my $months (keys %terms) { - my $hash = $terms{$months}; - my $term_total = sprintf('%.2f', $hash->{discounted}); - # possibly shouldn't include previous balance in these? - my $percent = sprintf('%.0f', 100 * (1 - $term_total / $hash->{base}) ); + my $term_total = sprintf('%.2f', $plan->discounted_total); + my $percent = sprintf('%.0f', + 100 * (1 - $term_total / $plan->base_total) ); my $permonth = sprintf('%.2f', $term_total / $months); - - $hash->{description} = $self->mt('Save [_1]% by paying for [_2] months', - $percent, $months - ); - $hash->{amount} = $self->mt('[_1] ([_2] per month)', - $term_total, $money_char.$permonth - ); - - my @detail; - if ( $list_pkgnums ) { - push @detail, $self->mt('discount on item'). ' '. - join(', ', map { "#$_" } @{ $hash->{pkgnums} }); + my $detail = $self->mt('discount on item'). ' '. + join(', ', map { "#$_" } $plan->pkgnums) + if $list_pkgnums; + + +{ + description => $self->mt('Save [_1]% by paying for [_2] months', + $percent, $months), + amount => $self->mt('[_1] ([_2] per month)', + $term_total, $money_char.$permonth), + ext_description => ($detail || ''), } - $hash->{ext_description} = join ', ', @detail; - } + } #map + sort { $b <=> $a } keys %plans; - map { $terms{$_} } sort {$b <=> $a} keys %terms; } =item call_details [ OPTION => VALUE ... ] @@ -5635,6 +5634,15 @@ sub search_sql_where { } + #promised_date - also has an option to accept nulls + if ( $param->{promised_date} ) { + my($beginning, $ending, $null) = @{$param->{promised_date}}; + + push @search, "(( cust_bill.promised_date >= $beginning AND ". + "cust_bill.promised_date < $ending )" . + ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')'); + } + #agent virtualization my $curuser = $FS::CurrentUser::CurrentUser; if ( $curuser->username eq 'fs_queue'