Überbleibsel von der Unterstützung für html2ps entfernt.
[kivitendo-erp.git] / SL / Form.pm
index b32fbd2..6e781b8 100644 (file)
 package Form;
 use Data::Dumper;
 
-use Cwd;
-use HTML::Template;
-use Template;
-use SL::Template;
+use CGI;
 use CGI::Ajax;
+use Cwd;
+use SL::Auth;
+use SL::Auth::DB;
+use SL::Auth::LDAP;
+use SL::AM;
+use SL::Common;
 use SL::DBUtils;
 use SL::Mailer;
 use SL::Menu;
+use SL::Template;
 use SL::User;
-use SL::Common;
-use CGI;
+use Template;
+use List::Util qw(first max min sum);
 
 my $standard_dbh;
 
@@ -59,36 +63,71 @@ sub DESTROY {
   }
 }
 
+sub _store_value {
+  $main::lxdebug->enter_sub(2);
+
+  my $self  = shift;
+  my $key   = shift;
+  my $value = shift;
+
+  my $curr  = $self;
+
+  while ($key =~ /\[\+?\]\.|\./) {
+    substr($key, 0, $+[0]) = '';
+
+    if ($& eq '.') {
+      $curr->{$`} ||= { };
+      $curr         = $curr->{$`};
+
+    } else {
+      $curr->{$`} ||= [ ];
+      if (!scalar @{ $curr->{$`} } || $& eq '[+].') {
+        push @{ $curr->{$`} }, { };
+      }
+
+      $curr = $curr->{$`}->[-1];
+    }
+  }
+
+  $curr->{$key} = $value;
+
+  $main::lxdebug->leave_sub(2);
+
+  return \$curr->{$key};
+}
+
 sub _input_to_hash {
   $main::lxdebug->enter_sub(2);
 
-  my $input = $_[0];
-  my %in    = ();
+  my $self  = shift;
+  my $input = shift;
+
   my @pairs = split(/&/, $input);
 
   foreach (@pairs) {
-    my ($name, $value) = split(/=/, $_, 2);
-    $in{$name} = unescape(undef, $value);
+    my ($key, $value) = split(/=/, $_, 2);
+    $self->_store_value($self->unescape($key), $self->unescape($value));
   }
 
   $main::lxdebug->leave_sub(2);
-
-  return %in;
 }
 
 sub _request_to_hash {
   $main::lxdebug->enter_sub(2);
 
-  my ($input) = @_;
+  my $self  = shift;
+  my $input = shift;
 
   if (!$ENV{'CONTENT_TYPE'}
       || ($ENV{'CONTENT_TYPE'} !~ /multipart\/form-data\s*;\s*boundary\s*=\s*(.+)$/)) {
+
+    $self->_input_to_hash($input);
+
     $main::lxdebug->leave_sub(2);
-    return _input_to_hash($input);
+    return;
   }
 
-  my ($name, $filename, $headers_done, $content_type, $boundary_found, $need_cr);
-  my %params;
+  my ($name, $filename, $headers_done, $content_type, $boundary_found, $need_cr, $previous);
 
   my $boundary = '--' . $1;
 
@@ -96,9 +135,10 @@ sub _request_to_hash {
     last if (($line eq "${boundary}--") || ($line eq "${boundary}--\r"));
 
     if (($line eq $boundary) || ($line eq "$boundary\r")) {
-      $params{$name} =~ s|\r?\n$|| if $name;
+      ${ $previous } =~ s|\r?\n$|| if $previous;
 
-      undef $name, $filename;
+      undef $previous;
+      undef $filename;
 
       $headers_done   = 0;
       $content_type   = "text/plain";
@@ -129,8 +169,8 @@ sub _request_to_hash {
           substr $line, $-[0], $+[0] - $-[0], "";
         }
 
-        $params{$name}    = "";
-        $params{FILENAME} = $filename if ($filename);
+        $previous         = $self->_store_value($name, '');
+        $self->{FILENAME} = $filename if ($filename);
 
         next;
       }
@@ -142,15 +182,14 @@ sub _request_to_hash {
       next;
     }
 
-    next unless $name;
+    next unless $previous;
 
-    $params{$name} .= "${line}\n";
+    ${ $previous } .= "${line}\n";
   }
 
-  $params{$name} =~ s|\r?\n$|| if $name;
+  ${ $previous } =~ s|\r?\n$|| if $previous;
 
   $main::lxdebug->leave_sub(2);
-  return %params;
 }
 
 sub new {
@@ -175,17 +214,86 @@ sub new {
     $_ = $ARGV[0];
   }
 
-  my %parameters = _request_to_hash($_);
-  map({ $self->{$_} = $parameters{$_}; } keys(%parameters));
+  bless $self, $type;
 
-  $self->{action} = lc $self->{action};
-  $self->{action} =~ s/( |-|,|\#)/_/g;
+  $self->_request_to_hash($_);
 
-  $self->{version}   = "2.4.3";
+  $self->{action}  =  lc $self->{action};
+  $self->{action}  =~ s/( |-|,|\#)/_/g;
+
+  $self->{version} =  "2.4.3";
 
   $main::lxdebug->leave_sub();
 
-  bless $self, $type;
+  return $self;
+}
+
+sub _flatten_variables_rec {
+  $main::lxdebug->enter_sub(2);
+
+  my $self   = shift;
+  my $curr   = shift;
+  my $prefix = shift;
+  my $key    = shift;
+
+  my @result;
+
+  if ('' eq ref $curr->{$key}) {
+    @result = ({ 'key' => $prefix . $key, 'value' => $curr->{$key} });
+
+  } elsif ('HASH' eq ref $curr->{$key}) {
+    foreach my $hash_key (sort keys %{ $curr->{$key} }) {
+      push @result, $self->_flatten_variables_rec($curr->{$key}, $prefix . $key . '.', $hash_key);
+    }
+
+  } else {
+    foreach my $idx (0 .. scalar @{ $curr->{$key} } - 1) {
+      my $first_array_entry = 1;
+
+      foreach my $hash_key (sort keys %{ $curr->{$key}->[$idx] }) {
+        push @result, $self->_flatten_variables_rec($curr->{$key}->[$idx], $prefix . $key . ($first_array_entry ? '[+].' : '[].'), $hash_key);
+        $first_array_entry = 0;
+      }
+    }
+  }
+
+  $main::lxdebug->leave_sub(2);
+
+  return @result;
+}
+
+sub flatten_variables {
+  $main::lxdebug->enter_sub(2);
+
+  my $self = shift;
+  my @keys = @_;
+
+  my @variables;
+
+  foreach (@keys) {
+    push @variables, $self->_flatten_variables_rec($self, '', $_);
+  }
+
+  $main::lxdebug->leave_sub(2);
+
+  return @variables;
+}
+
+sub flatten_standard_variables {
+  $main::lxdebug->enter_sub(2);
+
+  my $self      = shift;
+  my %skip_keys = map { $_ => 1 } (qw(login password header stylesheet titlebar version), @_);
+
+  my @variables;
+
+  foreach (grep { ! $skip_keys{$_} } keys %{ $self }) {
+    push @variables, $self->_flatten_variables_rec($self, '', $_);
+  }
+
+  $main::lxdebug->leave_sub(2);
+
+  return @variables;
 }
 
 sub debug {
@@ -200,6 +308,24 @@ sub debug {
   $main::lxdebug->leave_sub();
 }
 
+sub dumper {
+  $main::lxdebug->enter_sub(2);
+
+  my $self          = shift;
+  my $password      = $self->{password};
+
+  $self->{password} = 'X' x 8;
+
+  local $Data::Dumper::Sortkeys = 1;
+  my $output                    = Dumper($self);
+
+  $self->{password} = $password;
+
+  $main::lxdebug->leave_sub(2);
+
+  return $output;
+}
+
 sub escape {
   $main::lxdebug->enter_sub(2);
 
@@ -249,25 +375,6 @@ sub unquote {
 
 }
 
-sub quote_html {
-  $main::lxdebug->enter_sub(2);
-
-  my ($self, $str) = @_;
-
-  my %replace =
-    ('order' => ['"', '<', '>'],
-     '<'             => '&lt;',
-     '>'             => '&gt;',
-     '"'             => '&quot;',
-    );
-
-  map({ $str =~ s/$_/$replace{$_}/g; } @{ $replace{"order"} });
-
-  $main::lxdebug->leave_sub(2);
-
-  return $str;
-}
-
 sub hide_form {
   my $self = shift;
 
@@ -331,20 +438,20 @@ sub info {
   $main::lxdebug->leave_sub();
 }
 
+# calculates the number of rows in a textarea based on the content and column number
+# can be capped with maxrows
 sub numtextrows {
   $main::lxdebug->enter_sub();
+  my ($self, $str, $cols, $maxrows, $minrows) = @_;
 
-  my ($self, $str, $cols, $maxrows) = @_;
-
-  my $rows = 0;
+  $minrows ||= 1;
 
-  map { $rows += int(((length) - 2) / $cols) + 1 } split /\r/, $str;
-
-  $maxrows = $rows unless defined $maxrows;
+  my $rows   = sum map { int((length() - 2) / $cols) + 1 } split /\r/, $str;
+  $maxrows ||= $rows;
 
   $main::lxdebug->leave_sub();
 
-  return ($rows > $maxrows) ? $maxrows : $rows;
+  return max(min($rows, $maxrows), $minrows);
 }
 
 sub dberror {
@@ -362,12 +469,59 @@ sub isblank {
 
   my ($self, $name, $msg) = @_;
 
-  if ($self->{$name} =~ /^\s*$/) {
-    $self->error($msg);
+  my $curr = $self;
+  foreach my $part (split /\./, $name) {
+    if (!$curr->{$part} || ($curr->{$part} =~ /^\s*$/)) {
+      $self->error($msg);
+    }
+    $curr = $curr->{$part};
   }
+
   $main::lxdebug->leave_sub();
 }
 
+sub create_http_response {
+  $main::lxdebug->enter_sub();
+
+  my $self     = shift;
+  my %params   = @_;
+
+  my $cgi      = $main::cgi;
+  $cgi       ||= CGI->new('');
+
+  my $base_path;
+
+  if ($ENV{HTTP_X_FORWARDED_FOR}) {
+    $base_path =  $ENV{HTTP_REFERER};
+    $base_path =~ s|^.*?://.*?/|/|;
+  } else {
+    $base_path =  $ENV{REQUEST_URI};
+  }
+  $base_path =~ s|[^/]+$||;
+  $base_path =~ s|/$||;
+
+  my $session_cookie;
+  if (defined $main::auth) {
+    my $session_cookie_value   = $main::auth->get_session_id();
+    $session_cookie_value    ||= 'NO_SESSION';
+
+    $session_cookie = $cgi->cookie('-name'  => $main::auth->get_session_cookie_name(),
+                                   '-value' => $session_cookie_value,
+                                   '-path'  => $base_path);
+  }
+
+  my %cgi_params = ('-type' => $params{content_type});
+  $cgi_params{'-charset'} = $params{charset} if ($parmas{charset});
+
+  my $output = $cgi->header('-cookie' => $session_cookie,
+                            %cgi_params);
+
+  $main::lxdebug->leave_sub();
+
+  return $output;
+}
+
+
 sub header {
   $main::lxdebug->enter_sub();
 
@@ -381,6 +535,12 @@ sub header {
   my ($stylesheet, $favicon);
 
   if ($ENV{HTTP_USER_AGENT}) {
+    my $doctype;
+
+    if ($ENV{'HTTP_USER_AGENT'} =~ m/MSIE\s+\d/) {
+      # Only set the DOCTYPE for Internet Explorer. Other browsers have problems displaying the menu otherwise.
+      $doctype = qq|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n|;
+    }
 
     my $stylesheets = "$self->{stylesheet} $self->{stylesheets}";
 
@@ -416,6 +576,7 @@ sub header {
     if ($self->{jsscript} == 1) {
 
       $jsscript = qq|
+        <script type="text/javascript" src="js/common.js"></script>
         <style type="text/css">\@import url(js/jscalendar/calendar-win2k-1.css);</style>
         <script type="text/javascript" src="js/jscalendar/calendar.js"></script>
         <script type="text/javascript" src="js/jscalendar/lang/calendar-de.js"></script>
@@ -432,9 +593,10 @@ sub header {
     foreach $item (@ { $self->{AJAX} }) {
       $ajax .= $item->show_javascript();
     }
-    print qq|Content-Type: text/html; charset=${db_charset};
 
-<html>
+    print $self->create_http_response('content_type' => 'text/html',
+                                      'charset'      => $db_charset,);
+    print qq|${doctype}<html>
 <head>
   <title>$self->{titlebar}</title>
   $stylesheet
@@ -537,43 +699,18 @@ sub _prepare_html_template {
     map { $additional_params->{'DEBUG_' . uc($_)} = $main::debug_options{$_} } keys %main::debug_options;
   }
 
-  $main::lxdebug->leave_sub();
-
-  return $file;
-}
-
-sub parse_html_template {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $file, $additional_params) = @_;
-
-  $additional_params ||= { };
-
-  $file = $self->_prepare_html_template($file, $additional_params);
-
-  my $template = HTML::Template->new("filename" => $file,
-                                     "die_on_bad_params" => 0,
-                                     "strict" => 0,
-                                     "case_sensitive" => 1,
-                                     "loop_context_vars" => 1,
-                                     "global_vars" => 1);
-
-  foreach my $key ($template->param()) {
-    my $param = $additional_params->{$key} || $self->{$key};
-    $param = [] if (($template->query("name" => $key) eq "LOOP") && (ref($param) ne "ARRAY"));
-    $template->param($key => $param);
+  if ($main::auth && $main::auth->{RIGHTS} && $main::auth->{RIGHTS}->{$self->{login}}) {
+    while (my ($key, $value) = each %{ $main::auth->{RIGHTS}->{$self->{login}} }) {
+      $additional_params->{"AUTH_RIGHTS_" . uc($key)} = $value;
+    }
   }
 
-  my $output = $template->output();
-
-  $output = $main::locale->{iconv}->convert($output) if ($main::locale);
-
   $main::lxdebug->leave_sub();
 
-  return $output;
+  return $file;
 }
 
-sub parse_html_template2 {
+sub parse_html_template {
   $main::lxdebug->enter_sub();
 
   my ($self, $file, $additional_params) = @_;
@@ -582,16 +719,20 @@ sub parse_html_template2 {
 
   $file = $self->_prepare_html_template($file, $additional_params);
 
-  my $template = Template->new({ 'INTERPOLATE' => 0,
-                                 'EVAL_PERL'   => 0,
-                                 'ABSOLUTE'    => 1,
-                                 'CACHE_SIZE'  => 0,
+  my $template = Template->new({ 'INTERPOLATE'  => 0,
+                                 'EVAL_PERL'    => 0,
+                                 'ABSOLUTE'     => 1,
+                                 'CACHE_SIZE'   => 0,
+                                 'PLUGIN_BASE'  => 'SL::Template::Plugin',
+                                 'INCLUDE_PATH' => '.:templates/webpages',
                                }) || die;
 
   map { $additional_params->{$_} ||= $self->{$_} } keys %{ $self };
 
   my $output;
-  $template->process($file, $additional_params, \$output);
+  if (!$template->process($file, $additional_params, \$output)) {
+    print STDERR $template->error();
+  }
 
   $output = $main::locale->{iconv}->convert($output) if ($main::locale);
 
@@ -601,38 +742,47 @@ sub parse_html_template2 {
 }
 
 sub show_generic_error {
-  my ($self, $error, $title, $action) = @_;
+  my ($self, $error, %params) = @_;
 
-  my $add_params = {};
-  $add_params->{"title"} = $title if ($title);
-  $self->{"label_error"} = $error;
+  my $add_params = {
+    'title_error' => $params{title},
+    'label_error' => $error,
+  };
+
+  if ($params{action}) {
+    my @vars;
+
+    map { delete($self->{$_}); } qw(action);
+    map { push @vars, { "name" => $_, "value" => $self->{$_} } if (!ref($self->{$_})); } keys %{ $self };
 
-  my @vars;
-  if ($action) {
-    map({ delete($self->{$_}); } qw(action));
-    map({ push(@vars, { "name" => $_, "value" => $self->{$_} })
-            if (!ref($self->{$_})); }
-        keys(%{$self}));
-    $add_params->{"SHOW_BUTTON"} = 1;
-    $add_params->{"BUTTON_LABEL"} = $action;
+    $add_params->{SHOW_BUTTON}  = 1;
+    $add_params->{BUTTON_LABEL} = $params{label} || $params{action};
+    $add_params->{VARIABLES}    = \@vars;
+
+  } elsif ($params{back_button}) {
+    $add_params->{SHOW_BACK_BUTTON} = 1;
   }
-  $add_params->{"VARIABLES"} = \@vars;
+
+  $self->{title} = $title if ($title);
 
   $self->header();
-  print($self->parse_html_template("generic/error", $add_params));
+  print $self->parse_html_template("generic/error", $add_params);
 
   die("Error: $error\n");
 }
 
 sub show_generic_information {
-  my ($self, $error, $title) = @_;
+  my ($self, $text, $title) = @_;
 
-  my $add_params = {};
-  $add_params->{"title"} = $title if ($title);
-  $self->{"label_information"} = $error;
+  my $add_params = {
+    'title_information' => $title,
+    'label_information' => $text,
+  };
+
+  $self->{title} = $title if ($title);
 
   $self->header();
-  print($self->parse_html_template("generic/information", $add_params));
+  print $self->parse_html_template("generic/information", $add_params);
 
   die("Information: $error\n");
 }
@@ -724,18 +874,23 @@ sub format_amount {
   if ($amount eq "") {
     $amount = 0;
   }
-  my $neg = ($amount =~ s/-//);
-
+  
+  # Hey watch out! The amount can be an exponential term like 1.13686837721616e-13
+  
+  my $neg = ($amount =~ s/^-//);
+  my $exp = ($amount =~ m/[e]/) ? 1 : 0;
+  
   if (defined($places) && ($places ne '')) {
-    if ($places < 0) {
-      $amount *= 1;
-      $places *= -1;
-
-      my ($actual_places) = ($amount =~ /\.(\d+)/);
-      $actual_places = length($actual_places);
-      $places = $actual_places > $places ? $actual_places : $places;
+    if (not $exp) {
+      if ($places < 0) {
+        $amount *= 1;
+        $places *= -1;
+
+        my ($actual_places) = ($amount =~ /\.(\d+)/);
+        $actual_places = length($actual_places);
+        $places = $actual_places > $places ? $actual_places : $places;
+      }
     }
-
     $amount = $self->round_amount($amount, $places);
   }
 
@@ -757,7 +912,71 @@ sub format_amount {
   $main::lxdebug->leave_sub(2);
   return $amount;
 }
-#
+
+sub format_amount_units {
+  $main::lxdebug->enter_sub();
+
+  my $self             = shift;
+  my %params           = @_;
+
+  Common::check_params(\%params, qw(amount part_unit));
+
+  my $myconfig         = \%main::myconfig;
+  my $amount           = $params{amount};
+  my $places           = $params{places};
+  my $part_unit_name   = $params{part_unit};
+  my $amount_unit_name = $params{amount_unit};
+  my $conv_units       = $params{conv_units};
+  my $max_places       = $params{max_places};
+
+  AM->retrieve_all_units();
+  my $all_units        = $main::all_units;
+
+  if (('' eq ref $conv_units) && ($conv_units =~ /convertible/)) {
+    $conv_units = AM->convertible_units($all_units, $part_unit_name, $conv_units eq 'convertible_not_smaller');
+  }
+
+  if (!scalar @{ $conv_units }) {
+    my $result = $self->format_amount($myconfig, $amount, $places, undef, $max_places) . " " . $part_unit_name;
+    $main::lxdebug->leave_sub();
+    return $result;
+  }
+
+  my $part_unit  = $all_units->{$part_unit_name};
+  my $conv_unit  = ($amount_unit_name && ($amount_unit_name ne $part_unit_name)) ? $all_units->{$amount_unit_name} : $part_unit;
+
+  $amount       *= $conv_unit->{factor};
+
+  my @values;
+
+  foreach my $unit (@$conv_units) {
+    my $last = $unit->{name} eq $part_unit->{name};
+    if (!$last) {
+      $num     = int($amount / $unit->{factor});
+      $amount -= $num * $unit->{factor};
+    }
+
+    if ($last ? $amount : $num) {
+      push @values, { "unit"   => $unit->{name},
+                      "amount" => $last ? $amount / $unit->{factor} : $num,
+                      "places" => $last ? $places : 0 };
+    }
+
+    last if $last;
+  }
+
+  if (!@values) {
+    push @values, { "unit"   => $part_unit_name,
+                    "amount" => 0,
+                    "places" => 0 };
+  }
+
+  my $result = join " ", map { $self->format_amount($myconfig, $_->{amount}, $_->{places}, undef, $max_places), $_->{unit} } @values;
+
+  $main::lxdebug->leave_sub();
+
+  return $result;
+}
 
 sub format_string {
   $main::lxdebug->enter_sub(2);
@@ -774,6 +993,8 @@ sub format_string {
   return $input;
 }
 
+#
+
 sub parse_amount {
   $main::lxdebug->enter_sub(2);
 
@@ -857,9 +1078,10 @@ sub parse_template {
   map({ $self->{"employee_${_}"} = $myconfig->{$_}; }
       qw(email tel fax name signature company address businessnumber
          co_ustid taxnumber duns));
-  map({ $self->{"employee_${_}"} =~ s/\\n/\n/g; }
-      qw(company address signature));
-  map({ $self->{$_} =~ s/\\n/\n/g; } qw(company address signature));
+
+  map({ $self->{"${_}"} = $myconfig->{$_}; }
+      qw(co_ustid));
+              
 
   $self->{copies} = 1 if (($self->{copies} *= 1) <= 0);
 
@@ -910,14 +1132,15 @@ sub parse_template {
       $mail->{to} = $self->{EMAIL_RECIPIENT} ? $self->{EMAIL_RECIPIENT} : $self->{email};
       $mail->{from}   = qq|"$myconfig->{name}" <$myconfig->{email}>|;
       $mail->{fileid} = "$fileid.";
-      $myconfig->{signature} =~ s/\\r\\n/\\n/g;
+      $myconfig->{signature} =~ s/\r//g;
 
       # if we send html or plain text inline
       if (($self->{format} eq 'html') && ($self->{sendmode} eq 'inline')) {
         $mail->{contenttype} = "text/html";
 
-        $mail->{message}       =~ s/\r\n/<br>\n/g;
-        $myconfig->{signature} =~ s/\\n/<br>\n/g;
+        $mail->{message}       =~ s/\r//g;
+        $mail->{message}       =~ s/\n/<br>\n/g;
+        $myconfig->{signature} =~ s/\n/<br>\n/g;
         $mail->{message} .= "<br>\n-- <br>\n$myconfig->{signature}\n<br>";
 
         open(IN, $self->{tmpfile})
@@ -937,9 +1160,8 @@ sub parse_template {
                  $self->{"attachment_filename"} : $self->{"tmpfile"} });
         }
 
-        $mail->{message}       =~ s/\r\n/\n/g;
-        $myconfig->{signature} =~ s/\\n/\n/g;
-        $mail->{message} .= "\n-- \n$myconfig->{signature}";
+        $mail->{message}  =~ s/\r//g;
+        $mail->{message} .=  "\n-- \n$myconfig->{signature}";
 
       }
 
@@ -1005,43 +1227,59 @@ sub get_formname_translation {
   $formname ||= $self->{formname};
 
   my %formname_translations = (
-     bin_list            => $main::locale->text('Bin List'),
-     credit_note         => $main::locale->text('Credit Note'),
-     invoice             => $main::locale->text('Invoice'),
-     packing_list        => $main::locale->text('Packing List'),
-     pick_list           => $main::locale->text('Pick List'),
-     proforma            => $main::locale->text('Proforma Invoice'),
-     purchase_order      => $main::locale->text('Purchase Order'),
-     request_quotation   => $main::locale->text('RFQ'),
-     sales_order         => $main::locale->text('Confirmation'),
-     sales_quotation     => $main::locale->text('Quotation'),
-     storno_invoice      => $main::locale->text('Storno Invoice'),
-     storno_packing_list => $main::locale->text('Storno Packing List'),
+    bin_list                => $main::locale->text('Bin List'),
+    credit_note             => $main::locale->text('Credit Note'),
+    invoice                 => $main::locale->text('Invoice'),
+    packing_list            => $main::locale->text('Packing List'),
+    pick_list               => $main::locale->text('Pick List'),
+    proforma                => $main::locale->text('Proforma Invoice'),
+    purchase_order          => $main::locale->text('Purchase Order'),
+    request_quotation       => $main::locale->text('RFQ'),
+    sales_order             => $main::locale->text('Confirmation'),
+    sales_quotation         => $main::locale->text('Quotation'),
+    storno_invoice          => $main::locale->text('Storno Invoice'),
+    storno_packing_list     => $main::locale->text('Storno Packing List'),
+    sales_delivery_order    => $main::locale->text('Delivery Order'),
+    purchase_delivery_order => $main::locale->text('Delivery Order'),
   );
 
   return $formname_translations{$formname}
 }
 
+sub get_number_prefix_for_type {
+  my ($self) = @_;
+
+  my $prefix =
+      (first { $self->{type} eq $_ } qw(invoice credit_note)) ? 'inv'
+    : ($self->{type} =~ /_quotation$/)                        ? 'quo'
+    : ($self->{type} =~ /_delivery_order$/)                   ? 'do'
+    :                                                           'ord';
+
+  return $prefix;
+}
+
+sub get_extension_for_format {
+  my ($self)    = @_;
+
+  my $extension = $self->{format} =~ /pdf/i          ? ".pdf"
+                : $self->{format} =~ /postscript/i   ? ".ps"
+                : $self->{format} =~ /opendocument/i ? ".odt"
+                : $self->{format} =~ /html/i         ? ".html"
+                :                                      "";
+
+  return $extension;
+}
+
 sub generate_attachment_filename {
   my ($self) = @_;
 
-  my $attachment_filename = $self->get_formname_translation();
-  my $prefix = 
-      (grep { $self->{"type"} eq $_ } qw(invoice credit_note)) ? "inv"
-    : ($self->{"type"} =~ /_quotation$/)                       ? "quo"
-    :                                                            "ord";
+  my $attachment_filename = $main::locale->unquote_special_chars('HTML', $self->get_formname_translation());
+  my $prefix              = $self->get_number_prefix_for_type();
 
   if ($attachment_filename && $self->{"${prefix}number"}) {
-    $attachment_filename .= "_" . $self->{"${prefix}number"}
-                            . (  $self->{format} =~ /pdf/i          ? ".pdf"
-                               : $self->{format} =~ /postscript/i   ? ".ps"
-                               : $self->{format} =~ /opendocument/i ? ".odt"
-                               : $self->{format} =~ /html/i         ? ".html"
-                               :                                      "");
-    $attachment_filename =~ s/ /_/g;
-    my %umlaute = ( "ä" => "ae", "ö" => "oe", "ü" => "ue", 
-                    "Ä" => "Ae", "Ö" => "Oe", "Ü" => "Ue", "ß" => "ss");
-    map { $attachment_filename =~ s/$_/$umlaute{$_}/g } keys %umlaute;
+    $attachment_filename .=  "_" . $self->{"${prefix}number"} . $self->get_extension_for_format();
+    $attachment_filename  =  $main::locale->quote_special_chars('filenames', $attachment_filename);
+    $attachment_filename  =~ s|[\s/\\]+|_|g;
   } else {
     $attachment_filename = "";
   }
@@ -1049,6 +1287,19 @@ sub generate_attachment_filename {
   return $attachment_filename;
 }
 
+sub generate_email_subject {
+  my ($self) = @_;
+
+  my $subject = $main::locale->unquote_special_chars('HTML', $self->get_formname_translation());
+  my $prefix  = $self->get_number_prefix_for_type();
+
+  if ($subject && $self->{"${prefix}number"}) {
+    $subject .= " " . $self->{"${prefix}number"}
+  }
+
+  return $subject;
+}
+
 sub cleanup {
   $main::lxdebug->enter_sub();
 
@@ -1160,6 +1411,11 @@ sub get_standard_dbh {
 
   my ($self, $myconfig) = @_;
 
+  if ($standard_dbh && !$standard_dbh->{Active}) {
+    $main::lxdebug->message(LXDebug::INFO, "get_standard_dbh: \$standard_dbh is defined but not Active anymore");
+    undef $standard_dbh;
+  }
+
   $standard_dbh ||= $self->dbconnect_noauto($myconfig);
 
   $main::lxdebug->leave_sub(2);
@@ -1167,6 +1423,21 @@ sub get_standard_dbh {
   return $standard_dbh;
 }
 
+sub date_closed {
+  $main::lxdebug->enter_sub();
+
+  my ($self, $date, $myconfig) = @_;
+  my $dbh = $self->dbconnect($myconfig);
+
+  my $query = "SELECT 1 FROM defaults WHERE ? < closedto";
+  my $sth = prepare_execute_query($self, $dbh, $query, $date);
+  my ($closed) = $sth->fetchrow_array;
+
+  $main::lxdebug->leave_sub();
+
+  return $closed;
+}
+
 sub update_balance {
   $main::lxdebug->enter_sub();
 
@@ -1194,13 +1465,13 @@ sub update_exchangerate {
   $main::lxdebug->enter_sub();
 
   my ($self, $dbh, $curr, $transdate, $buy, $sell) = @_;
-
+  my ($query);
   # some sanity check for currency
   if ($curr eq '') {
     $main::lxdebug->leave_sub();
     return;
   }  
-  my $query = qq|SELECT curr FROM defaults|;
+  $query = qq|SELECT curr FROM defaults|;
 
   my ($currency) = selectrow_query($self, $dbh, $query);
   my ($defaultcurrency) = split m/:/, $currency;
@@ -1211,7 +1482,7 @@ sub update_exchangerate {
     return;
   }
 
-  my $query = qq|SELECT e.curr FROM exchangerate e
+  $query = qq|SELECT e.curr FROM exchangerate e
                  WHERE e.curr = ? AND e.transdate = ?
                  FOR UPDATE|;
   my $sth = prepare_execute_query($self, $dbh, $query, $curr, $transdate);
@@ -1276,13 +1547,14 @@ sub get_exchangerate {
   $main::lxdebug->enter_sub();
 
   my ($self, $dbh, $curr, $transdate, $fld) = @_;
+  my ($query);
 
   unless ($transdate) {
     $main::lxdebug->leave_sub();
     return 1;
   }
 
-  my $query = qq|SELECT curr FROM defaults|;
+  $query = qq|SELECT curr FROM defaults|;
 
   my ($currency) = selectrow_query($self, $dbh, $query);
   my ($defaultcurrency) = split m/:/, $currency;
@@ -1292,7 +1564,7 @@ sub get_exchangerate {
     return 1;
   }
 
-  my $query = qq|SELECT e.$fld FROM exchangerate e
+  $query = qq|SELECT e.$fld FROM exchangerate e
                  WHERE e.curr = ? AND e.transdate = ?|;
   my ($exchangerate) = selectrow_query($self, $dbh, $query, $curr, $transdate);
 
@@ -1326,8 +1598,6 @@ sub check_exchangerate {
 
   my ($exchangerate) = selectrow_query($self, $dbh, $query, $currency, $transdate);
 
-  $exchangerate = 1 if ($exchangerate eq "");
-
   $main::lxdebug->leave_sub();
 
   return $exchangerate;
@@ -1603,8 +1873,6 @@ sub get_salesman {
 
     $self->{salesman_name} = $login
       if ($self->{salesman_name} eq "");
-
-    map({ $self->{"salesman_$_"} =~ s/\\n/\n/g; } qw(address company));
   }
 
   $main::lxdebug->leave_sub();
@@ -1777,7 +2045,7 @@ sub _get_employees {
   my ($self, $dbh, $default_key, $key) = @_;
 
   $key = $default_key unless ($key);
-  $self->{$key} = selectall_hashref_query($self, $dbh, qq|SELECT * FROM employee ORDER BY name|);
+  $self->{$key} = selectall_hashref_query($self, $dbh, qq|SELECT * FROM employee ORDER BY lower(name)|);
 
   $main::lxdebug->leave_sub();
 }
@@ -1853,11 +2121,12 @@ $main::lxdebug->enter_sub();
 sub _get_customers {
   $main::lxdebug->enter_sub();
 
-  my ($self, $dbh, $key) = @_;
+  my ($self, $dbh, $key, $limit) = @_;
 
   $key = "all_customers" unless ($key);
+  $limit_clause = "LIMIT $limit" if $limit;
 
-  my $query = qq|SELECT * FROM customer WHERE NOT obsolete ORDER BY name|;
+  my $query = qq|SELECT * FROM customer WHERE NOT obsolete ORDER BY name $limit_clause|;
 
   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
 
@@ -1892,14 +2161,67 @@ sub _get_departments {
   $main::lxdebug->leave_sub();
 }
 
-sub _get_price_factors {
+sub _get_warehouses {
+  $main::lxdebug->enter_sub();
+
+  my ($self, $dbh, $param) = @_;
+
+  my ($key, $bins_key);
+
+  if ('' eq ref $param) {
+    $key = $param;
+
+  } else {
+    $key      = $param->{key};
+    $bins_key = $param->{bins};
+  }
+
+  my $query = qq|SELECT w.* FROM warehouse w
+                 WHERE (NOT w.invalid) AND
+                   ((SELECT COUNT(b.*) FROM bin b WHERE b.warehouse_id = w.id) > 0)
+                 ORDER BY w.sortkey|;
+
+  $self->{$key} = selectall_hashref_query($self, $dbh, $query);
+
+  if ($bins_key) {
+    $query = qq|SELECT id, description FROM bin WHERE warehouse_id = ?|;
+    my $sth = prepare_query($self, $dbh, $query);
+
+    foreach my $warehouse (@{ $self->{$key} }) {
+      do_statement($self, $sth, $query, $warehouse->{id});
+      $warehouse->{$bins_key} = [];
+
+      while (my $ref = $sth->fetchrow_hashref()) {
+        push @{ $warehouse->{$bins_key} }, $ref;
+      }
+    }
+    $sth->finish();
+  }
+
+  $main::lxdebug->leave_sub();
+}
+
+sub _get_simple {
+  $main::lxdebug->enter_sub();
+
+  my ($self, $dbh, $table, $key, $sortkey) = @_;
+
+  my $query  = qq|SELECT * FROM $table|;
+  $query    .= qq| ORDER BY $sortkey| if ($sortkey);
+
+  $self->{$key} = selectall_hashref_query($self, $dbh, $query);
+
+  $main::lxdebug->leave_sub();
+}
+
+sub _get_groups {
   $main::lxdebug->enter_sub();
 
   my ($self, $dbh, $key) = @_;
 
-  $key ||= "all_price_factors";
+  $key ||= "all_groups";
 
-  my $query = qq|SELECT * FROM price_factors ORDER BY sortkey|;
+  my $groups = $main::auth->read_groups();
 
   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
 
@@ -1973,11 +2295,19 @@ sub get_lists {
   }
   
   if($params{"customers"}) {
-    $self->_get_customers($dbh, $params{"customers"});
+    if (ref $params{"customers"} eq 'HASH') {
+      $self->_get_customers($dbh, $params{"customers"}{key}, $params{"customers"}{limit});
+    } else {
+      $self->_get_customers($dbh, $params{"customers"});
+    }
   }
   
   if($params{"vendors"}) {
-    $self->_get_vendors($dbh, $params{"vendors"});
+    if (ref $params{"vendors"} eq 'HASH') {
+      $self->_get_vendors($dbh, $params{"vendors"}{key}, $params{"vendors"}{limit});
+    } else {
+      $self->_get_vendors($dbh, $params{"vendors"});
+    }
   }
   
   if($params{"payments"}) {
@@ -1989,7 +2319,18 @@ sub get_lists {
   }
 
   if ($params{price_factors}) {
-    $self->_get_price_factors($dbh, $params{price_factors});
+    $self->_get_simple($dbh, 'price_factors', $params{price_factors}, 'sortkey');
+  }
+
+  if ($params{warehouses}) {
+    $self->_get_warehouses($dbh, $params{warehouses});
+  }
+
+  if ($params{groups}) {
+    $self->_get_groups($dbh, $params{groups});
+  }
+  if ($params{partsgroup}) {
+    $self->get_partsgroup(\%main::myconfig, { all => 1, target => $params{partsgroup} });
   }
 
   $main::lxdebug->leave_sub();
@@ -2229,7 +2570,7 @@ sub create_links {
     while ($ref = $sth->fetchrow_hashref(NAME_lc)) {
 
       foreach my $key (split(/:/, $ref->{link})) {
-        if ($key =~ /$module/) {
+        if ($key =~ /\Q$module\E/) {
 
           # cross reference for keys
           $xkeyref{ $ref->{accno} } = $key;
@@ -2288,7 +2629,7 @@ sub create_links {
                 LEFT JOIN taxkeys tk ON (tk.chart_id = c.id)
                 WHERE c.link LIKE ?
                   AND (tk.id = (SELECT id FROM taxkeys WHERE taxkeys.chart_id = c.id AND startdate <= $transdate ORDER BY startdate DESC LIMIT 1)
-                    OR c.link LIKE '%_tax%')
+                    OR c.link LIKE '%_tax%' OR c.taxkey_id IS NULL)
                 ORDER BY c.accno|;
 
     $sth = $dbh->prepare($query);
@@ -2298,7 +2639,7 @@ sub create_links {
     while ($ref = $sth->fetchrow_hashref(NAME_lc)) {
 
       foreach my $key (split(/:/, $ref->{link})) {
-        if ($key =~ /$module/) {
+        if ($key =~ /\Q$module\E/) {
 
           # cross reference for keys
           $xkeyref{ $ref->{accno} } = $key;
@@ -2406,36 +2747,52 @@ sub lastname_used {
 
   my ($self, $dbh, $myconfig, $table, $module) = @_;
 
-  my $arap  = ($table eq 'customer') ? "ar" : "ap";
-  $table = $table eq "customer" ? "customer" : "vendor";
-  my $where = "1 = 1";
+  my ($arap, $where);
 
-  if ($self->{type} =~ /_order/) {
+  $table         = $table eq "customer" ? "customer" : "vendor";
+  my %column_map = ("a.curr"                  => "currency",
+                    "a.${table}_id"           => "${table}_id",
+                    "a.department_id"         => "department_id",
+                    "d.description"           => "department",
+                    "ct.name"                 => $table,
+                    "current_date + ct.terms" => "duedate",
+    );
+
+  if ($self->{type} =~ /delivery_order/) {
+    $arap  = 'delivery_orders';
+    delete $column_map{"a.curr"};
+
+  } elsif ($self->{type} =~ /_order/) {
     $arap  = 'oe';
     $where = "quotation = '0'";
-  }
-  if ($self->{type} =~ /_quotation/) {
+
+  } elsif ($self->{type} =~ /_quotation/) {
     $arap  = 'oe';
     $where = "quotation = '1'";
+
+  } elsif ($table eq 'customer') {
+    $arap  = 'ar';
+
+  } else {
+    $arap  = 'ap';
+
   }
 
-  my $query = qq|SELECT MAX(id) FROM $arap
-                 WHERE $where AND ${table}_id > 0|;
-  my ($trans_id) = selectrow_query($self, $dbh, $query);
+  $where           = "($where) AND" if ($where);
+  my $query        = qq|SELECT MAX(id) FROM $arap
+                        WHERE $where ${table}_id > 0|;
+  my ($trans_id)   = selectrow_query($self, $dbh, $query);
+  $trans_id       *= 1;
 
-  $trans_id *= 1;
-  $query =
-    qq|SELECT
-         a.curr, a.${table}_id, a.department_id,
-         d.description AS department,
-         ct.name, current_date + ct.terms AS duedate
-       FROM $arap a
-       LEFT JOIN $table ct ON (a.${table}_id = ct.id)
-       LEFT JOIN department d ON (a.department_id = d.id)
-       WHERE a.id = ?|;
-  ($self->{currency},   $self->{"${table}_id"}, $self->{department_id},
-   $self->{department}, $self->{$table},        $self->{duedate})
-    = selectrow_query($self, $dbh, $query, $trans_id);
+  my $column_spec  = join(', ', map { "${_} AS $column_map{$_}" } keys %column_map);
+  $query           = qq|SELECT $column_spec
+                        FROM $arap a
+                        LEFT JOIN $table     ct ON (a.${table}_id = ct.id)
+                        LEFT JOIN department d  ON (a.department_id = d.id)
+                        WHERE a.id = ?|;
+  my $ref          = selectfirst_hashref_query($self, $dbh, $query, $trans_id);
+
+  map { $self->{$_} = $ref->{$_} } values %column_map;
 
   $main::lxdebug->leave_sub();
 }
@@ -2488,8 +2845,7 @@ sub redo_rows {
 
   my @ndx = ();
 
-  map { push @ndx, { num => $new->[$_ - 1]->{runningnumber}, ndx => $_ } }
-    (1 .. $count);
+  map { push @ndx, { num => $new->[$_ - 1]->{runningnumber}, ndx => $_ } } 1 .. $count;
 
   my $i = 0;
 
@@ -2530,8 +2886,8 @@ sub update_status {
   }
   $sth->finish();
 
-  my $printed = ($self->{printed} =~ /$self->{formname}/) ? "1" : "0";
-  my $emailed = ($self->{emailed} =~ /$self->{formname}/) ? "1" : "0";
+  my $printed = ($self->{printed} =~ /\Q$self->{formname}\E/) ? "1" : "0";
+  my $emailed = ($self->{emailed} =~ /\Q$self->{formname}\E/) ? "1" : "0";
 
   my %queued = split / /, $self->{queued};
   my @values;
@@ -2575,7 +2931,7 @@ sub save_status {
   my $formnames  = $self->{printed};
   my $emailforms = $self->{emailed};
 
-  my $query = qq|DELETE FROM status
+  $query = qq|DELETE FROM status
                  WHERE (formname = ?) AND (trans_id = ?)|;
   do_query($self, $dbh, $query, $self->{formname}, $self->{id});
 
@@ -2586,15 +2942,15 @@ sub save_status {
     my %queued = split / /, $self->{queued};
 
     foreach my $formname (keys %queued) {
-      $printed = ($self->{printed} =~ /$self->{formname}/) ? "1" : "0";
-      $emailed = ($self->{emailed} =~ /$self->{formname}/) ? "1" : "0";
+      $printed = ($self->{printed} =~ /\Q$self->{formname}\E/) ? "1" : "0";
+      $emailed = ($self->{emailed} =~ /\Q$self->{formname}\E/) ? "1" : "0";
 
       $query = qq|INSERT INTO status (trans_id, printed, emailed, spoolfile, formname)
                   VALUES (?, ?, ?, ?, ?)|;
       do_query($self, $dbh, $query, $self->{id}, $printed, $emailed, $queued{$formname}, $formname);
 
-      $formnames  =~ s/$self->{formname}//;
-      $emailforms =~ s/$self->{formname}//;
+      $formnames  =~ s/\Q$self->{formname}\E//;
+      $emailforms =~ s/\Q$self->{formname}\E//;
 
     }
   }
@@ -2608,8 +2964,8 @@ sub save_status {
   map { $status{$_}{emailed} = 1 } split / +/, $emailforms;
 
   foreach my $formname (keys %status) {
-    $printed = ($formnames  =~ /$self->{formname}/) ? "1" : "0";
-    $emailed = ($emailforms =~ /$self->{formname}/) ? "1" : "0";
+    $printed = ($formnames  =~ /\Q$self->{formname}\E/) ? "1" : "0";
+    $emailed = ($emailforms =~ /\Q$self->{formname}\E/) ? "1" : "0";
 
     $query = qq|INSERT INTO status (trans_id, printed, emailed, formname)
                 VALUES (?, ?, ?, ?)|;
@@ -2781,6 +3137,7 @@ sub get_partsgroup {
   $main::lxdebug->enter_sub();
 
   my ($self, $myconfig, $p) = @_;
+  my $target = $p->{target} || 'all_partsgroup';
 
   my $dbh = $self->get_standard_dbh($myconfig);
 
@@ -2819,7 +3176,7 @@ sub get_partsgroup {
     @values = ($p->{language_code});
   }
 
-  $self->{all_partsgroup} = selectall_hashref_query($self, $dbh, $query, @values);
+  $self->{$target} = selectall_hashref_query($self, $dbh, $query, @values);
 
   $main::lxdebug->leave_sub();
 }
@@ -2885,4 +3242,25 @@ sub all_years {
   $main::lxdebug->leave_sub();
 }
 
+sub backup_vars {
+  $main::lxdebug->enter_sub();
+  my $self = shift;
+  my @vars = @_;
+
+  map { $self->{_VAR_BACKUP}->{$_} = $self->{$_} if $self->{$_} } @vars;
+
+  $main::lxdebug->leave_sub();
+}
+
+sub restore_vars {
+  $main::lxdebug->enter_sub();
+
+  my $self = shift;
+  my @vars = @_;
+
+  map { $self->{$_} = $self->{_VAR_BACKUP}->{$_} if $self->{_VAR_BACKUP}->{$_} } @vars;
+
+  $main::lxdebug->leave_sub();
+}
+
 1;