X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=45f86eddafc036566ec37030e155695208ee5b98;hb=aaf20df651082ece4e891bf56a9fdff79c1165c1;hp=0a1577f63ad02bb51a4f02d88806896c493ec600;hpb=f306f5bf02319e5d89fb603ae34a900916e8cba5;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 0a1577f63..45f86edda 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 @@ -374,6 +377,25 @@ sub display_invnum { } } +=item previous_bill + +Returns the customer's last invoice before this one. + +=cut + +sub previous_bill { + my $self = shift; + if ( !$self->get('previous_bill') ) { + $self->set('previous_bill', qsearchs({ + 'table' => 'cust_bill', + 'hashref' => { 'custnum' => $self->custnum, + '_date' => { op=>'<', value=>$self->_date } }, + 'order_by' => 'ORDER BY _date DESC LIMIT 1', + }) ); + } + $self->get('previous_bill'); +} + =item previous Returns a list consisting of the total previous balance for this customer, @@ -385,13 +407,29 @@ sub previous { my $self = shift; my $total = 0; my @cust_bill = sort { $a->_date <=> $b->_date } - grep { $_->owed != 0 && $_->_date < $self->_date } - qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) + grep { $_->owed != 0 } + qsearch( 'cust_bill', { 'custnum' => $self->custnum, + #'_date' => { op=>'<', value=>$self->_date }, + 'invnum' => { op=>'<', value=>$self->invnum }, + } ) ; foreach ( @cust_bill ) { $total += $_->owed; } $total, @cust_bill; } +=item enable_previous + +Whether to show the 'Previous Charges' section when printing this invoice. +The negation of the 'disable_previous_balance' config setting. + +=cut + +sub enable_previous { + my $self = shift; + my $agentnum = $self->cust_main->agentnum; + !$self->conf->exists('disable_previous_balance', $agentnum); +} + =item cust_bill_pkg Returns the line items (see L) for this invoice. @@ -748,6 +786,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 +846,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 +1095,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 +1155,7 @@ sub generate_email { ' ', ' ', ' ', - $self->print_html({ 'cid'=>$content_id, %opt }), + $htmldata, ' ', '', ], @@ -1083,6 +1163,7 @@ sub generate_email { #'Filename' => 'invoice.pdf', ); + my @otherparts = (); if ( $cust_main->email_csv_cdr ) { @@ -1121,7 +1202,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 +1218,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'; @@ -1269,14 +1349,16 @@ sub send { $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/; } + my $cust_main = $self->cust_main; + return 'N/A' unless ! $agentnums - or grep { $_ == $self->cust_main->agentnum } @$agentnums; + or grep { $_ == $cust_main->agentnum } @$agentnums; return '' - unless $self->cust_main->total_owed_date($self->_date) > $balance_over; + unless $cust_main->total_owed_date($self->_date) > $balance_over; $invoice_from ||= $self->_agent_invoice_from || #XXX should go away - $conf->config('invoice_from', $self->cust_main->agentnum ); + $conf->config('invoice_from', $cust_main->agentnum ); my %opt = ( 'template' => $template, @@ -1284,11 +1366,12 @@ sub send { 'notice_name' => ( $notice_name || 'Invoice' ), ); - my @invoicing_list = $self->cust_main->invoicing_list; + my @invoicing_list = $cust_main->invoicing_list; #$self->email_invoice(\%opt) $self->email(\%opt) - if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list; + if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list ) + && ! $self->invoice_noemail; #$self->print_invoice(\%opt) $self->print(\%opt) @@ -1335,6 +1418,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 +1537,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; @@ -1474,7 +1560,10 @@ sub print { $self->batch_invoice(\%opt); } else { - do_print $self->lpr_data(\%opt); + do_print( + $self->lpr_data(\%opt), + 'agentnum' => $self->cust_main->agentnum, + ); } } @@ -1493,7 +1582,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; @@ -1654,6 +1745,7 @@ sub send_csv { my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill"; mkdir $spooldir, 0700 unless -d $spooldir; + # don't localize dates here, they're a defined format my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time); my $file = "$spooldir/$tracctnum.csv"; @@ -1913,7 +2005,7 @@ sub print_csv { my $pmt_cr_applied = 0; $pmt_cr_applied += $_->{'amount'} - foreach ( $self->_items_payments, $self->_items_credits ) ; + foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ; my $totaldue = sprintf('%.2f', $self->owed + $previous_balance); @@ -1955,6 +2047,44 @@ sub print_csv { '0', # 29 | Other Taxes & Fees*** NUM* 9 ); + } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name? + + my ($previous_balance) = $self->previous; + $previous_balance = sprintf('%.2f', $previous_balance); + my $totaldue = sprintf('%.2f', $self->owed + $previous_balance); + my @items = map { + $_->{pkgnum}, + $_->{description}, + $_->{amount} + } + $self->_items_pkg, #_items_nontax? no sections or anything + # with this format + $self->_items_tax; + + $csv->combine( + $cust_main->agentnum, + $cust_main->agent->agent, + $self->custnum, + $cust_main->first, + $cust_main->last, + $cust_main->company, + $cust_main->address1, + $cust_main->address2, + $cust_main->city, + $cust_main->state, + $cust_main->zip, + + # invoice fields + time2str("%x", $self->_date), + $self->invnum, + $self->charged, + $totaldue, + $previous_balance, + $self->due_date2str("%x"), + + @items, + ); + } else { $csv->combine( @@ -1994,6 +2124,10 @@ sub print_csv { } + } elsif ( lc($opt{'format'}) eq 'oneline' ) { + + #do nothing + } else { foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { @@ -2013,7 +2147,7 @@ sub print_csv { ? time2str("%x", $cust_bill_pkg->sdate) : '' ), ($cust_bill_pkg->edate - ?time2str("%x", $cust_bill_pkg->edate) + ? time2str("%x", $cust_bill_pkg->edate) : '' ), ); @@ -2362,11 +2496,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; @@ -2610,8 +2746,9 @@ sub print_generic { #invoice info 'invnum' => $self->invnum, - 'date' => time2str($date_format, $self->_date), - 'today' => time2str($date_format_long, $today), + '_date' => $self->_date, + 'date' => $self->time2str_local($date_format, $self->_date), + 'today' => $self->time2str_local($date_format_long, $today), 'terms' => $self->terms, 'template' => $template, #params{'template'}, 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function? @@ -2654,16 +2791,10 @@ sub print_generic { ); - #localization - my $lh = FS::L10N->get_handle($cust_main->locale); + #localization (see FS::cust_main_Mixin) $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 - my $dh = eval { Date::Language->new($info{'name'}) } || - Date::Language->new(); # fall back to English # prototype here to silence warnings - $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) }; - # eventually use this date handle everywhere in here, too + $invoice_data{'time2str'} = sub ($;$$) { $self->time2str_local(@_) }; my $min_sdate = 999999999999; my $max_edate = 0; @@ -2676,8 +2807,9 @@ sub print_generic { } $invoice_data{'bill_period'} = ''; - $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate) - . " to " . time2str('%e %h', $max_edate) + $invoice_data{'bill_period'} = $self->time2str_local('%e %h', $min_sdate) + . " to " . + $self->time2str_local('%e %h', $max_edate) if ($max_edate != 0 && $min_sdate != 999999999999); $invoice_data{finance_section} = ''; @@ -2757,10 +2889,9 @@ sub print_generic { # info from customer's last invoice before this one, for some # summary formats $invoice_data{'last_bill'} = {}; - my $last_bill = $pr_cust_bill[-1]; - if ( $last_bill ) { + if ( $self->previous_bill ) { $invoice_data{'last_bill'} = { - '_date' => $last_bill->_date, #unformatted + '_date' => $self->previous_bill->_date, #unformatted # all we need for now }; } @@ -2866,6 +2997,7 @@ sub print_generic { my $taxtotal = 0; my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'), 'subtotal' => $taxtotal, # adjusted below + 'tax_section' => 1, }; my $tax_weight = _pkg_category($tax_section->{description}) ? _pkg_category($tax_section->{description})->weight @@ -2875,10 +3007,11 @@ sub print_generic { my $adjusttotal = 0; - my $adjust_section = { 'description' => - $self->mt('Credits, Payments, and Adjustments'), - 'subtotal' => 0, # adjusted below - }; + my $adjust_section = { + 'description' => $self->mt('Credits, Payments, and Adjustments'), + 'adjust_section' => 1, + 'subtotal' => 0, # adjusted below + }; my $adjust_weight = _pkg_category($adjust_section->{description}) ? _pkg_category($adjust_section->{description})->weight : 0; @@ -2891,6 +3024,12 @@ sub print_generic { my $late_sections = []; my $extra_sections = []; my $extra_lines = (); + + my $default_section = { 'description' => '', + 'subtotal' => '', + 'no_subtotal' => 1, + }; + if ( $multisection ) { ($extra_sections, $extra_lines) = $self->_items_extra_usage_sections($escape_function_nonbsp, $format) @@ -2922,8 +3061,7 @@ sub print_generic { } } else {# not multisection # make a default section - push @sections, { 'description' => '', 'subtotal' => '', - 'no_subtotal' => 1 }; + push @sections, $default_section; # and calculate the finance charge total, since it won't get done otherwise. # XXX possibly other totals? # XXX possibly finance_pkgclass should not be used in this manner? @@ -2941,10 +3079,11 @@ sub print_generic { } } - unless ( $conf->exists('disable_previous_balance') - || $conf->exists('previous_balance-summary_only') - ) - { + # previous invoice balances in the Previous Charges section if there + # is one, otherwise in the main detail section + if ( $self->can('_items_previous') && + $self->enable_previous && + ! $conf->exists('previous_balance-summary_only') ) { warn "$me adding previous balances\n" if $DEBUG > 1; @@ -2955,8 +3094,10 @@ sub print_generic { ext_description => [], }; $detail->{'ref'} = $line_item->{'pkgnum'}; + $detail->{'pkgpart'} = $line_item->{'pkgpart'}; $detail->{'quantity'} = 1; - $detail->{'section'} = $previous_section; + $detail->{'section'} = $multisection ? $previous_section + : $default_section; $detail->{'description'} = &$escape_function($line_item->{'description'}); if ( exists $line_item->{'ext_description'} ) { @{$detail->{'ext_description'}} = map { @@ -2974,8 +3115,8 @@ sub print_generic { } } - - if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) { + + if ( @pr_cust_bill && $self->enable_previous ) { push @buf, ['','-----------']; push @buf, [ $self->mt('Total Previous Balance'), $money_char. sprintf("%10.2f", $pr_total) ]; @@ -3032,7 +3173,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'} = @@ -3052,6 +3193,7 @@ sub print_generic { ext_description => [], }; $detail->{'ref'} = $line_item->{'pkgnum'}; + $detail->{'pkgpart'} = $line_item->{'pkgpart'}; $detail->{'quantity'} = $line_item->{'quantity'}; $detail->{'section'} = $section; $detail->{'description'} = &$escape_function($line_item->{'description'}); @@ -3067,6 +3209,7 @@ sub print_generic { $detail->{'sdate'} = $line_item->{'sdate'}; $detail->{'edate'} = $line_item->{'edate'}; $detail->{'seconds'} = $line_item->{'seconds'}; + $detail->{'svc_label'} = $line_item->{'svc_label'}; push @detail_items, $detail; push @buf, ( [ $detail->{'description'}, @@ -3091,7 +3234,9 @@ sub print_generic { $invoice_data{current_less_finance} = sprintf('%.2f', $self->charged - $invoice_data{finance_amount} ); - if ( $multisection && !$conf->exists('disable_previous_balance') + # create a major section for previous balance if we have major sections, + # or if previous_section is in summary form + if ( ( $multisection && $self->enable_previous ) || $conf->exists('previous_balance-summary_only') ) { unshift @sections, $previous_section if $pr_total; @@ -3155,25 +3300,26 @@ sub print_generic { push @buf,['','-----------']; push @buf,[$self->mt( - $conf->exists('disable_previous_balance') + (!$self->enable_previous) ? 'Total Charges' : 'Total New Charges' ), $money_char. sprintf("%10.2f",$self->charged) ]; push @buf,['','']; + # calculate total, possibly including total owed on previous + # invoices { my $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 - ); + my $amount = $self->charged; + if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) { + $amount += $pr_total; + } + $total->{'total_item'} = &$embolden_function($self->mt($item)); $total->{'total_amount'} = &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) ); @@ -3185,7 +3331,7 @@ sub print_generic { $adjust_section->{'pretotal'} = $self->mt('New charges total').' '. $other_money_char. sprintf('%.2f', $self->charged ); } - }else{ + } else { push @total_items, $total; } push @buf,['','-----------']; @@ -3195,13 +3341,20 @@ sub print_generic { ]; push @buf,['','']; } - - unless ( $conf->exists('disable_previous_balance') ) { + + # if we're showing previous invoices, also show previous + # credits and payments + if ( $self->enable_previous + and $self->can('_items_credits') + and $self->can('_items_payments') ) + { #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments # credits my $credittotal = 0; - foreach my $credit ( $self->_items_credits('trim_len'=>60) ) { + foreach my $credit ( + $self->_items_credits( 'template' => $template, 'trim_len' => 60) + ) { my $total; $total->{'total_item'} = &$escape_function($credit->{'description'}); @@ -3227,13 +3380,17 @@ sub print_generic { $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal); #credits (again) - foreach my $credit ( $self->_items_credits('trim_len'=>32) ) { + foreach my $credit ( + $self->_items_credits( 'template' => $template, 'trim_len' => 32) + ) { push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ]; } # payments my $paymenttotal = 0; - foreach my $payment ( $self->_items_payments ) { + foreach my $payment ( + $self->_items_payments( 'template' => $template ) + ) { my $total = {}; $total->{'total_item'} = &$escape_function($payment->{'description'}); $paymenttotal += $payment->{'amount'}; @@ -3272,10 +3429,11 @@ sub print_generic { $total->{'total_item'} = &$embolden_function($self->balance_due_msg); $total->{'total_amount'} = &$embolden_function( - $other_money_char. sprintf('%.2f', $summarypage - ? $self->charged + - $self->billing_balance - : $self->owed + $pr_total + $other_money_char. sprintf('%.2f', #why? $summarypage + # ? $self->charged + + # $self->billing_balance + # : + $self->owed + $pr_total ) ); if ( $multisection && !$adjust_section->{sort_weight} ) { @@ -3346,6 +3504,10 @@ sub print_generic { } } @discounts_avail; } + # debugging hook: call this with 'diag' => 1 to just get a hash of + # the invoice variables + return \%invoice_data if ( $params{'diag'} ); + # All sections and items are built; now fill in templates. my @includelist = (); push @includelist, 'summary' if $summarypage; @@ -3662,7 +3824,7 @@ sub due_date { sub due_date2str { my $self = shift; - $self->due_date ? time2str(shift, $self->due_date) : ''; + $self->due_date ? $self->time2str_local(shift, $self->due_date) : ''; } sub balance_due_msg { @@ -3684,7 +3846,7 @@ sub balance_due_date { my $duedate = ''; if ( $conf->exists('invoice_default_terms') && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) { - $duedate = time2str($rdate_format, $self->_date + ($1*86400) ); + $duedate = $self->time2str_local($rdate_format, $self->_date + ($1*86400) ); } $duedate; } @@ -3714,7 +3876,7 @@ Returns a string with the date, for example: "3/20/2008" sub _date_pretty { my $self = shift; - time2str($date_format, $self->_date); + $self->time2str_local($date_format, $self->_date); } =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT @@ -3794,17 +3956,20 @@ sub _items_sections { if ( $display->post_total && !$summarypage ) { if (! $type || $type eq 'S') { $late_subtotal{$section} += $cust_bill_pkg->setup - if $cust_bill_pkg->setup != 0; + if $cust_bill_pkg->setup != 0 + || $cust_bill_pkg->setup_show_zero; } if (! $type) { $late_subtotal{$section} += $cust_bill_pkg->recur - if $cust_bill_pkg->recur != 0; + if $cust_bill_pkg->recur != 0 + || $cust_bill_pkg->recur_show_zero; } if ($type && $type eq 'R') { $late_subtotal{$section} += $cust_bill_pkg->recur - $usage - if $cust_bill_pkg->recur != 0; + if $cust_bill_pkg->recur != 0 + || $cust_bill_pkg->recur_show_zero; } if ($type && $type eq 'U') { @@ -3818,17 +3983,20 @@ sub _items_sections { if (! $type || $type eq 'S') { $subtotal{$section} += $cust_bill_pkg->setup - if $cust_bill_pkg->setup != 0; + if $cust_bill_pkg->setup != 0 + || $cust_bill_pkg->setup_show_zero; } if (! $type) { $subtotal{$section} += $cust_bill_pkg->recur - if $cust_bill_pkg->recur != 0; + if $cust_bill_pkg->recur != 0 + || $cust_bill_pkg->recur_show_zero; } if ($type && $type eq 'R') { $subtotal{$section} += $cust_bill_pkg->recur - $usage - if $cust_bill_pkg->recur != 0; + if $cust_bill_pkg->recur != 0 + || $cust_bill_pkg->recur_show_zero; } if ($type && $type eq 'U') { @@ -4651,7 +4819,7 @@ sub _items_previous { foreach ( @pr_cust_bill ) { my $date = $conf->exists('invoice_show_prior_due_date') ? 'due '. $_->due_date2str($date_format) - : time2str($date_format, $_->_date); + : $self->time2str_local($date_format, $_->_date); push @b, { 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)", #'pkgpart' => 'N/A', @@ -4760,6 +4928,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 +4957,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 @@ -4797,6 +4967,8 @@ sub _items_cust_bill_pkg { my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50; + my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style + my @b = (); my ($s, $r, $u) = ( undef, undef, undef ); foreach my $cust_bill_pkg ( @$cust_bill_pkgs ) @@ -4817,6 +4989,8 @@ sub _items_cust_bill_pkg { } } + my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display; + warn "$me _items_cust_bill_pkg considering cust_bill_pkg ". $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n" if $DEBUG > 1; @@ -4827,7 +5001,7 @@ sub _items_cust_bill_pkg { } #grep { !$_->summary || !$summary_page } # bunk! grep { !$_->summary || $multisection } - $cust_bill_pkg->cust_bill_pkg_display + @cust_bill_pkg_display ) { @@ -4844,6 +5018,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 ) { @@ -4853,9 +5028,14 @@ sub _items_cust_bill_pkg { my $cust_pkg = $cust_bill_pkg->cust_pkg; + # which pkgpart to show for display purposes? + my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart; + # start/end dates for invoice formats that do nonstandard # things with them - my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate'); + my %item_dates = (); + %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate') + unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1); if ( (!$type || $type eq 'S') && ( $cust_bill_pkg->setup != 0 @@ -4874,13 +5054,16 @@ sub _items_cust_bill_pkg { || $cust_bill_pkg->recur_show_zero; my @d = (); + my $svc_label; unless ( $cust_pkg->part_pkg->hide_svc_detail || $cust_bill_pkg->hidden ) { - push @d, map &{$escape_function}($_), - $cust_pkg->h_labels_short($self->_date, undef, 'I') + my @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date, undef, 'I'); + push @d, @svc_labels unless $cust_bill_pkg->pkgpart_override; #don't redisplay services + $svc_label = $svc_labels[0]; if ( $multilocation ) { my $loc = $cust_pkg->location_label; @@ -4902,13 +5085,14 @@ sub _items_cust_bill_pkg { $s = { _is_setup => 1, description => $description, - #pkgpart => $part_pkg->pkgpart, + pkgpart => $pkgpart, pkgnum => $cust_bill_pkg->pkgnum, amount => $cust_bill_pkg->setup, setup_show_zero => $cust_bill_pkg->setup_show_zero, unit_amount => $cust_bill_pkg->unitsetup, quantity => $cust_bill_pkg->quantity, ext_description => \@d, + svc_label => ($svc_label || ''), }; }; @@ -4931,14 +5115,45 @@ 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); + my $part_pkg = $cust_pkg->part_pkg; + + #pry be a bit more efficient to look some of this conf stuff up + # outside the loop + unless ( + $conf->exists('disable_line_item_date_ranges') + || $part_pkg->option('disable_line_item_date_ranges',1) + || ! $cust_bill_pkg->sdate + || ! $cust_bill_pkg->edate + ) { + my $time_period; + my $date_style = ''; + $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly', + $cust_main->agentnum + ) + if $part_pkg && $part_pkg->freq !~ /^1m?$/; + $date_style ||= $conf->config( 'cust_bill-line_item-date_style', + $cust_main->agentnum + ); + if ( defined($date_style) && $date_style eq 'month_of' ) { + $time_period = $self->mt('The month of [_1]', + $self->time2str_local('%B', $cust_bill_pkg->sdate) + ); + } elsif ( defined($date_style) && $date_style eq 'X_month' ) { + my $desc = $conf->config( 'cust_bill-line_item-date_description', + $cust_main->agentnum + ); + $desc .= ' ' unless $desc =~ /\s$/; + $time_period = $desc. $self->time2str_local('%B', $cust_bill_pkg->sdate); + } else { + $time_period = $self->time2str_local($date_format, $cust_bill_pkg->sdate). + " - ". $self->time2str_local($date_format, $cust_bill_pkg->edate); + } + $description .= " ($time_period)"; + } my @d = (); my @seconds = (); # for display of usage info + my $svc_label = ''; #at least until cust_bill_pkg has "past" ranges in addition to #the "future" sdate/edate ones... see #3032 @@ -4956,11 +5171,11 @@ sub _items_cust_bill_pkg { 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) + my @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short(@dates, 'I'); + push @d, @svc_labels unless $cust_bill_pkg->pkgpart_override; #don't redisplay services + $svc_label = $svc_labels[0]; warn "$me _items_cust_bill_pkg done adding service details\n" if $DEBUG > 1; @@ -5001,7 +5216,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); @@ -5031,7 +5246,7 @@ sub _items_cust_bill_pkg { } else { $r = { description => $description, - #pkgpart => $part_pkg->pkgpart, + pkgpart => $pkgpart, pkgnum => $cust_bill_pkg->pkgnum, amount => $amount, recur_show_zero => $cust_bill_pkg->recur_show_zero, @@ -5039,6 +5254,7 @@ sub _items_cust_bill_pkg { quantity => $cust_bill_pkg->quantity, %item_dates, ext_description => \@d, + svc_label => ($svc_label || ''), }; $r->{'seconds'} = \@seconds if grep {defined $_} @seconds; } @@ -5055,7 +5271,7 @@ sub _items_cust_bill_pkg { } else { $u = { description => $description, - #pkgpart => $part_pkg->pkgpart, + pkgpart => $pkgpart, pkgnum => $cust_bill_pkg->pkgnum, amount => $amount, recur_show_zero => $cust_bill_pkg->recur_show_zero, @@ -5083,8 +5299,8 @@ sub _items_cust_bill_pkg { if ( $cust_bill_pkg->recur != 0 ) { push @b, { 'description' => "$desc (". - time2str($date_format, $cust_bill_pkg->sdate). ' - '. - time2str($date_format, $cust_bill_pkg->edate). ')', + $self->time2str_local($date_format, $cust_bill_pkg->sdate). ' - '. + $self->time2str_local($date_format, $cust_bill_pkg->edate). ')', 'amount' => sprintf("%.2f", $cust_bill_pkg->recur), }; } @@ -5124,12 +5340,33 @@ sub _items_credits { my @b; #credits - foreach ( $self->cust_credited ) { + my @objects; + if ( $self->conf->exists('previous_balance-payments_since') ) { + if ( $opt{'template'} eq 'statement' ) { + # then the current bill is a "statement" (i.e. an invoice sent as + # a payment receipt) + # and in that case we want to see payments on or after THIS invoice + @objects = qsearch('cust_credit', { + 'custnum' => $self->custnum, + '_date' => {op => '>=', value => $self->_date}, + }); + } else { + my $date = 0; + $date = $self->previous_bill->_date if $self->previous_bill; + @objects = qsearch('cust_credit', { + 'custnum' => $self->custnum, + '_date' => {op => '>=', value => $date}, + }); + } + } else { + @objects = $self->cust_credited; + } - #something more elaborate if $_->amount ne $_->cust_credit->credited ? + foreach my $obj ( @objects ) { + my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit; - my $reason = substr($_->cust_credit->reason, 0, $trim_len); - $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, { @@ -5137,8 +5374,8 @@ sub _items_credits { # " (". time2str("%x",$_->cust_credit->_date) .")". # $reason, 'description' => $self->mt('Credit applied').' '. - time2str($date_format,$_->cust_credit->_date). $reason, - 'amount' => sprintf("%.2f",$_->amount), + $self->time2str_local($date_format,$obj->_date). $reason, + 'amount' => sprintf("%.2f",$obj->amount), }; } @@ -5148,17 +5385,47 @@ sub _items_credits { sub _items_payments { my $self = shift; + my %opt = @_; my @b; - #get & print payments - foreach ( $self->cust_bill_pay ) { + my $detailed = $self->conf->exists('invoice_payment_details'); + my @objects; + if ( $self->conf->exists('previous_balance-payments_since') ) { + # then show payments dated on/after the previous bill... + if ( $opt{'template'} eq 'statement' ) { + # then the current bill is a "statement" (i.e. an invoice sent as + # a payment receipt) + # and in that case we want to see payments on or after THIS invoice + @objects = qsearch('cust_pay', { + 'custnum' => $self->custnum, + '_date' => {op => '>=', value => $self->_date}, + }); + } else { + # the normal case: payments on or after the previous invoice + my $date = 0; + $date = $self->previous_bill->_date if $self->previous_bill; + @objects = qsearch('cust_pay', { + 'custnum' => $self->custnum, + '_date' => {op => '>=', value => $date}, + }); + # and before the current bill... + @objects = grep { $_->_date < $self->_date } @objects; + } + } else { + @objects = $self->cust_bill_pay; + } - #something more elaborate if $_->amount ne ->cust_pay->paid ? + foreach my $obj (@objects) { + my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay; + my $desc = $self->mt('Payment received').' '. + $self->time2str_local($date_format, $cust_pay->_date ); + $desc .= $self->mt(' via ') . + $cust_pay->payby_payinfo_pretty( $self->cust_main->locale ) + if $detailed; push @b, { - 'description' => $self->mt('Payment received').' '. - time2str($date_format,$_->cust_pay->_date ), - 'amount' => sprintf("%.2f", $_->amount ) + 'description' => $desc, + 'amount' => sprintf("%.2f", $obj->amount ) }; } @@ -5178,100 +5445,37 @@ 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 + my %plans = $self->discount_plans; - # 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; + $list_pkgnums = grep { $_->list_pkgnums } values %plans; - } + map { + my $months = $_; + my $plan = $plans{$months}; - $list_pkgnums = grep { $_->{list_pkgnums} > 0 } values %terms; - - 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; + + # discounts for non-integer months don't work anyway + $months = sprintf("%d", $months); + + +{ + 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 ... ] @@ -5559,11 +5763,25 @@ sub search_sql_where { push @search, "cust_main.agentnum = $1"; } - #agentnum + #refnum + if ( $param->{'refnum'} =~ /^(\d+)$/ ) { + push @search, "cust_main.refnum = $1"; + } + + #custnum if ( $param->{'custnum'} =~ /^(\d+)$/ ) { push @search, "cust_bill.custnum = $1"; } + #customer classnum + if ( $param->{'cust_classnum'} ) { + my $classnums = $param->{'cust_classnum'}; + $classnums = [ $classnums ] if !ref($classnums); + $classnums = [ grep /^\d+$/, @$classnums ]; + push @search, 'cust_main.classnum in ('.join(',',@$classnums).')' + if @$classnums; + } + #_date if ( $param->{_date} ) { my($beginning, $ending) = @{$param->{_date}}; @@ -5635,6 +5853,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'