Merge branch 'master' of github.com:kivitendo/kivitendo-erp
[kivitendo-erp.git] / SL / Form.pm
index 55ee46d..d7e0cda 100644 (file)
@@ -1,4 +1,4 @@
-#====================================================================
+#========= ===========================================================
 # LX-Office ERP
 # Copyright (C) 2004
 # Based on SQL-Ledger Version 2.1.9
@@ -53,14 +53,17 @@ use SL::CVar;
 use SL::DB;
 use SL::DBConnect;
 use SL::DBUtils;
+use SL::DB::Default;
 use SL::DO;
 use SL::IC;
 use SL::IS;
+use SL::Layout::Dispatcher;
 use SL::Locale;
 use SL::Mailer;
 use SL::Menu;
 use SL::MoreCommon qw(uri_encode uri_decode);
 use SL::OE;
+use SL::PrefixedNumber;
 use SL::Request;
 use SL::Template;
 use SL::User;
@@ -84,6 +87,17 @@ sub disconnect_standard_dbh {
   undef $standard_dbh;
 }
 
+sub read_version {
+  my ($self) = @_;
+
+  open VERSION_FILE, "VERSION";                 # New but flexible code reads version from VERSION-file
+  my $version =  <VERSION_FILE>;
+  $version    =~ s/[^0-9A-Za-z\.\_\-]//g; # only allow numbers, letters, points, underscores and dashes. Prevents injecting of malicious code.
+  close VERSION_FILE;
+
+  return $version;
+}
+
 sub new {
   $main::lxdebug->enter_sub();
 
@@ -99,10 +113,7 @@ sub new {
 
   bless $self, $type;
 
-  open VERSION_FILE, "VERSION";                 # New but flexible code reads version from VERSION-file
-  $self->{version} =  <VERSION_FILE>;
-  close VERSION_FILE;
-  $self->{version}  =~ s/[^0-9A-Za-z\.\_\-]//g; # only allow numbers, letters, points, underscores and dashes. Prevents injecting of malicious code.
+  $self->{version} = $self->read_version;
 
   $main::lxdebug->leave_sub();
 
@@ -136,9 +147,15 @@ sub _flatten_variables_rec {
     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;
+      my $element = $curr->{$key}[$idx];
+
+      if ('HASH' eq ref $element) {
+        foreach my $hash_key (sort keys %{ $element }) {
+          push @result, $self->_flatten_variables_rec($element, $prefix . $key . ($first_array_entry ? '[+].' : '[].'), $hash_key);
+          $first_array_entry = 0;
+        }
+      } else {
+        @result = ({ 'key' => $prefix . $key . ($first_array_entry ? '[+]' : '[]'), 'value' => $element });
       }
     }
   }
@@ -299,35 +316,13 @@ sub info {
   my ($self, $msg) = @_;
 
   if ($ENV{HTTP_USER_AGENT}) {
-    $msg =~ s/\n/<br>/g;
-
-    if (!$self->{header}) {
-      $self->header;
-      print qq|<body>|;
-    }
-
-    print qq|
-    <p class="message_ok"><b>$msg</b></p>
-
-    <script type="text/javascript">
-    <!--
-    // If JavaScript is enabled, the whole thing will be reloaded.
-    // The reason is: When one changes his menu setup (HTML / CSS ...)
-    // it now loads the correct code into the browser instead of do nothing.
-    setTimeout("top.frames.location.href='login.pl'",500);
-    //-->
-    </script>
-
-</body>
-    |;
+    $self->header;
+    print $self->parse_html_template('generic/form_info', { message => $msg });
 
+  } elsif ($self->{info_function}) {
+    &{ $self->{info_function} }($msg);
   } else {
-
-    if ($self->{info_function}) {
-      &{ $self->{info_function} }($msg);
-    } else {
-      print "$msg\n";
-    }
+    print "$msg\n";
   }
 
   $main::lxdebug->leave_sub();
@@ -379,9 +374,10 @@ sub _get_request_uri {
   my $self = shift;
 
   return URI->new($ENV{HTTP_REFERER})->canonical() if $ENV{HTTP_X_FORWARDED_FOR};
+  return URI->new                                  if !$ENV{REQUEST_URI}; # for testing
 
   my $scheme =  $ENV{HTTPS} && (lc $ENV{HTTPS} eq 'on') ? 'https' : 'http';
-  my $port   =  $ENV{SERVER_PORT} || '';
+  my $port   =  $ENV{SERVER_PORT};
   $port      =  undef if (($scheme eq 'http' ) && ($port == 80))
                       || (($scheme eq 'https') && ($port == 443));
 
@@ -447,52 +443,36 @@ sub create_http_response {
   return $output;
 }
 
-sub use_stylesheet {
-  my $self = shift;
-
-  $self->{stylesheet} = [ $self->{stylesheet} ] unless ref $self->{stylesheet} eq 'ARRAY';
-  $self->{stylesheet} = [ grep { -f                       }
-                          map  { m:^css/: ? $_ : "css/$_" }
-                          grep { $_                       }
-                               (@{ $self->{stylesheet} }, @_)
-                        ];
-
-  return @{ $self->{stylesheet} };
-}
-
-sub get_stylesheet_for_user {
-  my $css_path = 'css';
-  if (my $user_style = $::myconfig{stylesheet}) {
-    $user_style =~ s/\.css$//; # nuke trailing .css, this is a remnand of pre 2.7.0 stylesheet handling
-    if (-d "$css_path/$user_style" &&
-        -f "$css_path/$user_style/main.css") {
-      $css_path = "$css_path/$user_style";
-    } else {
-      $css_path = "$css_path/lx-office-erp";
-    }
-  } else {
-    $css_path = "$css_path/lx-office-erp";
-  }
-  $::myconfig{css_path} = $css_path; # needed for menunew, FIXME: don't do this here
-
-  return $css_path;
-}
-
 sub header {
   $::lxdebug->enter_sub;
 
-  # extra code is currently only used by menuv3 and menuv4 to set their css.
-  # it is strongly deprecated, and will be changed in a future version.
   my ($self, %params) = @_;
-  my $db_charset = $::lx_office_conf{system}->{dbcharset} || Common::DEFAULT_CHARSET;
   my @header;
 
   $::lxdebug->leave_sub and return if !$ENV{HTTP_USER_AGENT} || $self->{header}++;
 
-  my $css_path = $self->get_stylesheet_for_user;
+  if ($params{no_layout}) {
+    $::request->{layout} = SL::Layout::Dispatcher->new(style => 'none');
+  }
+
+  my $layout = $::request->{layout};
+
+  # standard css for all
+  # this should gradually move to the layouts that need it
+  $layout->use_stylesheet("$_.css") for qw(
+    main menu list_accounts jquery.autocomplete
+    jquery.multiselect2side frame_header/header
+    ui-lightness/jquery-ui
+    jquery-ui.custom jqModal
+  );
+
+  $layout->use_javascript("$_.js") for (qw(
+    jquery jquery-ui jquery.cookie jqModal jquery.checkall jquery.download
+    common part_selection switchmenuframe
+  ), "jquery/ui/i18n/jquery.ui.datepicker-$::myconfig{countrycode}");
 
   $self->{favicon} ||= "favicon.ico";
-  $self->{titlebar}  = "$self->{title} - $self->{titlebar}" if $self->{title};
+  $self->{titlebar} = join ' - ', grep $_, $self->{title}, $self->{login}, $::myconfig{dbname}, $self->{version} if $self->{title} || !$self->{titlebar};
 
   # build includes
   if ($self->{refresh_url} || $self->{refresh_time}) {
@@ -501,77 +481,68 @@ sub header {
     push @header, "<meta http-equiv='refresh' content='$refresh_time;$refresh_url'>";
   }
 
-  push @header, map { qq|<link rel="stylesheet" href="$_" type="text/css" title="Stylesheet">| } $self->use_stylesheet;
+  my $auto_reload_resources_param = $layout->auto_reload_resources_param;
 
-  push @header, "<style type='text/css'>\@page { size:landscape; }</style>" if $self->{landscape};
-  push @header, "<link rel='shortcut icon' href='$self->{favicon}' type='image/x-icon'>" if -f $self->{favicon};
-  push @header, map { qq|<script type="text/javascript" src="js/$_.js"></script>| }
-       qw(jquery common jscalendar/calendar jscalendar/lang/calendar-de jscalendar/calendar-setup part_selection jquery-ui jqModal switchmenuframe);
+  push @header, map { qq|<link rel="stylesheet" href="${_}${auto_reload_resources_param}" type="text/css" title="Stylesheet">| } $layout->stylesheets;
+  push @header, "<style type='text/css'>\@page { size:landscape; }</style> "                     if $self->{landscape};
+  push @header, "<link rel='shortcut icon' href='$self->{favicon}' type='image/x-icon'>"         if -f $self->{favicon};
+  push @header, map { qq|<script type="text/javascript" src="${_}${auto_reload_resources_param}"></script>| }                    $layout->javascripts;
   push @header, $self->{javascript} if $self->{javascript};
-  push @header, map { qq|<link rel="stylesheet" type="text/css" href="$css_path/$_.css">| }
-       qw(main menu tabcontent list_accounts jquery.autocomplete jquery.multiselect2side frame_header/header ui-lightness/jquery-ui-1.8.12.custom);
-  push @header, map { qq|<link rel="stylesheet" type="text/css" href="js/jscalendar/calendar-win2k-1.css">| }
   push @header, map { $_->show_javascript } @{ $self->{AJAX} || [] };
-  push @header, "<script type='text/javascript'>function fokus(){ document.$self->{fokus}.focus(); }</script>" if $self->{fokus};
-  push @header, sprintf "<script type='text/javascript'>top.document.title='%s';</script>",
-    join ' - ', grep $_, $self->{title}, $self->{login}, $::myconfig{dbname}, $self->{version} if $self->{title};
-
-  # if there is a title, we put some JavaScript in to the page, wich writes a
-  # meaningful title-tag for our frameset.
-  my $title_hack = '';
-  if ($self->{title}) {
-    $title_hack = qq|
-    <script type="text/javascript">
-    <!--
-      // Write a meaningful title-tag for our frameset.
-      top.document.title="| . $self->{"title"} . qq| - | . $self->{"login"} . qq| - | . $::myconfig{dbname} . qq| - V| . $self->{"version"} . qq|";
-    //-->
-    </script>|;
-  }
 
   my  %doctypes = (
     strict       => qq|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">|,
     transitional => qq|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">|,
     frameset     => qq|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">|,
+    html5        => qq|<!DOCTYPE html>|,
   );
 
   # output
-  print $self->create_http_response(content_type => 'text/html', charset => $db_charset);
+  print $self->create_http_response(content_type => 'text/html', charset => 'UTF-8');
   print $doctypes{$params{doctype} || 'transitional'}, $/;
   print <<EOT;
 <html>
  <head>
-  <meta http-equiv="Content-Type" content="text/html; charset=$db_charset">
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
   <title>$self->{titlebar}</title>
 EOT
   print "  $_\n" for @header;
   print <<EOT;
   <meta name="robots" content="noindex,nofollow">
-  <script type="text/javascript" src="js/tabcontent.js">
-
-  /***********************************************
-   * Tab Content script v2.2- © Dynamic Drive DHTML code library (www.dynamicdrive.com)
-   * This notice MUST stay intact for legal use
-   * Visit Dynamic Drive at http://www.dynamicdrive.com/ for full source code
-   ***********************************************/
-
-  </script>
-  $params{extra_code}
-  $title_hack
  </head>
+ <body>
 
 EOT
+  print $::request->{layout}->pre_content;
+  print $::request->{layout}->start_content;
+
+  $layout->header_done;
 
   $::lxdebug->leave_sub;
 }
 
+sub footer {
+  return unless $::request->{layout}->need_footer;
+
+  print $::request->{layout}->end_content;
+  print $::request->{layout}->post_content;
+
+  if (my @inline_scripts = $::request->{layout}->javascripts_inline) {
+    print "<script type='text/javascript'>@inline_scripts</script>\n";
+  }
+
+  print <<EOL
+ </body>
+</html>
+EOL
+}
+
 sub ajax_response_header {
   $main::lxdebug->enter_sub();
 
   my ($self) = @_;
 
-  my $db_charset = $::lx_office_conf{system}->{dbcharset} || Common::DEFAULT_CHARSET;
-  my $output     = $::request->{cgi}->header('-charset' => $db_charset);
+  my $output = $::request->{cgi}->header('-charset' => 'UTF-8');
 
   $main::lxdebug->leave_sub();
 
@@ -643,16 +614,13 @@ sub _prepare_html_template {
     map { $additional_params->{"myconfig_${_}"} = $main::myconfig{$_}; } keys %::myconfig;
   }
 
-  $additional_params->{"conf_dbcharset"}              = $::lx_office_conf{system}->{dbcharset};
   $additional_params->{"conf_webdav"}                 = $::lx_office_conf{features}->{webdav};
   $additional_params->{"conf_latex_templates"}        = $::lx_office_conf{print_templates}->{latex};
   $additional_params->{"conf_opendocument_templates"} = $::lx_office_conf{print_templates}->{opendocument};
   $additional_params->{"conf_vertreter"}              = $::lx_office_conf{features}->{vertreter};
-  $additional_params->{"conf_show_best_before"}       = $::lx_office_conf{features}->{show_best_before};
   $additional_params->{"conf_parts_image_css"}        = $::lx_office_conf{features}->{parts_image_css};
   $additional_params->{"conf_parts_listing_images"}   = $::lx_office_conf{features}->{parts_listing_images};
   $additional_params->{"conf_parts_show_image"}       = $::lx_office_conf{features}->{parts_show_image};
-  $additional_params->{"conf_payments_changeable"}    = $::lx_office_conf{features}->{payments_changeable};
   $additional_params->{"INSTANCE_CONF"}               = $::instance_conf;
 
   if (my $debug_options = $::lx_office_conf{debug}{options}) {
@@ -727,6 +695,14 @@ sub show_generic_error {
     return;
   }
 
+  if ($::request->is_ajax) {
+    $::lxdebug->message(0, "trying to render AJAX response...");
+    SL::ClientJS->new
+      ->error($error)
+      ->render(SL::Controller::Base->new);
+    ::end_of_request();
+  }
+
   my $add_params = {
     'title_error' => $params{title},
     'label_error' => $error,
@@ -778,51 +754,6 @@ sub show_generic_information {
   ::end_of_request();
 }
 
-# write Trigger JavaScript-Code ($qty = quantity of Triggers)
-# changed it to accept an arbitrary number of triggers - sschoeling
-sub write_trigger {
-  $main::lxdebug->enter_sub();
-
-  my $self     = shift;
-  my $myconfig = shift;
-  my $qty      = shift;
-
-  # set dateform for jsscript
-  # default
-  my %dateformats = (
-    "dd.mm.yy" => "%d.%m.%Y",
-    "dd/mm/yy" => "%d/%m/%Y",
-    "mm/dd/yy" => "%m/%d/%Y",
-    "yyyy-mm-dd" => "%Y-%m-%d",
-    );
-
-  my $ifFormat = defined($dateformats{$myconfig->{"dateformat"}}) ?
-    $dateformats{$myconfig->{"dateformat"}} : "%d.%m.%Y";
-
-  my @triggers;
-  while ($#_ >= 2) {
-    push @triggers, qq|
-       Calendar.setup(
-      {
-      inputField : "| . (shift) . qq|",
-      ifFormat :"$ifFormat",
-      align : "| .  (shift) . qq|",
-      button : "| . (shift) . qq|"
-      }
-      );
-       |;
-  }
-  my $jsscript = qq|
-       <script type="text/javascript">
-       <!--| . join("", @triggers) . qq|//-->
-        </script>
-        |;
-
-  $main::lxdebug->leave_sub();
-
-  return $jsscript;
-}    #end sub write_trigger
-
 sub _store_redirect_info_in_session {
   my ($self) = @_;
 
@@ -866,41 +797,30 @@ sub format_amount {
   $main::lxdebug->enter_sub(2);
 
   my ($self, $myconfig, $amount, $places, $dash) = @_;
-  $dash ||= '';
-
-  if ($amount eq "") {
-    $amount = 0;
-  }
+  $amount ||= 0;
+  $dash   ||= '';
+  my $neg = $amount < 0;
+  my $force_places = defined $places && $places >= 0;
 
-  $amount *= 1;
+  $amount = $self->round_amount($amount, abs $places) if $force_places;
+  $amount = sprintf "%.*f", ($force_places ? $places : 10), abs $amount; # 6 is default for %fa
 
-  # Hey watch out! The amount can be an exponential term like 1.13686837721616e-13
+  # before the sprintf amount was a number, afterwards it's a string. because of the dynamic nature of perl
+  # this is easy to confuse, so keep in mind: before this comment no s///, m//, concat or other strong ops on
+  # $amount. after this comment no +,-,*,/,abs. it will only introduce subtle bugs.
 
-  my $neg = ($amount =~ s/^-//);
-  my $exp = ($amount =~ m/[e]/) ? 1 : 0;
-
-  if (defined($places) && ($places ne '')) {
-    if (not $exp) {
-      if ($places < 0) {
-        $amount *= 1;
-        $places *= -1;
-
-        if ($amount =~ /\.(\d+)/) {
-          my $actual_places = length $1;
-          $places = $actual_places if $actual_places > $places;
-        }
-      }
-    }
-    $amount = $self->round_amount($amount, $places);
-  }
+  $amount =~ s/0*$// unless defined $places && $places == 0;             # cull trailing 0s
 
   my @d = map { s/\d//g; reverse split // } my $tmp = $myconfig->{numberformat}; # get delim chars
-  my @p = split(/\./, $amount); # split amount at decimal point
-
-  $p[0] =~ s/\B(?=(...)*$)/$d[1]/g if $d[1]; # add 1,000 delimiters
+  my @p = split(/\./, $amount);                                          # split amount at decimal point
 
+  $p[0] =~ s/\B(?=(...)*$)/$d[1]/g if $d[1];                             # add 1,000 delimiters
   $amount = $p[0];
-  $amount .= $d[0].($p[1]||'').(0 x ($places - length ($p[1]||''))) if ($places || $p[1] ne '');
+  if ($places || $p[1]) {
+    $amount .= $d[0]
+            .  ( $p[1] || '' )
+            .  (0 x (abs($places || 0) - length ($p[1]||'')));           # pad the fraction
+  }
 
   $amount = do {
     ($dash =~ /-/)    ? ($neg ? "($amount)"                            : "$amount" )                              :
@@ -908,7 +828,6 @@ sub format_amount {
                         ($neg ? "-$amount"                             : "$amount" )                              ;
   };
 
-
   $main::lxdebug->leave_sub(2);
   return $amount;
 }
@@ -1003,6 +922,11 @@ sub parse_amount {
 
   my ($self, $myconfig, $amount) = @_;
 
+  if (!defined($amount) || ($amount eq '')) {
+    $main::lxdebug->leave_sub(2);
+    return 0;
+  }
+
   if (   ($myconfig->{numberformat} eq '1.000,00')
       || ($myconfig->{numberformat} eq '1000,00')) {
     $amount =~ s/\.//g;
@@ -1051,6 +975,7 @@ sub parse_template {
 
   local (*IN, *OUT);
 
+  my $defaults  = SL::DB::Default->get;
   my $userspath = $::lx_office_conf{paths}->{userspath};
 
   $self->{"cwd"} = getcwd();
@@ -1064,7 +989,6 @@ sub parse_template {
     $ext_for_format = $self->{"format"} =~ m/pdf/ ? 'pdf' : 'odt';
 
   } elsif ($self->{"format"} =~ /(postscript|pdf)/i) {
-    $ENV{"TEXINPUTS"} = ".:" . getcwd() . "/" . $myconfig->{"templates"} . ":" . $ENV{"TEXINPUTS"};
     $template_type    = 'LaTeX';
     $ext_for_format   = 'pdf';
 
@@ -1103,11 +1027,13 @@ sub parse_template {
   $self->{"notes"} = $self->{ $self->{"formname"} . "notes" };
 
   if (!$self->{employee_id}) {
-    map { $self->{"employee_${_}"} = $myconfig->{$_}; } qw(email tel fax name signature company address businessnumber co_ustid taxnumber duns);
+    $self->{"employee_${_}"} = $myconfig->{$_} for qw(email tel fax name signature);
+    $self->{"employee_${_}"} = $defaults->$_   for qw(address businessnumber co_ustid company duns sepa_creditor_id taxnumber);
   }
 
-  map { $self->{"${_}"} = $myconfig->{$_}; } qw(co_ustid);
-  map { $self->{"myconfig_${_}"} = $myconfig->{$_} } grep { $_ ne 'dbpasswd' } keys %{ $myconfig };
+  $self->{"myconfig_${_}"} = $myconfig->{$_} for grep { $_ ne 'dbpasswd' } keys %{ $myconfig };
+  $self->{$_}              = $defaults->$_   for qw(co_ustid);
+  $self->{"myconfig_${_}"} = $defaults->$_   for qw(address businessnumber co_ustid company duns sepa_creditor_id taxnumber);
 
   $self->{copies} = 1 if (($self->{copies} *= 1) <= 0);
 
@@ -1123,6 +1049,7 @@ sub parse_template {
     UNLINK => ($::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files})? 0 : 1,
   );
   close $temp_fh;
+  (undef, undef, $self->{template_meta}{tmpfile}) = File::Spec->splitpath( $self->{tmpfile} );
 
   if ($template->uses_temp_file() || $self->{media} eq 'email') {
     $out              = $self->{OUT};
@@ -1170,7 +1097,6 @@ sub parse_template {
 
       map { $mail->{$_} = $self->{$_} }
         qw(cc bcc subject message version format);
-      $mail->{charset} = $::lx_office_conf{system}->{dbcharset} || Common::DEFAULT_CHARSET;
       $mail->{to} = $self->{EMAIL_RECIPIENT} ? $self->{EMAIL_RECIPIENT} : $self->{email};
       $mail->{from}   = qq|"$myconfig->{name}" <$myconfig->{email}>|;
       $mail->{fileid} = time() . '.' . $$ . '.';
@@ -1427,22 +1353,13 @@ sub datetonum {
 
 # Database routines used throughout
 
-sub _dbconnect_options {
-  my $self    = shift;
-  my $options = { pg_enable_utf8 => $::locale->is_utf8,
-                  @_ };
-
-  return $options;
-}
-
 sub dbconnect {
   $main::lxdebug->enter_sub(2);
 
   my ($self, $myconfig) = @_;
 
   # connect to database
-  my $dbh = SL::DBConnect->connect($myconfig->{dbconnect}, $myconfig->{dbuser}, $myconfig->{dbpasswd}, $self->_dbconnect_options)
-    or $self->dberror;
+  my $dbh = SL::DBConnect->connect or $self->dberror;
 
   # set db options
   if ($myconfig->{dboptions}) {
@@ -1460,8 +1377,7 @@ sub dbconnect_noauto {
   my ($self, $myconfig) = @_;
 
   # connect to database
-  my $dbh = SL::DBConnect->connect($myconfig->{dbconnect}, $myconfig->{dbuser}, $myconfig->{dbpasswd}, $self->_dbconnect_options(AutoCommit => 0))
-    or $self->dberror;
+  my $dbh = SL::DBConnect->connect(SL::DBConnect->get_connect_args(AutoCommit => 0)) or $self->dberror;
 
   # set db options
   if ($myconfig->{dboptions}) {
@@ -1501,18 +1417,18 @@ sub date_closed {
   my $sth = prepare_execute_query($self, $dbh, $query, conv_date($date));
 
   # Falls $date = '' - Fehlermeldung aus der Datenbank. Ich denke,
-  # es ist sicher ein conv_date vorher IMMER auszuführen.
-  # Testfälle ohne definiertes closedto:
+  # es ist sicher ein conv_date vorher IMMER auszuführen.
+  # Testfälle ohne definiertes closedto:
   #   Leere Datumseingabe i.O.
   #     SELECT 1 FROM defaults WHERE '' < closedto
-  #   normale Zahlungsbuchung über Rechnungsmaske i.O.
+  #   normale Zahlungsbuchung über Rechnungsmaske i.O.
   #     SELECT 1 FROM defaults WHERE '10.05.2011' < closedto
-  # Testfälle mit definiertem closedto (30.04.2011):
+  # Testfälle mit definiertem closedto (30.04.2011):
   #  Leere Datumseingabe i.O.
   #   SELECT 1 FROM defaults WHERE '' < closedto
-  # normale Buchung im geschloßenem Zeitraum i.O.
+  # normale Buchung im geschloßenem Zeitraum i.O.
   #   SELECT 1 FROM defaults WHERE '21.04.2011' < closedto
-  #     Fehlermeldung: Es können keine Zahlungen für abgeschlossene Bücher gebucht werden!
+  #     Fehlermeldung: Es können keine Zahlungen für abgeschlossene Bücher gebucht werden!
   # normale Buchung in aktiver Buchungsperiode i.O.
   #   SELECT 1 FROM defaults WHERE '01.05.2011' < closedto
 
@@ -1523,6 +1439,24 @@ sub date_closed {
   return $closed;
 }
 
+# prevents bookings to the to far away future
+sub date_max_future {
+  $main::lxdebug->enter_sub();
+
+  my ($self, $date, $myconfig) = @_;
+  my $dbh = $self->dbconnect($myconfig);
+
+  my $query = "SELECT 1 FROM defaults WHERE ? - current_date > max_future_booking_interval";
+  my $sth = prepare_execute_query($self, $dbh, $query, conv_date($date));
+
+  my ($max_future_booking_interval) = $sth->fetchrow_array;
+
+  $main::lxdebug->leave_sub();
+
+  return $max_future_booking_interval;
+}
+
+
 sub update_balance {
   $main::lxdebug->enter_sub();
 
@@ -1556,19 +1490,17 @@ sub update_exchangerate {
     $main::lxdebug->leave_sub();
     return;
   }
-  $query = qq|SELECT curr FROM defaults|;
-
-  my ($currency) = selectrow_query($self, $dbh, $query);
-  my ($defaultcurrency) = split m/:/, $currency;
+  $query = qq|SELECT name AS curr FROM currencies WHERE id=(SELECT currency_id FROM defaults)|;
 
+  my ($defaultcurrency) = selectrow_query($self, $dbh, $query);
 
   if ($curr eq $defaultcurrency) {
     $main::lxdebug->leave_sub();
     return;
   }
 
-  $query = qq|SELECT e.curr FROM exchangerate e
-                 WHERE e.curr = ? AND e.transdate = ?
+  $query = qq|SELECT e.currency_id FROM exchangerate e
+                 WHERE e.currency_id = (SELECT cu.id FROM currencies cu WHERE cu.name=?) AND e.transdate = ?
                  FOR UPDATE|;
   my $sth = prepare_execute_query($self, $dbh, $query, $curr, $transdate);
 
@@ -1594,12 +1526,12 @@ sub update_exchangerate {
   if ($sth->fetchrow_array) {
     $query = qq|UPDATE exchangerate
                 SET $set
-                WHERE curr = ?
+                WHERE currency_id = (SELECT id FROM currencies WHERE name = ?)
                 AND transdate = ?|;
 
   } else {
-    $query = qq|INSERT INTO exchangerate (curr, buy, sell, transdate)
-                VALUES (?, $buy, $sell, ?)|;
+    $query = qq|INSERT INTO exchangerate (currency_id, buy, sell, transdate)
+                VALUES ((SELECT id FROM currencies WHERE name = ?), $buy, $sell, ?)|;
   }
   $sth->finish;
   do_query($self, $dbh, $query, $curr, $transdate);
@@ -1634,23 +1566,22 @@ sub get_exchangerate {
   my ($self, $dbh, $curr, $transdate, $fld) = @_;
   my ($query);
 
-  unless ($transdate) {
+  unless ($transdate && $curr) {
     $main::lxdebug->leave_sub();
     return 1;
   }
 
-  $query = qq|SELECT curr FROM defaults|;
+  $query = qq|SELECT name AS curr FROM currencies WHERE id = (SELECT currency_id FROM defaults)|;
 
-  my ($currency) = selectrow_query($self, $dbh, $query);
-  my ($defaultcurrency) = split m/:/, $currency;
+  my ($defaultcurrency) = selectrow_query($self, $dbh, $query);
 
-  if ($currency eq $defaultcurrency) {
+  if ($curr eq $defaultcurrency) {
     $main::lxdebug->leave_sub();
     return 1;
   }
 
   $query = qq|SELECT e.$fld FROM exchangerate e
-                 WHERE e.curr = ? AND e.transdate = ?|;
+                 WHERE e.currency_id = (SELECT id FROM currencies WHERE name = ?) AND e.transdate = ?|;
   my ($exchangerate) = selectrow_query($self, $dbh, $query, $curr, $transdate);
 
 
@@ -1683,7 +1614,7 @@ sub check_exchangerate {
 
   my $dbh   = $self->get_standard_dbh($myconfig);
   my $query = qq|SELECT e.$fld FROM exchangerate e
-                 WHERE e.curr = ? AND e.transdate = ?|;
+                 WHERE e.currency_id = (SELECT id FROM currencies WHERE name = ?) AND e.transdate = ?|;
 
   my ($exchangerate) = selectrow_query($self, $dbh, $query, $currency, $transdate);
 
@@ -1699,10 +1630,8 @@ sub get_all_currencies {
   my $myconfig = shift || \%::myconfig;
   my $dbh      = $self->get_standard_dbh($myconfig);
 
-  my $query = qq|SELECT curr FROM defaults|;
-
-  my ($curr)     = selectrow_query($self, $dbh, $query);
-  my @currencies = grep { $_ } map { s/\s//g; $_ } split m/:/, $curr;
+  my $query = qq|SELECT name FROM currencies|;
+  my @currencies = map { $_->{name} } selectall_hashref_query($self, $dbh, $query);
 
   $main::lxdebug->leave_sub();
 
@@ -1713,11 +1642,14 @@ sub get_default_currency {
   $main::lxdebug->enter_sub();
 
   my ($self, $myconfig) = @_;
-  my @currencies        = $self->get_all_currencies($myconfig);
+  my $dbh      = $self->get_standard_dbh($myconfig);
+  my $query = qq|SELECT name AS curr FROM currencies WHERE id = (SELECT currency_id FROM defaults)|;
+
+  my ($defaultcurrency) = selectrow_query($self, $dbh, $query);
 
   $main::lxdebug->leave_sub();
 
-  return $currencies[0];
+  return $defaultcurrency;
 }
 
 sub set_payment_options {
@@ -1767,10 +1699,9 @@ sub set_payment_options {
     $amounts{invtotal} = $self->{invtotal};
     $amounts{total}    = $self->{total};
   }
-  $amounts{skonto_in_percent} = 100.0 * $self->{percent_skonto};
-
   map { $amounts{$_} = $self->parse_amount($myconfig, $amounts{$_}) } keys %amounts;
 
+  $amounts{skonto_in_percent}  = 100.0 * $self->{percent_skonto};
   $amounts{skonto_amount}      = $amounts{invtotal} * $self->{percent_skonto};
   $amounts{invtotal_wo_skonto} = $amounts{invtotal} * (1 - $self->{percent_skonto});
   $amounts{total_wo_skonto}    = $amounts{total}    * (1 - $self->{percent_skonto});
@@ -1965,6 +1896,7 @@ sub get_employee_data {
 
   my $self     = shift;
   my %params   = @_;
+  my $defaults = SL::DB::Default->get;
 
   Common::check_params(\%params, qw(prefix));
   Common::check_params_x(\%params, qw(id));
@@ -1981,7 +1913,8 @@ sub get_employee_data {
 
   if ($login) {
     my $user = User->new(login => $login);
-    map { $self->{$params{prefix} . "_${_}"} = $user->{$_}; } qw(address businessnumber co_ustid company duns email fax name signature taxnumber tel);
+    $self->{$params{prefix} . "_${_}"}    = $user->{$_}   for qw(email fax name signature tel);
+    $self->{$params{prefix} . "_${_}"}    = $defaults->$_ for qw(address businessnumber co_ustid company duns taxnumber);
 
     $self->{$params{prefix} . '_login'}   = $login;
     $self->{$params{prefix} . '_name'}  ||= $login;
@@ -1998,7 +1931,7 @@ sub get_duedate {
   $reference_date = $reference_date ? conv_dateq($reference_date) . '::DATE' : 'current_date';
 
   my $dbh         = $self->get_standard_dbh($myconfig);
-  my $payment_id;
+  my ($payment_id, $duedate);
 
   if($self->{payment_id}) {
     $payment_id = $self->{payment_id};
@@ -2007,8 +1940,10 @@ sub get_duedate {
     ($payment_id) = selectrow_query($self, $dbh, $query, $self->{vendor_id});
   }
 
-  my $query       = qq|SELECT ${reference_date} + terms_netto FROM payment_terms WHERE id = ?|;
-  my ($duedate)   = selectrow_query($self, $dbh, $query, $payment_id);
+  if ($payment_id) {
+    my $query  = qq|SELECT ${reference_date} + terms_netto FROM payment_terms WHERE id = ?|;
+    ($duedate) = selectrow_query($self, $dbh, $query, $payment_id);
+  }
 
   $main::lxdebug->leave_sub();
 
@@ -2171,7 +2106,7 @@ sub _get_taxcharts {
 
   my $where = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
 
-  my $query = qq|SELECT * FROM tax $where ORDER BY taxkey|;
+  my $query = qq|SELECT * FROM tax $where ORDER BY taxkey, rate|;
 
   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
 
@@ -2256,9 +2191,7 @@ $main::lxdebug->enter_sub();
 
   $key = "all_currencies" unless ($key);
 
-  my $query = qq|SELECT curr AS currency FROM defaults|;
-
-  $self->{$key} = [split(/\:/ , selectfirst_hashref_query($self, $dbh, $query)->{currency})];
+  $self->{$key} = [$self->get_all_currencies()];
 
   $main::lxdebug->leave_sub();
 }
@@ -2559,14 +2492,14 @@ sub all_vc {
   $table = $table eq "customer" ? "customer" : "vendor";
 
   # build selection list
-  # Hotfix für Bug 1837 - Besser wäre es alte Buchungsbelege
+  # Hotfix für Bug 1837 - Besser wäre es alte Buchungsbelege
   # OHNE Auswahlliste (reines Textfeld) zu laden. Hilft aber auch
-  # nicht für veränderbare Belege (oe, do, ...)
-  my $obsolete = "WHERE NOT obsolete" unless $self->{id};
+  # nicht für veränderbare Belege (oe, do, ...)
+  my $obsolete = $self->{id} ? '' : "WHERE NOT obsolete";
   my $query = qq|SELECT count(*) FROM $table $obsolete|;
   my ($count) = selectrow_query($self, $dbh, $query);
 
-  if ($count < $myconfig->{vclimit}) {
+  if ($count <= $myconfig->{vclimit}) {
     $query = qq|SELECT id, name, salesman_id
                 FROM $table $obsolete
                 ORDER BY name|;
@@ -2762,14 +2695,17 @@ sub create_links {
     $self->{TAX} = selectall_hashref_query($self, $dbh, $query);
   }
 
+  my $extra_columns = '';
+  $extra_columns   .= 'a.direct_debit, ' if ($module eq 'AR') || ($module eq 'AP');
+
   if ($self->{id}) {
     $query =
       qq|SELECT
            a.cp_id, a.invnumber, a.transdate, a.${table}_id, a.datepaid,
-           a.duedate, a.ordnumber, a.taxincluded, a.curr AS currency, a.notes,
+           a.duedate, a.ordnumber, a.taxincluded, (SELECT cu.name FROM currencies cu WHERE cu.id=a.currency_id) AS currency, a.notes,
            a.intnotes, a.department_id, a.amount AS oldinvtotal,
            a.paid AS oldtotalpaid, a.employee_id, a.gldate, a.type,
-           a.globalproject_id,
+           a.globalproject_id, ${extra_columns}
            c.name AS $table,
            d.description AS department,
            e.name AS employee
@@ -2784,9 +2720,6 @@ sub create_links {
       $self->{$key} = $ref->{$key};
     }
 
-    # remove any trailing whitespace
-    $self->{currency} =~ s/\s*$//;
-
     my $transdate = "current_date";
     if ($self->{transdate}) {
       $transdate = $dbh->quote($self->{transdate});
@@ -2870,9 +2803,11 @@ sub create_links {
     }
 
     $sth->finish;
+    #check das:
     $query =
       qq|SELECT
-           d.curr AS currencies, d.closedto, d.revtrans,
+           d.closedto, d.revtrans,
+           (SELECT cu.name FROM currencies cu WHERE cu.id=d.currency_id) AS defaultcurrency,
            (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id = c.id) AS fxgain_accno,
            (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id) AS fxloss_accno
          FROM defaults d|;
@@ -2884,7 +2819,8 @@ sub create_links {
     # get date
     $query =
        qq|SELECT
-            current_date AS transdate, d.curr AS currencies, d.closedto, d.revtrans,
+            current_date AS transdate, d.closedto, d.revtrans,
+            (SELECT cu.name FROM currencies cu WHERE cu.id=d.currency_id) AS defaultcurrency,
             (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id = c.id) AS fxgain_accno,
             (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id) AS fxloss_accno
           FROM defaults d|;
@@ -2894,7 +2830,7 @@ sub create_links {
     if ($self->{"$self->{vc}_id"}) {
 
       # only setup currency
-      ($self->{currency}) = split(/:/, $self->{currencies}) if !$self->{currency};
+      ($self->{currency}) = $self->{defaultcurrency} if !$self->{currency};
 
     } else {
 
@@ -2919,19 +2855,17 @@ sub lastname_used {
   my ($arap, $where);
 
   $table         = $table eq "customer" ? "customer" : "vendor";
-  my %column_map = ("a.curr"                  => "currency",
-                    "a.${table}_id"           => "${table}_id",
+  my %column_map = ("a.${table}_id"           => "${table}_id",
                     "a.department_id"         => "department_id",
                     "d.description"           => "department",
                     "ct.name"                 => $table,
-                    "ct.curr"                 => "cv_curr",
+                    "cu.name"                 => "currency",
                     "current_date + ct.terms" => "duedate",
     );
 
   if ($self->{type} =~ /delivery_order/) {
     $arap  = 'delivery_orders';
-    delete $column_map{"a.curr"};
-    delete $column_map{"ct.curr"};
+    delete $column_map{"cu.currency"};
 
   } elsif ($self->{type} =~ /_order/) {
     $arap  = 'oe';
@@ -2960,18 +2894,12 @@ sub lastname_used {
                         FROM $arap a
                         LEFT JOIN $table     ct ON (a.${table}_id = ct.id)
                         LEFT JOIN department d  ON (a.department_id = d.id)
+                        LEFT JOIN currencies cu ON (cu.id=ct.currency_id)
                         WHERE a.id = ?|;
   my $ref          = selectfirst_hashref_query($self, $dbh, $query, $trans_id);
 
   map { $self->{$_} = $ref->{$_} } values %column_map;
 
-  # remove any trailing whitespace
-  $self->{currency} =~ s/\s*$// if $self->{currency};
-  $self->{cv_curr} =~ s/\s*$// if $self->{cv_curr};
-
-  # if customer/vendor currency is set use this
-  $self->{currency} = $self->{cv_curr} if $self->{cv_curr};
-
   $main::lxdebug->leave_sub();
 }
 
@@ -3251,15 +3179,8 @@ sub update_defaults {
   my ($var) = $sth->fetchrow_array;
   $sth->finish;
 
-  if ($var =~ m/\d+$/) {
-    my $new_var  = (substr $var, $-[0]) * 1 + 1;
-    my $len_diff = length($var) - $-[0] - length($new_var);
-    $var         = substr($var, 0, $-[0]) . ($len_diff > 0 ? '0' x $len_diff : '') . $new_var;
-
-  } else {
-    $var = $var . '1';
-  }
-
+  $var   = 0 if !defined($var) || ($var eq '');
+  $var   = SL::PrefixedNumber->new(number => $var)->get_next;
   $query = qq|UPDATE defaults SET $fld = ?|;
   do_query($self, $dbh, $query, $var);
 
@@ -3448,17 +3369,24 @@ sub restore_vars {
 sub prepare_for_printing {
   my ($self) = @_;
 
-  $self->{templates} ||= $::myconfig{templates};
+  my $defaults         = SL::DB::Default->get;
+
+  $self->{templates} ||= $defaults->templates;
   $self->{formname}  ||= $self->{type};
   $self->{media}     ||= 'email';
 
   die "'media' other than 'email', 'file', 'printer' is not supported yet" unless $self->{media} =~ m/^(?:email|file|printer)$/;
 
+  # Several fields that used to reside in %::myconfig (stored in
+  # auth.user_config) are now stored in defaults. Copy them over for
+  # compatibility.
+  $self->{$_} = $defaults->$_ for qw(company address taxnumber co_ustid duns sepa_creditor_id);
+
   # set shipto from billto unless set
   my $has_shipto = any { $self->{"shipto$_"} } qw(name street zipcode city country contact);
   if (!$has_shipto && ($self->{type} =~ m/^(?:purchase_order|request_quotation)$/)) {
-    $self->{shiptoname}   = $::myconfig{company};
-    $self->{shiptostreet} = $::myconfig{address};
+    $self->{shiptoname}   = $defaults->company;
+    $self->{shiptostreet} = $defaults->address;
   }
 
   my $language = $self->{language} ? '_' . $self->{language} : '';
@@ -3476,7 +3404,7 @@ sub prepare_for_printing {
   IC->retrieve_accounts(\%::myconfig, $self, map { $_ => $self->{"id_$_"} } 1 .. $self->{rowcount});
 
   if ($self->{type} =~ /_delivery_order$/) {
-    DO->order_details();
+    DO->order_details(\%::myconfig, $self);
   } elsif ($self->{type} =~ /sales_order|sales_quotation|request_quotation|purchase_order/) {
     OE->order_details(\%::myconfig, $self);
   } else {
@@ -3500,7 +3428,7 @@ sub prepare_for_printing {
   }
 
   my $printer_code    = $self->{printer_code} ? '_' . $self->{printer_code} : '';
-  my $email_extension = -f "$::myconfig{templates}/$self->{formname}_email${language}.${extension}" ? '_email' : '';
+  my $email_extension = -f ($defaults->templates . "/$self->{formname}_email${language}.${extension}") ? '_email' : '';
   $self->{IN}         = "$self->{formname}${email_extension}${language}${printer_code}.${extension}";
 
   # Format dates.
@@ -3602,6 +3530,29 @@ sub reformat_numbers {
   $::myconfig{numberformat} = $saved_numberformat;
 }
 
+sub layout {
+  my ($self) = @_;
+  $::lxdebug->enter_sub;
+
+  my %style_to_script_map = (
+    v3  => 'v3',
+    neu => 'new',
+  );
+
+  my $menu_script = $style_to_script_map{$::myconfig{menustyle}} || '';
+
+  package main;
+  require "bin/mozilla/menu$menu_script.pl";
+  package Form;
+  require SL::Controller::FrameHeader;
+
+
+  my $layout = SL::Controller::FrameHeader->new->action_header . ::render();
+
+  $::lxdebug->leave_sub;
+  return $layout;
+}
+
 1;
 
 __END__
@@ -3612,7 +3563,7 @@ SL::Form.pm - main data object.
 
 =head1 SYNOPSIS
 
-This is the main data object of Lx-Office.
+This is the main data object of kivitendo.
 Unfortunately it also acts as a god object for certain data retrieval procedures used in the entry points.
 Points of interest for a beginner are:
 
@@ -3637,7 +3588,7 @@ will in this case not increase the value, and return undef.
 
 Generates a HTTP redirection header for the new C<$url>. Constructs an
 absolute URL including scheme, host name and port. If C<$url> is a
-relative URL then it is considered relative to Lx-Office base URL.
+relative URL then it is considered relative to kivitendo base URL.
 
 This function C<die>s if headers have already been created with
 C<$::form-E<gt>header>.