X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2FTemplate_Mixin.pm;h=62d15a3e6d92884a96915f82753e0dbcd813da7c;hp=ffaef970751e17eb10d1bd319f5195a6d9b2cb6d;hb=32a1571c24a90b89676e646f58436446df7deafb;hpb=80c2d997c5c983344c530ecbb46f94f1c299b35f diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index ffaef9707..62d15a3e6 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -12,8 +12,9 @@ use Date::Format; use Date::Language; use Text::Template 1.20; use File::Temp 0.14; +use Archive::Zip qw( :ERROR_CODES :CONSTANTS ); +use IO::Scalar; use HTML::Entities; -use Locale::Country; use Cwd; use FS::UID; use FS::Misc qw( send_email ); @@ -146,6 +147,10 @@ sub print_latex { $template ||= $self->_agent_template if $self->can('_agent_template'); + #the new way + $self->set('mode', $params{mode}) + if $params{mode}; + my $pkey = $self->primary_key; my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX'; @@ -648,7 +653,7 @@ sub print_generic { if ( $cust_main->country eq $countrydefault ) { $invoice_data{'country'} = ''; } else { - $invoice_data{'country'} = &$escape_function(code2country($cust_main->country)); + $invoice_data{'country'} = &$escape_function($cust_main->bill_country_full); } my @address = (); @@ -712,6 +717,8 @@ sub print_generic { # "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", @@ -744,19 +751,31 @@ sub print_generic { # longer stored in the database) $invoice_data{'true_previous_balance'} = $last_bill_balance; - # the change in balance from immediately after that invoice - # to immediately before this one - my $before_this_bill_balance = 0; + # 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 - 1, + $self->_date, + $last_bill->_date, $self->custnum, ); - $before_this_bill_balance += $delta; + $adjustments += $delta; } - $invoice_data{'balance_adjustments'} = - sprintf("%.2f", $last_bill_balance - $before_this_bill_balance); + $invoice_data{'balance_adjustments'} = sprintf("%.2f", $adjustments); warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n", $invoice_data{'balance_adjustments'} @@ -873,13 +892,15 @@ sub print_generic { if ($format eq 'latex'); } - # let invoices use either of these as needed - $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL') - ? $cust_main->payinfo : ''; - $invoice_data{'po_line'} = - ( $cust_main->payby eq 'BILL' && $cust_main->payinfo ) - ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo) - : $nbsp; +# if (well, probably when) we still need PO numbers in the brave new world of +# 4.x, then we'll have to add them back as their own customer fields +# # let invoices use either of these as needed +# $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL') +# ? $cust_main->payinfo : ''; +# $invoice_data{'po_line'} = +# ( $cust_main->payby eq 'BILL' && $cust_main->payinfo ) +# ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo) +# : $nbsp; my %money_chars = ( 'latex' => '', 'html' => $conf->config('money_char') || '$', @@ -1219,11 +1240,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. @@ -1238,9 +1261,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'}; @@ -1652,6 +1672,13 @@ sub print_generic { } else { # this is where we actually create the invoice + if ( $params{no_addresses} ) { + delete $invoice_data{$_} foreach qw( + payname company address1 address2 city state zip country + ); + $invoice_data{returnaddress} = '~'; + } + warn "filling in template for invoice ". $self->invnum. "\n" if $DEBUG; warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n" @@ -1925,7 +1952,8 @@ sub balance_due_msg { # (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'); + $self->due_date2str('short') + unless $self->conf->config_bool('invoice_omit_due_date',$self->agentnum); } elsif ( $self->terms ) { $msg .= ' - '. $self->mt($self->terms); } @@ -2092,13 +2120,22 @@ sub generate_email { my $msg_template = FS::msg_template->by_key($msgnum) or die "${tc}email_pdf_msgnum $msgnum not found\n"; - my %prepared = $msg_template->prepare( + my $cust_msg = $msg_template->prepare( cust_main => $self->cust_main, - object => $self + object => $self, + msgtype => 'invoice', ); - @text = split(/(?=\n)/, $prepared{'text_body'}); - $html = $prepared{'html_body'}; + # XXX hack to make this work in the new cust_msg era; consider replacing + # with cust_bill_send_with_notice events. + my @parts = $cust_msg->parts; + foreach my $part (@parts) { # will only have two parts, normally + if ( $part->mime_type eq 'text/plain' ) { + @text = @{ $part->body }; + } elsif ( $part->mime_type eq 'text/html' ) { + $html = $part->bodyhandle->as_string; + } + } } elsif ( my @note = $conf->config($tc.'email_pdf_note') ) { @@ -2240,15 +2277,42 @@ sub generate_email { my @otherparts = (); if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) { - push @otherparts, build MIME::Entity - 'Type' => 'text/csv', - 'Encoding' => '7bit', - 'Data' => [ map { "$_\n" } - $self->call_details('prepend_billed_number' => 1) - ], - 'Disposition' => 'attachment', - 'Filename' => 'usage-'. $self->invnum. '.csv', - ; + if ( $conf->config('voip-cdr_email_attach') eq 'zip' ) { + + my $data = join('', map "$_\n", + $self->call_details(prepend_billed_number=>1) + ); + + my $zip = new Archive::Zip; + my $file = $zip->addString( $data, 'usage-'.$self->invnum.'.csv' ); + $file->desiredCompressionMethod( COMPRESSION_DEFLATED ); + + my $zipdata = ''; + my $SH = IO::Scalar->new(\$zipdata); + my $status = $zip->writeToFileHandle($SH); + die "Error zipping CDR attachment: $!" unless $status == AZ_OK; + + push @otherparts, build MIME::Entity + 'Type' => 'application/zip', + 'Encoding' => 'base64', + 'Data' => $zipdata, + 'Disposition' => 'attachment', + 'Filename' => 'usage-'. $self->invnum. '.zip', + ; + + } else { # } elsif ( $conf->config('voip-cdr_email_attach') eq 'csv' ) { + + push @otherparts, build MIME::Entity + 'Type' => 'text/csv', + 'Encoding' => '7bit', + 'Data' => [ map { "$_\n" } + $self->call_details('prepend_billed_number' => 1) + ], + 'Disposition' => 'attachment', + 'Filename' => 'usage-'. $self->invnum. '.csv', + ; + + } } @@ -2307,6 +2371,106 @@ sub mimebuild_pdf { ); } +=item postal_mail_fsinc + +Sends this invoice to the Freeside Internet Services, Inc. print and mail +service. + +=cut + +use CAM::PDF; +use IO::Socket::SSL; +use LWP::UserAgent; +use HTTP::Request::Common qw( POST ); +use Cpanel::JSON::XS; +use MIME::Base64; +sub postal_mail_fsinc { + my ( $self, %opt ) = @_; + + my $url = 'https://ws.freeside.biz/print'; + + my $cust_main = $self->cust_main; + my $agentnum = $cust_main->agentnum; + my $bill_location = $cust_main->bill_location; + + die "Extra charges for international mailing; contact support\@freeside.biz to enable\n" + if $bill_location->country ne 'US'; + + my $conf = new FS::Conf; + + my @company_address = $conf->config('company_address', $agentnum); + my ( $company_address1, $company_address2, $company_city, $company_state, $company_zip ); + if ( $company_address[2] =~ /^\s*(\S.*\S)\s*[\s,](\w\w),?\s*(\d{5}(-\d{4})?)\s*$/ ) { + $company_address1 = $company_address[0]; + $company_address2 = $company_address[1]; + $company_city = $1; + $company_state = $2; + $company_zip = $3; + } elsif ( $company_address[1] =~ /^\s*(\S.*\S)\s*[\s,](\w\w),?\s*(\d{5}(-\d{4})?)\s*$/ ) { + $company_address1 = $company_address[0]; + $company_address2 = ''; + $company_city = $1; + $company_state = $2; + $company_zip = $3; + } else { + die "Unparsable company_address; contact support\@freeside.biz\n"; + } + $company_city =~ s/,$//; + + my $file = $self->print_pdf(%opt, 'no_addresses' => 1); + my $pages = CAM::PDF->new($file)->numPages; + + my $ua = LWP::UserAgent->new( + 'ssl_opts' => { + verify_hostname => 0, + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE, + } + ); + my $response = $ua->request( POST $url, [ + 'support-key' => scalar($conf->config('support-key')), + 'file' => encode_base64($file), + 'pages' => $pages, + + #from: + 'company_name' => scalar( $conf->config('company_name', $agentnum) ), + 'company_address1' => $company_address1, + 'company_address2' => $company_address2, + 'company_city' => $company_city, + 'company_state' => $company_state, + 'company_zip' => $company_zip, + 'company_country' => 'US', + 'company_phonenum' => scalar($conf->config('company_phonenum', $agentnum)), + 'company_email' => scalar($conf->config('invoice_from', $agentnum)), + + #to: + 'name' => $cust_main->invoice_attn + || $cust_main->contact_firstlast, + 'company' => $cust_main->company, + 'address1' => $bill_location->address1, + 'address2' => $bill_location->address2, + 'city' => $bill_location->city, + 'state' => $bill_location->state, + 'zip' => $bill_location->zip, + 'country' => $bill_location->country, + ]); + + die "Print connection error: ". $response->message. "\n" + unless $response->is_success; + + local $@; + my $content = eval { decode_json($response->content) }; + die "Print JSON error : $@\n" if $@; + + die $content->{error}."\n" + if $content->{error}; + + #TODO: store this so we can query for a status later + warn "Invoice printed, ID ". $content->{id}. "\n"; + + $content->{id}; + +} + =item _items_sections OPTIONS Generate section information for all items appearing on this invoice. @@ -2986,9 +3150,6 @@ location (whichever is defined). multisection: a flag indicating that this is a multisection invoice, which does something complicated. -preref_callback: coderef run for each line item, code should return HTML to be -displayed before that line item (quotations only) - Returns a list of hashrefs, each of which may contain: pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and @@ -3025,7 +3186,9 @@ sub _items_cust_bill_pkg { # for location labels: use default location on the invoice date my $default_locationnum; - if ( $self->custnum ) { + if ( $conf->exists('invoice-all_pkg_addresses') ) { + $default_locationnum = 0; # treat them all as non-default + } elsif ( $self->custnum ) { my $h_cust_main; my @h_search = FS::h_cust_main->sql_h_search($self->_date); $h_cust_main = qsearchs({ @@ -3125,51 +3288,7 @@ sub _items_cust_bill_pkg { 'no_usage' => $opt{'no_usage'}, ); - if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) { - # XXX this should be pulled out into quotation_pkg - - warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n" - if $DEBUG > 1; - # quotation_pkgs are never fees, so don't worry about the case where - # part_pkg is undefined - - # and I guess they're never bundled either? - if ( $cust_bill_pkg->setup != 0 ) { - my $description = $desc; - $description .= ' Setup' - if $cust_bill_pkg->recur != 0 - || $discount_show_always - || $cust_bill_pkg->recur_show_zero; - #push @b, { - # keep it consistent, please - $s = { - 'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref - 'description' => $description, - 'amount' => sprintf("%.2f", $cust_bill_pkg->setup), - 'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup), - 'quantity' => $cust_bill_pkg->quantity, - 'preref_html' => ( $opt{preref_callback} - ? &{ $opt{preref_callback} }( $cust_bill_pkg ) - : '' - ), - }; - } - if ( $cust_bill_pkg->recur != 0 ) { - #push @b, { - $r = { - 'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref - 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")", - 'amount' => sprintf("%.2f", $cust_bill_pkg->recur), - 'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur), - 'quantity' => $cust_bill_pkg->quantity, - 'preref_html' => ( $opt{preref_callback} - ? &{ $opt{preref_callback} }( $cust_bill_pkg ) - : '' - ), - }; - } - - } elsif ( $cust_bill_pkg->pkgnum > 0 ) { + if ( $cust_bill_pkg->pkgnum > 0 ) { # a "normal" package line item (not a quotation, not a fee, not a tax) warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n" @@ -3203,6 +3322,7 @@ sub _items_cust_bill_pkg { # append the word 'Setup' to the setup line if there's going to be # a recur line for the same package (i.e. not a one-time charge) + # XXX localization my $description = $desc; $description .= ' Setup' if $cust_bill_pkg->recur != 0 @@ -3218,16 +3338,24 @@ sub _items_cust_bill_pkg { && ! $cust_bill_pkg->recur_show_zero; my @d = (); - my $svc_label; + my @svc_labels = (); + my $svc_label = ''; - # always pass the svc_label through to the template, even if - # not displaying it as an ext_description - my @svc_labels = map &{$escape_function}($_), - $cust_pkg->h_labels_short($self->_date, undef, 'I'); + unless ( $part_pkg->hide_svc_detail ) { - $svc_label = $svc_labels[0]; + # still pass the svc_label through to the template, even if + # not displaying it as an ext_description + @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date, + undef, + 'I', + $self->conf->{locale}, + ); + $svc_label = $svc_labels[0]; - unless ( $cust_pkg->part_pkg->hide_svc_detail + } + + unless ( $part_pkg->hide_svc_detail || $cust_bill_pkg->hidden ) { @@ -3304,6 +3432,7 @@ sub _items_cust_bill_pkg { my @d = (); my @seconds = (); # for display of usage info + my @svc_labels = (); my $svc_label = ''; #at least until cust_bill_pkg has "past" ranges in addition to @@ -3313,9 +3442,13 @@ sub _items_cust_bill_pkg { push @dates, $prev->sdate if $prev; push @dates, undef if !$prev; - my @svc_labels = map &{$escape_function}($_), - $cust_pkg->h_labels_short(@dates, 'I'); - $svc_label = $svc_labels[0]; + unless ( $part_pkg->hide_svc_detail ) { + @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short(@dates, + 'I', + $self->conf->{locale}); + $svc_label = $svc_labels[0]; + } # show service labels, unless... # the package is set not to display them @@ -3469,7 +3602,7 @@ sub _items_cust_bill_pkg { + $cust_bill_pkg->recur) }; - } # if quotation / package line item / other line item + } # if package line item / other line item # decide whether to show active discounts here if (