PriceSource: credit_notes auch im Popup unterstützen
[kivitendo-erp.git] / SL / ReportGenerator.pm
index 6c61acb..6fc50e9 100644 (file)
@@ -1,17 +1,11 @@
 package SL::ReportGenerator;
 
-#use strict;
-
-use Encode;
-use IO::Wrap;
+use Data::Dumper;
 use List::Util qw(max);
 use Text::CSV_XS;
-use Text::Iconv;
 #use PDF::API2;    # these two eat up to .75s on startup. only load them if we actually need them
 #use PDF::Table;
 
-use SL::Form;
-
 use strict;
 
 # Cause locales.pl to parse these files:
@@ -29,6 +23,7 @@ sub new {
   $self->{options}  = {
     'std_column_visibility' => 0,
     'output_format'         => 'HTML',
+    'controller_class   '   => '',
     'allow_pdf_export'      => 1,
     'allow_csv_export'      => 1,
     'html_template'         => 'report_generator/html_report',
@@ -78,6 +73,12 @@ sub set_columns {
     $column->{visible} = $self->{options}->{std_column_visibility} unless defined $column->{visible};
   }
 
+  if( $::form->{report_generator_csv_options_for_import} ) {
+    foreach my $key (keys %{ $self->{columns} }) {
+      $self->{columns}{$key}{text} = $key;
+    }
+  }
+
   $self->set_column_order(sort keys %{ $self->{columns} });
 }
 
@@ -120,7 +121,7 @@ sub add_data {
         $row->{$column}->{align}   = $self->{columns}->{$column}->{align} unless (defined $row->{$column}->{align});
       }
 
-      foreach my $field (qw(data link)) {
+      foreach my $field (qw(data link link_class)) {
         map { $row->{$_}->{$field} = [ $row->{$_}->{$field} ] if (ref $row->{$_}->{$field} ne 'ARRAY') } keys %{ $row };
       }
     }
@@ -209,14 +210,17 @@ sub set_custom_headers {
 sub get_attachment_basename {
   my $self     = shift;
   my $filename =  $self->{options}->{attachment_basename} || 'report';
+
+  # FIXME: this is bonkers. add a real sluggify method somewhere or import one.
   $filename    =~ s|.*\\||;
   $filename    =~ s|.*/||;
+  $filename    =~ s| |_|g;
 
   return $filename;
 }
 
 sub generate_with_headers {
-  my $self   = shift;
+  my ($self, %params) = @_;
   my $format = lc $self->{options}->{output_format};
   my $form   = $self->{form};
 
@@ -227,16 +231,19 @@ sub generate_with_headers {
   if ($format eq 'html') {
     my $title      = $form->{title};
     $form->{title} = $self->{title} if ($self->{title});
-    $form->header();
+    $form->header(no_layout => $params{no_layout});
     $form->{title} = $title;
 
     print $self->generate_html_content();
 
   } elsif ($format eq 'csv') {
+    # FIXME: don't do mini http in here
     my $filename = $self->get_attachment_basename();
     print qq|content-type: text/csv\n|;
     print qq|content-disposition: attachment; filename=${filename}.csv\n\n|;
-    $self->generate_csv_content();
+    $::locale->with_raw_io(\*STDOUT, sub {
+      $self->generate_csv_content();
+    });
 
   } elsif ($format eq 'pdf') {
     $self->generate_pdf_content();
@@ -338,12 +345,13 @@ sub prepare_html_content {
           next;
         }
 
-        my $col = $row->{$col_name};
+        my $col = $row->{$col_name} || { data => [] };
         $col->{CELL_ROWS} = [ ];
         foreach my $i (0 .. scalar(@{ $col->{data} }) - 1) {
           push @{ $col->{CELL_ROWS} }, {
-            'data' => $self->html_format($col->{data}->[$i]),
+            'data' => '' . $self->html_format($col->{data}->[$i]),
             'link' => $col->{link}->[$i],
+            link_class => $col->{link_class}->[$i],
           };
         }
 
@@ -393,6 +401,8 @@ sub prepare_html_content {
     'EXPORT_VARIABLE_LIST' => join(' ', @{ $self->{export}->{variable_list} }),
     'EXPORT_NEXTSUB'       => $self->{export}->{nextsub},
     'DATA_PRESENT'         => $self->{data_present},
+    'CONTROLLER_DISPATCH'  => $opts->{controller_class},
+    'TABLE_CLASS'          => $opts->{table_class},
   };
 
   return $variables;
@@ -402,7 +412,8 @@ sub generate_html_content {
   my $self      = shift;
   my $variables = $self->prepare_html_content();
 
-  return $self->{form}->parse_html_template($self->{options}->{html_template}, $variables);
+  my $stuff  = $self->{form}->parse_html_template($self->{options}->{html_template}, $variables);
+  return $stuff;
 }
 
 sub _cm2bp {
@@ -411,15 +422,6 @@ sub _cm2bp {
   return $_[0] * 72 / 2.54;
 }
 
-sub _decode_text {
-  my $self = shift;
-  my $text = shift;
-
-  $text    = decode('UTF-8', $text) if ($self->{text_is_utf8});
-
-  return $text;
-}
-
 sub generate_pdf_content {
   eval {
     require PDF::API2;
@@ -437,12 +439,11 @@ sub generate_pdf_content {
   my (@data, @column_props, @cell_props);
 
   my ($data_row, $cell_props_row);
-  my @visible_columns = $self->get_visible_columns('HTML');
+  my @visible_columns = $self->get_visible_columns('PDF');
   my $num_columns     = scalar @visible_columns;
   my $num_header_rows = 1;
 
-  my $font_encoding     = $main::dbcharset || 'ISO-8859-15';
-  $self->{text_is_utf8} = $font_encoding =~ m/^utf-?8$/i;
+  my $font_encoding   = 'UTF-8';
 
   foreach my $name (@visible_columns) {
     push @column_props, { 'justify' => $self->{columns}->{$name}->{align} eq 'right' ? 'right' : 'left' };
@@ -457,7 +458,7 @@ sub generate_pdf_content {
     foreach my $name (@visible_columns) {
       my $column = $self->{columns}->{$name};
 
-      push @{ $data_row },       $self->_decode_text($column->{text});
+      push @{ $data_row },       $column->{text};
       push @{ $cell_props_row }, {};
     }
 
@@ -471,7 +472,7 @@ sub generate_pdf_content {
       push @cell_props, $cell_props_row;
 
       foreach my $custom_header_col (@{ $custom_header_row }) {
-        push @{ $data_row }, $self->_decode_text($custom_header_col->{text});
+        push @{ $data_row }, $custom_header_col->{text};
 
         my $num_output  = ($custom_header_col->{colspan} * 1 > 1) ? $custom_header_col->{colspan} : 1;
         if ($num_output > 1) {
@@ -489,7 +490,7 @@ sub generate_pdf_content {
   foreach my $row_set (@{ $self->{data} }) {
     if ('HASH' eq ref $row_set) {
       if ($row_set->{type} eq 'colspan_data') {
-        push @data, [ $self->_decode_text($row_set->{data}) ];
+        push @data, [ $row_set->{data} ];
 
         $cell_props_row = [];
         push @cell_props, $cell_props_row;
@@ -513,7 +514,7 @@ sub generate_pdf_content {
       my $col_idx = 0;
       foreach my $col_name (@visible_columns) {
         my $col = $row->{$col_name};
-        push @{ $data_row }, $self->_decode_text(join("\n", @{ $col->{data} }));
+        push @{ $data_row }, join("\n", @{ $col->{data} || [] });
 
         $column_props[$col_idx]->{justify} = 'right' if ($col->{align} eq 'right');
 
@@ -578,13 +579,13 @@ sub generate_pdf_content {
   my $font_height       = $font_size + 2 * $padding;
   my $title_font_height = $font_size + 2 * $padding;
 
-  my $header_height     = 2 * $title_font_height if ($opts->{title});
-  my $footer_height     = 2 * $font_height       if ($pdfopts->{number});
+  my $header_height     = $opts->{title}     ? 2 * $title_font_height : undef;
+  my $footer_height     = $pdfopts->{number} ? 2 * $font_height       : undef;
 
   my $top_text_height   = 0;
 
   if ($self->{options}->{top_info_text}) {
-    my $top_text     =  $self->_decode_text($self->{options}->{top_info_text});
+    my $top_text     =  $self->{options}->{top_info_text};
     $top_text        =~ s/\r//g;
     $top_text        =~ s/\n+$//;
 
@@ -632,7 +633,7 @@ sub generate_pdf_content {
     my $curpage  = $pdf->openpage($page_num);
 
     if ($pdfopts->{number}) {
-      my $label    = $self->_decode_text($main::locale->text("Page #1/#2", $page_num, $pdf->pages()));
+      my $label    = $main::locale->text("Page #1/#2", $page_num, $pdf->pages());
       my $text_obj = $curpage->text();
 
       $text_obj->font($font, $font_size);
@@ -641,7 +642,7 @@ sub generate_pdf_content {
     }
 
     if ($opts->{title}) {
-      my $title    = $self->_decode_text($opts->{title});
+      my $title    = $opts->{title};
       my $text_obj = $curpage->text();
 
       $text_obj->font($font, $title_font_size);
@@ -672,7 +673,9 @@ sub generate_pdf_content {
     print qq|content-type: application/pdf\n|;
     print qq|content-disposition: attachment; filename=${filename}.pdf\n\n|;
 
-    print $content;
+    $::locale->with_raw_io(\*STDOUT, sub {
+      print $content;
+    });
   }
 }
 
@@ -698,19 +701,26 @@ sub _print_content {
   }
 }
 
-sub unescape_string {
-  my $self  = shift;
-  my $text  = shift;
-  my $iconv = $main::locale->{iconv};
+sub _handle_quoting_and_encoding {
+  my ($self, $text, $do_unquote) = @_;
 
-  $text     = $main::locale->unquote_special_chars('HTML', $text);
-  $text     = $main::locale->{iconv}->convert($text) if ($main::locale->{iconv});
+  $text = $main::locale->unquote_special_chars('HTML', $text) if $do_unquote;
+  $text = Encode::encode('UTF-8', $text);
 
   return $text;
 }
 
 sub generate_csv_content {
-  my $self = shift;
+  my $self   = shift;
+  my $stdout = ($::dispatcher->get_standard_filehandles)[1];
+
+  # Text::CSV_XS seems to downgrade to bytes already (see
+  # SL/FCGIFixes.pm). Therefore don't let FCGI do that again.
+  $::locale->with_raw_io($stdout, sub { $self->_generate_csv_content($stdout) });
+}
+
+sub _generate_csv_content {
+  my ($self, $stdout) = @_;
 
   my %valid_sep_chars    = (';' => ';', ',' => ',', ':' => ':', 'TAB' => "\t");
   my %valid_escape_chars = ('"' => 1, "'" => 1);
@@ -730,12 +740,11 @@ sub generate_csv_content {
                                 'quote_char'  => $quote_char,
                                 'eol'         => $eol, });
 
-  my $stdout          = wraphandle(\*STDOUT);
   my @visible_columns = $self->get_visible_columns('CSV');
 
   if ($opts->{headers}) {
     if (!$self->{custom_headers}) {
-      $csv->print($stdout, [ map { $self->unescape_string($self->{columns}->{$_}->{text}) } @visible_columns ]);
+      $csv->print($stdout, [ map { $self->_handle_quoting_and_encoding($self->{columns}->{$_}->{text}, 1) } @visible_columns ]);
 
     } else {
       foreach my $row (@{ $self->{custom_headers} }) {
@@ -743,7 +752,7 @@ sub generate_csv_content {
 
         foreach my $col (@{ $row }) {
           my $num_output = ($col->{colspan} && ($col->{colspan} > 1)) ? $col->{colspan} : 1;
-          push @{ $fields }, ($self->unescape_string($col->{text})) x $num_output;
+          push @{ $fields }, ($self->_handle_quoting_and_encoding($col->{text}, 1)) x $num_output;
         }
 
         $csv->print($stdout, $fields);
@@ -762,10 +771,10 @@ sub generate_csv_content {
           next;
         }
 
-        my $num_output = ($col->{colspan} && ($col->{colspan} > 1)) ? $col->{colspan} : 1;
+        my $num_output = ($row->{$col}{colspan} && ($row->{$col}->{colspan} > 1)) ? $row->{$col}->{colspan} : 1;
         $skip_next     = $num_output - 1;
 
-        push @data, join($eol, map { s/\r?\n/$eol/g; $_ } @{ $row->{$col}->{data} });
+        push @data, join($eol, map { s/\r?\n/$eol/g; $self->_handle_quoting_and_encoding($_, 0) } @{ $row->{$col}->{data} });
         push @data, ('') x $skip_next if ($skip_next);
       }
 
@@ -774,13 +783,17 @@ sub generate_csv_content {
   }
 }
 
+sub check_for_pdf_api {
+  return eval { require PDF::API2; 1; } ? 1 : 0;
+}
+
 1;
 
 __END__
 
 =head1 NAME
 
-SL::ReportGenerator.pm: the Lx-Office way of getting data in shape
+SL::ReportGenerator.pm: the kivitendo way of getting data in shape
 
 =head1 SYNOPSIS
 
@@ -804,7 +817,7 @@ Then there are some options made by the user, such as hidden columns. You add mo
 Then it lacks usability. You want it to be able to sort the data. You add code for that.
 Then there are too many results, you need pagination, you want to print or export that data..... and so on.
 
-The ReportGenerator class was designed because this exact scenario happened about half a dozen times in Lx-Office.
+The ReportGenerator class was designed because this exact scenario happened about half a dozen times in kivitendo.
 It's purpose is to manage all those formating, culling, sorting, and templating.
 Which makes it almost as complicated to use as doing the work for yourself.
 
@@ -927,6 +940,12 @@ Used to determine if a button for CSV export should be displayed. Default is yes
 
 The template to be used for HTML reports. Default is 'report_generator/html_report'.
 
+=item controller_class
+
+If this is used from a C<SL::Controller::Base> based controller class, pass the
+class name here and make sure C<SL::Controller::Helper::ReportGenerator> is
+used in the controller. That way the exports stay functional.
+
 =back
 
 =head2 PDF Options