X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2FTemplate_Mixin.pm;h=38c5066922986205e7bb8b1a4c08fae0246c5530;hb=596f08196fd33e134009833b6453809789088dc8;hp=14e52c5b6f0bfd59d4e59319260f96e6bcd019bd;hpb=d77758b525a427835c119cd534cd1c1f21117c49;p=freeside.git diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index 14e52c5b6..38c506692 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -344,6 +344,8 @@ sub print_generic { $templatefile .= "_$template" if length($template) && $conf->exists($templatefile."_$template"); + $self->set('_template',$template); + # the base template my @invoice_template = map "$_\n", $conf->config($templatefile) or die "cannot load config data $templatefile"; @@ -692,18 +694,34 @@ sub print_generic { $invoice_data{'barcode_cid'} = $params{'barcode_cid'} if $params{'barcode_cid'}; - my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance -# my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits - #my $balance_due = $self->owed + $pr_total - $cr_total; - my $balance_due = $self->owed; - if ( $self->enable_previous ) { - $balance_due += $pr_total; - } - # otherwise the previous balance is not shown, so including it in the - # balance due is just confusing - # the sum of amount owed on all invoices - # (this is used in the summary & on the payment coupon) + # re: rt:78190 + # using owed_on_invoice() instead of owed() here for $balance_due + # using _items_previous_total() instead of ->previous() for $pr_total + # + # owed_on_invoice() is aware of configuration flags that affect how an + # invoice is rendered. May not return actual current balance. Will + # return balance appropriate for the invoice being rendered, based + # on which past due items, current charges, and future payments are + # displayed. + # + # Going forward, usage of owed(), or bypassing cust_bill helper methods + # when generating invoice lines may lead to incorrect or misleading + # math on invoices. + # + # Helper methods that are aware of invoicing conf flags: + # - owed_on_invoice # use instead of owed() + # - _items_previous() # use instead of previous() + # - _items_credits() # use instead of cust_credit() + # - _items_payments() + # - _items_total() + # - _items_previous_total() # use instead of previous() + # - _items_payments_total() + # - _items_credits_total() # use instead of cust_credit() + + my $pr_total = $self->_items_previous_total(); + + my $balance_due = $self->owed_on_invoice(); $invoice_data{'balance'} = sprintf("%.2f", $balance_due); # flag telling this invoice to have a first-page summary @@ -717,124 +735,97 @@ sub print_generic { # summary formats $invoice_data{'last_bill'} = {}; - my $last_bill = $self->previous_bill; - if ( $last_bill ) { + # my $last_bill = $self->previous_bill; + # if ( $last_bill ) { - # "balance_date_range" unfortunately is unsuitable for this, since it - # cares about application dates. We want to know the sum of all - # _top-level transactions_ dated before the last invoice. - # - # still do this for the "Previous Balance" line of the summary block - my @sql = - map "$_ WHERE _date <= ? AND custnum = ?", ( - "SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill", - "SELECT -1 * COALESCE( SUM(amount), 0 ) FROM cust_credit", - "SELECT -1 * COALESCE( SUM(paid), 0 ) FROM cust_pay", - "SELECT COALESCE( SUM(refund), 0 ) FROM cust_refund", + # Populate template stash for previous balance and payments + if ($pr_total) { + # Used on summary page as "Previous Balance" + $invoice_data{'true_previous_balance'} = sprintf("%.2f", $pr_total); + + # Used on summary page as "Payments" + $invoice_data{'balance_adjustments'} = sprintf("%.2f", + $self->_items_payments_total() + $self->_items_credits_total() ); - # the customer's current balance immediately after generating the last - # bill + # Used in invoice template as "Previous Balance" + $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total); - my $last_bill_balance = $last_bill->charged; - foreach (@sql) { - my $delta = FS::Record->scalar_sql( - $_, - $last_bill->_date - 1, - $self->custnum, - ); - $last_bill_balance += $delta; - } + # $invoice_data{last_bill}{_date}: + # Not used in default templates, but may be in use by someone + # + # ! May be a problem field if they are using it... this field + # stores the date of the previous invoice... it is possible to + # carry a balance, but have the immediately previous invoice paid off. + # In this case, this field might be presenting bad data? Not + # altering the problematic behavior, because someone might be + # expecting this bad behavior in their templates for some other + # purpose, such as a "your last bill was dated %_date%" + my $last_bill = $self->previous_bill; + $invoice_data{'last_bill'}{'_date'} + = ref $last_bill + ? $last_bill->_date() + : undef; + + # $invoice_data{previous_payments} + # Not used in default templates, but may be in use by someone + # + # Returns an array of hrefs representing payments, each with keys: + # - _date: epoch timestamp + # - date: text formatted date + # - amount: money formatted amount string + # - payinfo: string from payby_payinfo_pretty() + # - paynum: id for cust_pay + # - description: Text description for bill line item + # + my @payments = $self->_items_payments(); + $invoice_data{previous_payments} = \@payments; - $last_bill_balance = sprintf("%.2f", $last_bill_balance); - - warn sprintf("LAST BILL: INVNUM %d, DATE %s, BALANCE %.2f\n\n", - $last_bill->invnum, - $self->time2str_local('%D', $last_bill->_date), - $last_bill_balance - ) if $DEBUG > 0; - # ("true_previous_balance" is a terrible name, but at least it's no - # longer stored in the database) - $invoice_data{'true_previous_balance'} = $last_bill_balance; - - # Now, get all applications of credits/payments dated on or after the - # previous bill, to invoices before the current bill. (The - # credit/payment date restriction prevents these from intersecting - # the "Previous Balance" set.) - # These are "adjustments". The past due balance will be shown as - # Previous Balance - Adjustments. - my $adjustments = 0; - @sql = map { - "SELECT COALESCE(SUM(y.amount),0) FROM $_ JOIN cust_bill USING (invnum) - WHERE cust_bill._date < ? - AND x._date >= ? - AND cust_bill.custnum = ?" - } "cust_credit AS x JOIN cust_credit_bill y USING (crednum)", - "cust_pay AS x JOIN cust_bill_pay y USING (paynum)" - ; - foreach (@sql) { - my $delta = FS::Record->scalar_sql( - $_, - $self->_date, - $last_bill->_date, - $self->custnum, + # $invoice_data{previous_credits} + # Not used in default templates, but may be in use by someone + # + # Returns an array of hrefs representing credits, each with keys: + # - _date: epoch timestamp + # - date: text formatted date + # - amount: money formatted amount string + # - crednum: id for cust_credit + # - description: Text description for bill line item + # - creditreason: reason() from cust_credit + # + my @credits = $self->_items_credits(); + $invoice_data{previous_credits} = \@credits; + + # Populate formatted date field + for my $pmt_href (@payments, @credits) { + $pmt_href->{date} = $self->time2str_local( + 'long', + $pmt_href->{_date}, + $format ); - $adjustments += $delta; } - $invoice_data{'balance_adjustments'} = sprintf("%.2f", $adjustments); - - warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n", - $invoice_data{'balance_adjustments'} - ) if $DEBUG > 0; - # the sum of amount owed on all previous invoices - # ($pr_total is used elsewhere but not as $previous_balance) - $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total); - - $invoice_data{'last_bill'}{'_date'} = $last_bill->_date; #unformatted - my (@payments, @credits); - # for formats that itemize previous payments - foreach my $cust_pay ( qsearch('cust_pay', { - 'custnum' => $self->custnum, - '_date' => { op => '>=', - value => $last_bill->_date } - } ) ) - { - next if $cust_pay->_date > $self->_date; - push @payments, { - '_date' => $cust_pay->_date, - 'date' => $self->time2str_local('long', $cust_pay->_date, $format), - 'payinfo' => $cust_pay->payby_payinfo_pretty, - 'amount' => sprintf('%.2f', $cust_pay->paid), - }; - # not concerned about applications - } - foreach my $cust_credit ( qsearch('cust_credit', { - 'custnum' => $self->custnum, - '_date' => { op => '>=', - value => $last_bill->_date } - } ) ) - { - next if $cust_credit->_date > $self->_date; - push @credits, { - '_date' => $cust_credit->_date, - 'date' => $self->time2str_local('long', $cust_credit->_date, $format), - 'creditreason'=> $cust_credit->reason, - 'amount' => sprintf('%.2f', $cust_credit->amount), - }; - } - $invoice_data{'previous_payments'} = \@payments; - $invoice_data{'previous_credits'} = \@credits; } else { - # there is no $last_bill + # There are no outstanding invoices = YAPH $invoice_data{'true_previous_balance'} = $invoice_data{'balance_adjustments'} = $invoice_data{'previous_balance'} = '0.00'; - $invoice_data{'previous_payments'} = []; + $invoice_data{'previous_payments'} = $invoice_data{'previous_credits'} = []; } - - if ( $conf->config_bool('invoice_usesummary', $agentnum) ) { + + # Condencing a lot of debug staements here + if ($DEBUG) { + warn "\$invoice_data{$_}: $invoice_data{$_}" + for qw( + true_previous_balance + balance_adjustments + previous_balance + previous_payments + previous_credits + ); + } + + if ( $conf->exists('invoice_usesummary', $agentnum) ) { $invoice_data{'summarypage'} = $summarypage = 1; } @@ -942,7 +933,9 @@ sub print_generic { my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y'; my $multisection = $self->has_sections; - $invoice_data{'multisection'} = $multisection; + if ( $multisection ) { + $invoice_data{multisection} = $conf->config($tc.'sections_method') || 1; + } my $late_sections; my $extra_sections = []; my $extra_lines = (); @@ -968,11 +961,21 @@ sub print_generic { sprintf('%.2f', $pr_total), 'summarized' => '', #why? $summarypage ? 'Y' : '', }; - $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. - join(' / ', map { $cust_main->balance_date_range(@$_) } - $self->_prior_month30s - ) - if $conf->exists('invoice_include_aging'); + + # Include balance aging line and template variables + my @aged_balances = $self->_items_aging_balances(); + ( $invoice_data{aged_balance_current}, + $invoice_data{aged_balance_30d}, + $invoice_data{aged_balance_60d}, + $invoice_data{aged_balance_90d} + ) = @aged_balances; + + if ($conf->exists('invoice_include_aging')) { + $previous_section->{posttotal} = sprintf( + '0 / 30 / 60 / 90 days overdue %.2f / %.2f / %.2f / %.2f', + @aged_balances, + ); + } } else { # otherwise put them in the main section @@ -1089,7 +1092,7 @@ sub print_generic { } } else { # subtotal sectioning is the same as for the actual invoice sections - @summary_subtotals = @sections; + @summary_subtotals = grep $_->{subtotal}, @sections; } # Hereafter, push sections to both @sections and @summary_subtotals @@ -1129,7 +1132,7 @@ sub print_generic { } - if ( @pr_cust_bill && $self->enable_previous ) { + if ( $pr_total && $self->enable_previous ) { push @buf, ['','-----------']; push @buf, [ $self->mt('Total Previous Balance'), $money_char. sprintf("%10.2f", $pr_total) ]; @@ -1194,7 +1197,8 @@ sub print_generic { my %options = (); $options{'section'} = $section if $multisection; $options{'section_with_taxes'} = 1 - if $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum); + if $multisection + && $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum); $options{'format'} = $format; $options{'escape_function'} = $escape_function; $options{'no_usage'} = 1 unless $unsquelched; @@ -1203,6 +1207,8 @@ sub print_generic { $options{'skip_usage'} = scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections; $options{'preref_callback'} = $params{'preref_callback'}; + $options{'disable_line_item_date_ranges'} = + $conf->exists('disable_line_item_date_ranges'); warn "$me searching for line items\n" if $DEBUG > 1; @@ -1213,6 +1219,17 @@ sub print_generic { foreach my $line_item ( $self->_items_pkg(%options), $self->_items_fee(%options) ) { + # When bill is sectioned by location, fees may be displayed within the + # appropriate location section. Suppress this fee from the taxes/fees + # end section, so it doesn't appear to be charged twice and make the + # subtotals seem incorrect + next + if $line_item->{locationnum} + && ref $options{section} + && !exists $options{section}->{locationnum} + && $self->has_sections + && $conf->config($tc.'sections_method') eq 'location'; + warn "$me adding line item ". join(', ', map "$_=>".$line_item->{$_}, keys %$line_item). "\n" if $DEBUG > 1; @@ -1329,27 +1346,36 @@ sub print_generic { #$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : ''; #$tax_section->{'sort_weight'} = $tax_weight; + my $invoice_sections_with_taxes = $conf->config_bool( + 'invoice_sections_with_taxes', $cust_main->agentnum + ); + foreach my $tax ( @items_tax ) { - $taxtotal += $tax->{'amount'}; my $description = &$escape_function( $tax->{'description'} ); my $amount = sprintf( '%.2f', $tax->{'amount'} ); if ( $multisection ) { + if ( !$invoice_sections_with_taxes ) { - push @detail_items, { - ext_description => [], - ref => '', - quantity => '', - description => $description, - amount => $money_char. $amount, - product_code => '', - section => $tax_section, - }; + $taxtotal += $tax->{'amount'}; + push @detail_items, { + ext_description => [], + ref => '', + quantity => '', + description => $description, + amount => $money_char. $amount, + product_code => '', + section => $tax_section, + }; + + } } else { + $taxtotal += $tax->{'amount'}; + push @total_items, { 'total_item' => $description, 'total_amount' => $other_money_char. $amount, @@ -1370,6 +1396,14 @@ sub print_generic { $other_money_char. sprintf('%.2f', $self->charged - $taxtotal ); if ( $multisection ) { + + if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) { + # If all tax items are displayed in location/category sections, + # remove the empty tax section + @sections = grep{ $_ ne $tax_section } @sections + unless grep{ $_->{section} eq $tax_section } @detail_items; + } + if ( $taxtotal > 0 ) { # there are taxes, so prepare the section to be displayed. # $taxtotal already includes any line items that were already in the @@ -1383,18 +1417,12 @@ sub print_generic { $tax_section->{'description'} = $self->mt($tax_description); $tax_section->{'summarized'} = ''; - if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) { - - # remove tax section if taxes are itemized within other sections - @sections = grep{ $_ ne $tax_section } @sections; - - } elsif ( !grep $tax_section, @sections ) { + # append tax section unless it's already there + push @sections, $tax_section + unless grep {$_ eq $tax_section} @sections; - # append it if it's not already there - push @sections, $tax_section; - push @summary_subtotals, $tax_section; - - } + push @summary_subtotals, $tax_section + unless grep {$_ eq $tax_section} @summary_subtotals; } @@ -1547,7 +1575,7 @@ sub print_generic { # ? $self->charged + # $self->billing_balance # : - $self->owed + $pr_total + $balance_due ) ); if ( $multisection && !$adjust_section->{sort_weight} ) { @@ -1593,7 +1621,7 @@ sub print_generic { $total->{'total_item'} = &$embolden_function($self->balance_due_msg); $total->{'total_amount'} = &$embolden_function( - $other_money_char. sprintf('%.2f', $self->owed + $pr_total) + $other_money_char. sprintf('%.2f', $balance_due) ); my $last_section = pop @sections; $last_section->{'posttotal'} = $total->{'total_item'}. ' '. @@ -1780,22 +1808,6 @@ sub template_conf { warn "bare FS::Template_Mixin::template_conf"; 'invoice_'; } -# helper routine for generating date ranges -sub _prior_month30s { - my $self = shift; - my @ranges = ( - [ 1, 2592000 ], # 0-30 days ago - [ 2592000, 5184000 ], # 30-60 days ago - [ 5184000, 7776000 ], # 60-90 days ago - [ 7776000, 0 ], # 90+ days ago - ); - - map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '', - $_->[1] ? $self->_date - $_->[1] - 1 : '', - ] } - @ranges; -} - =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ] Returns an postscript invoice, as a scalar. @@ -2028,6 +2040,23 @@ sub due_date2str { $self->due_date ? $self->time2str_local(shift, $self->due_date) : ''; } +=item invoice_pay_by_msg + + displays the invoice_pay_by_msg or default Please pay by [_1] if empty. + +=cut + +sub invoice_pay_by_msg { + my $self = shift; + my $msg = ''; + my $please_pay_by = + $self->conf->config('invoice_pay_by_msg', $self->agentnum) + || 'Please pay by [_1]'; + $msg .= ' - ' . $self->mt($please_pay_by, $self->due_date2str('short')) . ' '; + + $msg; +} + =item balance_due_msg =cut @@ -2042,11 +2071,7 @@ sub balance_due_msg { # _items_total) and not here # (yes, or if invoice_sections is enabled; this is just for compatibility) if ( $self->due_date ) { - my $please_pay_by = - $self->conf->config('invoice_pay_by_msg', $self->agentnum) - || 'Please pay by [_1]'; - $msg .= ' - ' . $self->mt($please_pay_by, $self->due_date2str('short')). - ' ' + $msg .= $self->invoice_pay_by_msg unless $self->conf->config_bool('invoice_omit_due_date',$self->agentnum); } elsif ( $self->terms ) { $msg .= ' - '. $self->mt($self->terms); @@ -2249,8 +2274,7 @@ sub generate_email { warn "$me generating plain text invoice" if $DEBUG; - # 'print_text' argument is no longer used - @text = map Encode::encode_utf8($_), $self->print_text(\%args); + @text = $self->print_text(\%args); } else { @@ -2266,7 +2290,11 @@ sub generate_email { 'Encoding' => 'quoted-printable', 'Charset' => 'UTF-8', #'Encoding' => '7bit', - 'Data' => \@text, + 'Data' => [ + map + { Encode::encode('UTF-8', $_, Encode::FB_WARN | Encode::LEAVE_SRC ) } + @text + ], 'Disposition' => 'inline', ); @@ -2345,7 +2373,11 @@ sub generate_email { ' ', ' ', ' ', - Encode::encode_utf8($html), + Encode::encode( + 'UTF-8', + $html, + Encode::FB_WARN | Encode::LEAVE_SRC + ), ' ', '', ], @@ -2677,7 +2709,13 @@ sub _items_sections { foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) { next if ( $display->summary && $opt{summary} ); - my $section = $display->section; + #my $section = $display->section; + #false laziness with the method, but for efficiency inside this loop + my $section = $display->get('section'); + if ( !$section && !$cust_bill_pkg->hidden ) { + $section = $cust_bill_pkg->get('categoryname'); #cust_bill->cust_bill_pkg added it (XXX quotations / quotation_section) + } + my $type = $display->type; # Set $section = undef if we're sectioning by location and this # line item _has_ a location (i.e. isn't a fee). @@ -3102,6 +3140,10 @@ sub _items_fee { my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg; my $escape_function = $options{escape_function}; + my $locale = $self->cust_main + ? $self->cust_main->locale + : $self->prospect_main->locale; + my @items; foreach my $cust_bill_pkg (@cust_bill_pkg) { # cache this, so we don't look it up again in every section @@ -3113,17 +3155,31 @@ sub _items_fee { warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; next; } - if ( exists($options{section}) and exists($options{section}{category}) ) - { - my $categoryname = $options{section}{category}; - # then filter for items that have that section - if ( $part_fee->categoryname ne $categoryname ) { - warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG; - next; - } - } # otherwise include them all in the main section - # XXX what to do when sectioning by location? - + + # If _items_fee is called while building a sectioned invoice, + # - invoice_sections_method: category + # Skip fee records that do not match the section category. + # - invoice_sections_method: location + # Skip fee records always for location sections. + # The fee records will be presented in the tax/fee section instead. + if ( + exists( $options{section} ) + and + ( + ( + exists( $options{section}{category} ) + and + $part_fee->categoryname ne $options{section}{category} + ) + or + exists( $options{section}{location}) + ) + ) { + warn "skipping fee '".$part_fee->itemdesc. + "'--not in section $options{section}{category}\n" if $DEBUG; + next; + } + my @ext_desc; my %base_invnums; # invnum => invoice date foreach ($cust_bill_pkg->cust_bill_pkg_fee) { @@ -3142,7 +3198,7 @@ sub _items_fee { $self->mt('from invoice #[_1] on [_2]', $_, $base_invnums{$_}) ); } - my $desc = $part_fee->itemdesc_locale($self->cust_main->locale); + my $desc = $part_fee->itemdesc_locale($locale); # but not escape the base description line my @pkg_tax = $cust_bill_pkg->_pkg_tax_list @@ -3150,6 +3206,7 @@ sub _items_fee { push @items, { feepart => $cust_bill_pkg->feepart, + billpkgnum => $cust_bill_pkg->billpkgnum, amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur), description => $desc, pkg_tax => \@pkg_tax, @@ -3287,6 +3344,8 @@ sub _items_cust_bill_pkg { my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style + my $agentnum = $self->agentnum; + # for location labels: use default location on the invoice date my $default_locationnum; if ( $conf->exists('invoice-all_pkg_addresses') ) { @@ -3435,8 +3494,15 @@ sub _items_cust_bill_pkg { || ($discount_show_always and $cust_bill_pkg->unitrecur > 0) || $cust_bill_pkg->recur_show_zero; - $description .= $cust_bill_pkg->time_period_pretty( $part_pkg, - $self->agentnum ) + my $disable_date_ranges = + $opt{disable_line_item_date_ranges} + || $part_pkg->option('disable_line_item_date_ranges', 1); + + $description .= $cust_bill_pkg->time_period_pretty( + $part_pkg, + $agentnum, + disable_date_ranges => $disable_date_ranges, + ) if $part_pkg->is_prepaid #for prepaid, "display the validity period # triggered by the recurring charge freq # (RT#26274) @@ -3528,10 +3594,15 @@ sub _items_cust_bill_pkg { $description = $self->mt('Usage charges'); } - my $part_pkg = $cust_pkg->part_pkg; + my $disable_date_ranges = + $opt{disable_line_item_date_ranges} + || $part_pkg->option('disable_line_item_date_ranges', 1); - $description .= $cust_bill_pkg->time_period_pretty( $part_pkg, - $self->agentnum ); + $description .= $cust_bill_pkg->time_period_pretty( + $part_pkg, + $agentnum, + disable_date_ranges => $disable_date_ranges, + ); my @d = (); my @seconds = (); # for display of usage info