X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2FTemplate_Mixin.pm;h=6c4f6fe7a00699472fa8104e1b60e7da20d2d129;hb=61536edebfca5fab5892614a3714f68e43d52327;hp=c8ddffd79225158b9b43d6e3edfcb3d83f8e07ca;hpb=92aeca588c2e42aae2d50dc0d6b1115c4e4a768d;p=freeside.git diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index c8ddffd79..6c4f6fe7a 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -10,6 +10,7 @@ use vars qw( $invoice_lines @buf ); #yuck use List::Util qw(sum); #can't import first, it conflicts with cust_main.first use Date::Format; use Date::Language; +use Time::Local qw( timelocal ); use Text::Template 1.20; use File::Temp 0.14; use Archive::Zip qw( :ERROR_CODES :CONSTANTS ); @@ -18,7 +19,7 @@ use HTML::Entities; use Cwd; use FS::UID; use FS::Misc qw( send_email ); -use FS::Record qw( qsearch qsearchs ); +use FS::Record qw( qsearch qsearchs dbh ); use FS::Conf; use FS::Misc qw( generate_ps generate_pdf ); use FS::pkg_category; @@ -298,7 +299,7 @@ before that line item (quotations only) =item template -Dprecated. Used as a suffix for a configuration template. Please +Deprecated. Used as a suffix for a configuration template. Please don't use this, it deprecated in favor of more flexible alternatives. =back @@ -562,6 +563,7 @@ sub print_generic { 'notice_name' => $notice_name, # escape? 'current_charges' => sprintf("%.2f", $self->charged), 'duedate' => $self->due_date2str('rdate'), #date_format? + 'duedate_long' => $self->due_date2str('long'), #customer info 'custnum' => $cust_main->display_custnum, @@ -601,6 +603,9 @@ sub print_generic { 'total_pages' => 1, ); + + #quotations have $name + $invoice_data{'name'} = $invoice_data{'payname'}; #localization $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) }; @@ -650,10 +655,11 @@ sub print_generic { $invoice_data{'cid'} = $params{'cid'} if $params{'cid'}; - if ( $cust_main->country eq $countrydefault ) { - $invoice_data{'country'} = ''; - } else { + if ( $cust_main->bill_locationnum + && $cust_main->bill_location->country ne $countrydefault ) { $invoice_data{'country'} = &$escape_function($cust_main->bill_country_full); + } else { + $invoice_data{'country'} = ''; } my @address = (); @@ -936,8 +942,6 @@ sub print_generic { my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y'; my $multisection = $self->has_sections; - $conf->exists($tc.'sections', $cust_main->agentnum) || - $conf->exists($tc.'sections_by_location', $cust_main->agentnum); $invoice_data{'multisection'} = $multisection; my $late_sections; my $extra_sections = []; @@ -1156,14 +1160,27 @@ sub print_generic { if ( $invoice_data{finance_section} && $section->{'description'} eq $invoice_data{finance_section} ); - $section->{'subtotal'} = $other_money_char. - sprintf('%.2f', $section->{'subtotal'}) - if $multisection; + if ( $multisection ) { + + if ( ref($section->{'subtotal'}) ) { - # continue some normalization - $section->{'amount'} = $section->{'subtotal'} - if $multisection; + $section->{'subtotal'} = + sprintf("$other_money_char%.2f to $other_money_char%.2f", + $section->{'subtotal'}[0], + $section->{'subtotal'}[1] + ); + + } else { + $section->{'subtotal'} = $other_money_char. + sprintf('%.2f', $section->{'subtotal'}) + + } + + # continue some normalization + $section->{'amount'} = $section->{'subtotal'} + + } if ( $section->{'description'} ) { push @buf, ( [ &$escape_function($section->{'description'}), '' ], @@ -1176,6 +1193,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); $options{'format'} = $format; $options{'escape_function'} = $escape_function; $options{'no_usage'} = 1 unless $unsquelched; @@ -1188,6 +1207,9 @@ sub print_generic { warn "$me searching for line items\n" if $DEBUG > 1; + my %section_tax_lines; + my %seen_tax_lines; + foreach my $line_item ( $self->_items_pkg(%options), $self->_items_fee(%options) ) { @@ -1211,10 +1233,56 @@ sub print_generic { $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'}; } $line_item->{'ext_description'} ||= []; - + + if ( $options{section_with_taxes} && ref $line_item->{pkg_tax} ) { + for my $line_tax ( @{$ line_item->{pkg_tax} } ) { + + # It is rarely possible for the same tax record to be presented here + # multiple times. See cust_bill_pkg::_pkg_tax_list for more info + next if $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} }; + $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} } = 1; + + $section_tax_lines{ $line_tax->{taxname} } += $line_tax->{amount}; + } + } + push @detail_items, $line_item; } + # If conf flag invoice_sections_with_taxes: + # - Add @detail_items for taxes into each section + # - Update section subtotal to include taxes + if ( $options{section_with_taxes} && %section_tax_lines ) { + for my $taxname ( keys %section_tax_lines ) { + + push @detail_items, { + section => $section, + amount => sprintf($money_char."%.2f",$section_tax_lines{$taxname}), + description => &$escape_function($taxname), + }; + + # Append taxes to total. If line format resembles "$5.00 to $12.00" + # append to the second value. + + if ($section->{subtotal} =~ /to/) { + my @subtotal = split /\s/, $section->{subtotal}; + $subtotal[2] =~ s/[^\d\.]//g; + $subtotal[2] = sprintf( + $money_char."%.2f", + ( $subtotal[2] + $section_tax_lines{$taxname} ) + ); + $section->{subtotal} = join ' ', @subtotal; + } else { + $section->{subtotal} =~ s/[^\d\.]//g; + $section->{subtotal} = sprintf( + $money_char . "%.2f", + ( $section->{subtotal} + $section_tax_lines{$taxname} ) + ); + } + + } + } + if ( $section->{'description'} ) { push @buf, ( ['','-----------'], [ $section->{'description'}. ' sub-total', @@ -1240,11 +1308,13 @@ sub print_generic { if $DEBUG > 1; # create a tax section if we don't yet have one + my @items_tax = $self->_items_tax; my $tax_description = 'Taxes, Surcharges, and Fees'; my $tax_section = List::Util::first { $_->{description} eq $tax_description } @sections; if (!$tax_section) { $tax_section = { 'description' => $tax_description }; + push @sections, $tax_section if $multisection and @items_tax > 0; } $tax_section->{tax_section} = 1; # mark this section as containing taxes # if this is an existing tax section, we're merging the tax items into it. @@ -1259,9 +1329,6 @@ sub print_generic { #$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : ''; #$tax_section->{'sort_weight'} = $tax_weight; - my @items_tax = $self->_items_tax; - push @sections, $tax_section if $multisection and @items_tax > 0; - foreach my $tax ( @items_tax ) { $taxtotal += $tax->{'amount'}; @@ -1316,11 +1383,19 @@ sub print_generic { $tax_section->{'description'} = $self->mt($tax_description); $tax_section->{'summarized'} = ''; - # append it if it's not already there - if ( !grep $tax_section, @sections ) { + 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 it if it's not already there push @sections, $tax_section; push @summary_subtotals, $tax_section; + } + } } else { @@ -1363,7 +1438,16 @@ sub print_generic { foreach ( @new_total_items ) { my ($item, $amount) = ($_->{'total_item'}, $_->{'total_amount'}); $_->{'total_item'} = &$embolden_function( $item ); + + if ( ref($amount) ) { + $_->{'total_amount'} = &$embolden_function( + $other_money_char.$amount->[0]. ' to '. + $other_money_char.$amount->[1] + ); + } else { $_->{'total_amount'} = &$embolden_function( $other_money_char.$amount ); + } + # but if it's multisection, don't append to @total_items. the adjust # section has all this stuff push @total_items, $_ if !$multisection; @@ -1642,24 +1726,24 @@ sub print_generic { die "no invoice_lines() functions in template?" if ( $format eq 'template' && !$wasfunc ); - if ($format eq 'template') { + if ( $invoice_lines ) { + $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines ); + $invoice_data{'total_pages'}++ + if scalar(@buf) % $invoice_lines; + } - if ( $invoice_lines ) { - $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines ); - $invoice_data{'total_pages'}++ - if scalar(@buf) % $invoice_lines; + #setup subroutine for the template + $invoice_data{invoice_lines} = sub { + my $lines = shift || scalar(@buf); + map { + scalar(@buf) + ? shift @buf + : [ '', '' ]; } + ( 1 .. $lines ); + }; - #setup subroutine for the template - $invoice_data{invoice_lines} = sub { - my $lines = shift || scalar(@buf); - map { - scalar(@buf) - ? shift @buf - : [ '', '' ]; - } - ( 1 .. $lines ); - }; + if ($format eq 'template') { my $lines; my @collect; @@ -1925,6 +2009,12 @@ sub due_date { my $duedate = ''; if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) { $duedate = $self->_date() + ( $1 * 86400 ); + } elsif ( $self->terms =~ /^End of Month$/ ) { + my ($mon,$year) = (localtime($self->_date) )[4,5]; + $mon++; + until ( $mon < 12 ) { $mon -= 12; $year++; } + my $nextmonth_first = timelocal(0,0,0,1,$mon,$year); + $duedate = $nextmonth_first - 86400; } $duedate; } @@ -1947,13 +2037,17 @@ sub balance_due_msg { my $msg = $self->mt('Balance Due'); return $msg unless $self->terms; # huh? if ( !$self->conf->exists('invoice_show_prior_due_date') - or $self->conf->exists('invoice_sections') ) { + || $self->has_sections ) { # if enabled, the due date is shown with Total New Charges (see # _items_total) and not here # (yes, or if invoice_sections is enabled; this is just for compatibility) if ( $self->due_date ) { - $msg .= ' - ' . $self->mt('Please pay by'). ' '. - $self->due_date2str('short'); + 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')). + ' ' + unless $self->conf->config_bool('invoice_omit_due_date',$self->agentnum); } elsif ( $self->terms ) { $msg .= ' - '. $self->mt($self->terms); } @@ -2079,6 +2173,7 @@ Returns an argument list to be passed to L. =cut use MIME::Entity; +use Encode; sub generate_email { @@ -2155,7 +2250,7 @@ sub generate_email { if $DEBUG; # 'print_text' argument is no longer used - @text = $self->print_text(\%args); + @text = map Encode::encode_utf8($_), $self->print_text(\%args); } else { @@ -2250,7 +2345,7 @@ sub generate_email { ' ', ' ', ' ', - $html, + Encode::encode_utf8($html), ' ', '', ], @@ -2356,7 +2451,7 @@ sub generate_email { =item mimebuild_pdf Returns a list suitable for passing to MIME::Entity->build(), representing -this invoice as PDF attachment. +this quotation or invoice as PDF attachment. =cut @@ -2367,7 +2462,7 @@ sub mimebuild_pdf { 'Encoding' => 'base64', 'Data' => [ $self->print_pdf(@_) ], 'Disposition' => 'attachment', - 'Filename' => 'invoice-'. $self->invnum. '.pdf', + 'Filename' => $self->pdf_filename, ); } @@ -2424,6 +2519,7 @@ sub postal_mail_fsinc { 'ssl_opts' => { verify_hostname => 0, SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE, + SSL_version => 'SSLv3', } ); my $response = $ua->request( POST $url, [ @@ -2454,7 +2550,8 @@ sub postal_mail_fsinc { 'country' => $bill_location->country, ]); - die "Print connection error: ". $response->message. "\n" + die "Print connection error: ". $response->message. + ' ('. $response->as_string. ")\n" unless $response->is_success; local $@; @@ -3048,10 +3145,14 @@ sub _items_fee { my $desc = $part_fee->itemdesc_locale($self->cust_main->locale); # but not escape the base description line + my @pkg_tax = $cust_bill_pkg->_pkg_tax_list + if $options{section_with_taxes}; + push @items, { feepart => $cust_bill_pkg->feepart, amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur), description => $desc, + pkg_tax => \@pkg_tax, ext_description => \@ext_desc # sdate/edate? }; @@ -3150,6 +3251,8 @@ location (whichever is defined). multisection: a flag indicating that this is a multisection invoice, which does something complicated. +section_with_taxes: Look up and include applied taxes for each record + Returns a list of hashrefs, each of which may contain: pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and @@ -3309,6 +3412,9 @@ sub _items_cust_bill_pkg { # not normally used, but pass this to the template anyway $classname = $part_pkg->classname; + my @pkg_tax = $cust_bill_pkg->_pkg_tax_list + if $self->conf->exists('invoice_sections_with_taxes'); + if ( (!$type || $type eq 'S') && ( $cust_bill_pkg->setup != 0 || $cust_bill_pkg->setup_show_zero @@ -3377,6 +3483,7 @@ sub _items_cust_bill_pkg { push @{ $s->{ext_description} }, @d; } else { $s = { + billpkgnum => $cust_bill_pkg->billpkgnum, _is_setup => 1, description => $description, pkgpart => $pkgpart, @@ -3388,6 +3495,7 @@ sub _items_cust_bill_pkg { ext_description => \@d, svc_label => ($svc_label || ''), locationnum => $cust_pkg->locationnum, # sure, why not? + pkg_tax => \@pkg_tax, }; }; @@ -3538,6 +3646,7 @@ sub _items_cust_bill_pkg { push @{ $r->{ext_description} }, @d; } else { $r = { + billpkgnum => $cust_bill_pkg->billpkgnum, description => $description, pkgpart => $pkgpart, pkgnum => $cust_bill_pkg->pkgnum, @@ -3549,6 +3658,7 @@ sub _items_cust_bill_pkg { ext_description => \@d, svc_label => ($svc_label || ''), locationnum => $cust_pkg->locationnum, + pkg_tax => \@pkg_tax, }; $r->{'seconds'} = \@seconds if grep {defined $_} @seconds; } @@ -3567,6 +3677,7 @@ sub _items_cust_bill_pkg { } elsif ( $amount ) { # create a new usage line $u = { + billpkgnum => $cust_bill_pkg->billpkgnum, description => $description, pkgpart => $pkgpart, pkgnum => $cust_bill_pkg->pkgnum, @@ -3576,6 +3687,7 @@ sub _items_cust_bill_pkg { %item_dates, ext_description => \@d, locationnum => $cust_pkg->locationnum, + pkg_tax => \@pkg_tax, }; } # else this has no usage, so don't create a usage section } @@ -3709,4 +3821,66 @@ sub _items_discounts_avail { } +=item has_sections AGENTNUM + +Return true if invoice_sections should be enabled for this bill. + (Inherited by both cust_bill and cust_bill_void) + +Determination: +* False if not an invoice +* True always if conf invoice_sections is enabled +* True always if sections_by_location is enabled +* True if conf invoice_sections_multilocation > 1, + and location_count >= invoice_sections_multilocation +* Else, False + +=cut + +sub has_sections { + my ($self, $agentnum) = @_; + + return 0 unless $self->invnum > 0; + + $agentnum ||= $self->agentnum; + return 1 if $self->conf->config_bool('invoice_sections', $agentnum); + return 1 if $self->conf->exists('sections_by_location', $agentnum); + + my $location_min = $self->conf->config( + 'invoice_sections_multilocation', $agentnum, + ); + + return 1 + if $location_min + && $self->location_count >= $location_min; + + 0; +} + + +=item location_count + +Return the number of locations billed on this invoice + +=cut + +sub location_count { + my ($self) = @_; + return 0 unless $self->invnum; + + # SELECT COUNT( DISTINCT cust_pkg.locationnum ) + # FROM cust_bill_pkg + # LEFT JOIN cust_pkg USING (pkgnum) + # WHERE invnum = 278 + # AND cust_bill_pkg.pkgnum > 0 + + my $result = qsearchs({ + select => 'COUNT(DISTINCT cust_pkg.locationnum) as location_count', + table => 'cust_bill_pkg', + addl_from => 'LEFT JOIN cust_pkg USING (pkgnum)', + extra_sql => 'WHERE invnum = '.dbh->quote( $self->invnum ) + . ' AND cust_bill_pkg.pkgnum > 0' + }); + ref $result ? $result->location_count : 0; +} + 1;