Merge branch 'clients'
authorMoritz Bunkus <m.bunkus@linet-services.de>
Fri, 14 Jun 2013 15:27:41 +0000 (17:27 +0200)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Fri, 14 Jun 2013 15:27:41 +0000 (17:27 +0200)
158 files changed:
SL/AM.pm
SL/Auth.pm
SL/BackgroundJob/CreatePeriodicInvoices.pm
SL/BackgroundJob/SelfTest.pm
SL/CA.pm
SL/CT.pm
SL/Common.pm
SL/Controller/Admin.pm [new file with mode: 0644]
SL/Controller/Base.pm
SL/Controller/ClientConfig.pm
SL/Controller/LoginScreen.pm
SL/DB.pm
SL/DB/AuthClient.pm [new file with mode: 0644]
SL/DB/AuthClientGroup.pm [new file with mode: 0644]
SL/DB/AuthClientUser.pm [new file with mode: 0644]
SL/DB/AuthGroup.pm
SL/DB/AuthGroupRight.pm
SL/DB/AuthUser.pm
SL/DB/AuthUserConfig.pm
SL/DB/AuthUserGroup.pm
SL/DB/Bin.pm
SL/DB/Currency.pm
SL/DB/Helper/ALL.pm
SL/DB/Helper/Mappings.pm
SL/DB/Helper/Util.pm [new file with mode: 0644]
SL/DB/Manager/AuthClient.pm [new file with mode: 0644]
SL/DB/Manager/AuthGroup.pm [new file with mode: 0644]
SL/DB/Manager/AuthUser.pm [new file with mode: 0644]
SL/DB/Manager/Currency.pm [new file with mode: 0644]
SL/DB/Manager/Printer.pm [new file with mode: 0644]
SL/DB/Manager/Warehouse.pm [new file with mode: 0644]
SL/DB/MetaSetup/AuthClient.pm [new file with mode: 0644]
SL/DB/MetaSetup/AuthClientGroup.pm [new file with mode: 0644]
SL/DB/MetaSetup/AuthClientUser.pm [new file with mode: 0644]
SL/DB/MetaSetup/AuthGroup.pm
SL/DB/MetaSetup/AuthGroupRight.pm
SL/DB/MetaSetup/AuthUser.pm
SL/DB/MetaSetup/AuthUserConfig.pm
SL/DB/MetaSetup/AuthUserGroup.pm
SL/DB/MetaSetup/Default.pm
SL/DB/MetaSetup/Employee.pm
SL/DB/Object.pm
SL/DB/Printer.pm
SL/DB/Warehouse.pm
SL/DBConnect.pm
SL/DBUpgrade2.pm
SL/DN.pm
SL/DO.pm
SL/Dispatcher.pm
SL/Dispatcher/AuthHandler.pm
SL/Dispatcher/AuthHandler/Admin.pm
SL/Dispatcher/AuthHandler/User.pm
SL/Form.pm
SL/GL.pm
SL/IS.pm
SL/InstallationCheck.pm
SL/OE.pm
SL/Printer.pm [deleted file]
SL/RC.pm
SL/System/InstallationLock.pm [new file with mode: 0644]
SL/Template/LaTeX.pm
SL/User.pm
bin/mozilla/admin.pl
bin/mozilla/admin_groups.pl [deleted file]
bin/mozilla/admin_printer.pl [deleted file]
bin/mozilla/am.pl
bin/mozilla/amtemplates.pl
bin/mozilla/ca.pl
bin/mozilla/io.pl
bin/mozilla/login.pl
bin/mozilla/rp.pl
bin/mozilla/sepa.pl
bin/mozilla/ustva.pl
config/kivitendo.conf.default
css/kivitendo/main.css
css/lx-office-erp/jquery.multiselect2side.css
css/lx-office-erp/main.css
doc/UPGRADE
doc/dokumentation.xml
js/common.js
locale/de/all
menu.ini
modules/override/Rose/DBx/Cache/Anywhere.pm
scripts/console
scripts/dbupgrade2_tool.pl
scripts/locales.pl
scripts/rose_auto_create_model.pl
scripts/task_server.pl
sql/Pg-upgrade2-auth/clients.pl [new file with mode: 0644]
sql/Pg-upgrade2-auth/clients_webdav.pl [new file with mode: 0644]
sql/Pg-upgrade2-auth/foreign_key_constraints_on_delete.sql [new file with mode: 0644]
sql/Pg-upgrade2/clients.pl [new file with mode: 0644]
sql/Pg-upgrade2/remove_role_from_employee.sql [new file with mode: 0644]
t/rdbo_relationship_consistency.t [new file with mode: 0644]
templates/webpages/admin/adminlogin.html
templates/webpages/admin/backup_dataset.html
templates/webpages/admin/backup_dataset_email_done.html
templates/webpages/admin/check_auth_database.html
templates/webpages/admin/check_auth_tables.html
templates/webpages/admin/create_dataset.html
templates/webpages/admin/dbadmin.html
templates/webpages/admin/dbcreate.html
templates/webpages/admin/dbdelete.html
templates/webpages/admin/dbupgrade_all_done.html
templates/webpages/admin/delete_dataset.html
templates/webpages/admin/delete_group_confirm.html [deleted file]
templates/webpages/admin/edit_client.html [new file with mode: 0644]
templates/webpages/admin/edit_group.html
templates/webpages/admin/edit_group_membership.html [deleted file]
templates/webpages/admin/edit_groups.html [deleted file]
templates/webpages/admin/edit_printer.html [new file with mode: 0644]
templates/webpages/admin/edit_user.html
templates/webpages/admin/list_printers.html [new file with mode: 0644]
templates/webpages/admin/list_users.html [deleted file]
templates/webpages/admin/restore_dataset.html
templates/webpages/admin/restore_dataset_start_footer.html
templates/webpages/admin/show.html [new file with mode: 0644]
templates/webpages/admin/test_db_connection.html
templates/webpages/admin/update_dataset.html
templates/webpages/admin_printer/_login_form.html [deleted file]
templates/webpages/admin_printer/edit.html [deleted file]
templates/webpages/admin_printer/list.html [deleted file]
templates/webpages/admin_printer/login.html [deleted file]
templates/webpages/am/config.html
templates/webpages/am/edit_defaults.html [deleted file]
templates/webpages/client_config/_datev_check_configuration.html [new file with mode: 0644]
templates/webpages/client_config/_default_accounts.html [new file with mode: 0644]
templates/webpages/client_config/_miscellaneous.html [new file with mode: 0644]
templates/webpages/client_config/_orders_deleteable.html [new file with mode: 0644]
templates/webpages/client_config/_posting_configuration.html [new file with mode: 0644]
templates/webpages/client_config/_ranges_of_numbers.html [new file with mode: 0644]
templates/webpages/client_config/_warehouse.html [new file with mode: 0644]
templates/webpages/client_config/form.html
templates/webpages/dbupgrade/auth/clients.html [new file with mode: 0644]
templates/webpages/dbupgrade/auth/clients_webdav.html [new file with mode: 0644]
templates/webpages/dbupgrade/footer.html
templates/webpages/generic/form_info.html [new file with mode: 0644]
templates/webpages/login/company_logo.html
templates/webpages/login_screen/user_login.html
templates/webpages/menu/header.html
templates/webpages/menu/menunew.html
templates/webpages/menu/menuv3.html
templates/webpages/ustva/config_step2.html
webdav/.gitignore [new file with mode: 0644]
webdav/anfragen/.dummy [deleted file]
webdav/anfragen/.gitignore [deleted file]
webdav/angebote/.dummy [deleted file]
webdav/angebote/.gitignore [deleted file]
webdav/bestellungen/.dummy [deleted file]
webdav/bestellungen/.gitignore [deleted file]
webdav/einkaufsrechnungen/.dummy [deleted file]
webdav/einkaufsrechnungen/.gitignore [deleted file]
webdav/gutschriften/.dummy [deleted file]
webdav/gutschriften/.gitignore [deleted file]
webdav/lieferantenbestellungen/.dummy [deleted file]
webdav/lieferantenbestellungen/.gitignore [deleted file]
webdav/rechnungen/.dummy [deleted file]
webdav/rechnungen/.gitignore [deleted file]

index 5a9f236..e2ea0be 100644 (file)
--- a/SL/AM.pm
+++ b/SL/AM.pm
@@ -42,6 +42,9 @@ use Data::Dumper;
 use Encode;
 use List::MoreUtils qw(any);
 use SL::DBUtils;
+use SL::DB::AuthUser;
+use SL::DB::Default;
+use SL::DB::Employee;
 
 use strict;
 
@@ -992,7 +995,7 @@ sub prepare_template_filename {
       $filename =~ s|.*/||;
     }
     $display_filename = $filename;
-    $filename = "$myconfig->{templates}/$filename";
+    $filename = SL::DB::Default->get->templates . "/$filename";
   }
 
   $main::lxdebug->leave_sub();
@@ -1048,109 +1051,23 @@ sub save_template {
   return $error;
 }
 
-sub save_defaults {
-  $main::lxdebug->enter_sub();
-
-  my $self     = shift;
-  my %params   = @_;
-
-  my $myconfig = \%main::myconfig;
-  my $form     = $main::form;
-
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-
-  my %accnos;
-  map { ($accnos{$_}) = split(m/--/, $form->{$_}) } qw(inventory_accno income_accno expense_accno fxgain_accno fxloss_accno ar_paid_accno);
-
-  # these defaults are database wide
-
-  my $query =
-    qq|UPDATE defaults SET
-        inventory_accno_id = (SELECT c.id FROM chart c WHERE c.accno = ?),
-        income_accno_id    = (SELECT c.id FROM chart c WHERE c.accno = ?),
-        expense_accno_id   = (SELECT c.id FROM chart c WHERE c.accno = ?),
-        fxgain_accno_id    = (SELECT c.id FROM chart c WHERE c.accno = ?),
-        fxloss_accno_id    = (SELECT c.id FROM chart c WHERE c.accno = ?),
-        ar_paid_accno_id   = (SELECT c.id FROM chart c WHERE c.accno = ?),
-        invnumber          = ?,
-        cnnumber           = ?,
-        sonumber           = ?,
-        ponumber           = ?,
-        sqnumber           = ?,
-        rfqnumber          = ?,
-        customernumber     = ?,
-        vendornumber       = ?,
-        articlenumber      = ?,
-        servicenumber      = ?,
-        assemblynumber     = ?,
-        sdonumber          = ?,
-        pdonumber          = ?,
-        businessnumber     = ?,
-        weightunit         = ?,
-        language_id        = ?|;
-  my @values = ($accnos{inventory_accno}, $accnos{income_accno}, $accnos{expense_accno},
-                $accnos{fxgain_accno},    $accnos{fxloss_accno}, $accnos{ar_paid_accno},
-                $form->{invnumber},       $form->{cnnumber},
-                $form->{sonumber},        $form->{ponumber},
-                $form->{sqnumber},        $form->{rfqnumber},
-                $form->{customernumber},  $form->{vendornumber},
-                $form->{articlenumber},   $form->{servicenumber},
-                $form->{assemblynumber},
-                $form->{sdonumber},       $form->{pdonumber},
-                $form->{businessnumber},  $form->{weightunit},
-                conv_i($form->{language_id}));
-  do_query($form, $dbh, $query, @values);
-
-  $main::lxdebug->message(0, "es gibt rowcount: " . $form->{rowcount});
-
-  for my $i (1..$form->{rowcount}) {
-    if ($form->{"curr_$i"} ne $form->{"old_curr_$i"}) {
-      $query = qq|UPDATE currencies SET name = ? WHERE name = ?|;
-      do_query($form, $dbh, $query, $form->{"curr_$i"}, $form->{"old_curr_$i"});
-    }
-  }
-
-  if (length($form->{new_curr}) > 0) {
-    $query = qq|INSERT INTO currencies (name) VALUES (?)|;
-    do_query($form, $dbh, $query, $form->{new_curr});
-  }
-
-  $dbh->commit();
-
-  $main::lxdebug->leave_sub();
-}
-
-
 sub save_preferences {
   $main::lxdebug->enter_sub();
 
-  my ($self, $myconfig, $form) = @_;
-
-  my $dbh = $form->get_standard_dbh($myconfig);
-
-  my ($businessnumber) = selectrow_query($form, $dbh, qq|SELECT businessnumber FROM defaults|);
-
-  # update name
-  my $query = qq|UPDATE employee SET name = ? WHERE login = ?|;
-  do_query($form, $dbh, $query, $form->{name}, $form->{login});
-
-  my $rc = $dbh->commit();
+  my ($self, $form) = @_;
 
-  $form->{businessnumber} =  $businessnumber;
+  my $employee = SL::DB::Manager::Employee->find_by(login => $form->{login});
+  $employee->update_attributes(name => $form->{name});
 
-  $myconfig = User->new(login => $form->{login});
-
-  foreach my $item (keys %$form) {
-    $myconfig->{$item} = $form->{$item};
-  }
-
-  $myconfig->save_member;
-
-  my $auth = $main::auth;
+  my $user = SL::DB::Manager::AuthUser->find_by(login => $form->{login});
+  $user->update_attributes(
+    config_values => {
+      map { ($_ => $form->{$_}) } SL::DB::AuthUser::CONFIG_VARS(),
+    });
 
   $main::lxdebug->leave_sub();
 
-  return $rc;
+  return 1;
 }
 
 sub get_defaults {
@@ -1173,139 +1090,6 @@ sub get_defaults {
   return $defaults;
 }
 
-sub defaultaccounts {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  # get defaults from defaults table
-  my $query = qq|SELECT * FROM defaults|;
-  my $sth   = $dbh->prepare($query);
-  $sth->execute || $form->dberror($query);
-
-  $form->{defaults}               = $sth->fetchrow_hashref("NAME_lc");
-  $form->{defaults}{IC}           = $form->{defaults}{inventory_accno_id};
-  $form->{defaults}{IC_income}    = $form->{defaults}{income_accno_id};
-  $form->{defaults}{IC_expense}   = $form->{defaults}{expense_accno_id};
-  $form->{defaults}{FX_gain}      = $form->{defaults}{fxgain_accno_id};
-  $form->{defaults}{FX_loss}      = $form->{defaults}{fxloss_accno_id};
-  $form->{defaults}{AR_paid}      = $form->{defaults}{ar_paid_accno_id};
-
-  $form->{defaults}{weightunit} ||= 'kg';
-
-  $sth->finish;
-
-  $query = qq|SELECT c.id, c.accno, c.description, c.link
-              FROM chart c
-              WHERE c.link LIKE '%IC%'
-              ORDER BY c.accno|;
-  $sth = $dbh->prepare($query);
-  $sth->execute || $self->dberror($query);
-
-  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-    foreach my $key (split(/:/, $ref->{link})) {
-      if ($key =~ /IC/) {
-        my $nkey = $key;
-        if ($key =~ /cogs/) {
-          $nkey = "IC_expense";
-        }
-        if ($key =~ /sale/) {
-          $nkey = "IC_income";
-        }
-        %{ $form->{IC}{$nkey}{ $ref->{accno} } } = (
-                                             id          => $ref->{id},
-                                             description => $ref->{description}
-        );
-      }
-    }
-  }
-  $sth->finish;
-
-  $query = qq|SELECT c.id, c.accno, c.description
-              FROM chart c
-              WHERE c.category = 'I'
-              AND c.charttype = 'A'
-              ORDER BY c.accno|;
-  $sth = $dbh->prepare($query);
-  $sth->execute || $self->dberror($query);
-
-  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-    %{ $form->{IC}{FX_gain}{ $ref->{accno} } } = (
-                                             id          => $ref->{id},
-                                             description => $ref->{description}
-    );
-  }
-  $sth->finish;
-
-  $query = qq|SELECT c.id, c.accno, c.description
-              FROM chart c
-              WHERE c.category = 'E'
-              AND c.charttype = 'A'
-              ORDER BY c.accno|;
-  $sth = $dbh->prepare($query);
-  $sth->execute || $self->dberror($query);
-
-  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-    %{ $form->{IC}{FX_loss}{ $ref->{accno} } } = (
-                                             id          => $ref->{id},
-                                             description => $ref->{description}
-    );
-  }
-  $sth->finish;
-
-  # now get the tax rates and numbers
-  $query = qq|SELECT c.id, c.accno, c.description,
-              t.rate * 100 AS rate, t.taxnumber
-              FROM chart c, tax t
-              WHERE c.id = t.chart_id|;
-
-  $sth = $dbh->prepare($query);
-  $sth->execute || $form->dberror($query);
-
-  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-    $form->{taxrates}{ $ref->{accno} }{id}          = $ref->{id};
-    $form->{taxrates}{ $ref->{accno} }{description} = $ref->{description};
-    $form->{taxrates}{ $ref->{accno} }{taxnumber}   = $ref->{taxnumber}
-      if $ref->{taxnumber};
-    $form->{taxrates}{ $ref->{accno} }{rate} = $ref->{rate} if $ref->{rate};
-  }
-  # Abfrage für Standard Umlaufvermögenskonto
-  $query =
-    qq|SELECT id, accno, description, link | .
-    qq|FROM chart | .
-    qq|WHERE link LIKE ? |.
-    qq|ORDER BY accno|;
-  $sth = prepare_execute_query($form, $dbh, $query, '%AR%');
-  $sth->execute || $form->dberror($query);#
-  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-    foreach my $item (split(/:/, $ref->{link})) {
-      if ($item eq "AR_paid") {
-        %{ $form->{IC}{AR_paid}{ $ref->{accno} } } = (
-                                             id          => $ref->{id},
-                                             description => $ref->{description}
-          );
-      }
-    }
-  }
-
-  $sth->finish;
-
-  #Get currencies:
-  $query              = qq|SELECT name AS curr FROM currencies ORDER BY id|;
-  $form->{CURRENCIES} = selectall_hashref_query($form, $dbh, $query);
-
-  #Which of them is the default currency?
-  $query = qq|SELECT name AS defaultcurrency FROM currencies WHERE id = (SELECT currency_id FROM defaults LIMIT 1);|;
-  ($form->{defaultcurrency}) = selectrow_query($form, $dbh, $query);
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
 sub closedto {
   $main::lxdebug->enter_sub();
 
index 23a9771..184468e 100644 (file)
@@ -26,6 +26,11 @@ use strict;
 use constant SESSION_KEY_ROOT_AUTH => 'session_auth_status_root';
 use constant SESSION_KEY_USER_AUTH => 'session_auth_status_user';
 
+use Rose::Object::MakeMethods::Generic (
+  scalar => [ qw(client) ],
+);
+
+
 sub new {
   $main::lxdebug->enter_sub();
 
@@ -51,32 +56,25 @@ sub reset {
   $self->{unique_counter}     = 0;
   $self->{column_information} = SL::Auth::ColumnInformation->new(auth => $self);
   $self->{authenticator}->reset;
+
+  $self->client(undef);
 }
 
-sub get_user_dbh {
-  my ($self, $login, %params) = @_;
-  my $may_fail = delete $params{may_fail};
-
-  my %user = $self->read_user(login => $login);
-  my $dbh  = SL::DBConnect->connect(
-    $user{dbconnect},
-    $user{dbuser},
-    $user{dbpasswd},
-    {
-      pg_enable_utf8 => $::locale->is_utf8,
-      AutoCommit     => 0
-    }
-  );
+sub set_client {
+  my ($self, $id_or_name) = @_;
 
-  if (!$may_fail && !$dbh) {
-    $::form->error($::locale->text('The connection to the authentication database failed:') . "\n" . $DBI::errstr);
-  }
+  $self->client(undef);
 
-  if ($user{dboptions} && $dbh) {
-    $dbh->do($user{dboptions}) or $::form->dberror($user{dboptions});
-  }
+  return undef unless $id_or_name;
+
+  my $column = $id_or_name =~ m/^\d+$/ ? 'id' : 'name';
+  my $dbh    = $self->dbconnect;
 
-  return $dbh;
+  return undef unless $dbh;
+
+  $self->client($dbh->selectrow_hashref(qq|SELECT * FROM auth.clients WHERE ${column} = ?|, undef, $id_or_name));
+
+  return $self->client;
 }
 
 sub DESTROY {
@@ -144,6 +142,23 @@ sub _read_auth_config {
   $main::lxdebug->leave_sub();
 }
 
+sub has_access_to_client {
+  my ($self, $login) = @_;
+
+  return 0 if !$self->client || !$self->client->{id};
+
+  my $sql = <<SQL;
+    SELECT cu.client_id
+    FROM auth.clients_users cu
+    LEFT JOIN auth."user" u ON (cu.user_id = u.id)
+    WHERE (u.login      = ?)
+      AND (cu.client_id = ?)
+SQL
+
+  my ($has_access) = $self->dbconnect->selectrow_array($sql, undef, $login, $self->client->{id});
+  return $has_access;
+}
+
 sub authenticate_root {
   $main::lxdebug->enter_sub();
 
@@ -175,6 +190,11 @@ sub authenticate {
 
   my ($self, $login, $password) = @_;
 
+  if (!$self->client || !$self->has_access_to_client($login)) {
+    $::lxdebug->leave_sub;
+    return ERR_PASSWORD;
+  }
+
   my $session_auth = $self->get_session_value(SESSION_KEY_USER_AUTH());
   if (defined $session_auth && $session_auth == OK) {
     $::lxdebug->leave_sub;
@@ -187,7 +207,7 @@ sub authenticate {
   }
 
   my $result = $login ? $self->{authenticator}->authenticate($login, $password) : ERR_USER;
-  $self->set_session_value(SESSION_KEY_USER_AUTH() => $result, login => $login);
+  $self->set_session_value(SESSION_KEY_USER_AUTH() => $result, login => $login, client_id => $self->client->{id});
 
   $::lxdebug->leave_sub;
   return $result;
@@ -528,24 +548,19 @@ sub delete_user {
 
   my $dbh   = $self->dbconnect;
   my $id    = $self->get_user_id($login);
-  my $user_db_exists;
 
   $dbh->rollback and return $::lxdebug->leave_sub if (!$id);
 
-  my $u_dbh = $self->get_user_dbh($login, may_fail => 1);
-  $user_db_exists = $self->check_tables($u_dbh) if $u_dbh;
-
-  $u_dbh->begin_work if $u_dbh && $user_db_exists;
-
   $dbh->begin_work;
 
   do_query($::form, $dbh, qq|DELETE FROM auth.user_group WHERE user_id = ?|, $id);
   do_query($::form, $dbh, qq|DELETE FROM auth.user_config WHERE user_id = ?|, $id);
   do_query($::form, $dbh, qq|DELETE FROM auth.user WHERE id = ?|, $id);
-  do_query($::form, $u_dbh, qq|UPDATE employee SET deleted = 't' WHERE login = ?|, $login) if $u_dbh && $user_db_exists;
+
+  # TODO: SL::Auth::delete_user
+  # do_query($::form, $u_dbh, qq|UPDATE employee SET deleted = 't' WHERE login = ?|, $login) if $u_dbh && $user_db_exists;
 
   $dbh->commit;
-  $u_dbh->commit if $u_dbh && $user_db_exists;
 
   $::lxdebug->leave_sub;
 }
@@ -566,7 +581,7 @@ sub restore_session {
 
   if (!$session_id) {
     $main::lxdebug->leave_sub();
-    return SESSION_NONE;
+    return $self->session_restore_result(SESSION_NONE());
   }
 
   my ($dbh, $query, $sth, $cookie, $ref, $form);
@@ -576,7 +591,7 @@ sub restore_session {
   # Don't fail if the auth DB doesn't yet.
   if (!( $dbh = $self->dbconnect(1) )) {
     $::lxdebug->leave_sub;
-    return SESSION_NONE;
+    return $self->session_restore_result(SESSION_NONE());
   }
 
   # Don't fail if the "auth" schema doesn't exist yet, e.g. if the
@@ -586,7 +601,7 @@ sub restore_session {
   if (!($sth = $dbh->prepare($query)) || !$sth->execute($session_id)) {
     $sth->finish if $sth;
     $::lxdebug->leave_sub;
-    return SESSION_NONE;
+    return $self->session_restore_result(SESSION_NONE());
   }
 
   $cookie = $sth->fetchrow_hashref;
@@ -605,7 +620,7 @@ sub restore_session {
   if ($cookie_is_bad) {
     $self->destroy_session();
     $main::lxdebug->leave_sub();
-    return $cookie ? SESSION_EXPIRED : SESSION_NONE;
+    return $self->session_restore_result($cookie ? SESSION_EXPIRED() : SESSION_NONE());
   }
 
   if ($self->{column_information}->has('auto_restore')) {
@@ -616,7 +631,15 @@ sub restore_session {
 
   $main::lxdebug->leave_sub();
 
-  return SESSION_OK;
+  return $self->session_restore_result(SESSION_OK());
+}
+
+sub session_restore_result {
+  my $self = shift;
+  if (@_) {
+    $self->{session_restore_result} = $_[0];
+  }
+  return $self->{session_restore_result};
 }
 
 sub _load_without_auto_restore_column {
@@ -1337,9 +1360,13 @@ sub load_rights_for_user {
          (SELECT ug.group_id
           FROM auth.user_group ug
           LEFT JOIN auth."user" u ON (ug.user_id = u.id)
-          WHERE u.login = ?)|;
+          WHERE u.login = ?)
+       AND group_id IN
+         (SELECT cg.group_id
+          FROM auth.clients_groups cg
+          WHERE cg.client_id = ?)|;
 
-  $sth = prepare_execute_query($::form, $dbh, $query, $login);
+  $sth = prepare_execute_query($::form, $dbh, $query, $login, $self->client->{id});
 
   while ($row = $sth->fetchrow_hashref()) {
     $rights->{$row->{right}} |= $row->{granted};
index a316c3a..0817ae6 100644 (file)
@@ -8,6 +8,7 @@ use Config::Std;
 use English qw(-no_match_vars);
 
 use SL::DB::AuthUser;
+use SL::DB::Default;
 use SL::DB::Order;
 use SL::DB::Invoice;
 use SL::DB::PeriodicInvoice;
@@ -202,7 +203,7 @@ sub _send_email {
   return unless $template;
 
   my $email_template = $config{periodic_invoices}->{email_template};
-  my $filename       = $email_template || ( ($user->get_config_value('templates') || "templates/webpages") . "/periodic_invoices_email.txt" );
+  my $filename       = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/periodic_invoices_email.txt" );
   my %params         = ( POSTED_INVOICES  => $posted_invoices,
                          PRINTED_INVOICES => $printed_invoices );
 
index 37a02eb..aab2796 100644 (file)
@@ -11,6 +11,7 @@ use Sys::Hostname;
 use FindBin;
 
 use SL::DB::AuthUser;
+use SL::DB::Default;
 use SL::Common;
 
 use Rose::Object::MakeMethods::Generic (
@@ -145,7 +146,7 @@ sub _prepare_report {
 
   return unless $template;
   my $email_template = $self->config->{email_template};
-  my $filename       = $email_template || ( ($user->get_config_value('templates') || "templates/mails") . "/self_test/status_mail.txt" );
+  my $filename       = $email_template || ( (SL::DB::Default->get->templates || "templates/mails") . "/self_test/status_mail.txt" );
   my $content_type   = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
 
 
index e24be69..ef6e9c3 100644 (file)
--- a/SL/CA.pm
+++ b/SL/CA.pm
@@ -182,13 +182,6 @@ sub all_transactions {
 
   my $sortorder = join ', ',
     $form->sort_columns(qw(transdate reference description));
-  my $false = ($myconfig->{dbdriver} eq 'Pg') ? "FALSE" : q|'0'|;
-
-  # Oracle workaround, use ordinal positions
-  my %ordinal = (transdate   => 4,
-                 reference   => 2,
-                 description => 3);
-  map { $sortorder =~ s/$_/$ordinal{$_}/ } keys %ordinal;
 
   my ($null, $department_id) = split(/--/, $form->{department});
   my ($dpt_where, $dpt_join, @department_values);
@@ -272,7 +265,7 @@ sub all_transactions {
     # get all transactions
     $query =
       qq|SELECT a.id, a.reference, a.description, ac.transdate, ac.chart_id, | .
-      qq|  $false AS invoice, ac.amount, 'gl' as module, | .
+      qq|  FALSE AS invoice, ac.amount, 'gl' as module, | .
       qq§(SELECT accno||'--'||rate FROM tax LEFT JOIN chart ON (tax.chart_id=chart.id) WHERE tax.id = (SELECT tax_id FROM taxkeys WHERE taxkey_id = ac.taxkey AND taxkeys.startdate <= ac.transdate ORDER BY taxkeys.startdate DESC LIMIT 1)) AS taxinfo, ac.source || ' ' || ac.memo AS memo Â§ .
       qq|FROM acc_trans ac, gl a | .
       $dpt_join .
index 66b3e25..44707f9 100644 (file)
--- a/SL/CT.pm
+++ b/SL/CT.pm
@@ -42,6 +42,7 @@ use Data::Dumper;
 use SL::Common;
 use SL::CVar;
 use SL::DBUtils;
+use SL::DB::Default;
 use SL::FU;
 use SL::Notes;
 use SL::TransNumber;
@@ -1080,6 +1081,10 @@ sub parse_excel_file {
   my ($self, $myconfig, $form) = @_;
   my $locale = $main::locale;
 
+  my $defaults = SL::DB::Default->get;
+  $form->error($::locale->text('No print templates have been created for this client yet. Please do so in the client configuration.')) if !$defaults->templates;
+  $form->{templates} = $defaults->templates;
+
   $form->{formname}   = 'sales_quotation';
   $form->{type}   = 'sales_quotation';
   $form->{format} = 'excel';
@@ -1122,8 +1127,6 @@ sub parse_excel_file {
 
   $form->{notes} =~ s/^\s+//g;
 
-  $form->{templates} = $myconfig->{templates};
-
   delete $form->{printer_command};
 
   $form->get_employee_info($myconfig);
index dc1cc04..b58d877 100644 (file)
@@ -11,6 +11,7 @@ package Common;
 use utf8;
 use strict;
 
+use Carp;
 use Time::HiRes qw(gettimeofday);
 use Data::Dumper;
 
@@ -354,6 +355,8 @@ sub webdav_folder {
   return $main::lxdebug->leave_sub()
     unless ($::lx_office_conf{features}->{webdav} && $form->{id});
 
+  croak "No client set in \$::auth" unless $::auth->client;
+
   my ($path, $number);
 
   $form->{WEBDAV} = [];
@@ -382,7 +385,7 @@ sub webdav_folder {
 
   $number =~ s|[/\\]|_|g;
 
-  $path = "webdav/${path}/${number}";
+  $path = "webdav/" . $::auth->client->{id} . "/${path}/${number}";
 
   if (!-d $path) {
     mkdir_with_parents($path);
@@ -390,7 +393,6 @@ sub webdav_folder {
   } else {
     my $base_path = $ENV{'SCRIPT_NAME'};
     $base_path =~ s|[^/]+$||;
-    # wo kommt der wert für dir her? es wird doch gar nichts Ã¼bergeben? fix für strict my $dir jb 21.2.
     if (opendir my $dir, $path) {
       foreach my $file (sort { lc $a cmp lc $b } readdir $dir) {
         next if (($file eq '.') || ($file eq '..'));
diff --git a/SL/Controller/Admin.pm b/SL/Controller/Admin.pm
new file mode 100644 (file)
index 0000000..7277e79
--- /dev/null
@@ -0,0 +1,527 @@
+package SL::Controller::Admin;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use IO::File;
+use List::Util qw(first);
+
+use SL::DB::AuthUser;
+use SL::DB::AuthGroup;
+use SL::DB::Printer;
+use SL::Helper::Flash;
+use SL::Locale::String qw(t8);
+use SL::System::InstallationLock;
+use SL::User;
+
+use Rose::Object::MakeMethods::Generic
+(
+  'scalar --get_set_init' => [ qw(client user group printer db_cfg is_locked
+                                  all_dateformats all_numberformats all_countrycodes all_stylesheets all_menustyles all_clients all_groups all_users all_rights all_printers) ],
+);
+
+__PACKAGE__->run_before(\&setup_layout);
+__PACKAGE__->run_before(\&setup_client, only => [ qw(list_printers new_printer edit_printer save_printer delete_printer) ]);
+
+sub get_auth_level { "admin" };
+sub keep_auth_vars {
+  my ($class, %params) = @_;
+  return $params{action} eq 'login';
+}
+
+#
+# actions: login, logout
+#
+
+sub action_login {
+  my ($self) = @_;
+
+  return $self->login_form if !$::form->{do_login};
+  return                   if !$self->authenticate_root;
+  return                   if !$self->check_auth_db_and_tables;
+  return                   if  $self->apply_dbupgrade_scripts;
+  $self->redirect_to(action => 'show');
+}
+
+sub action_logout {
+  my ($self) = @_;
+  $::auth->destroy_session;
+  $self->redirect_to(action => 'login');
+}
+
+#
+# actions: creating the authentication database & tables, applying database ugprades
+#
+
+sub action_apply_dbupgrade_scripts {
+  my ($self) = @_;
+
+  return if $self->apply_dbupgrade_scripts;
+  $self->action_show;
+}
+
+sub action_create_auth_db {
+  my ($self) = @_;
+
+  $::auth->create_database(superuser          => $::form->{db_superuser},
+                           superuser_password => $::form->{db_superuser_password},
+                           template           => $::form->{db_template});
+  $self->check_auth_db_and_tables;
+}
+
+sub action_create_auth_tables {
+  my ($self) = @_;
+
+  $::auth->create_tables;
+  $::auth->set_session_value('admin_password', $::lx_office_conf{authentication}->{admin_password});
+  $::auth->create_or_refresh_session;
+
+  my $group = (SL::DB::Manager::AuthGroup->get_all(limit => 1))[0];
+  if (!$group) {
+    SL::DB::AuthGroup->new(
+      name        => t8('Full Access'),
+      description => t8('Full access to all functions'),
+      rights      => [ map { SL::DB::AuthGroupRight->new(right => $_, granted => 1) } SL::Auth::all_rights() ],
+    )->save;
+  }
+
+  if (!$self->apply_dbupgrade_scripts) {
+    $self->action_login;
+  }
+}
+
+#
+# actions: users
+#
+
+sub action_show {
+  my ($self) = @_;
+
+  $self->render(
+    "admin/show",
+    title => "kivitendo " . t8('Administration'),
+  );
+}
+
+sub action_new_user {
+  my ($self) = @_;
+
+  $self->user(SL::DB::AuthUser->new(
+    config_values => {
+      vclimit      => 200,
+      countrycode  => "de",
+      numberformat => "1.000,00",
+      dateformat   => "dd.mm.yy",
+      stylesheet   => "kivitendo.css",
+      menustyle    => "neu",
+    },
+  ));
+
+  $self->edit_user_form(title => t8('Create a new user'));
+}
+
+sub action_edit_user {
+  my ($self) = @_;
+  $self->edit_user_form(title => t8('Edit User'));
+}
+
+sub action_save_user {
+  my ($self) = @_;
+  my $params = delete($::form->{user})          || { };
+  my $props  = delete($params->{config_values}) || { };
+  my $is_new = !$params->{id};
+
+  $self->user($is_new ? SL::DB::AuthUser->new : SL::DB::AuthUser->new(id => $params->{id})->load)
+    ->assign_attributes(%{ $params })
+    ->config_values({ %{ $self->user->config_values }, %{ $props } });
+
+  my @errors = $self->user->validate;
+
+  if (@errors) {
+    flash('error', @errors);
+    $self->edit_user_form(title => $is_new ? t8('Create a new user') : t8('Edit User'));
+    return;
+  }
+
+  $self->user->save;
+
+  if ($::auth->can_change_password && $::form->{new_password}) {
+    $::auth->change_password($self->user->login, $::form->{new_password});
+  }
+
+  flash_later('info', $is_new ? t8('The user has been created.') : t8('The user has been saved.'));
+  $self->redirect_to(action => 'show');
+}
+
+sub action_delete_user {
+  my ($self) = @_;
+
+  my @clients = @{ $self->user->clients || [] };
+
+  if (!$self->user->delete) {
+    flash('error', t8('The user could not be deleted.'));
+    $self->edit_user_form(title => t8('Edit User'));
+    return;
+  }
+
+  # Flag corresponding entries in 'employee' as deleted.
+  foreach my $client (@clients) {
+    my $dbh = $client->dbconnect(AutoCommit => 1) || next;
+    $dbh->do(qq|UPDATE employee SET deleted = TRUE WHERE login = ?|, undef, $self->user->login);
+    $dbh->disconnect;
+  }
+
+  flash_later('info', t8('The user has been deleted.'));
+  $self->redirect_to(action => 'show');
+}
+
+#
+# actions: clients
+#
+
+sub action_new_client {
+  my ($self) = @_;
+
+  $self->client(SL::DB::AuthClient->new(
+    dbhost   => $::auth->{DB_config}->{host},
+    dbport   => $::auth->{DB_config}->{port},
+    dbuser   => $::auth->{DB_config}->{user},
+    dbpasswd => $::auth->{DB_config}->{password},
+  ));
+
+  $self->edit_client_form(title => t8('Create a new client'));
+}
+
+sub action_edit_client {
+  my ($self) = @_;
+  $self->edit_client_form(title => t8('Edit Client'));
+}
+
+sub action_save_client {
+  my ($self) = @_;
+  my $params = delete($::form->{client}) || { };
+  my $is_new = !$params->{id};
+
+  $self->client($is_new ? SL::DB::AuthClient->new : SL::DB::AuthClient->new(id => $params->{id})->load)->assign_attributes(%{ $params });
+
+  my @errors = $self->client->validate;
+
+  if (@errors) {
+    flash('error', @errors);
+    $self->edit_client_form(title => $is_new ? t8('Create a new client') : t8('Edit Client'));
+    return;
+  }
+
+  $self->client->save;
+  if ($self->client->is_default) {
+    SL::DB::Manager::AuthClient->update_all(set => { is_default => 0 }, where => [ '!id' => $self->client->id ]);
+  }
+
+  flash_later('info', $is_new ? t8('The client has been created.') : t8('The client has been saved.'));
+  $self->redirect_to(action => 'show');
+}
+
+sub action_delete_client {
+  my ($self) = @_;
+
+  if (!$self->client->delete) {
+    flash('error', t8('The client could not be deleted.'));
+    $self->edit_client_form(title => t8('Edit Client'));
+    return;
+  }
+
+  flash_later('info', t8('The client has been deleted.'));
+  $self->redirect_to(action => 'show');
+}
+
+sub action_test_database_connectivity {
+  my ($self)    = @_;
+
+  my %cfg       = %{ $::form->{client} || {} };
+  my $dbconnect = 'dbi:Pg:dbname=' . $cfg{dbname} . ';host=' . $cfg{dbhost} . ';port=' . $cfg{dbport};
+  my $dbh       = DBI->connect($dbconnect, $cfg{dbuser}, $cfg{dbpasswd});
+
+  my $ok        = !!$dbh;
+  my $error     = $DBI::errstr;
+
+  $dbh->disconnect if $dbh;
+
+  $self->render('admin/test_db_connection',
+                title => t8('Database Connection Test'),
+                ok    => $ok,
+                error => $error);
+}
+
+#
+# actions: groups
+#
+
+sub action_new_group {
+  my ($self) = @_;
+
+  $self->group(SL::DB::AuthGroup->new);
+  $self->edit_group_form(title => t8('Create a new group'));
+}
+
+sub action_edit_group {
+  my ($self) = @_;
+  $self->edit_group_form(title => t8('Edit User Group'));
+}
+
+sub action_save_group {
+  my ($self) = @_;
+
+  my $params = delete($::form->{group}) || { };
+  my $is_new = !$params->{id};
+
+  $self->group($is_new ? SL::DB::AuthGroup->new : SL::DB::AuthGroup->new(id => $params->{id})->load)->assign_attributes(%{ $params });
+
+  my @errors = $self->group->validate;
+
+  if (@errors) {
+    flash('error', @errors);
+    $self->edit_group_form(title => $is_new ? t8('Create a new user group') : t8('Edit User Group'));
+    return;
+  }
+
+  $self->group->save;
+
+  flash_later('info', $is_new ? t8('The user group has been created.') : t8('The user group has been saved.'));
+  $self->redirect_to(action => 'show');
+}
+
+sub action_delete_group {
+  my ($self) = @_;
+
+  if (!$self->group->delete) {
+    flash('error', t8('The user group could not be deleted.'));
+    $self->edit_group_form(title => t8('Edit User Group'));
+    return;
+  }
+
+  flash_later('info', t8('The user group has been deleted.'));
+  $self->redirect_to(action => 'show');
+}
+
+#
+# actions: printers
+#
+
+sub action_list_printers {
+  my ($self) = @_;
+  $self->render('admin/list_printers', title => t8('Printer management'));
+}
+
+sub action_new_printer {
+  my ($self) = @_;
+
+  $self->printer(SL::DB::Printer->new);
+  $self->edit_printer_form(title => t8('Create a new printer'));
+}
+
+sub action_edit_printer {
+  my ($self) = @_;
+  $self->edit_printer_form(title => t8('Edit Printer'));
+}
+
+sub action_save_printer {
+  my ($self) = @_;
+  my $params = delete($::form->{printer}) || { };
+  my $is_new = !$params->{id};
+
+  $self->printer($is_new ? SL::DB::Printer->new : SL::DB::Printer->new(id => $params->{id})->load)->assign_attributes(%{ $params });
+
+  my @errors = $self->printer->validate;
+
+  if (@errors) {
+    flash('error', @errors);
+    $self->edit_printer_form(title => $is_new ? t8('Create a new printer') : t8('Edit Printer'));
+    return;
+  }
+
+  $self->printer->save;
+
+  flash_later('info', $is_new ? t8('The printer has been created.') : t8('The printer has been saved.'));
+  $self->redirect_to(action => 'list_printers', 'client.id' => $self->client->id);
+}
+
+sub action_delete_printer {
+  my ($self) = @_;
+
+  if (!$self->printer->delete) {
+    flash('error', t8('The printer could not be deleted.'));
+    $self->edit_printer_form(title => t8('Edit Printer'));
+    return;
+  }
+
+  flash_later('info', t8('The printer has been deleted.'));
+  $self->redirect_to(action => 'list_printers', 'client.id' => $self->client->id);
+}
+
+#
+# actions: locking, unlocking
+#
+
+sub action_unlock_system {
+  my ($self) = @_;
+
+  SL::System::InstallationLock->unlock;
+  flash_later('info', t8('Lockfile removed!'));
+  $self->redirect_to(action => 'show');
+}
+
+sub action_lock_system {
+  my ($self) = @_;
+
+  SL::System::InstallationLock->unlock;
+  flash_later('info', t8('Lockfile created!'));
+  $self->redirect_to(action => 'show');
+}
+
+#
+# initializers
+#
+
+sub init_db_cfg            { $::lx_office_conf{'authentication/database'}                                                    }
+sub init_is_locked         { SL::System::InstallationLock->is_locked                                                         }
+sub init_client            { SL::DB::Manager::AuthClient->find_by(id => ($::form->{id} || ($::form->{client}  || {})->{id})) }
+sub init_user              { SL::DB::AuthUser  ->new(id => ($::form->{id} || ($::form->{user}    || {})->{id}))->load        }
+sub init_group             { SL::DB::AuthGroup ->new(id => ($::form->{id} || ($::form->{group}   || {})->{id}))->load        }
+sub init_printer           { SL::DB::Printer   ->new(id => ($::form->{id} || ($::form->{printer} || {})->{id}))->load        }
+sub init_all_clients       { SL::DB::Manager::AuthClient->get_all_sorted                                                     }
+sub init_all_users         { SL::DB::Manager::AuthUser  ->get_all_sorted                                                     }
+sub init_all_groups        { SL::DB::Manager::AuthGroup ->get_all_sorted                                                     }
+sub init_all_printers      { SL::DB::Manager::Printer   ->get_all_sorted                                                     }
+sub init_all_dateformats   { [ qw(mm/dd/yy dd/mm/yy dd.mm.yy yyyy-mm-dd)      ]                                              }
+sub init_all_numberformats { [ qw(1,000.00 1000.00 1.000,00 1000,00)          ]                                              }
+sub init_all_stylesheets   { [ qw(lx-office-erp.css Mobile.css kivitendo.css) ]                                              }
+sub init_all_menustyles    {
+  return [
+    { id => 'old', title => $::locale->text('Old (on the side)') },
+    { id => 'v3',  title => $::locale->text('Top (CSS)') },
+    { id => 'neu', title => $::locale->text('Top (Javascript)') },
+  ];
+}
+
+sub init_all_rights {
+  my (@sections, $current_section);
+
+  foreach my $entry ($::auth->all_rights_full) {
+    if ($entry->[0] =~ m/^--/) {
+      push @sections, { description => $entry->[1], rights => [] };
+
+    } elsif (@sections) {
+      push @{ $sections[-1]->{rights} }, {
+        name        => $entry->[0],
+        description => $entry->[1],
+      };
+
+    } else {
+      die "Right without sections: " . join('::', @{ $entry });
+    }
+  }
+
+  return \@sections;
+}
+
+sub init_all_countrycodes {
+  my %cc = User->country_codes;
+  return [ map { id => $_, title => $cc{$_} }, sort { $cc{$a} cmp $cc{$b} } keys %cc ];
+}
+
+#
+# filters
+#
+
+sub setup_layout {
+  my ($self, $action) = @_;
+
+  $::request->layout(SL::Layout::Dispatcher->new(style => 'admin'));
+  $::request->layout->use_stylesheet("lx-office-erp.css");
+  $::form->{favicon} = "favicon.ico";
+}
+
+sub setup_client {
+  my ($self) = @_;
+
+  $self->client((first { $_->is_default } @{ $self->all_clients }) || $self->all_clients->[0]) if !$self->client;
+  $::auth->set_client($self->client->id);
+}
+
+
+#
+# displaying forms
+#
+
+sub use_multiselect_js {
+  my ($self) = @_;
+
+  $::request->layout->use_javascript("${_}.js") for qw(jquery.selectboxes jquery.multiselect2side);
+  return $self;
+}
+
+sub login_form {
+  my ($self, %params) = @_;
+  $::request->layout->focus('#admin_password');
+  $self->render('admin/adminlogin', title => t8('kivitendo v#1 administration', $::form->read_version), %params);
+}
+
+sub edit_user_form {
+  my ($self, %params) = @_;
+  $self->use_multiselect_js->render('admin/edit_user', %params);
+}
+
+sub edit_client_form {
+  my ($self, %params) = @_;
+  $self->use_multiselect_js->render('admin/edit_client', %params);
+}
+
+sub edit_group_form {
+  my ($self, %params) = @_;
+  $self->use_multiselect_js->render('admin/edit_group', %params);
+}
+
+sub edit_printer_form {
+  my ($self, %params) = @_;
+  $self->render('admin/edit_printer', %params);
+}
+
+#
+# helpers
+#
+
+sub check_auth_db_and_tables {
+  my ($self) = @_;
+
+  if (!$::auth->check_database) {
+    $self->render('admin/check_auth_database', title => t8('Authentification database creation'));
+    return 0;
+  }
+
+  if (!$::auth->check_tables) {
+    $self->render('admin/check_auth_tables', title => t8('Authentification tables creation'));
+    return 0;
+  }
+
+  return 1;
+}
+
+sub apply_dbupgrade_scripts {
+  return SL::DBUpgrade2->new(form => $::form, auth => 1)->apply_admin_dbupgrade_scripts(1);
+}
+
+sub authenticate_root {
+  my ($self) = @_;
+
+  return 1 if $::auth->authenticate_root($::form->{'{AUTH}admin_password'}) == $::auth->OK();
+
+  $::auth->punish_wrong_login;
+  $::auth->delete_session_value('admin_password');
+
+  $self->login_form(error => t8('Incorrect password!'));
+
+  return undef;
+}
+
+1;
index c6977d0..88f10b7 100644 (file)
@@ -562,13 +562,16 @@ C<user> (authentication as a normal user suffices) with a possible
 future value C<none> (which would require no authentication but is not
 yet implemented).
 
-=item C<keep_auth_vars_in_form>
+=item C<keep_auth_vars_in_form %params>
 
 May be overridden by a controller. If falsish (the default) all form
 variables whose name starts with C<{AUTH}> are removed before the
 request is routed. Only controllers that handle login requests
 themselves should return trueish for this function.
 
+C<$params{action}> contains the action name that the request will be
+dispatched to.
+
 =item C<controller_name>
 
 Returns the name of the curernt controller package without the
index e16b090..29965e1 100644 (file)
@@ -3,118 +3,212 @@ package SL::Controller::ClientConfig;
 use strict;
 use parent qw(SL::Controller::Base);
 
+use File::Copy::Recursive ();
+use List::Util qw(first);
+
+use SL::DB::Chart;
+use SL::DB::Currency;
 use SL::DB::Default;
+use SL::DB::Language;
+use SL::DB::Unit;
 use SL::Helper::Flash;
+use SL::Locale::String qw(t8);
+use SL::Template;
 
 __PACKAGE__->run_before('check_auth');
 
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(defaults all_warehouses all_weightunits all_languages all_currencies all_templates posting_options payment_options accounting_options inventory_options profit_options accounts) ],
+);
 
 sub action_edit {
   my ($self, %params) = @_;
 
-  $self->{posting_options} = [ { title => $::locale->text("never"), value => 0 },
-                               { title => $::locale->text("every time"), value => 1 },
-                               { title => $::locale->text("on the same day"), value => 2 }, ];
-  $self->{payment_options} = [ { title => $::locale->text("never"), value => 0 },
-                               { title => $::locale->text("every time"), value => 1 },
-                               { title => $::locale->text("on the same day"), value => 2 }, ];
-  $self->{accounting_options} = [ { title => $::locale->text("Accrual"), value => "accrual" },
-                                  { title => $::locale->text("cash"), value => "cash" }, ];
-  $self->{inventory_options} = [ { title => $::locale->text("perpetual"), value => "perpetual" },
-                                 { title => $::locale->text("periodic"), value => "periodic" }, ];
-  $self->{profit_options} = [ { title => $::locale->text("balance"), value => "balance" },
-                              { title => $::locale->text("income"), value => "income" }, ];
-
-  map { $self->{$_} = SL::DB::Default->get->$_ } qw(is_changeable ir_changeable ar_changeable ap_changeable gl_changeable);
-
-  $self->{payments_changeable} = SL::DB::Default->get->payments_changeable;
-
-  map { $self->{$_} = SL::DB::Default->get->$_ } qw(is_show_mark_as_paid ir_show_mark_as_paid ar_show_mark_as_paid ap_show_mark_as_paid);
-
-  map { $self->{$_} = SL::DB::Default->get->$_ } qw(accounting_method inventory_system profit_determination);
-
-  $self->{show_bestbefore}     = SL::DB::Default->get->show_bestbefore;
-
-  map { $self->{$_} = SL::DB::Default->get->$_ } qw(datev_check_on_sales_invoice datev_check_on_purchase_invoice datev_check_on_ar_transaction datev_check_on_ap_transaction datev_check_on_gl_transaction);
-  # datev check: not implemented yet:
-  #check_on_cash_and_receipt = 0
-  #check_on_dunning = 0
-  #check_on_sepa_import = 0
-
-  map { $self->{$_} = SL::DB::Default->get->$_ } qw(sales_order_show_delete purchase_order_show_delete sales_delivery_order_show_delete purchase_delivery_order_show_delete);
-
-  # All warehouse / transfer default values
-  map { $self->{$_} = SL::DB::Default->get->$_ } qw(transfer_default transfer_default_use_master_default_bin transfer_default_ignore_onhand
-                                                    warehouse_id_ignore_onhand bin_id_ignore_onhand warehouse_id bin_id);
-
-  # for the default warehouse and bin we get the list and
-  # set a empty value with warehouse_id and bin_id = 0
-  map { $self->{$_} = SL::DB::Default->get->$_ } qw(warehouse_id bin_id);
-  $::form->get_lists('warehouses' => { 'key'    => 'WAREHOUSES',
-                                       'bins'   => 'BINS', });
-  $self->{WAREHOUSES} = $::form->{WAREHOUSES};
-  # leerer lagerplatz mit id 0
-  my $no_default_bin_entry = { 'id' => '0', description => '--', 'BINS' => [ { id => '0', description => ''} ] };
-  push @ { $self->{WAREHOUSES} }, $no_default_bin_entry;
-
-  # set defaults to empty
-  if (my $max = scalar @{ $self->{WAREHOUSES} }) {
-    $self->{warehouse_id} ||= $self->{WAREHOUSES}->[$max -1]->{id};
-    $self->{bin_id}       ||= $self->{WAREHOUSES}->[$max -1]->{BINS}->[0]->{id};
-    $self->{warehouse_id_ignore_onhand} ||= $self->{WAREHOUSES}->[$max -1]->{id};
-    $self->{bin_id_ignore_onhand}       ||= $self->{WAREHOUSES}->[$max -1]->{BINS}->[0]->{id};
+  $::form->{use_templates} = $self->defaults->templates ? 'existing' : 'new';
+  $self->edit_form;
+}
+
+sub action_save {
+  my ($self, %params)      = @_;
+
+  my $defaults             = delete($::form->{defaults}) || {};
+  my $entered_currencies   = delete($::form->{currencies}) || [];
+  my $original_currency_id = $self->defaults->currency_id;
+
+  # undef several fields if an empty value has been selected.
+  foreach (qw(warehouse_id bin_id warehouse_id_ignore_onhand bin_id_ignore_onhand)) {
+    undef $defaults->{$_} if !$defaults->{$_};
   }
 
-  $self->{show_weight} = SL::DB::Default->get->show_weight;
+  $self->defaults->assign_attributes(%{ $defaults });
 
-  $self->render('client_config/form', title => $::locale->text('Client Configuration'));
-}
+  my %errors_idx;
 
+  # Handle currencies
+  my (%new_currency_names);
+  foreach my $existing_currency (@{ $self->all_currencies }) {
+    my $new_name     = $existing_currency->name;
+    my $new_currency = first { $_->{id} == $existing_currency->id } @{ $entered_currencies };
+    $new_name        = $new_currency->{name} if $new_currency;
 
-sub action_save {
-  my ($self, %params) = @_;
+    if (!$new_name) {
+      $errors_idx{0} = t8('Currency names must not be empty.');
+    } elsif ($new_currency_names{$new_name}) {
+      $errors_idx{1} = t8('Currency names must be unique.');
+    }
 
-  map { SL::DB::Default->get->update_attributes($_ => $::form->{$_}); } qw(is_changeable ir_changeable ar_changeable ap_changeable gl_changeable);
+    if ($new_name) {
+      $new_currency_names{$new_name} = 1;
+      $existing_currency->name($new_name);
+    }
+  }
 
-  SL::DB::Default->get->update_attributes('payments_changeable' => $::form->{payments_changeable});
+  if ($::form->{new_currency} && $new_currency_names{ $::form->{new_currency} }) {
+    $errors_idx{1} = t8('Currency names must be unique.');
+  }
 
-  map { SL::DB::Default->get->update_attributes($_ => $::form->{$_}); } qw(is_show_mark_as_paid ir_show_mark_as_paid ar_show_mark_as_paid ap_show_mark_as_paid);
+  my @errors = map { $errors_idx{$_} } sort keys %errors_idx;
 
-  map { SL::DB::Default->get->update_attributes($_ => $::form->{$_}); } qw(accounting_method inventory_system profit_determination);
+  # Check templates
+  $::form->{new_templates}        =~ s:/::g;
+  $::form->{new_master_templates} =~ s:/::g;
 
-  SL::DB::Default->get->update_attributes('show_bestbefore'     => $::form->{show_bestbefore});
+  if (($::form->{use_templates} eq 'existing') && ($self->defaults->templates !~ m:^templates/[^/]+$:)) {
+    push @errors, t8('You must select existing print templates or create a new set.');
 
-  map { SL::DB::Default->get->update_attributes($_ => $::form->{$_}); } qw(datev_check_on_sales_invoice datev_check_on_purchase_invoice datev_check_on_ar_transaction datev_check_on_ap_transaction datev_check_on_gl_transaction);
+  } elsif ($::form->{use_templates} eq 'new') {
+    if (!$::form->{new_templates}) {
+      push @errors, t8('You must enter a name for your new print templates.');
+    } elsif (-d "templates/" . $::form->{new_templates}) {
+      push @errors, t8('A directory with the name for the new print templates exists already.');
+    } elsif (! -d "templates/print/" . $::form->{new_master_templates}) {
+      push @errors, t8('The master templates where not found.');
+    }
+  }
 
-  map { SL::DB::Default->get->update_attributes($_ => $::form->{$_}); } qw(sales_order_show_delete purchase_order_show_delete sales_delivery_order_show_delete purchase_delivery_order_show_delete);
+  # Show form again if there were any errors. Nothing's been changed
+  # yet in the database.
+  if (@errors) {
+    flash('error', @errors);
+    return $self->edit_form;
+  }
 
-  # undef warehouse_id if the empty value is selected
-  if ( ($::form->{warehouse_id} == 0) && ($::form->{bin_id} == 0) ) {
-    undef $::form->{warehouse_id};
-    undef $::form->{bin_id};
+  # Save currencies. As the names must be unique we cannot simply save
+  # them as they are -- the user might want to swap to names. So make
+  # them unique first and assign the actual names in a second step.
+  my %currency_names_by_id = map { ($_->id => $_->name) } @{ $self->all_currencies };
+  $_->update_attributes(name => '__039519735__' . $_->{id})        for @{ $self->all_currencies };
+  $_->update_attributes(name => $currency_names_by_id{ $_->{id} }) for @{ $self->all_currencies };
+
+  # Create new currency if required
+  my $new_currency;
+  if ($::form->{new_currency}) {
+    $new_currency = SL::DB::Currency->new(name => $::form->{new_currency});
+    $new_currency->save;
   }
-  # undef warehouse_id_ignore_onhand if the empty value is selected
-  if ( ($::form->{warehouse_id_ignore_onhand} == 0) && ($::form->{bin_id_ignore_onhand} == 0) ) {
-    undef $::form->{warehouse_id_ignore_onhand};
-    undef $::form->{bin_id_ignore_onhand};
+
+  # If the user wants the new currency to be the default then replace
+  # the ID placeholder with the proper value. However, if no new
+  # currency has been created then don't change the value at all.
+  if (-1 == $self->defaults->currency_id) {
+    $self->defaults->currency_id($new_currency ? $new_currency->id : $original_currency_id);
   }
 
-  # All warehouse / transfer default values
-  map { SL::DB::Default->get->update_attributes($_ => $::form->{$_}); } qw(transfer_default transfer_default_use_master_default_bin transfer_default_ignore_onhand
-                                                                            warehouse_id_ignore_onhand bin_id_ignore_onhand warehouse_id bin_id);
+  # Create new templates if requested.
+  if ($::form->{use_templates} eq 'new') {
+    local $File::Copy::Recursive::SkipFlop = 1;
+    File::Copy::Recursive::dircopy('templates/print/' . $::form->{new_master_templates}, 'templates/' . $::form->{new_templates});
+    $self->defaults->templates('templates/' . $::form->{new_templates});
+  }
 
-  SL::DB::Default->get->update_attributes('show_weight'     => $::form->{show_weight});
+  # Finally save defaults.
+  $self->defaults->save;
 
-  flash_later('info', $::locale->text('Client Configuration saved!'));
+  flash_later('info', t8('Client Configuration saved!'));
 
   $self->redirect_to(action => 'edit');
 }
 
+#
+# initializers
+#
+
+sub init_defaults        { SL::DB::Default->get                                           }
+sub init_all_warehouses  { SL::DB::Manager::Warehouse->get_all_sorted                     }
+sub init_all_languages   { SL::DB::Manager::Language->get_all_sorted                      }
+sub init_all_currencies  { SL::DB::Manager::Currency->get_all_sorted                      }
+sub init_all_weightunits { SL::DB::Manager::Unit->find_by(name => 'g')->convertible_units }
+sub init_all_templates   { +{ SL::Template->available_templates }                         }
+
+sub init_posting_options {
+  [ { title => t8("never"),           value => 0           },
+    { title => t8("every time"),      value => 1           },
+    { title => t8("on the same day"), value => 2           }, ]
+}
+
+sub init_payment_options {
+  [ { title => t8("never"),           value => 0           },
+    { title => t8("every time"),      value => 1           },
+    { title => t8("on the same day"), value => 2           }, ]
+}
+
+sub init_accounting_options {
+  [ { title => t8("Accrual"),         value => "accrual"   },
+    { title => t8("cash"),            value => "cash"      }, ]
+}
+
+sub init_inventory_options {
+  [ { title => t8("perpetual"),       value => "perpetual" },
+    { title => t8("periodic"),        value => "periodic"  }, ]
+}
+
+sub init_profit_options {
+  [ { title => t8("balance"),         value => "balance"   },
+    { title => t8("income"),          value => "income"    }, ]
+}
+
+sub init_accounts {
+  my %accounts;
+
+  foreach my $chart (@{ SL::DB::Manager::Chart->get_all(where => [ link => { like => '%IC%' } ], sort_by => 'accno ASC') }) {
+    my %added;
+
+    foreach my $link (split m/:/, $chart->link) {
+      my $key = lc($link =~ /cogs/ ? 'IC_expense' : $link =~ /sale/ ? 'IC_income' : $link);
+      next if $added{$key};
 
-#################### private stuff ##########################
+      $added{$key}      = 1;
+      $accounts{$key} ||= [];
+      push @{ $accounts{$key} }, $chart;
+    }
+  }
+
+  $accounts{fx_gain} = SL::DB::Manager::Chart->get_all(where => [ category => 'I', charttype => 'A' ], sort_by => 'accno ASC');
+  $accounts{fx_loss} = SL::DB::Manager::Chart->get_all(where => [ category => 'E', charttype => 'A' ], sort_by => 'accno ASC');
+  $accounts{ar_paid} = SL::DB::Manager::Chart->get_all(where => [ link => { like => '%AR_paid%' }   ], sort_by => 'accno ASC');
+
+  return \%accounts;
+}
+
+#
+# filters
+#
 
 sub check_auth {
   $::auth->assert('admin');
 }
 
+#
+# helpers
+#
+
+sub edit_form {
+  my ($self) = @_;
+
+  $self->render('client_config/form', title => t8('Client Configuration'),
+                make_chart_title     => sub { $_[0]->accno . '--' . $_[0]->description },
+                make_templates_value => sub { 'templates/' . $_[0] },
+              );
+}
+
 1;
index 13b7b8a..50cae18 100644 (file)
@@ -4,10 +4,22 @@ use strict;
 
 use parent qw(SL::Controller::Base);
 
+use List::Util qw(first);
+
 use SL::Dispatcher::AuthHandler::User;
+use SL::DB::AuthClient;
+use SL::DB::AuthGroup;
+use SL::DB::AuthUser;
+use SL::DB::Employee;
+use SL::Locale::String qw(t8);
 use SL::User;
 
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(clients default_client_id) ],
+);
+
 __PACKAGE__->run_before('set_layout');
+
 #
 # actions
 #
@@ -20,7 +32,7 @@ sub action_user_login {
   return if $self->_redirect_to_main_script_if_already_logged_in;
 
   # Otherwise show the login form.
-  $self->render('login_screen/user_login', error => error_state($::form->{error}));
+  $self->show_login_form(error => error_state($::form->{error}));
 }
 
 sub action_logout {
@@ -28,22 +40,30 @@ sub action_logout {
 
   $::auth->destroy_session;
   $::auth->create_or_refresh_session;
-  $self->render('login_screen/user_login', error => $::locale->text('You are logged out!'));
+  $self->show_login_form(info => $::locale->text('You are logged out!'));
 }
 
 sub action_login {
   my ($self) = @_;
 
-  my $login        = $::form->{'{AUTH}login'} || $::auth->get_session_value('login');
+  my $login     = $::form->{'{AUTH}login'}     || $::auth->get_session_value('login');
+  my $client_id = $::form->{'{AUTH}client_id'} || $::auth->get_session_value('client_id');
+  my $error     = t8('Incorrect username or password or no access to selected client!');
+
+  if (!$::auth->set_client($client_id)) {
+    $::auth->punish_wrong_login;
+    return $self->show_login_form(error => $error);
+  }
+
   %::myconfig      = $login ? $::auth->read_user(login => $login) : ();
-  SL::Dispatcher::AuthHandler::User->new->handle(countrycode => $::myconfig{countrycode});
-  $::form->{login} = $::myconfig{login};
+  $::form->{login} = $login;
   $::locale        = Locale->new($::myconfig{countrycode}) if $::myconfig{countrycode};
-  my $user         = User->new(login => $::myconfig{login});
-  $::request->{layout} = SL::Layout::Dispatcher->new(style => $user->{menustyle});
+  SL::Dispatcher::AuthHandler::User->new->handle(countrycode => $::myconfig{countrycode});
+
+  $::request->layout(SL::Layout::Dispatcher->new(style => $::myconfig{menustyle}));
 
   # if we get an error back, bale out
-  my $result = $user->login($::form);
+  my $result = User->new(login => $::myconfig{login})->login($::form);
 
   # Database update available?
   ::end_of_request() if -2 == $result;
@@ -57,13 +77,13 @@ sub action_login {
   # Other login errors.
   if (0 > $result) {
     $::auth->punish_wrong_login;
-    return $self->render('login_screen/user_login', error => $::locale->text('Incorrect username or password!'));
+    return $self->show_login_form(error => $error);
   }
 
   # Everything is fine.
   $::auth->set_cookie_environment_variable();
 
-  $self->_redirect_to_main_script($user);
+  $self->_redirect_to_main_script;
 }
 
 #
@@ -82,7 +102,9 @@ sub keep_auth_vars_in_form {
 #
 
 sub _redirect_to_main_script {
-  my ($self, $user) = @_;
+  my ($self) = @_;
+
+  $self->_ensure_employees_for_authorized_users_exist;
 
   return $self->redirect_to($::form->{callback}) if $::form->{callback};
 
@@ -111,10 +133,27 @@ sub _redirect_to_main_script_if_already_logged_in {
   return 1;
 }
 
+sub _ensure_employees_for_authorized_users_exist {
+  my ($self) = @_;
+
+  my %employees_by_login = map { ($_->login => $_) } @{ SL::DB::Manager::Employee->get_all };
+
+  foreach my $user (@{ SL::DB::AuthClient->new(id => $::auth->client->{id})->load->users || [] }) {
+    my $user_config = $user->config_values;
+    my $employee    = $employees_by_login{$user->login} || SL::DB::Employee->new(login => $user->login);
+
+    $employee->update_attributes(
+      name      => $user_config->{name},
+      workphone => $user_config->{tel},
+      deleted   => 0,
+    );
+  }
+}
+
 sub error_state {
   return {
     session  => $::locale->text('The session is invalid or has expired.'),
-    password => $::locale->text('Incorrect password!'),
+    password => $::locale->text('Incorrect username or password or no access to selected client!'),
   }->{$_[0]};
 }
 
@@ -122,4 +161,21 @@ sub set_layout {
   $::request->{layout} = SL::Layout::Dispatcher->new(style => 'login');
 }
 
+sub init_clients {
+  return SL::DB::Manager::AuthClient->get_all_sorted;
+}
+
+sub init_default_client_id {
+  my ($self)         = @_;
+  my $default_client = first { $_->is_default } @{ $self->clients };
+  return $default_client ? $default_client->id : undef;
+}
+
+sub show_login_form {
+  my ($self, %params) = @_;
+
+  $::request->layout->focus('#auth_login');
+  $self->render('login_screen/user_login', %params);
+}
+
 1;
index 5a22c5d..54214c8 100644 (file)
--- a/SL/DB.pm
+++ b/SL/DB.pm
@@ -14,7 +14,7 @@ use base qw(Rose::DB);
 __PACKAGE__->db_cache_class('Rose::DBx::Cache::Anywhere');
 __PACKAGE__->use_private_registry;
 
-my (%_db_registered, %_initial_sql_executed);
+my (%_db_registered);
 
 sub dbi_connect {
   shift;
@@ -30,61 +30,60 @@ sub create {
 
   my $db = __PACKAGE__->new_or_cached(domain => $domain, type => $type);
 
-  _execute_initial_sql($db);
-
   return $db;
 }
 
-my %_dateformats = ( 'yy-mm-dd'   => 'ISO',
-                     'yyyy-mm-dd' => 'ISO',
-                     'mm/dd/yy'   => 'SQL, US',
-                     'dd/mm/yy'   => 'SQL, EUROPEAN',
-                     'dd.mm.yy'   => 'GERMAN'
-                   );
-
 sub _register_db {
   my $domain = shift;
   my $type   = shift;
 
-  my %connect_settings;
-  my $initial_sql;
-
-  if (!%::myconfig) {
-    $type = 'LXOFFICE_EMPTY';
-    %connect_settings = ( driver => 'Pg' );
-
-  } elsif ($type eq 'LXOFFICE_AUTH') {
-    %connect_settings = ( driver          => $::myconfig{dbdriver} || 'Pg',
-                          database        => $::auth->{DB_config}->{db},
-                          host            => $::auth->{DB_config}->{host} || 'localhost',
-                          port            => $::auth->{DB_config}->{port} || 5432,
-                          username        => $::auth->{DB_config}->{user},
-                          password        => $::auth->{DB_config}->{password},
-                          connect_options => { pg_enable_utf8 => $::locale && $::locale->is_utf8,
-                                             });
-  } else {
-    my $european_dates = 0;
-    if ($::myconfig{dateformat}) {
-      $european_dates = 1 if $_dateformats{ $::myconfig{dateformat} }
-                          && $_dateformats{ $::myconfig{dateformat} } =~ m/european/i;
-    }
+  my %specific_connect_settings;
+  my %common_connect_settings = (
+    driver           => 'Pg',
+    european_dates   => ((SL::DBConnect->get_datestyle || '') =~ m/european/i) ? 1 : 0,
+    connect_options  => {
+      pg_enable_utf8 => $::locale && $::locale->is_utf8,
+    },
+  );
+
+  if (($type eq 'KIVITENDO_AUTH') && $::auth && $::auth->{DB_config} && $::auth->session_tables_present) {
+    %specific_connect_settings = (
+      database        => $::auth->{DB_config}->{db},
+      host            => $::auth->{DB_config}->{host} || 'localhost',
+      port            => $::auth->{DB_config}->{port} || 5432,
+      username        => $::auth->{DB_config}->{user},
+      password        => $::auth->{DB_config}->{password},
+    );
+
+  } elsif ($::auth && $::auth->client) {
+    my $client        = $::auth->client;
+    %specific_connect_settings = (
+      database        => $client->{dbname},
+      host            => $client->{dbhost} || 'localhost',
+      port            => $client->{dbport} || 5432,
+      username        => $client->{dbuser},
+      password        => $client->{dbpasswd},
+    );
+
+  } elsif (%::myconfig && $::myconfig{dbname}) {
+    %specific_connect_settings = (
+      database        => $::myconfig{dbname},
+      host            => $::myconfig{dbhost} || 'localhost',
+      port            => $::myconfig{dbport} || 5432,
+      username        => $::myconfig{dbuser},
+      password        => $::myconfig{dbpasswd},
+    );
 
-    %connect_settings = ( driver          => $::myconfig{dbdriver} || 'Pg',
-                          database        => $::myconfig{dbname},
-                          host            => $::myconfig{dbhost} || 'localhost',
-                          port            => $::myconfig{dbport} || 5432,
-                          username        => $::myconfig{dbuser},
-                          password        => $::myconfig{dbpasswd},
-                          connect_options => { pg_enable_utf8 => $::locale && $::locale->is_utf8,
-                                             },
-                          european_dates  => $european_dates);
+  } else {
+    $type = 'KIVITENDO_EMPTY';
   }
 
+  my %connect_settings   = (%common_connect_settings, %specific_connect_settings);
   my %flattened_settings = _flatten_settings(%connect_settings);
 
-  $domain = 'LXOFFICE' if $type =~ m/^LXOFFICE/;
-  $type  .= join($SUBSCRIPT_SEPARATOR, map { ($_, $flattened_settings{$_} || '') } sort keys %flattened_settings);
-  my $idx = "${domain}::${type}";
+  $domain                = 'KIVITENDO' if $type =~ m/^KIVITENDO/;
+  $type                 .= join($SUBSCRIPT_SEPARATOR, map { ($_, $flattened_settings{$_} || '') } sort grep { $_ ne 'dbpasswd' } keys %flattened_settings);
+  my $idx                = "${domain}::${type}";
 
   if (!$_db_registered{$idx}) {
     $_db_registered{$idx} = 1;
@@ -98,19 +97,6 @@ sub _register_db {
   return ($domain, $type);
 }
 
-sub _execute_initial_sql {
-  my ($db) = @_;
-
-  return if $_initial_sql_executed{$db} || !%::myconfig || !$::myconfig{dateformat};
-
-  $_initial_sql_executed{$db} = 1;
-
-  # Don't rely on dboptions being set properly. Chose them from
-  # dateformat instead.
-  my $pg_dateformat = $_dateformats{ $::myconfig{dateformat} };
-  $db->dbh->do("set DateStyle to '${pg_dateformat}'") if $pg_dateformat;
-}
-
 sub _flatten_settings {
   my %settings  = @_;
   my %flattened = ();
diff --git a/SL/DB/AuthClient.pm b/SL/DB/AuthClient.pm
new file mode 100644 (file)
index 0000000..849fc30
--- /dev/null
@@ -0,0 +1,195 @@
+package SL::DB::AuthClient;
+
+use strict;
+
+use Carp;
+use File::Path ();
+
+use SL::DBConnect;
+use SL::DB::MetaSetup::AuthClient;
+use SL::DB::Manager::AuthClient;
+use SL::DB::Helper::Util;
+
+__PACKAGE__->meta->add_relationship(
+  users => {
+    type      => 'many to many',
+    map_class => 'SL::DB::AuthClientUser',
+    map_from  => 'client',
+    map_to    => 'user',
+  },
+  groups => {
+    type      => 'many to many',
+    map_class => 'SL::DB::AuthClientGroup',
+    map_from  => 'client',
+    map_to    => 'group',
+  },
+);
+
+__PACKAGE__->meta->initialize;
+
+__PACKAGE__->before_save('_before_save_remember_old_name');
+__PACKAGE__->after_save('_after_save_ensure_webdav_symlink_correctness');
+__PACKAGE__->after_delete('_after_delete_delete_webdav_symlink');
+
+sub _before_save_remember_old_name {
+  my ($self) = @_;
+
+  delete $self->{__before_save_remember_old_name};
+  if ($self->id && $::lx_office_conf{features}->{webdav}) {
+    $self->{__before_save_remember_old_name} = SL::DB::AuthClient->new(id => $self->id)->load->name;
+  }
+
+  return 1;
+}
+
+sub _after_save_ensure_webdav_symlink_correctness {
+  my ($self) = @_;
+
+  $self->ensure_webdav_symlink_correctness($self->{__before_save_remember_old_name}) if $self->id && $::lx_office_conf{features}->{webdav};
+  return 1;
+}
+
+sub _after_delete_delete_webdav_symlink {
+  my ($self) = @_;
+
+  return 1 if !$::lx_office_conf{features}->{webdav};
+  my $name = $self->webdav_symlink_basename;
+  unlink "webdav/links/${name}";
+  return 1;
+}
+
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+  push @errors, $::locale->text('The name is missing.')                                           if !$self->name;
+  push @errors, $::locale->text('The database name is missing.')                                  if !$self->dbname;
+  push @errors, $::locale->text('The database host is missing.')                                  if !$self->dbhost;
+  push @errors, $::locale->text('The database port is missing.')                                  if !$self->dbport;
+  push @errors, $::locale->text('The database user is missing.')                                  if !$self->dbuser;
+  push @errors, $::locale->text('The name is not unique.')                                        if !SL::DB::Helper::Util::is_unique($self, 'name');
+  push @errors, $::locale->text('The combination of database host, port and name is not unique.') if !SL::DB::Helper::Util::is_unique($self, 'dbhost', 'dbport', 'dbname');
+
+  return @errors;
+}
+
+sub webdav_symlink_basename {
+  my ($self, $name) =  @_;
+
+  $name             =  $name || $self->name || '';
+  $name             =~ s:/+:_:g;
+
+  return $name;
+}
+
+sub ensure_webdav_symlink_correctness {
+  my ($self, $old_name) = @_;
+
+  return unless $::lx_office_conf{features}->{webdav};
+
+  croak "Need object ID" unless $self->id;
+
+  my $new_symlink = $self->webdav_symlink_basename;
+
+  croak "Need name" unless $new_symlink;
+
+  my $base_path = 'webdav/links';
+
+  if ($old_name) {
+    my $old_symlink = $self->webdav_symlink_basename($old_name);
+    return if $old_symlink eq $new_symlink;
+
+    if (-l "${base_path}/${old_symlink}") {
+      rename "${base_path}/${old_symlink}", "${base_path}/${new_symlink}";
+      return;
+    }
+  }
+
+  File::Path::make_path('webdav/' . $self->id);
+  symlink '../' . $self->id, "${base_path}/${new_symlink}";
+}
+
+sub get_dbconnect_args {
+  my ($self, %params) = @_;
+
+  return (
+    'dbi:Pg:dbname=' . $self->dbname . ';host=' . ($self->dbhost || 'localhost') . ';port=' . ($self->dbport || 5432),
+    $self->dbuser,
+    $self->dbpasswd,
+    SL::DBConnect->get_options(%params),
+  );
+}
+
+sub dbconnect {
+  my ($self, %params) = @_;
+  return SL::DBConnect->connect($self->get_dbconnect_args(%params));
+}
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::AuthClient - RDBO model for the auth.clients table
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<dbconnect [%params]>
+
+Establishes a new database connection to the database configured for
+C<$self>. Returns a database handle as returned by
+L<SL::DBConnect/connect> (which is either a normal L<DBI> handle or
+one handled by L<DBIx::Log4perl>).
+
+C<%params> are optional parameters passed as the fourth argument to
+L<SL::DBConnect/connect>. They're first filtered through
+L<SL::DBConnect/get_options> so the UTF-8 flag will be set properly.
+
+=item C<ensure_webdav_symlink_correctness>
+
+Handles the symlink creation/deletion for the WebDAV folder. Does
+nothing if WebDAV is not enabled in the configuration.
+
+For each existing client a symbolic link should exist in the directory
+C<webdav/links> pointing to the actual WebDAV directory which is the
+client's database ID.
+
+The symbolic link's name is the client's name sanitized a bit. It's
+calculated by L</webdav_symlink_basename>.
+
+=item C<get_dbconnect_args [%params]>
+
+Returns an array of database connection parameters suitable for
+passing to L<SL::DBConnect/connect>.
+
+C<%params> are optional parameters passed as the fourth argument to
+L<SL::DBConnect/connect>. They're first filtered through
+L<SL::DBConnect/get_options> so the UTF-8 flag will be set properly.
+
+=item C<validate>
+
+Returns an array of human-readable error message if the object must
+not be saved and an empty list if nothing's wrong.
+
+=item C<webdav_symlink_basename>
+
+Returns the base name of the symbolic link for the WebDAV C<links>
+sub-folder.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
diff --git a/SL/DB/AuthClientGroup.pm b/SL/DB/AuthClientGroup.pm
new file mode 100644 (file)
index 0000000..8d31fbf
--- /dev/null
@@ -0,0 +1,9 @@
+package SL::DB::AuthClientGroup;
+
+use strict;
+
+use SL::DB::MetaSetup::AuthClientGroup;
+
+__PACKAGE__->meta->make_manager_class;
+
+1;
diff --git a/SL/DB/AuthClientUser.pm b/SL/DB/AuthClientUser.pm
new file mode 100644 (file)
index 0000000..c01447e
--- /dev/null
@@ -0,0 +1,9 @@
+package SL::DB::AuthClientUser;
+
+use strict;
+
+use SL::DB::MetaSetup::AuthClientUser;
+
+__PACKAGE__->meta->make_manager_class;
+
+1;
index fe755c7..23dabb5 100644 (file)
@@ -1,17 +1,10 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::AuthGroup;
 
 use strict;
 
 use SL::DB::MetaSetup::AuthGroup;
-use SL::DB::AuthGroupRight;
-
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
-__PACKAGE__->meta->make_manager_class;
-
-__PACKAGE__->meta->schema('auth');
+use SL::DB::Manager::AuthGroup;
+use SL::DB::Helper::Util;
 
 __PACKAGE__->meta->add_relationship(
   users => {
@@ -25,13 +18,100 @@ __PACKAGE__->meta->add_relationship(
     class      => 'SL::DB::AuthGroupRight',
     column_map => { id => 'group_id' },
   },
+  clients => {
+    type      => 'many to many',
+    map_class => 'SL::DB::AuthClientGroup',
+    map_from  => 'group',
+    map_to    => 'client',
+  },
 );
 
 __PACKAGE__->meta->initialize;
 
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+  push @errors, $::locale->text('The name is missing.')    if !$self->name;
+  push @errors, $::locale->text('The name is not unique.') if !SL::DB::Helper::Util::is_unique($self, 'name');
+
+  return @errors;
+}
+
 sub get_employees {
   my @logins = map { $_->login } $_[0]->users;
   return @logins ? @{ SL::DB::Manager::Employee->get_all(query => [ login => \@logins ]) } : ();
 }
 
+sub rights_map {
+  my $self = shift;
+
+  if (@_) {
+    my %new_rights = ref($_[0]) eq 'HASH' ? %{ $_[0] } : @_;
+    $self->rights([ map { SL::DB::AuthGroupRight->new(right => $_, granted => $new_rights{$_} ? 1 : 0) } SL::Auth::all_rights() ]);
+  }
+
+  return {
+    map({ ($_        => 0)           } SL::Auth::all_rights()),
+    map({ ($_->right => $_->granted) } @{ $self->rights || [] })
+  };
+}
+
 1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::AuthGroup - RDBO model for auth.group
+
+=head1 SYNOPSIS
+
+  # Outputting all rights granted to this group:
+  my $group  = SL::DB::Manager::AuthGroup->get_first;
+  my %rights = %{ $group->rights_map };
+  print "Granted rights:\n";
+  print "  $_\n" for sort grep { $rights{$_} } keys %rights;
+
+  # Set a right to 'yes':
+  $group->rights_map(%{ $group->rights_map }, invoice_edit => 1);
+  $group->save;
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<get_employees>
+
+Returns all employees (as instances of L<SL::DB::Employee>) whose
+corresponding logins are members in this group.
+
+=item C<rights_map [$new_rights]>
+
+Gets/sets the rights for this group as hashes. Returns a hash
+references containing the right names as the keys and trueish/falsish
+values for 'granted'/'not granted'.
+
+If C<$new_rights> is given as a hash reference or a plain hash then it
+will also set all rights from this hash.
+
+=item C<validate>
+
+Validates the object before saving (checks uniqueness, attribute
+presence etc). Returns a list of human-readable errors and an empty
+list on success.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index 49e4344..1ba9ae1 100644 (file)
@@ -1,16 +1,9 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::AuthGroupRight;
 
 use strict;
 
 use SL::DB::MetaSetup::AuthGroupRight;
 
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
 __PACKAGE__->meta->make_manager_class;
 
-__PACKAGE__->meta->schema('auth');
-__PACKAGE__->meta->initialize;
-
 1;
index 2a15449..43d72f9 100644 (file)
@@ -1,6 +1,3 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::AuthUser;
 
 use strict;
@@ -8,12 +5,13 @@ use strict;
 use List::Util qw(first);
 
 use SL::DB::MetaSetup::AuthUser;
+use SL::DB::Manager::AuthUser;
+use SL::DB::AuthClient;
 use SL::DB::AuthUserGroup;
+use SL::DB::Helper::Util;
 
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
-__PACKAGE__->meta->make_manager_class;
-
-__PACKAGE__->meta->schema('auth');
+use constant CONFIG_VARS => qw(copies countrycode dateformat default_media default_printer_id email favorites fax hide_cvar_search_options mandatory_departments menustyle name
+                               numberformat show_form_details signature stylesheet taxincluded_checked tel template_format vclimit);
 
 __PACKAGE__->meta->add_relationship(
   groups => {
@@ -27,15 +25,42 @@ __PACKAGE__->meta->add_relationship(
     class      => 'SL::DB::AuthUserConfig',
     column_map => { id => 'user_id' },
   },
+  clients => {
+    type      => 'many to many',
+    map_class => 'SL::DB::AuthClientUser',
+    map_from  => 'user',
+    map_to    => 'client',
+  },
 );
 
 __PACKAGE__->meta->initialize;
 
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+  push @errors, $::locale->text('The login is missing.')    if !$self->login;
+  push @errors, $::locale->text('The login is not unique.') if !SL::DB::Helper::Util::is_unique($self, 'login');
+
+  return @errors;
+}
+
 sub get_config_value {
   my ($self, $key) = @_;
 
-  my $cfg = first { $_->cfg_key eq $key } @{ $self->configs };
+  my $cfg = first { $_->cfg_key eq $key } @{ $self->configs || [] };
   return $cfg ? $cfg->cfg_value : undef;
 }
 
+sub config_values {
+  my $self = shift;
+
+  if (0 != scalar(@_)) {
+    my %settings = (ref($_[0]) || '') eq 'HASH' ? %{ $_[0] } : @_;
+    $self->configs([ map { SL::DB::AuthUserConfig->new(cfg_key => $_, cfg_value => $settings{$_}) } keys %settings ]);
+  }
+
+  return { map { ($_->cfg_key => $_->cfg_value) } @{ $self->configs || [] } };
+}
+
 1;
index 2cb8e6b..b52a1a1 100644 (file)
@@ -1,16 +1,9 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::AuthUserConfig;
 
 use strict;
 
 use SL::DB::MetaSetup::AuthUserConfig;
 
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
 __PACKAGE__->meta->make_manager_class;
 
-__PACKAGE__->meta->schema('auth');
-__PACKAGE__->meta->initialize;
-
 1;
index 495cb66..be88c4e 100644 (file)
@@ -1,29 +1,9 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::AuthUserGroup;
 
 use strict;
 
 use SL::DB::MetaSetup::AuthUserGroup;
 
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
 __PACKAGE__->meta->make_manager_class;
 
-__PACKAGE__->meta->schema('auth');
-
-__PACKAGE__->meta->add_foreign_keys(
-  user => {
-    class       => 'SL::DB::AuthUser',
-    key_columns => { user_id => 'id' },
-  },
-
-  group => {
-    class       => 'SL::DB::AuthGroup',
-    key_columns => { group_id => 'id' },
-  },
-);
-
-__PACKAGE__->meta->initialize;
-
 1;
index d9d29b4..e5397ef 100644 (file)
@@ -1,13 +1,9 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::Bin;
 
 use strict;
 
 use SL::DB::MetaSetup::Bin;
 
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
 __PACKAGE__->meta->make_manager_class;
 
 1;
index 14ba066..9b24f41 100644 (file)
@@ -1,13 +1,8 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::Currency;
 
 use strict;
 
 use SL::DB::MetaSetup::Currency;
-
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
-__PACKAGE__->meta->make_manager_class;
+use SL::DB::Manager::Currency;
 
 1;
index f951bf9..9962406 100644 (file)
@@ -5,6 +5,9 @@ use strict;
 use SL::DB::AccTransaction;
 use SL::DB::Assembly;
 use SL::DB::AuditTrail;
+use SL::DB::AuthClient;
+use SL::DB::AuthClientUser;
+use SL::DB::AuthClientGroup;
 use SL::DB::AuthGroup;
 use SL::DB::AuthGroupRight;
 use SL::DB::AuthUser;
index f0b8be9..a5b6f05 100644 (file)
@@ -11,13 +11,13 @@ our @EXPORT_OK = qw(get_table_for_package get_package_for_table get_package_name
 
 # these will not be managed as Rose::DB models, because they are not normalized,
 # significant changes are needed to get them done, or they were done by CRM.
-my @lxoffice_blacklist_permanent = qw(
+my @kivitendo_blacklist_permanent = qw(
   leads
 );
 
 # these are not managed _yet_, but will hopefully at some point.
 # if you are confident that one of these works, remove it here.
-my @lxoffice_blacklist_temp = qw(
+my @kivitendo_blacklist_temp = qw(
 );
 
 # tables created by crm module
@@ -29,19 +29,23 @@ my @cash_register_blacklist = qw(
 ekartikel ekbon ekkunde ektext erptasten
 );
 
-my @lxoffice_blacklist = (@lxoffice_blacklist_permanent, @lxoffice_blacklist_temp, @crm_blacklist, @cash_register_blacklist);
+my @kivitendo_blacklist = (@kivitendo_blacklist_permanent, @kivitendo_blacklist_temp, @crm_blacklist, @cash_register_blacklist);
 
 # map table names to their models.
 # unlike rails we have no singular<->plural magic.
 # remeber: tables should be named as the plural of the model name.
-my %lxoffice_package_names = (
+my %kivitendo_package_names = (
+  # TABLE                           # MODEL (given in C style)
   acc_trans                      => 'acc_transaction',
   audittrail                     => 'audit_trail',
-  auth_group                     => 'auth_groups',
-  auth_group_right               => 'auth_group_rights',
-  auth_user                      => 'auth_users',
-  auth_user_config               => 'auth_user_configs',
-  auth_user_group                => 'auth_user_groups',
+  'auth.clients'                 => 'auth_client',
+  'auth.clients_users'           => 'auth_client_user',
+  'auth.clients_groups'          => 'auth_client_group',
+  'auth.group'                   => 'auth_group',
+  'auth.group_rights'            => 'auth_group_right',
+  'auth.user'                    => 'auth_user',
+  'auth.user_config'             => 'auth_user_config',
+  'auth.user_group'              => 'auth_user_group',
   ar                             => 'invoice',
   ap                             => 'purchase_invoice',
   background_jobs                => 'background_job',
@@ -109,42 +113,42 @@ my %lxoffice_package_names = (
   vendor                         => 'vendor',
 );
 
-my (%lxoffice_tables_to_packages, %lxoffice_tables_to_manager_packages, %lxoffice_packages_to_tables);
+my (%kivitendo_tables_to_packages, %kivitendo_tables_to_manager_packages, %kivitendo_packages_to_tables);
 
 sub get_blacklist {
-  return LXOFFICE => \@lxoffice_blacklist;
+  return KIVITENDO => \@kivitendo_blacklist;
 }
 
 sub get_package_names {
-  return LXOFFICE => \%lxoffice_package_names;
+  return KIVITENDO => \%kivitendo_package_names;
 }
 
 sub get_package_for_table {
-  %lxoffice_tables_to_packages = map { ($_ => "SL::DB::" . camelify($lxoffice_package_names{$_})) } keys %lxoffice_package_names
-    unless %lxoffice_tables_to_packages;
+  %kivitendo_tables_to_packages = map { ($_ => "SL::DB::" . camelify($kivitendo_package_names{$_})) } keys %kivitendo_package_names
+    unless %kivitendo_tables_to_packages;
 
-  return $lxoffice_tables_to_packages{ $_[0] };
+  return $kivitendo_tables_to_packages{ $_[0] };
 }
 
 sub get_manager_package_for_table {
-  %lxoffice_tables_to_manager_packages = map { ($_ => "SL::DB::Manager::" . camelify($lxoffice_package_names{$_})) } keys %lxoffice_package_names
-    unless %lxoffice_tables_to_manager_packages;
+  %kivitendo_tables_to_manager_packages = map { ($_ => "SL::DB::Manager::" . camelify($kivitendo_package_names{$_})) } keys %kivitendo_package_names
+    unless %kivitendo_tables_to_manager_packages;
 
-  return $lxoffice_tables_to_manager_packages{ $_[0] };
+  return $kivitendo_tables_to_manager_packages{ $_[0] };
 }
 
 sub get_table_for_package {
-  get_package_for_table('dummy') if !%lxoffice_tables_to_packages;
-  %lxoffice_packages_to_tables = reverse %lxoffice_tables_to_packages unless %lxoffice_packages_to_tables;
+  get_package_for_table('dummy') if !%kivitendo_tables_to_packages;
+  %kivitendo_packages_to_tables = reverse %kivitendo_tables_to_packages unless %kivitendo_packages_to_tables;
 
   my $package = $_[0] =~ m/^SL::DB::/ ? $_[0] : "SL::DB::" . $_[0];
-  return $lxoffice_packages_to_tables{ $package };
+  return $kivitendo_packages_to_tables{ $package };
 }
 
 sub db {
   my $string = $_[0];
-  my $lookup = $lxoffice_package_names{$_[0]} ||
-      plurify($lxoffice_package_names{singlify($_[0])});
+  my $lookup = $kivitendo_package_names{$_[0]} ||
+      plurify($kivitendo_package_names{singlify($_[0])});
 
   for my $thing ($string, $lookup) {
 
diff --git a/SL/DB/Helper/Util.pm b/SL/DB/Helper/Util.pm
new file mode 100644 (file)
index 0000000..a7f2410
--- /dev/null
@@ -0,0 +1,84 @@
+package SL::DB::Helper::Util;
+
+use strict;
+
+use Rose::DB::Object::Util;
+
+use parent qw(Exporter);
+our @EXPORT_OK = qw(is_unique);
+
+#
+# Public functions not exported by default
+#
+
+sub is_unique {
+  my ($self, @columns) = @_;
+
+  my @filter =  map { ($_ => $self->$_) } @columns;
+  if (Rose::DB::Object::Util::is_in_db($self)) {
+    push @filter, map { ("!${_}" => $self->$_ )} $self->meta->primary_key;
+  }
+
+  return !$self->_get_manager_class->get_first(where => [ and => \@filter ]);
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Helper::Util - Helper functions for Rose::DB::Object instances
+
+=head1 SYNOPSIS
+
+  package SL::DB::AuthUser;
+
+  use SL::DB::Helper::Util;
+
+  sub validate {
+    ...
+    if (!SL::DB::Helper::Util::is_unique($self, 'login')) {
+      push @errors, "Login not unique";
+    }
+  }
+
+=head1 OVERVIEW
+
+This is a collection of assorted helper and utility functions for
+Rose::DB::Object instances that don't require full-blown mixin helpers
+like L<SL::DB::ActsAsList>. The module does not export any function by
+default, but all of the public ones can be requested in the usual
+way.
+
+Each function can be called either as fully qualified name with the
+object instance as the first argument or (if the function is imported)
+as an intance method on the object instance.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<is_unique @columns>
+
+Returns trueish if C<$self> is unique in its table regarding the
+columns C<@columns>. What it does is look for existing records in the
+database whose stored column values match C<$self>'s current values
+for these columns. If C<$self> already exists in the database then
+that row is not considered during the search.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
diff --git a/SL/DB/Manager/AuthClient.pm b/SL/DB/Manager/AuthClient.pm
new file mode 100644 (file)
index 0000000..ccfb97e
--- /dev/null
@@ -0,0 +1,20 @@
+package SL::DB::Manager::AuthClient;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::AuthClient' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'name', 1 ],
+           columns => { SIMPLE => 'ALL' } );
+}
+
+1;
diff --git a/SL/DB/Manager/AuthGroup.pm b/SL/DB/Manager/AuthGroup.pm
new file mode 100644 (file)
index 0000000..985f834
--- /dev/null
@@ -0,0 +1,20 @@
+package SL::DB::Manager::AuthGroup;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::AuthGroup' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'name', 1 ],
+           columns => { SIMPLE => 'ALL' } );
+}
+
+1;
diff --git a/SL/DB/Manager/AuthUser.pm b/SL/DB/Manager/AuthUser.pm
new file mode 100644 (file)
index 0000000..8095622
--- /dev/null
@@ -0,0 +1,20 @@
+package SL::DB::Manager::AuthUser;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::AuthUser' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'login', 1 ],
+           columns => { SIMPLE => 'ALL' } );
+}
+
+1;
diff --git a/SL/DB/Manager/Currency.pm b/SL/DB/Manager/Currency.pm
new file mode 100644 (file)
index 0000000..b18cd4c
--- /dev/null
@@ -0,0 +1,21 @@
+package SL::DB::Manager::Currency;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::Currency' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'id', 1 ],
+           columns => { SIMPLE => 'ALL',
+                        map { ( $_ => "lower(currencies.$_)" ) } qw(name)
+                      });
+}
+
+1;
diff --git a/SL/DB/Manager/Printer.pm b/SL/DB/Manager/Printer.pm
new file mode 100644 (file)
index 0000000..ec1e831
--- /dev/null
@@ -0,0 +1,20 @@
+package SL::DB::Manager::Printer;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::Printer' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'printer_description', 1 ],
+           columns => { SIMPLE => 'ALL' } );
+}
+
+1;
diff --git a/SL/DB/Manager/Warehouse.pm b/SL/DB/Manager/Warehouse.pm
new file mode 100644 (file)
index 0000000..6642f87
--- /dev/null
@@ -0,0 +1,21 @@
+package SL::DB::Manager::Warehouse;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::Warehouse' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'sortkey', 1 ],
+           columns => { SIMPLE => 'ALL',
+                        map { ( $_ => "lower(warehouse.$_)" ) } qw(description)
+                      });
+}
+
+1;
diff --git a/SL/DB/MetaSetup/AuthClient.pm b/SL/DB/MetaSetup/AuthClient.pm
new file mode 100644 (file)
index 0000000..b78050c
--- /dev/null
@@ -0,0 +1,33 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::AuthClient;
+
+use strict;
+
+use base qw(SL::DB::Object);
+
+__PACKAGE__->meta->setup(
+  table   => 'clients',
+  schema  => 'auth',
+
+  columns => [
+    id         => { type => 'serial', not_null => 1 },
+    name       => { type => 'text', not_null => 1 },
+    dbhost     => { type => 'text', not_null => 1 },
+    dbport     => { type => 'integer', default => 5432, not_null => 1 },
+    dbname     => { type => 'text', not_null => 1 },
+    dbuser     => { type => 'text', not_null => 1 },
+    dbpasswd   => { type => 'text', not_null => 1 },
+    is_default => { type => 'boolean', default => 'false', not_null => 1 },
+  ],
+
+  primary_key_columns => [ 'id' ],
+
+  unique_keys => [
+    [ 'dbhost', 'dbport', 'dbname' ],
+    [ 'name' ],
+  ],
+);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/AuthClientGroup.pm b/SL/DB/MetaSetup/AuthClientGroup.pm
new file mode 100644 (file)
index 0000000..f4e2f87
--- /dev/null
@@ -0,0 +1,34 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::AuthClientGroup;
+
+use strict;
+
+use base qw(SL::DB::Object);
+
+__PACKAGE__->meta->setup(
+  table   => 'clients_groups',
+  schema  => 'auth',
+
+  columns => [
+    client_id => { type => 'integer', not_null => 1 },
+    group_id  => { type => 'integer', not_null => 1 },
+  ],
+
+  primary_key_columns => [ 'client_id', 'group_id' ],
+
+  foreign_keys => [
+    client => {
+      class       => 'SL::DB::AuthClient',
+      key_columns => { client_id => 'id' },
+    },
+
+    group => {
+      class       => 'SL::DB::AuthGroup',
+      key_columns => { group_id => 'id' },
+    },
+  ],
+);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/AuthClientUser.pm b/SL/DB/MetaSetup/AuthClientUser.pm
new file mode 100644 (file)
index 0000000..85d0ef9
--- /dev/null
@@ -0,0 +1,34 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::AuthClientUser;
+
+use strict;
+
+use base qw(SL::DB::Object);
+
+__PACKAGE__->meta->setup(
+  table   => 'clients_users',
+  schema  => 'auth',
+
+  columns => [
+    client_id => { type => 'integer', not_null => 1 },
+    user_id   => { type => 'integer', not_null => 1 },
+  ],
+
+  primary_key_columns => [ 'client_id', 'user_id' ],
+
+  foreign_keys => [
+    client => {
+      class       => 'SL::DB::AuthClient',
+      key_columns => { client_id => 'id' },
+    },
+
+    user => {
+      class       => 'SL::DB::AuthUser',
+      key_columns => { user_id => 'id' },
+    },
+  ],
+);
+
+1;
+;
index 31d4915..9e75aac 100644 (file)
@@ -8,6 +8,7 @@ use base qw(SL::DB::Object);
 
 __PACKAGE__->meta->setup(
   table   => 'group',
+  schema  => 'auth',
 
   columns => [
     id          => { type => 'serial', not_null => 1 },
index 0cf5d72..7c98d2a 100644 (file)
@@ -8,6 +8,7 @@ use base qw(SL::DB::Object);
 
 __PACKAGE__->meta->setup(
   table   => 'group_rights',
+  schema  => 'auth',
 
   columns => [
     group_id => { type => 'integer', not_null => 1 },
@@ -16,6 +17,13 @@ __PACKAGE__->meta->setup(
   ],
 
   primary_key_columns => [ 'group_id', 'right' ],
+
+  foreign_keys => [
+    group => {
+      class       => 'SL::DB::AuthGroup',
+      key_columns => { group_id => 'id' },
+    },
+  ],
 );
 
 1;
index 04bc23e..2175190 100644 (file)
@@ -8,6 +8,7 @@ use base qw(SL::DB::Object);
 
 __PACKAGE__->meta->setup(
   table   => 'user',
+  schema  => 'auth',
 
   columns => [
     id       => { type => 'serial', not_null => 1 },
index 2d132eb..ef9e348 100644 (file)
@@ -8,6 +8,7 @@ use base qw(SL::DB::Object);
 
 __PACKAGE__->meta->setup(
   table   => 'user_config',
+  schema  => 'auth',
 
   columns => [
     user_id   => { type => 'integer', not_null => 1 },
@@ -16,6 +17,13 @@ __PACKAGE__->meta->setup(
   ],
 
   primary_key_columns => [ 'user_id', 'cfg_key' ],
+
+  foreign_keys => [
+    user => {
+      class       => 'SL::DB::AuthUser',
+      key_columns => { user_id => 'id' },
+    },
+  ],
 );
 
 1;
index f185014..226921f 100644 (file)
@@ -8,6 +8,7 @@ use base qw(SL::DB::Object);
 
 __PACKAGE__->meta->setup(
   table   => 'user_group',
+  schema  => 'auth',
 
   columns => [
     user_id  => { type => 'integer', not_null => 1 },
@@ -15,6 +16,18 @@ __PACKAGE__->meta->setup(
   ],
 
   primary_key_columns => [ 'user_id', 'group_id' ],
+
+  foreign_keys => [
+    group => {
+      class       => 'SL::DB::AuthGroup',
+      key_columns => { group_id => 'id' },
+    },
+
+    user => {
+      class       => 'SL::DB::AuthUser',
+      key_columns => { user_id => 'id' },
+    },
+  ],
 );
 
 1;
index 6e75bc5..ce06162 100644 (file)
@@ -70,6 +70,13 @@ __PACKAGE__->meta->setup(
     assemblynumber                          => { type => 'text' },
     warehouse_id                            => { type => 'integer' },
     bin_id                                  => { type => 'integer' },
+    company                                 => { type => 'text' },
+    address                                 => { type => 'text' },
+    taxnumber                               => { type => 'text' },
+    co_ustid                                => { type => 'text' },
+    duns                                    => { type => 'text' },
+    sepa_creditor_id                        => { type => 'text' },
+    templates                               => { type => 'text' },
     show_weight                             => { type => 'boolean', default => 'false', not_null => 1 },
     transfer_default                        => { type => 'boolean', default => 'true' },
     transfer_default_use_master_default_bin => { type => 'boolean', default => 'false' },
@@ -82,6 +89,7 @@ __PACKAGE__->meta->setup(
   primary_key_columns => [ 'id' ],
 
   allow_inline_column_values => 1,
+
   foreign_keys => [
     bin => {
       class       => 'SL::DB::Bin',
index a99121d..b7b06be 100644 (file)
@@ -15,7 +15,6 @@ __PACKAGE__->meta->setup(
     startdate => { type => 'date', default => 'now' },
     enddate   => { type => 'date' },
     notes     => { type => 'text' },
-    role      => { type => 'text' },
     sales     => { type => 'boolean', default => 'true' },
     itime     => { type => 'timestamp', default => 'now()' },
     mtime     => { type => 'timestamp' },
index 8b86e97..c072e90 100755 (executable)
@@ -26,7 +26,7 @@ sub new {
 sub init_db {
   my $class_or_self = shift;
   my $class         = ref($class_or_self) || $class_or_self;
-  my $type          = $class =~ m/::Auth/ ? 'LXOFFICE_AUTH' : 'LXOFFICE';
+  my $type          = $class =~ m/::Auth/ ? 'KIVITENDO_AUTH' : 'KIVITENDO';
 
   return SL::DB::create(undef, $type);
 }
index fd58eb5..83f9148 100644 (file)
@@ -3,11 +3,22 @@ package SL::DB::Printer;
 use strict;
 
 use SL::DB::MetaSetup::Printer;
-
-__PACKAGE__->meta->make_manager_class;
+use SL::DB::Manager::Printer;
+use SL::DB::Helper::Util;
 
 sub description {
   goto &printer_description;
 }
 
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+  push @errors, $::locale->text('The description is missing.')    if !$self->printer_description;
+  push @errors, $::locale->text('The command is missing.')        if !$self->printer_command;
+  push @errors, $::locale->text('The description is not unique.') if !SL::DB::Helper::Util::is_unique($self, 'printer_description');
+
+  return @errors;
+}
+
 1;
index 0557976..8345709 100644 (file)
@@ -1,15 +1,11 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::Warehouse;
 
 use strict;
 
 use SL::DB::MetaSetup::Warehouse;
+use SL::DB::Manager::Warehouse;
 use SL::DB::Helper::ActsAsList;
 
-__PACKAGE__->meta->make_manager_class;
-
 __PACKAGE__->meta->add_relationship(
   bins => {
     type         => 'one to many',
@@ -18,13 +14,14 @@ __PACKAGE__->meta->add_relationship(
   }
 );
 
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
-#__PACKAGE__->meta->make_manager_class;
-
 __PACKAGE__->meta->initialize;
 
+sub bins_sorted {
+  return [ sort { $a->id <=> $b->id } @{ shift()->bins || [] } ];
+}
+
 sub first_bin {
-  return shift()->bins->[0];
+  return shift()->bins_sorted->[0];
 }
 
 1;
index 8dc9787..afdf2c9 100644 (file)
@@ -3,11 +3,21 @@ package SL::DBConnect;
 use strict;
 
 use DBI;
+use SL::DB;
 
-sub connect {
-  shift;
+my %dateformat_to_datestyle = (
+  'yy-mm-dd'   => 'ISO',
+  'yyyy-mm-dd' => 'ISO',
+  'mm/dd/yy'   => 'SQL, US',
+  'dd/mm/yy'   => 'SQL, EUROPEAN',
+  'dd.mm.yy'   => 'GERMAN'
+);
+
+sub _connect {
+  my ($self, @args) = @_;
+  @args = $self->get_connect_args if !@args;
 
-  return DBI->connect(@_) unless $::lx_office_conf{debug} && $::lx_office_conf{debug}->{dbix_log4perl};
+  return DBI->connect(@args) unless $::lx_office_conf{debug} && $::lx_office_conf{debug}->{dbix_log4perl};
 
   require Log::Log4perl;
   require DBIx::Log4perl;
@@ -17,7 +27,46 @@ sub connect {
   $config      =~ s/LXDEBUGFILE/${filename}/g;
 
   Log::Log4perl->init(\$config);
-  return DBIx::Log4perl->connect(@_);
+  return DBIx::Log4perl->connect(@args);
+}
+
+sub connect {
+  my ($self, @args) = @_;
+
+  my $dbh = $self->_connect(@args);
+  return undef if !$dbh;
+
+  my $initial_sql = $self->get_initial_sql;
+  $dbh->do($initial_sql) if $initial_sql;
+
+  return $dbh;
+}
+
+sub get_datestyle {
+  my ($self, $dateformat) = @_;
+  return $dateformat_to_datestyle{ $dateformat || $::myconfig{dateformat} };
+}
+
+sub get_initial_sql {
+  my ($self) = @_;
+
+  return undef if !%::myconfig || !$::myconfig{dateformat};
+
+  my $datestyle = $self->get_datestyle;
+  return $datestyle ? qq|SET DateStyle to '${datestyle}'| : '';
+}
+
+sub get_connect_args {
+  my ($self, @args)   = @_;
+  my ($domain, $type) = SL::DB::_register_db(SL::DB->default_domain, 'KIVITENDO');
+  my $db_cfg          = SL::DB->registry->entry(domain => $domain, type => $type) || { };
+
+  return (
+    'dbi:Pg:dbname=' . $db_cfg->{database} . ';host=' . ($db_cfg->{host} || 'localhost') . ';port=' . ($db_cfg->{port} || 5432),
+    $db_cfg->{username},
+    $db_cfg->{password},
+    $self->get_options(%{ $db_cfg->{connect_options} || {} }, @args),
+  );
 }
 
 sub get_options {
@@ -31,3 +80,83 @@ sub get_options {
 }
 
 1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DBConnect - Connect to database for configured client/user,
+optionally routing through DBIx::Log4perl
+
+=head1 SYNOPSIS
+
+  # Connect to default database of current user/client, disabling auto
+  # commit mode:
+  my @options_suitable_for_dbi_connect =
+    SL::DBConnect->get_connect_args(AutoCommit => 0);
+  my $dbh = SL::DBConnect->connect(@options_suitable_for_dbi_connect);
+
+  # Connect to a very specific database:
+  my $dbh = SL::DBConnect->connect('dbi:Pg:dbname=demo', 'user', 'password');
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<connect [@dbi_args]>
+
+Connects to the database. If the configuration parameter
+C<debug.dbix_log4perl> is set then the call is made through
+L<DBIx::Log4per/connect>. Otherwise L<DBI/connect> is called directly.
+
+In each case C<@dbi_args> is passed through as-is.
+
+If C<@dbi_args> are not given they're generated by a call to
+L</get_connect_args>.
+
+=item C<get_connect_args [%options]>
+
+Returns an array of database connection settings suitable to a call to
+L<DBI/connect> or L</connect>. The settings to use are retrieved by
+calling L<SL::DB/_register_db>.
+
+This requires that a client has been set up with
+L<SL::Auth/set_client> or that C<%::myconfig> contains legacy
+connection settings.
+
+C<%options> are optional database options like C<AutoCommit> (fourth
+parameter to L<DBI/connect>). They're merged with default settings by
+filtering them through L/get_options>.
+
+=item C<get_datestyle [$dateformat]>
+
+Returns the appropriate value for the C<SET DateStyle to...> SQL call
+depending on C<$dateformat> (e.g. C<SQL, EUROPEAN> if C<$dateformat>
+equals C<dd.mm.yy>). If C<$dateformat> is not given then it defaults
+to C<$::myconfig{dateformat}>.
+
+=item C<get_initial_sql>
+
+Returns SQL commands that should be executed right after a connection
+has been established. This is usually the call to configure the
+C<DateStyle> format used by the database.
+
+=item C<get_options [%options]>
+
+Returns a hash reference of database options (fourth parameter to
+L<DBI/connect>) merged with certain default options.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index 5bf31d7..cdfca9b 100644 (file)
@@ -27,7 +27,7 @@ sub init {
 
   $params{path_suffix} ||= '';
   $params{schema}      ||= '';
-  $params{path}          = "sql/" . $params{dbdriver} . "-upgrade2" . $params{path_suffix};
+  $params{path}          = "sql/Pg-upgrade2" . $params{path_suffix};
 
   map { $self->{$_} = $params{$_} } keys %params;
 
@@ -235,15 +235,15 @@ sub process_query {
 
 # Process a Perl script which updates the database.
 # If the script returns 1 then the update was successful.
-# Return code "2" means "needs more interaction; remove
-# users/nologin and end current request".
+# Return code "2" means "needs more interaction; unlock
+# the system and end current request".
 # All other return codes are fatal errors.
 sub process_perl_script {
   $::lxdebug->enter_sub();
 
   my ($self, $dbh, $filename, $version_or_control, $db_charset) = @_;
 
-  my %form_values = map { $_ => $::form->{$_} } qw(dbconnect dbdefault dbdriver dbhost dbmbkiviunstable dbname dboptions dbpasswd dbport dbupdate dbuser login template_object version);
+  my %form_values = map { $_ => $::form->{$_} } qw(dbconnect dbdefault dbhost dbmbkiviunstable dbname dboptions dbpasswd dbport dbupdate dbuser login template_object version);
 
   $dbh->begin_work;
 
@@ -266,7 +266,7 @@ sub process_perl_script {
     print $::form->parse_html_template("dbupgrade/error", { file  => $filename, error => $error });
     ::end_of_request();
   } elsif (1 != $result) {
-    unlink("users/nologin") if (2 == $result);
+    SL::System::InstallationLock->unlock if 2 == $result;
     ::end_of_request();
   }
 
@@ -302,9 +302,8 @@ sub update_available {
 
   local *SQLDIR;
 
-  my $dbdriver = $self->{dbdriver};
-  opendir SQLDIR, "sql/${dbdriver}-upgrade" || error("", "sql/${dbdriver}-upgrade: $!");
-  my @upgradescripts = grep /${dbdriver}-upgrade-\Q$cur_version\E.*\.(sql|pl)$/, readdir SQLDIR;
+  opendir SQLDIR, "sql/Pg-upgrade" || error("", "sql/Pg-upgrade: $!");
+  my @upgradescripts = grep /Pg-upgrade-\Q$cur_version\E.*\.(sql|pl)$/, readdir SQLDIR;
   closedir SQLDIR;
 
   return ($#upgradescripts > -1);
@@ -374,7 +373,7 @@ sub apply_admin_dbupgrade_scripts {
     $::lxdebug->message(LXDebug->DEBUG2(), "Applying Update $control->{file}");
     print $self->{form}->parse_html_template("dbupgrade/upgrade_message2", $control);
 
-    $self->process_file($dbh, "sql/$self->{dbdriver}-upgrade2-auth/$control->{file}", $control, $db_charset);
+    $self->process_file($dbh, "sql/Pg-upgrade2-auth/$control->{file}", $control, $db_charset);
   }
 
   print $self->{form}->parse_html_template("dbupgrade/footer", { is_admin => 1 }) if $called_from_admin;
@@ -461,7 +460,6 @@ C<SQL/Pg-upgrade>)
   # Apply outstanding updates to the authentication database
   my $scripts = SL::DBUpgrade2->new(
     form     => $::form,
-    dbdriver => 'Pg',
     auth     => 1
   );
   $scripts->apply_admin_dbupgrade_scripts(1);
@@ -469,10 +467,11 @@ C<SQL/Pg-upgrade>)
   # Apply updates to a user database
   my $scripts = SL::DBUpgrade2->new(
     form     => $::form,
-    dbdriver => $::form->{dbdriver},
     auth     => 1
   );
-  User->dbupdate2($form, $scripts->parse_dbupdate_controls);
+  User->dbupdate2(form     => $form,
+                  updater  => $scripts->parse_dbupdate_controls,
+                  database => $dbname);
 
 =head1 OVERVIEW
 
@@ -603,11 +602,6 @@ Path to the upgrade files to parse. Required.
 
 C<SL::Form> object to use. Required.
 
-=item dbdriver
-
-Name of the database driver. Currently only C<Pg> for PostgreSQL is
-supported.
-
 =item auth
 
 Optional parameter defaulting to 0. If trueish then the scripts read
@@ -646,7 +640,7 @@ charset recoding of the script if required, C<$db_charset>).
 Perl scripts are executed via L<eval>. If L<eval> returns falsish then
 an error is expected. There are two special return values: If the
 script returns C<1> then the update was successful. Return code C<2>
-means "needs more interaction from the user; remove users/nologin and
+means "needs more interaction from the user; unlock the system and
 end current upgrade process". All other return codes are fatal errors.
 
 Inside the Perl script several local variables exist that can be used:
index 8eb407b..2eeed67 100644 (file)
--- a/SL/DN.pm
+++ b/SL/DN.pm
@@ -36,6 +36,7 @@ package DN;
 
 use SL::Common;
 use SL::DBUtils;
+use SL::DB::Default;
 use SL::GenericTranslations;
 use SL::IS;
 use SL::Mailer;
@@ -394,7 +395,9 @@ sub set_template_options {
 
   my ($self, $myconfig, $form) = @_;
 
-  $form->{templates}    = "$myconfig->{templates}";
+  my $defaults = SL::DB::Default->get;
+  $form->error($::locale->text('No print templates have been created for this client yet. Please do so in the client configuration.')) if !$defaults->templates;
+  $form->{templates}    = $defaults->templates;
   $form->{language}     = $form->get_template_language($myconfig);
   $form->{printer_code} = $form->get_printer_code($myconfig);
 
@@ -433,7 +436,7 @@ sub set_template_options {
 
   $form->{IN} = undef;
   for my $filename (@template_files) {
-    if (-f "$form->{templates}/$filename") {
+    if (-f ($defaults->templates . "/$filename")) {
       $form->{IN} = $filename;
       last;
     }
index a4677bb..fb8672c 100644 (file)
--- a/SL/DO.pm
+++ b/SL/DO.pm
@@ -672,9 +672,6 @@ sub retrieve {
     delete $form->{id};
   }
 
-  my %oid = ('Pg'     => 'oid',
-             'Oracle' => 'rowid');
-
   # retrieve individual items
   # this query looks up all information about the items
   # stuff different from the whole will not be overwritten, but saved with a suffix.
@@ -693,7 +690,7 @@ sub retrieve {
        LEFT JOIN project pr ON (doi.project_id = pr.id)
        LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
        WHERE doi.delivery_order_id IN ($do_ids_placeholders)
-       ORDER BY doi.$oid{$myconfig->{dbdriver}}|;
+       ORDER BY doi.oid|;
 
   $form->{form_details} = selectall_hashref_query($form, $dbh, $query, @do_ids);
 
@@ -752,9 +749,6 @@ sub order_details {
   my $partsgroup;
   my $position = 0;
 
-  my %oid = ('Pg'     => 'oid',
-             'Oracle' => 'rowid');
-
   my (@project_ids, %projectnumbers, %projectdescriptions);
 
   push(@project_ids, $form->{"globalproject_id"}) if ($form->{"globalproject_id"});
@@ -869,9 +863,9 @@ sub order_details {
       my $sortorder = "";
       if ($form->{groupitems}) {
         $sortorder =
-          qq|ORDER BY pg.partsgroup, a.$oid{$myconfig->{dbdriver}}|;
+          qq|ORDER BY pg.partsgroup, a.oid|;
       } else {
-        $sortorder = qq|ORDER BY a.$oid{$myconfig->{dbdriver}}|;
+        $sortorder = qq|ORDER BY a.oid|;
       }
 
       do_statement($form, $h_pg, $q_pg, conv_i($form->{"id_$i"}));
index 01b6aff..8329c5b 100644 (file)
@@ -59,16 +59,18 @@ sub interface_type {
   return $self->{interface} eq 'cgi' ? 'CGI' : 'FastCGI';
 }
 
+sub is_admin_request {
+  my %params = @_;
+  return ($params{script} eq 'admin.pl') || (($params{routing_type} eq 'controller') && ($params{script_name} eq 'Admin'));
+}
+
 sub pre_request_checks {
+  my (%params) = @_;
+
   _check_for_old_config_files();
 
-  if (!$::auth->session_tables_present) {
-    if ($::form->{script} eq 'admin.pl') {
-      ::run();
-      ::end_of_request();
-    } else {
-      show_error('login_screen/auth_db_unreachable');
-    }
+  if (!$::auth->session_tables_present && !is_admin_request(%params)) {
+    show_error('login_screen/auth_db_unreachable');
   }
 
   if ($::request->type !~ m/^ (?: html | js | json ) $/x) {
@@ -231,9 +233,12 @@ sub handle_request {
   }
 
   eval {
-    pre_request_checks();
+    pre_request_checks(script => $script, action => $action, routing_type => $routing_type, script_name => $script_name);
 
-    $::form->error($::locale->text('System currently down for maintenance!')) if -e ($::lx_office_conf{paths}->{userspath} . "/nologin") && $script ne 'admin';
+    if (   SL::System::InstallationLock->is_locked
+        && !is_admin_request(script => $script, script_name => $script_name, routing_type => $routing_type)) {
+      $::form->error($::locale->text('System currently down for maintenance!'));
+    }
 
     # For compatibility with a lot of database upgrade scripts etc:
     # Re-write request to old 'login.pl?action=login' to new
@@ -247,14 +252,10 @@ sub handle_request {
       print $::request->{cgi}->redirect('controller.pl?action=LoginScreen/user_login');
 
     } elsif ($script eq 'admin') {
-      $::form->{titlebar} = "kivitendo " . $::locale->text('Version') . " $::form->{version}";
       ::run($session_result);
 
     } else {
-      if (SL::Auth::SESSION_EXPIRED == $session_result) {
-        print $::request->{cgi}->redirect('controller.pl?action=LoginScreen/user_login&error=session');
-        ::end_of_request();
-      }
+      $self->redirect_to_login($script) if SL::Auth::SESSION_EXPIRED == $session_result;
 
       my %auth_result = $self->{auth_handler}->handle(
         routing_type => $routing_type,
@@ -320,6 +321,13 @@ sub handle_request {
   $::lxdebug->leave_sub;
 }
 
+sub redirect_to_login {
+  my ($self, $script) = @_;
+  my $action          = $script =~ m/^admin/i ? 'Admin/login' : 'LoginScreen/user_login&error=session';
+  print $::request->cgi->redirect("controller.pl?action=${action}");
+  ::end_of_request();
+}
+
 sub unrequire_bin_mozilla {
   my $self = shift;
   return unless $self->_interface_is_fcgi;
index 4c352bd..449d684 100644 (file)
@@ -1,4 +1,4 @@
-  package SL::Dispatcher::AuthHandler;
+package SL::Dispatcher::AuthHandler;
 
 use strict;
 
@@ -18,7 +18,7 @@ sub handle {
   my $handler_name                     = "SL::Dispatcher::AuthHandler::" . ucfirst($auth_level);
   $self->{handlers}                  ||= {};
   $self->{handlers}->{$handler_name} ||= $handler_name->new;
-  my $ok = $self->{handlers}->{$handler_name}->handle;
+  my $ok = $self->{handlers}->{$handler_name}->handle(%param);
 
   return (
     auth_level     => $auth_level,
@@ -40,7 +40,7 @@ sub get_auth_level {
 sub get_keep_auth_vars {
   my ($self, %param) = @_;
 
-  return $param{routing_type} eq 'controller' ? "SL::Controller::$param{controller}"->keep_auth_vars_in_form : 0;
+  return $param{routing_type} eq 'controller' ? "SL::Controller::$param{controller}"->keep_auth_vars_in_form(action => $param{action}) : 0;
 }
 
 1;
index a7b649c..babdc22 100644 (file)
@@ -6,16 +6,25 @@ use parent qw(Rose::Object);
 use SL::Layout::Dispatcher;
 
 sub handle {
+  my ($self, %params) = @_;
+
   %::myconfig = ();
 
-  return 1 if  $::auth->get_api_token_cookie;
-  return 1 if  $::form->{'{AUTH}admin_password'} && ($::auth->authenticate_root($::form->{'{AUTH}admin_password'})            == $::auth->OK());
-  return 1 if !$::form->{'{AUTH}admin_password'} && ($::auth->authenticate_root($::auth->get_session_value('admin_password')) == $::auth->OK());
+  my $ok =  $::auth->get_api_token_cookie ? 1 : 0;
+  $ok  ||=  $::form->{'{AUTH}admin_password'} && ($::auth->authenticate_root($::form->{'{AUTH}admin_password'})            == $::auth->OK());
+  $ok  ||= !$::form->{'{AUTH}admin_password'} && ($::auth->authenticate_root($::auth->get_session_value('admin_password')) == $::auth->OK());
+  $ok  ||=  $params{action} eq 'login';
 
-  $::request->{layout} = SL::Layout::Dispatcher->new(style => 'admin');
+  $::auth->create_or_refresh_session;
 
-  $::auth->punish_wrong_login;
+  if ($ok) {
+    $::auth->delete_session_value('FLASH');
+    return 1;
+  }
+
+  $::request->{layout} = SL::Layout::Dispatcher->new(style => 'admin');
   $::auth->delete_session_value('admin_password');
+  $::auth->punish_wrong_login;
   SL::Dispatcher::show_error('admin/adminlogin', 'password');
 
   return 0;
index e126d87..5ee543b 100644 (file)
@@ -11,6 +11,9 @@ sub handle {
   my $login = $::form->{'{AUTH}login'} || $::auth->get_session_value('login');
   return $self->_error(%param) if !defined $login;
 
+  my $client_id = $::form->{'{AUTH}client_id'} || $::auth->get_session_value('client_id');
+  return $self->_error(%param) if !$client_id || !$::auth->set_client($client_id);
+
   %::myconfig = $::auth->read_user(login => $login);
 
   return $self->_error(%param) unless $::myconfig{login};
index 81727ed..3e56b69 100644 (file)
@@ -53,6 +53,7 @@ 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;
@@ -86,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();
 
@@ -101,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();
 
@@ -307,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();
@@ -991,6 +978,7 @@ sub parse_template {
 
   local (*IN, *OUT);
 
+  my $defaults  = SL::DB::Default->get;
   my $userspath = $::lx_office_conf{paths}->{userspath};
 
   $self->{"cwd"} = getcwd();
@@ -1042,11 +1030,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);
 
@@ -1373,8 +1363,7 @@ sub dbconnect {
   my ($self, $myconfig) = @_;
 
   # connect to database
-  my $dbh = SL::DBConnect->connect($myconfig->{dbconnect}, $myconfig->{dbuser}, $myconfig->{dbpasswd}, SL::DBConnect->get_options)
-    or $self->dberror;
+  my $dbh = SL::DBConnect->connect or $self->dberror;
 
   # set db options
   if ($myconfig->{dboptions}) {
@@ -1392,8 +1381,7 @@ sub dbconnect_noauto {
   my ($self, $myconfig) = @_;
 
   # connect to database
-  my $dbh = SL::DBConnect->connect($myconfig->{dbconnect}, $myconfig->{dbuser}, $myconfig->{dbpasswd}, SL::DBConnect->get_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}) {
@@ -1912,6 +1900,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));
@@ -1928,7 +1917,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;
@@ -3383,17 +3373,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} : '';
@@ -3435,7 +3432,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.
index ada29ac..e6c87b3 100644 (file)
--- a/SL/GL.pm
+++ b/SL/GL.pm
@@ -354,8 +354,6 @@ sub all_transactions {
     }
   }
 
-  my $false = ($myconfig->{dbdriver} eq 'Pg') ? "FALSE" : q|'0'|;
-
   my %sort_columns =  (
     'id'           => [ qw(id)                   ],
     'transdate'    => [ qw(transdate id)         ],
@@ -385,7 +383,7 @@ sub all_transactions {
 
   $query =
     qq|SELECT
-        ac.acc_trans_id, g.id, 'gl' AS type, $false AS invoice, g.reference, ac.taxkey, c.link,
+        ac.acc_trans_id, g.id, 'gl' AS type, FALSE AS invoice, g.reference, ac.taxkey, c.link,
         g.description, ac.transdate, ac.gldate, ac.source, ac.trans_id,
         ac.amount, c.accno, g.notes, t.chart_id,
         CASE WHEN (COALESCE(e.name, '') = '') THEN e.login ELSE e.name END AS employee
index 1dc1eff..71d547b 100644 (file)
--- a/SL/IS.pm
+++ b/SL/IS.pm
@@ -114,8 +114,6 @@ sub invoice_details {
   my $i;
   my @partsgroup = ();
   my $partsgroup;
-  my %oid = ('Pg'     => 'oid',
-             'Oracle' => 'rowid');
 
   # sort items by partsgroup
   for $i (1 .. $form->{rowcount}) {
@@ -336,9 +334,9 @@ sub invoice_details {
         my $sortorder = "";
         if ($form->{groupitems}) {
           $sortorder =
-            qq|ORDER BY pg.partsgroup, a.$oid{$myconfig->{dbdriver}}|;
+            qq|ORDER BY pg.partsgroup, a.oid|;
         } else {
-          $sortorder = qq|ORDER BY a.$oid{$myconfig->{dbdriver}}|;
+          $sortorder = qq|ORDER BY a.oid|;
         }
 
         $query =
index b3bd020..7a9cfc6 100644 (file)
@@ -19,6 +19,7 @@ BEGIN {
   { name => "Email::Address",                      url => "http://search.cpan.org/~rjbs/",      debian => 'libemail-address-perl' },
   { name => "Email::MIME",                         url => "http://search.cpan.org/~rjbs/",      debian => 'libemail-mime-perl' },
   { name => "FCGI",            version => '0.72',  url => "http://search.cpan.org/~mstrout/",   debian => 'libfcgi-perl' },
+  { name => "File::Copy::Recursive",               url => "http://search.cpan.org/~dmuey/",     debian => 'libfile-copy-recursive-perl' },
   { name => "JSON",                                url => "http://search.cpan.org/~makamaka",   debian => 'libjson-perl' },
   { name => "List::MoreUtils", version => '0.21',  url => "http://search.cpan.org/~vparseval/", debian => 'liblist-moreutils-perl' },
   { name => "Params::Validate",                    url => "http://search.cpan.org/~drolsky/",   debian => 'libparams-validate-perl' },
index 0c0b767..fec7507 100644 (file)
--- a/SL/OE.pm
+++ b/SL/OE.pm
@@ -853,9 +853,6 @@ sub retrieve {
       map { $form->{$_} =~ s/ +$//g } qw(printed emailed queued);
     }    # if !@ids
 
-    my %oid = ('Pg'     => 'oid',
-               'Oracle' => 'rowid');
-
     my $transdate = $form->{transdate} ? $dbh->quote($form->{transdate}) : "current_date";
 
     $form->{taxzone_id} = 0 unless ($form->{taxzone_id});
@@ -887,7 +884,7 @@ sub retrieve {
       ($form->{id}
        ? qq|WHERE o.trans_id = ?|
        : qq|WHERE o.trans_id IN (| . join(", ", map("?", @ids)) . qq|)|) .
-      qq|ORDER BY o.$oid{$myconfig->{dbdriver}}|;
+      qq|ORDER BY o.oid|;
 
     @ids = $form->{id} ? ($form->{id}) : @ids;
     $sth = prepare_execute_query($form, $dbh, $query, @values);
@@ -1055,10 +1052,6 @@ sub order_details {
   my $tax_rate;
   my $taxamount;
 
-
-  my %oid = ('Pg'     => 'oid',
-             'Oracle' => 'rowid');
-
   my (@project_ids, %projectnumbers, %projectdescriptions);
 
   push(@project_ids, $form->{"globalproject_id"}) if ($form->{"globalproject_id"});
@@ -1277,9 +1270,9 @@ sub order_details {
         # get parts and push them onto the stack
         my $sortorder = "";
         if ($form->{groupitems}) {
-          $sortorder = qq|ORDER BY pg.partsgroup, a.$oid{$myconfig->{dbdriver}}|;
+          $sortorder = qq|ORDER BY pg.partsgroup, a.oid|;
         } else {
-          $sortorder = qq|ORDER BY a.$oid{$myconfig->{dbdriver}}|;
+          $sortorder = qq|ORDER BY a.oid|;
         }
 
         $query = qq|SELECT p.partnumber, p.description, p.unit, a.qty, | .
diff --git a/SL/Printer.pm b/SL/Printer.pm
deleted file mode 100644 (file)
index 5ed072d..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-package SL::Printer;
-
-use strict;
-
-use SL::DBUtils;
-
-sub all_printers {
-  $::lxdebug->enter_sub;
-
-  my ($self, %params) = @_;
-
-  my $dbh = $::auth->get_user_dbh($params{login});
-
-  my $query = qq|SELECT * FROM printers ORDER BY printer_description|;
-  my @printers = selectall_hashref_query($::form, $dbh, $query);
-
-  $dbh->disconnect;
-
-  $::lxdebug->leave_sub;
-
-  return wantarray ? @printers : \@printers;
-}
-
-sub get_printer {
-  $::lxdebug->enter_sub;
-
-  my ($self, %params) = @_;
-
-  my $dbh = $::auth->get_user_dbh($params{login});
-
-  my $query = qq|SELECT * FROM printers WHERE id = ?|;
-  my ($printer) = selectfirst_hashref_query($::form, $dbh, $query, $params{id});
-
-  $dbh->disconnect;
-
-  $::lxdebug->leave_sub;
-
-  return $printer;
-}
-
-sub save_printer {
-  $main::lxdebug->enter_sub();
-
-  my ($self, %params) = @_;
-
-  # connect to database
-  my $dbh = $::auth->get_user_dbh($params{login});
-  my $printer = $params{printer};
-
-  unless ($printer->{id}) {
-    ($printer->{id}) = selectfirst_array_query($::form, $dbh, "SELECT nextval('id'::text)");
-    do_query($::form, $dbh, "INSERT INTO printers (id, printer_description) VALUES (?, '')", $printer->{id});
-  }
-
-  my $query = <<SQL;
-    UPDATE printers SET
-      printer_description = ?,
-      template_code = ?,
-      printer_command = ?
-    WHERE id = ?
-SQL
-  do_query($::form, $dbh, $query,
-    $printer->{printer_description},
-    $printer->{template_code},
-    $printer->{printer_command},
-    $printer->{id},
-  );
-
-  $dbh->commit;
-  $dbh->disconnect;
-
-  $::lxdebug->leave_sub;
-}
-
-sub delete_printer {
-  $::lxdebug->enter_sub;
-
-  my ($self, %params) = @_;
-
-  my $dbh = $::auth->get_user_dbh($params{login});
-
-  my $query = qq|DELETE FROM printers WHERE id = ?|;
-  do_query($::form, $dbh, $query, $params{id});
-
-  $dbh->commit;
-  $dbh->disconnect;
-
-  $::lxdebug->leave_sub;
-}
-
-1;
index 9493f0f..9c73d45 100644 (file)
--- a/SL/RC.pm
+++ b/SL/RC.pm
@@ -93,13 +93,11 @@ sub payment_transactions {
   ($form->{beginningbalance}, $form->{category}) =
     selectrow_query($form, $dbh, $query, @values);
 
-  my %oid = ('Pg'     => 'ac.acc_trans_id',
-             'Oracle' => 'ac.rowid');
   @values = ();
   $query =
     qq|SELECT c.name, ac.source, ac.transdate, ac.cleared, | .
     qq|  ac.fx_transaction, ac.amount, a.id, | .
-    qq|  $oid{$myconfig->{dbdriver}} AS oid | .
+    qq|  ac.acc_trans_id AS oid | .
     qq|FROM customer c, acc_trans ac, ar a, chart ch | .
     qq|WHERE c.id = a.customer_id | .
     qq|  AND ac.cleared = '0' | .
@@ -123,7 +121,7 @@ sub payment_transactions {
 
     qq|SELECT v.name, ac.source, ac.transdate, ac.cleared, | .
     qq|  ac.fx_transaction, ac.amount, a.id, | .
-    qq|  $oid{$myconfig->{dbdriver}} AS oid | .
+    qq|  ac.acc_trans_id AS oid | .
     qq|FROM vendor v, acc_trans ac, ap a, chart ch | .
     qq|WHERE v.id = a.vendor_id | .
     qq|  AND ac.cleared = '0' | .
@@ -148,7 +146,7 @@ sub payment_transactions {
 
     qq|SELECT g.description, ac.source, ac.transdate, ac.cleared, | .
     qq|  ac.fx_transaction, ac.amount, g.id, | .
-    qq|  $oid{$myconfig->{dbdriver}} AS oid | .
+    qq|  ac.acc_trans_id AS oid | .
     qq|FROM gl g, acc_trans ac, chart ch | .
     qq|WHERE g.id = ac.trans_id | .
     qq|  AND ac.cleared = '0' | .
@@ -186,22 +184,20 @@ sub reconcile {
   my $dbh = $form->dbconnect($myconfig);
 
   my ($query, $i);
-  my %oid = ('Pg'     => 'acc_trans_id',
-             'Oracle' => 'rowid');
 
   # clear flags
   for $i (1 .. $form->{rowcount}) {
     if ($form->{"cleared_$i"}) {
       $query =
         qq|UPDATE acc_trans SET cleared = '1' | .
-        qq|WHERE $oid{$myconfig->{dbdriver}} = ?|;
+        qq|WHERE acc_trans_id = ?|;
       do_query($form, $dbh, $query, $form->{"oid_$i"});
 
       # clear fx_transaction
       if ($form->{"fxoid_$i"}) {
         $query =
           qq|UPDATE acc_trans SET cleared = '1' | .
-          qq|WHERE $oid{$myconfig->{dbdriver}} = ?|;
+          qq|WHERE acc_trans_id = ?|;
         do_query($form, $dbh, $query, $form->{"fxoid_$i"});
       }
     }
diff --git a/SL/System/InstallationLock.pm b/SL/System/InstallationLock.pm
new file mode 100644 (file)
index 0000000..5020b5e
--- /dev/null
@@ -0,0 +1,106 @@
+package SL::System::InstallationLock;
+
+use strict;
+
+sub lock {
+  my ($class) = @_;
+
+  return 1 if $::lx_office_conf{debug}->{keep_installation_unlocked};
+
+  my $fh;
+  if (!open($fh, ">", $class->lock_file_name)) {
+    die $::locale->text('Lock file handling failed. Please verify that the directory "#1" is writeable by the webserver.', $::lx_office_conf{paths}->{userspath});
+  }
+
+  close $fh;
+
+  return 1;
+}
+
+sub unlock {
+  my ($class) = @_;
+
+  return 1 if $::lx_office_conf{debug}->{keep_installation_unlocked};
+
+  my $name = $class->lock_file_name;
+  if ((-f $name)  && !unlink($name)) {
+    die $::locale->text('Lock file handling failed. Please verify that the directory "#1" is writeable by the webserver.', $::lx_office_conf{paths}->{userspath});
+  }
+
+  return 1;
+}
+
+sub is_locked {
+  my ($class) = @_;
+
+  return 0 if $::lx_office_conf{debug}->{keep_installation_unlocked};
+  return -f $class->lock_file_name;
+}
+
+sub lock_file_name {
+  $::lx_office_conf{paths}->{userspath} . "/nologin";
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::System::InstallationLock - Handle locking the installation with a
+global lock file
+
+=head1 SYNOPSIS
+
+  SL::System::InstallationLock->lock;
+  # Do important and uninterruptable work!
+  SL::System::InstallationLock->unlock;
+
+
+=head1 OVERVIEW
+
+If the global lock file exists then no user may login. The
+administration area is not affected.
+
+There's a configuration setting
+C<debug.keep_installation_unlocked>. If it is trueish then all of
+these commands will always keep the installation unlocked: L</lock>
+and L</unlock> won't do anything and L</is_locked> always returns 0.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<is_locked>
+
+Returns 1 or 0 depending on whether or not the installation is currently locked.
+
+=item C<lock>
+
+Creates the lock file. Throws an exception if writing to the lock file
+location fails.
+
+=item C<lock_file_name>
+
+Returns the file name for the global lock file.
+
+=item C<unlock>
+
+Removed the lock file. Throws an exception if the lock exists and
+removing the lock file fails.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index 291eb9f..d99afa4 100644 (file)
@@ -12,6 +12,8 @@ use File::Temp;
 use List::MoreUtils qw(any);
 use Unicode::Normalize qw();
 
+use SL::DB::Default;
+
 sub new {
   my $type = shift;
 
@@ -516,7 +518,7 @@ sub parse_and_create_pdf {
   $local_form->{IN}        = $template_file_name;
   $local_form->{tmpdir}    = $::lx_office_conf{paths}->{userspath};
   $local_form->{tmpfile}   = $tex_file_name;
-  $local_form->{templates} = $::myconfig{templates};
+  $local_form->{templates} = SL::DB::Default->get->templates;
 
   foreach (keys %params) {
     croak "The parameter '$_' must not be used." if exists $local_form->{$_};
index 871a453..7f4ec1a 100644 (file)
@@ -43,6 +43,7 @@ use SL::DBUpgrade2;
 use SL::DBUtils;
 use SL::Iconv;
 use SL::Inifile;
+use SL::System::InstallationLock;
 
 use strict;
 
@@ -92,95 +93,61 @@ sub country_codes {
 }
 
 sub login {
-  $main::lxdebug->enter_sub();
-
   my ($self, $form) = @_;
-  our $sid;
-
-  local *FH;
-
-  my $rc = -3;
-
-  if ($self->{login}) {
-    my %myconfig = $main::auth->read_user(login => $self->{login});
-
-    # check if database is down
-    my $dbh = SL::DBConnect->connect($myconfig{dbconnect}, $myconfig{dbuser}, $myconfig{dbpasswd}, SL::DBConnect->get_options)
-      or $self->error($DBI::errstr);
-
-    # we got a connection, check the version
-    my $query = qq|SELECT version FROM defaults|;
-    my $sth   = $dbh->prepare($query);
-    $sth->execute || $form->dberror($query);
 
-    my ($dbversion) = $sth->fetchrow_array;
-    $sth->finish;
+  return -3 if !$self->{login} || !$::auth->client;
 
-    $self->create_employee_entry($form, $dbh, \%myconfig);
+  my %myconfig = $main::auth->read_user(login => $self->{login});
 
-    $self->create_schema_info_table($form, $dbh);
+  # check if database is down
+  my $dbh = $form->dbconnect_noauto;
 
-    my $dbupdater_auth = SL::DBUpgrade2->new(form => $form, dbdriver => 'Pg', auth => 1)->parse_dbupdate_controls;
-    if ($dbupdater_auth->unapplied_upgrade_scripts($::auth->dbconnect)) {
-      $::lxdebug->leave_sub;
-      return -3;
-    }
+  # we got a connection, check the version
+  my ($dbversion) = $dbh->selectrow_array(qq|SELECT version FROM defaults|);
 
-    $rc = 0;
+  $self->create_schema_info_table($form, $dbh);
 
-    my $dbupdater = SL::DBUpgrade2->new(form => $form, dbdriver => $myconfig{dbdriver})->parse_dbupdate_controls;
+  # Auth DB upgrades available?
+  my $dbupdater_auth = SL::DBUpgrade2->new(form => $form, auth => 1)->parse_dbupdate_controls;
+  return -3 if $dbupdater_auth->unapplied_upgrade_scripts($::auth->dbconnect);
 
-    map({ $form->{$_} = $myconfig{$_} } qw(dbname dbhost dbport dbdriver dbuser dbpasswd dbconnect dateformat));
-    dbconnect_vars($form, $form->{dbname});
-    my $update_available = $dbupdater->update_available($dbversion) || $dbupdater->update2_available($dbh);
-    $dbh->disconnect;
+  my $dbupdater = SL::DBUpgrade2->new(form => $form)->parse_dbupdate_controls;
 
-    if ($update_available) {
-      $form->{"title"} = $main::locale->text("Dataset upgrade");
-      $form->header(no_layout => $form->{no_layout});
-      print $form->parse_html_template("dbupgrade/header");
-
-      $form->{dbupdate} = "db$myconfig{dbname}";
-      $form->{ $form->{dbupdate} } = 1;
+  my $update_available = $dbupdater->update_available($dbversion) || $dbupdater->update2_available($dbh);
+  $dbh->disconnect;
 
-      if ($form->{"show_dbupdate_warning"}) {
-        print $form->parse_html_template("dbupgrade/warning");
-        ::end_of_request();
-      }
+  return 0 if !$update_available;
 
-      # update the tables
-      if (!$::lx_office_conf{debug}->{keep_installation_unlocked} && !open(FH, ">", $::lx_office_conf{paths}->{userspath} . "/nologin")) {
-        $form->show_generic_error($main::locale->text('A temporary file could not be created. ' .
-                                                      'Please verify that the directory "#1" is writeable by the webserver.',
-                                                      $::lx_office_conf{paths}->{userspath}),
-                                  'back_button' => 1);
-      }
+  $form->{$_} = $::auth->client->{$_} for qw(dbname dbhost dbport dbuser dbpasswd);
+  $form->{$_} = $myconfig{$_}         for qw(datestyle);
 
-      # required for Oracle
-      $form->{dbdefault} = $sid;
+  $form->{"title"} = $main::locale->text("Dataset upgrade");
+  $form->header(no_layout => $form->{no_layout});
+  print $form->parse_html_template("dbupgrade/header");
 
-      # ignore HUP, QUIT in case the webserver times out
-      $SIG{HUP}  = 'IGNORE';
-      $SIG{QUIT} = 'IGNORE';
+  $form->{dbupdate} = "db" . $::auth->client->{dbname};
 
-      $self->dbupdate($form);
-      $self->dbupdate2($form, $dbupdater);
-      SL::DBUpgrade2->new(form => $::form, dbdriver => 'Pg', auth => 1)->apply_admin_dbupgrade_scripts(0);
+  if ($form->{"show_dbupdate_warning"}) {
+    print $form->parse_html_template("dbupgrade/warning");
+    ::end_of_request();
+  }
 
-      close(FH);
+  # update the tables
+  SL::System::InstallationLock->lock;
 
-      # remove lock file
-      unlink($::lx_office_conf{paths}->{userspath} . "/nologin");
+  # ignore HUP, QUIT in case the webserver times out
+  $SIG{HUP}  = 'IGNORE';
+  $SIG{QUIT} = 'IGNORE';
 
-      print $form->parse_html_template("dbupgrade/footer");
+  $self->dbupdate($form);
+  $self->dbupdate2(form => $form, updater => $dbupdater, database => $::auth->client->{dbname});
+  SL::DBUpgrade2->new(form => $::form, auth => 1)->apply_admin_dbupgrade_scripts(0);
 
-      $rc = -2;
-    }
-  }
+  SL::System::InstallationLock->unlock;
 
-  $main::lxdebug->leave_sub();
+  print $form->parse_html_template("dbupgrade/footer");
 
-  return $rc;
+  return -2;
 }
 
 sub dbconnect_vars {
@@ -189,48 +156,17 @@ sub dbconnect_vars {
   my ($form, $db) = @_;
 
   my %dboptions = (
-        'Pg' => { 'yy-mm-dd'   => 'set DateStyle to \'ISO\'',
-                  'yyyy-mm-dd' => 'set DateStyle to \'ISO\'',
-                  'mm/dd/yy'   => 'set DateStyle to \'SQL, US\'',
-                  'dd/mm/yy'   => 'set DateStyle to \'SQL, EUROPEAN\'',
-                  'dd.mm.yy'   => 'set DateStyle to \'GERMAN\''
-        },
-        'Oracle' => {
-          'yy-mm-dd'   => 'ALTER SESSION SET NLS_DATE_FORMAT = \'YY-MM-DD\'',
-          'yyyy-mm-dd' => 'ALTER SESSION SET NLS_DATE_FORMAT = \'YYYY-MM-DD\'',
-          'mm/dd/yy'   => 'ALTER SESSION SET NLS_DATE_FORMAT = \'MM/DD/YY\'',
-          'dd/mm/yy'   => 'ALTER SESSION SET NLS_DATE_FORMAT = \'DD/MM/YY\'',
-          'dd.mm.yy'   => 'ALTER SESSION SET NLS_DATE_FORMAT = \'DD.MM.YY\'',
-        });
-
-  $form->{dboptions} = $dboptions{ $form->{dbdriver} }{ $form->{dateformat} };
-
-  if ($form->{dbdriver} eq 'Pg') {
-    $form->{dbconnect} = "dbi:Pg:dbname=$db";
-  }
-
-  if ($form->{dbdriver} eq 'Oracle') {
-    $form->{dbconnect} = "dbi:Oracle:sid=$form->{sid}";
-  }
-
-  if ($form->{dbhost}) {
-    $form->{dbconnect} .= ";host=$form->{dbhost}";
-  }
-  if ($form->{dbport}) {
-    $form->{dbconnect} .= ";port=$form->{dbport}";
-  }
-
-  $main::lxdebug->leave_sub();
-}
-
-sub dbdrivers {
-  $main::lxdebug->enter_sub();
+    'yy-mm-dd'   => 'set DateStyle to \'ISO\'',
+    'yyyy-mm-dd' => 'set DateStyle to \'ISO\'',
+    'mm/dd/yy'   => 'set DateStyle to \'SQL, US\'',
+    'dd/mm/yy'   => 'set DateStyle to \'SQL, EUROPEAN\'',
+    'dd.mm.yy'   => 'set DateStyle to \'GERMAN\''
+  );
 
-  my @drivers = DBI->available_drivers();
+  $form->{dboptions} = $dboptions{ $form->{dateformat} };
+  $form->{dbconnect} = "dbi:Pg:dbname=${db};host=" . ($form->{dbhost} || 'localhost') . ";port=" . ($form->{dbport} || 5432);
 
   $main::lxdebug->leave_sub();
-
-  return (grep { /(Pg|Oracle)/ } @drivers);
 }
 
 sub dbsources {
@@ -242,62 +178,42 @@ sub dbsources {
   my ($sth, $query);
 
   $form->{dbdefault} = $form->{dbuser} unless $form->{dbdefault};
-  $form->{sid} = $form->{dbdefault};
   &dbconnect_vars($form, $form->{dbdefault});
 
   my $dbh = SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options)
     or $form->dberror;
 
-  if ($form->{dbdriver} eq 'Pg') {
-    $query =
-      qq|SELECT datname FROM pg_database | .
-      qq|WHERE NOT datname IN ('template0', 'template1')|;
-    $sth = $dbh->prepare($query);
-    $sth->execute() || $form->dberror($query);
-
-    while (my ($db) = $sth->fetchrow_array) {
-
-      if ($form->{only_acc_db}) {
-
-        next if ($db =~ /^template/);
-
-        &dbconnect_vars($form, $db);
-        my $dbh = SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options)
-          or $form->dberror;
-
-        $query =
-          qq|SELECT tablename FROM pg_tables | .
-          qq|WHERE (tablename = 'defaults') AND (tableowner = ?)|;
-        my $sth = $dbh->prepare($query);
-        $sth->execute($form->{dbuser}) ||
-          $form->dberror($query . " ($form->{dbuser})");
-
-        if ($sth->fetchrow_array) {
-          push(@dbsources, $db);
-        }
-        $sth->finish;
-        $dbh->disconnect;
-        next;
-      }
-      push(@dbsources, $db);
-    }
-  }
+  $query =
+    qq|SELECT datname FROM pg_database | .
+    qq|WHERE NOT datname IN ('template0', 'template1')|;
+  $sth = $dbh->prepare($query);
+  $sth->execute() || $form->dberror($query);
+
+  while (my ($db) = $sth->fetchrow_array) {
 
-  if ($form->{dbdriver} eq 'Oracle') {
     if ($form->{only_acc_db}) {
-      $query =
-        qq|SELECT owner FROM dba_objects | .
-        qq|WHERE object_name = 'DEFAULTS' AND object_type = 'TABLE'|;
-    } else {
-      $query = qq|SELECT username FROM dba_users|;
-    }
 
-    $sth = $dbh->prepare($query);
-    $sth->execute || $form->dberror($query);
+      next if ($db =~ /^template/);
+
+      &dbconnect_vars($form, $db);
+      my $dbh = SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options)
+        or $form->dberror;
 
-    while (my ($db) = $sth->fetchrow_array) {
-      push(@dbsources, $db);
+      $query =
+        qq|SELECT tablename FROM pg_tables | .
+        qq|WHERE (tablename = 'defaults') AND (tableowner = ?)|;
+      my $sth = $dbh->prepare($query);
+      $sth->execute($form->{dbuser}) ||
+        $form->dberror($query . " ($form->{dbuser})");
+
+      if ($sth->fetchrow_array) {
+        push(@dbsources, $db);
+      }
+      $sth->finish;
+      $dbh->disconnect;
+      next;
     }
+    push(@dbsources, $db);
   }
 
   $sth->finish;
@@ -332,49 +248,29 @@ sub dbcreate {
 
   my ($self, $form) = @_;
 
-  $form->{sid} = $form->{dbdefault};
   &dbconnect_vars($form, $form->{dbdefault});
   my $dbh =
     SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options)
     or $form->dberror;
   $form->{db} =~ s/\"//g;
-  my %dbcreate = (
-    'Pg'     => qq|CREATE DATABASE "$form->{db}"|,
-    'Oracle' =>
-    qq|CREATE USER "$form->{db}" DEFAULT TABLESPACE USERS | .
-    qq|TEMPORARY TABLESPACE TEMP IDENTIFIED BY "$form->{db}"|
-  );
 
-  my %dboptions = (
-    'Pg' => [],
-  );
+  my @dboptions;
 
-  push(@{$dboptions{"Pg"}}, "ENCODING = " . $dbh->quote($form->{"encoding"}))
-    if ($form->{"encoding"});
+  push @dboptions, "ENCODING = " . $dbh->quote($form->{"encoding"}) if $form->{"encoding"};
   if ($form->{"dbdefault"}) {
     my $dbdefault = $form->{"dbdefault"};
     $dbdefault =~ s/[^a-zA-Z0-9_\-]//g;
-    push(@{$dboptions{"Pg"}}, "TEMPLATE = $dbdefault");
+    push @dboptions, "TEMPLATE = $dbdefault";
   }
 
-  my $query = $dbcreate{$form->{dbdriver}};
-  $query .= " WITH " . join(" ", @{$dboptions{"Pg"}}) if (@{$dboptions{"Pg"}});
+  my $query = qq|CREATE DATABASE "$form->{db}"|;
+  $query   .= " WITH " . join(" ", @dboptions) if @dboptions;
 
   # Ignore errors if the database exists.
   $dbh->do($query);
 
-  if ($form->{dbdriver} eq 'Oracle') {
-    $query = qq|GRANT CONNECT, RESOURCE TO "$form->{db}"|;
-    do_query($form, $dbh, $query);
-  }
   $dbh->disconnect;
 
-  # setup variables for the new database
-  if ($form->{dbdriver} eq 'Oracle') {
-    $form->{dbuser}   = $form->{db};
-    $form->{dbpasswd} = $form->{db};
-  }
-
   &dbconnect_vars($form, $form->{db});
 
   $dbh = SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options)
@@ -383,7 +279,7 @@ sub dbcreate {
   my $db_charset = $Common::db_encoding_to_charset{$form->{encoding}};
   $db_charset ||= Common::DEFAULT_CHARSET;
 
-  my $dbupdater = SL::DBUpgrade2->new(form => $form, dbdriver => $form->{dbdriver});
+  my $dbupdater = SL::DBUpgrade2->new(form => $form);
   # create the tables
   $dbupdater->process_query($dbh, "sql/lx-office.sql", undef, $db_charset);
 
@@ -411,14 +307,11 @@ sub dbdelete {
 
   my ($self, $form) = @_;
   $form->{db} =~ s/\"//g;
-  my %dbdelete = ('Pg'     => qq|DROP DATABASE "$form->{db}"|,
-                  'Oracle' => qq|DROP USER "$form->{db}" CASCADE|);
 
-  $form->{sid} = $form->{dbdefault};
   &dbconnect_vars($form, $form->{dbdefault});
   my $dbh = SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options)
     or $form->dberror;
-  my $query = $dbdelete{$form->{dbdriver}};
+  my $query = qq|DROP DATABASE "$form->{db}"|;
   do_query($form, $dbh, $query);
 
   $dbh->disconnect;
@@ -452,7 +345,7 @@ sub dbneedsupdate {
   my ($self, $form) = @_;
 
   my %members   = $main::auth->read_all_users();
-  my $dbupdater = SL::DBUpgrade2->new(form => $form, dbdriver => $form->{dbdriver})->parse_dbupdate_controls;
+  my $dbupdater = SL::DBUpgrade2->new(form => $form)->parse_dbupdate_controls;
 
   my ($query, $sth, %dbs_needing_updates);
 
@@ -561,8 +454,6 @@ sub dbupdate {
 
   local *SQLDIR;
 
-  $form->{sid} = $form->{dbdefault};
-
   my @upgradescripts = ();
   my $query;
   my $rc = -2;
@@ -570,11 +461,11 @@ sub dbupdate {
   if ($form->{dbupdate}) {
 
     # read update scripts into memory
-    opendir(SQLDIR, "sql/" . $form->{dbdriver} . "-upgrade")
-      or &error("", "sql/" . $form->{dbdriver} . "-upgrade : $!");
+    opendir(SQLDIR, "sql/Pg-upgrade")
+      or &error("", "sql/Pg-upgrade : $!");
     @upgradescripts =
       sort(cmp_script_version
-           grep(/$form->{dbdriver}-upgrade-.*?\.(sql|pl)$/,
+           grep(/Pg-upgrade-.*?\.(sql|pl)$/,
                 readdir(SQLDIR)));
     closedir(SQLDIR);
   }
@@ -582,7 +473,7 @@ sub dbupdate {
   my $db_charset = $::lx_office_conf{system}->{dbcharset};
   $db_charset ||= Common::DEFAULT_CHARSET;
 
-  my $dbupdater = SL::DBUpgrade2->new(form => $form, dbdriver => $form->{dbdriver});
+  my $dbupdater = SL::DBUpgrade2->new(form => $form);
 
   foreach my $db (split(/ /, $form->{dbupdate})) {
 
@@ -607,7 +498,7 @@ sub dbupdate {
 
     foreach my $upgradescript (@upgradescripts) {
       my $a = $upgradescript;
-      $a =~ s/^\Q$form->{dbdriver}\E-upgrade-|\.(sql|pl)$//g;
+      $a =~ s/^Pg-upgrade-|\.(sql|pl)$//g;
 
       my ($mindb, $maxdb) = split /-/, $a;
       my $str_maxdb = $maxdb;
@@ -621,7 +512,7 @@ sub dbupdate {
 
       # apply upgrade
       $main::lxdebug->message(LXDebug->DEBUG2(), "Applying Update $upgradescript");
-      $dbupdater->process_file($dbh, "sql/" . $form->{"dbdriver"} . "-upgrade/$upgradescript", $str_maxdb, $db_charset);
+      $dbupdater->process_file($dbh, "sql/Pg-upgrade/$upgradescript", $str_maxdb, $db_charset);
 
       $version = $maxdb;
 
@@ -640,144 +531,42 @@ sub dbupdate {
 sub dbupdate2 {
   $main::lxdebug->enter_sub();
 
-  my ($self, $form, $dbupdater) = @_;
-
-  $form->{sid} = $form->{dbdefault};
+  my ($self, %params) = @_;
 
-  my $rc         = -2;
-  my $db_charset = $::lx_office_conf{system}->{dbcharset} || Common::DEFAULT_CHARSET;
+  my $form            = $params{form};
+  my $dbupdater       = $params{updater};
+  my $db              = $params{database};
+  my $rc              = -2;
+  my $db_charset      = $::lx_office_conf{system}->{dbcharset} || Common::DEFAULT_CHARSET;
 
   map { $_->{description} = SL::Iconv::convert($_->{charset}, $db_charset, $_->{description}) } values %{ $dbupdater->{all_controls} };
 
-  foreach my $db (split / /, $form->{dbupdate}) {
-    next unless $form->{$db};
+  &dbconnect_vars($form, $db);
 
-    # strip db from dataset
-    $db =~ s/^db//;
-    &dbconnect_vars($form, $db);
+  my $dbh = SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options) or $form->dberror;
 
-    my $dbh = SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options) or $form->dberror;
+  $dbh->do($form->{dboptions}) if ($form->{dboptions});
 
-    $dbh->do($form->{dboptions}) if ($form->{dboptions});
-
-    $self->create_schema_info_table($form, $dbh);
-
-    my @upgradescripts = $dbupdater->unapplied_upgrade_scripts($dbh);
-
-    $dbh->disconnect and next if !@upgradescripts;
-
-    foreach my $control (@upgradescripts) {
-      # apply upgrade
-      $main::lxdebug->message(LXDebug->DEBUG2(), "Applying Update $control->{file}");
-      print $form->parse_html_template("dbupgrade/upgrade_message2", $control);
-
-      $dbupdater->process_file($dbh, "sql/" . $form->{"dbdriver"} . "-upgrade2/$control->{file}", $control, $db_charset);
-    }
-
-    $rc = 0;
-    $dbh->disconnect;
-
-  }
-
-  $main::lxdebug->leave_sub();
-
-  return $rc;
-}
-
-sub save_member {
-  $main::lxdebug->enter_sub();
-
-  my ($self) = @_;
-
-  # format dbconnect and dboptions string
-  dbconnect_vars($self, $self->{dbname});
-
-  map { $self->{$_} =~ s/\r//g; } qw(address signature);
-
-  $main::auth->save_user($self->{login}, map { $_, $self->{$_} } config_vars());
-
-  my $dbh = SL::DBConnect->connect($self->{dbconnect}, $self->{dbuser}, $self->{dbpasswd}, SL::DBConnect->get_options);
-  if ($dbh) {
-    $self->create_employee_entry($::form, $dbh, $self, 1);
-    $dbh->disconnect();
-  }
-
-  $main::lxdebug->leave_sub();
-}
+  $self->create_schema_info_table($form, $dbh);
 
-sub create_employee_entry {
-  $main::lxdebug->enter_sub();
+  my @upgradescripts = $dbupdater->unapplied_upgrade_scripts($dbh);
 
-  my $self            = shift;
-  my $form            = shift;
-  my $dbh             = shift;
-  my $myconfig        = shift;
-  my $update_existing = shift;
+  $dbh->disconnect and next if !@upgradescripts;
 
-  if (!does_table_exist($dbh, 'employee')) {
-    $main::lxdebug->leave_sub();
-    return;
-  }
+  foreach my $control (@upgradescripts) {
+    # apply upgrade
+    $main::lxdebug->message(LXDebug->DEBUG2(), "Applying Update $control->{file}");
+    print $form->parse_html_template("dbupgrade/upgrade_message2", $control);
 
-  # add login to employee table if it does not exist
-  # no error check for employee table, ignore if it does not exist
-  my ($id)         = selectrow_query($form, $dbh, qq|SELECT id FROM employee WHERE login = ?|, $self->{login});
-  my ($good_db)    = selectrow_query($form, $dbh, qq|select * from pg_tables where tablename = ? and schemaname = ?|, 'schema_info', 'public');
-  my  $can_delete;
-     ($can_delete) = selectrow_query($form, $dbh, qq|SELECT tag FROM schema_info WHERE tag = ?|, 'employee_deleted') if $good_db;
-
-  if (!$id) {
-    my $query = qq|INSERT INTO employee (login, name, workphone, role) VALUES (?, ?, ?, ?)|;
-    do_query($form, $dbh, $query, ($self->{login}, $myconfig->{name}, $myconfig->{tel}, "user"));
-
-  } elsif ($update_existing && $can_delete) {
-    my $query = qq|UPDATE employee SET name = ?, workphone = ?, role = 'user', deleted = 'f' WHERE id = ?|;
-    do_query($form, $dbh, $query, $myconfig->{name}, $myconfig->{tel}, $id);
+    $dbupdater->process_file($dbh, "sql/Pg-upgrade2/$control->{file}", $control, $db_charset);
   }
 
-  $main::lxdebug->leave_sub();
-}
-
-sub config_vars {
-  $main::lxdebug->enter_sub();
-
-  my @conf = qw(address admin businessnumber company countrycode
-    currency dateformat dbconnect dbdriver dbhost dbport dboptions
-    dbname dbuser dbpasswd email fax name numberformat password
-    printer sid signature stylesheet tel templates vclimit angebote
-    bestellungen rechnungen anfragen lieferantenbestellungen einkaufsrechnungen
-    taxnumber co_ustid duns menustyle template_format default_media
-    default_printer_id copies show_form_details favorites
-    pdonumber sdonumber hide_cvar_search_options mandatory_departments
-    sepa_creditor_id taxincluded_checked);
+  $rc = 0;
+  $dbh->disconnect;
 
   $main::lxdebug->leave_sub();
 
-  return @conf;
-}
-
-sub error {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $msg) = @_;
-
-  $main::lxdebug->show_backtrace();
-
-  if ($ENV{HTTP_USER_AGENT}) {
-    print qq|Content-Type: text/html
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
-
-<body bgcolor=ffffff>
-
-<h2><font color=red>Error!</font></h2>
-<p><b>$msg</b>|;
-
-  }
-
-  die "Error: $msg\n";
-
-  $main::lxdebug->leave_sub();
+  return $rc;
 }
 
 sub data {
index 464c6de..5f3e0b1 100755 (executable)
@@ -47,6 +47,8 @@ use Sys::Hostname;
 
 use SL::Auth;
 use SL::Auth::PasswordPolicy;
+use SL::DB::AuthClient;
+use SL::DB::AuthUser;
 use SL::Form;
 use SL::Iconv;
 use SL::Mailer;
@@ -58,8 +60,6 @@ use SL::DBUtils;
 use SL::Template;
 
 require "bin/mozilla/common.pl";
-require "bin/mozilla/admin_groups.pl";
-require "bin/mozilla/admin_printer.pl";
 
 use strict;
 
@@ -75,15 +75,6 @@ our $form;
 our $locale;
 our $auth;
 
-my @valid_dateformats = qw(mm/dd/yy dd/mm/yy dd.mm.yy yyyy-mm-dd);
-my @valid_numberformats = ('1,000.00', '1000.00', '1.000,00', '1000,00');
-my @all_stylesheets = qw(lx-office-erp.css Mobile.css kivitendo.css);
-my @all_menustyles = (
-  { id => 'old', title => $::locale->text('Old (on the side)') },
-  { id => 'v3',  title => $::locale->text('Top (CSS)') },
-  { id => 'neu', title => $::locale->text('Top (Javascript)') },
-);
-
 sub run {
   $::lxdebug->enter_sub;
   my $session_result = shift;
@@ -99,395 +90,29 @@ sub run {
   if ($form->{action}) {
     if ($auth->authenticate_root($form->{'{AUTH}admin_password'}) != $auth->OK()) {
       $auth->punish_wrong_login;
-      $form->{error} = $locale->text('Incorrect Password!');
+      $form->{error} = $locale->text('Incorrect password!');
       $auth->delete_session_value('admin_password');
       adminlogin();
     } else {
       if ($auth->session_tables_present()) {
         delete $::form->{'{AUTH}admin_password'};
-        _apply_dbupgrade_scripts();
       }
 
       call_sub($locale->findsub($form->{action}));
     }
   } else {
-    # if there are no drivers bail out
-    $form->error($locale->text('No Database Drivers available!'))
-      unless (User->dbdrivers);
-
     adminlogin();
   }
   $::lxdebug->leave_sub;
 }
 
 sub adminlogin {
-  my $form   = $main::form;
-  my $locale = $main::locale;
-
-  $form->{title} = qq|kivitendo $form->{version} | . $locale->text('Administration');
-
-  $form->header();
-  print $form->parse_html_template('admin/adminlogin');
-}
-
-sub login {
-  check_auth_db_and_tables();
-  list_users();
-}
-
-sub logout {
-  $main::auth->destroy_session();
-  adminlogin();
-}
-
-sub check_auth_db_and_tables {
-  my $form   = $main::form;
-  my $locale = $main::locale;
-
-  my %params;
-
-  map { $params{"db_${_}"} = $main::auth->{DB_config}->{$_} } keys %{ $auth->{DB_config} };
-
-  $params{admin_password} = $::lx_office_conf{authentication}->{admin_password};
-
-  if (!$main::auth->check_database()) {
-    $form->{title} = $locale->text('Authentification database creation');
-    $form->header();
-    print $form->parse_html_template('admin/check_auth_database', \%params);
-
-    ::end_of_request();
-  }
-
-  if (!$main::auth->check_tables()) {
-    $form->{title} = $locale->text('Authentification tables creation');
-    $form->header();
-    print $form->parse_html_template('admin/check_auth_tables', \%params);
-
-    ::end_of_request();
-  }
-}
-
-sub create_auth_db {
-  my $form = $main::form;
-
-  $main::auth->create_database('superuser'          => $form->{db_superuser},
-                               'superuser_password' => $form->{db_superuser_password},
-                               'template'           => $form->{db_template});
-  login();
-}
-
-sub create_auth_tables {
-  my $form   = $main::form;
-  my $locale = $main::locale;
-
-  $main::auth->create_tables();
-  $main::auth->set_session_value('admin_password', $form->{'{AUTH}admin_password'});
-  $main::auth->create_or_refresh_session();
-
-  my $memberfile = $::lx_office_conf{paths}->{memberfile};
-  if (!-f $memberfile) {
-    # New installation -- create a standard group with full access
-    my %members;
-    my $group = {
-      'name'        => $locale->text('Full Access'),
-      'description' => $locale->text('Full access to all functions'),
-      'rights'      => { map { $_ => 1 } SL::Auth::all_rights() },
-      'members'     => [ map { $_->{id} } values %members ],
-    };
-
-    $main::auth->save_group($group);
-  }
-
-  _apply_dbupgrade_scripts();
-  login();
-}
-
-sub list_users {
-  my $form    = $main::form;
-  my $locale  = $main::locale;
-
-  my %members = $main::auth->read_all_users();
-
-  delete $members{"root login"};
-
-  for (values %members) {
-    $_->{templates} =~ s|.*/||;
-    $_->{login_url} =  $::locale->is_utf8 ? Encode::encode('utf-8-strict', $_->{login}) : $_->{login_url};
-  }
-
-  $form->{title}   = "kivitendo " . $locale->text('Administration');
-  $form->{LOCKED}  = -e _nologin_file_name();
-  $form->{MEMBERS} = [ @members{sort { lc $a cmp lc $b } keys %members} ];
-
-  $form->header();
-  print $form->parse_html_template("admin/list_users");
-}
-
-sub add_user {
-  $::form->{title}   = "kivitendo " . $::locale->text('Administration') . " / " . $::locale->text('Add User');
-
-  # User does not have a well behaved new constructor, so we'll just have to build one ourself
-  my $user     = bless {
-    "vclimit"      => 200,
-    "countrycode"  => "de",
-    "numberformat" => "1.000,00",
-    "dateformat"   => "dd.mm.yy",
-    "stylesheet"   => "kivitendo.css",
-    "menustyle"    => "neu",
-    dbport         => $::auth->{DB_config}->{port} || 5432,
-    dbuser         => $::auth->{DB_config}->{user} || 'lxoffice',
-    dbhost         => $::auth->{DB_config}->{host} || 'localhost',
-  }, 'User';
-
-  edit_user_form($user);
-}
-
-sub edit_user {
-  $::form->{title} = "kivitendo " . $::locale->text('Administration') . " / " . $::locale->text('Edit User');
-  $::form->{edit}  = 1;
-
-  # get user
-  my $user = User->new(id => $::form->{user}{id});
-
-  # strip basedir from templates directory
-  $user->{templates} =~ s|.*/||;
-
-  edit_user_form($user);
-}
-
-sub edit_user_form {
-  my ($user) = @_;
-
-  my %cc = $user->country_codes;
-  my @all_countrycodes = map { id => $_, title => $cc{$_} }, sort { $cc{$a} cmp $cc{$b} } keys %cc;
-  my ($all_dir, $all_master) = _search_templates();
-  my $groups = [];
-
-  if ($::form->{edit}) {
-    my $user_id    = $::auth->get_user_id($user->{login});
-    my $all_groups = $::auth->read_groups();
-
-    for my $group (values %{ $all_groups }) {
-      push @{ $groups }, $group if (grep { $user_id == $_ } @{ $group->{members} });
-    }
-
-    $groups = [ sort { lc $a->{name} cmp lc $b->{name} } @{ $groups } ];
-  }
-
-  $::form->header;
-  print $::form->parse_html_template("admin/edit_user", {
-    GROUPS               => $groups,
-    CAN_CHANGE_PASSWORD  => $::auth->can_change_password,
-    user                 => $user->data,
-    all_stylesheets      => \@all_stylesheets,
-    all_numberformats    => \@valid_numberformats,
-    all_dateformats      => \@valid_dateformats,
-    all_countrycodes     => \@all_countrycodes,
-    all_menustyles       => \@all_menustyles,
-    all_templates        => $all_dir,
-    all_master_templates => $all_master,
-  });
-}
-
-sub save_user {
-  my $form          = $main::form;
-  my $locale        = $main::locale;
-
-  my $user = $form->{user};
-
-  $user->{dbdriver} = 'Pg';
-
-  if (!$::form->{edit}) {
-    # no spaces allowed in login name
-    $user->{login} =~ s/\s//g;
-    $::form->show_generic_error($::locale->text('Login name missing!')) unless $user->{login};
-
-    # check for duplicates
-    my %members = $::auth->read_all_users;
-    if ($members{$user->{login}}) {
-      $::form->show_generic_error($locale->text('Another user with the login #1 does already exist.', $user->{login}), 'back_button' => 1);
-    }
-  }
-
-  # no spaces allowed in directories
-  ($::form->{newtemplates}) = split / /, $::form->{newtemplates};
-  $user->{templates} = $::form->{newtemplates} || $::form->{usetemplates} || $user->{login};
-
-  # is there a basedir
-  if (!-d $::lx_office_conf{paths}->{templates}) {
-    $::form->error(sprintf($::locale->text("The directory %s does not exist."), $::lx_office_conf{paths}->{templates}));
-  }
-
-  # add base directory to $form->{templates}
-  $user->{templates} =~ s|.*/||;
-  $user->{templates} =  $::lx_office_conf{paths}->{templates} . "/$user->{templates}";
-
-  my $myconfig = new User(id => $user->{id});
-
-  $::form->show_generic_error($::locale->text('Dataset missing!'))       unless $user->{dbname};
-  $::form->show_generic_error($::locale->text('Database User missing!')) unless $user->{dbuser};
-
-  foreach my $item (keys %{$user}) {
-    $myconfig->{$item} = $user->{$item};
-  }
-
-  $myconfig->save_member;
-
-  $user->{templates}       =~ s|.*/||;
-  $user->{templates}       =  $::lx_office_conf{paths}->{templates} . "/$user->{templates}";
-  $::form->{mastertemplates} =~ s|.*/||;
-
-  # create user template directory and copy master files
-  if (!-d "$user->{templates}") {
-    umask(002);
-
-    if (mkdir "$user->{templates}", oct("771")) {
-
-      umask(007);
-
-      # copy templates to the directory
-
-      my $oldcurrdir = getcwd();
-      if (!chdir("$::lx_office_conf{paths}->{templates}/print/$::form->{mastertemplates}")) {
-        $form->error("$ERRNO: chdir $::lx_office_conf{paths}->{templates}/print/$::form->{mastertemplates}");
-      }
-
-      my $newdir = File::Spec->catdir($oldcurrdir, $user->{templates});
-
-      find(
-        sub
-        {
-          next if ($_ eq ".");
-
-          if (-d $_) {
-            if (!mkdir (File::Spec->catdir($newdir, $File::Find::name))) {
-              chdir($oldcurrdir);
-              $form->error("$ERRNO: mkdir $File::Find::name");
-            }
-          } elsif (-l $_) {
-            if (!symlink (readlink($_),
-                          File::Spec->catfile($newdir, $File::Find::name))) {
-              chdir($oldcurrdir);
-              $form->error("$ERRNO: symlink $File::Find::name");
-            }
-          } elsif (-f $_) {
-            if (!copy($_, File::Spec->catfile($newdir, $File::Find::name))) {
-              chdir($oldcurrdir);
-              $form->error("$ERRNO: cp $File::Find::name");
-            }
-          }
-        }, "./");
-
-      chdir($oldcurrdir);
-
-    } else {
-      $form->error("$ERRNO: $user->{templates}");
-    }
-  }
-
-  # Add new user to his groups.
-  if (ref $form->{new_user_group_ids} eq 'ARRAY') {
-    my $all_groups = $main::auth->read_groups();
-    my %user       = $main::auth->read_user(login => $myconfig->{login});
-
-    foreach my $group_id (@{ $form->{new_user_group_ids} }) {
-      my $group = $all_groups->{$group_id};
-
-      next if !$group;
-
-      push @{ $group->{members} }, $user{id};
-      $main::auth->save_group($group);
-    }
-  }
-
-  if ($main::auth->can_change_password()
-      && defined $::form->{new_password}
-      && ($::form->{new_password} ne '********')) {
-    my $verifier = SL::Auth::PasswordPolicy->new;
-    my $result   = $verifier->verify($::form->{new_password}, 1);
-
-    if ($result != SL::Auth::PasswordPolicy->OK()) {
-      $form->error($::locale->text('The settings were saved, but the password was not changed.') . ' ' . join(' ', $verifier->errors($result)));
-    }
-
-    $main::auth->change_password($myconfig->{login}, $::form->{new_password});
-  }
-
-  $::form->redirect($::locale->text('User saved!'));
-}
-
-sub save_user_as_new {
-  my $form       = $main::form;
-
-  $form->{user}{login} = $::form->{new_user_login};
-  delete $form->{user}{id};
-  delete @{$form}{qw(id edit new_user_login)};
-
-  save_user();
-}
-
-sub delete_user {
-  my $form      = $main::form;
-  my $locale    = $main::locale;
-
-  my $user = $::form->{user} || {};
-
-  $::form->show_generic_error($::locale->text('Missing user id!')) unless $user->{id};
-
-  my $loaded_user = User->new(id => $user->{id});
-
-  my %members   = $main::auth->read_all_users();
-  my $templates = $members{$loaded_user->{login}}->{templates};
-
-  $main::auth->delete_user($loaded_user->{login});
-
-  if ($templates) {
-    my $templates_in_use = 0;
-
-    foreach my $login (keys %members) {
-      next if $loaded_user->{login} eq $login;
-      next if $members{$login}->{templates} ne $templates;
-      $templates_in_use = 1;
-      last;
-    }
-
-    if (!$templates_in_use && -d $templates) {
-      unlink <$templates/*>;
-      rmdir $templates;
-    }
-  }
-
-  $form->redirect($locale->text('User deleted!'));
-
-}
-
-sub login_name {
-  my $login = shift;
-
-  $login =~ s/\[\]//g;
-  return ($login) ? $login : undef;
-
-}
-
-sub get_value {
-  my $line           = shift;
-  my ($null, $value) = split(/=/, $line, 2);
-
-  # remove comments
-  $value =~ s/\s#.*//g;
-
-  # remove any trailing whitespace
-  $value =~ s/^\s*(.*?)\s*$/$1/;
-
-  $value;
+  print $::request->cgi->redirect('controller.pl?action=Admin/login');
 }
 
 sub pg_database_administration {
   my $form = $main::form;
-
-  $form->{dbdriver} = 'Pg';
   dbselect_source();
-
 }
 
 sub dbselect_source {
@@ -508,25 +133,6 @@ sub dbselect_source {
   print $form->parse_html_template("admin/dbadmin");
 }
 
-sub test_db_connection {
-  my $form   = $main::form;
-  my $locale = $main::locale;
-
-  $form->{dbdriver} = 'Pg';
-  User::dbconnect_vars($form, $form->{dbname});
-
-  my $dbh = DBI->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd});
-
-  $form->{connection_ok} = $dbh ? 1 : 0;
-  $form->{errstr}        = $DBI::errstr;
-
-  $dbh->disconnect() if ($dbh);
-
-  $form->{title} = $locale->text('Database Connection Test');
-  $form->header();
-  print $form->parse_html_template("admin/test_db_connection");
-}
-
 sub continue {
   call_sub($main::form->{"nextsub"});
 }
@@ -566,15 +172,12 @@ sub dbupdate {
     restore_form($saved_form);
 
     %::myconfig = ();
-    map { $form->{$_} = $::myconfig{$_} = $form->{"${_}_${i}"} } qw(dbname dbdriver dbhost dbport dbuser dbpasswd);
+    map { $form->{$_} = $::myconfig{$_} = $form->{"${_}_${i}"} } qw(dbname dbhost dbport dbuser dbpasswd);
 
     print $form->parse_html_template("admin/dbupgrade_header");
 
-    $form->{dbupdate}        = $form->{dbname};
-    $form->{$form->{dbname}} = 1;
-
     User->dbupdate($form);
-    User->dbupdate2($form, SL::DBUpgrade2->new(form => $form, dbdriver => $form->{dbdriver})->parse_dbupdate_controls);
+    User->dbupdate2(form => $form, updater => SL::DBUpgrade2->new(form => $form)->parse_dbupdate_controls, database => $form->{dbname});
 
     print $form->parse_html_template("admin/dbupgrade_footer");
   }
@@ -923,101 +526,4 @@ sub restore_dataset_start {
   rmdir $tmpdir;
 }
 
-sub unlock_system {
-  my $form   = $main::form;
-  my $locale = $main::locale;
-
-  unlink _nologin_file_name();;
-
-  $form->{callback} = "admin.pl?action=list_users";
-
-  $form->redirect($locale->text('Lockfile removed!'));
-
-}
-
-sub lock_system {
-  my $form   = $main::form;
-  my $locale = $main::locale;
-
-  open(FH, ">", _nologin_file_name())
-    or $form->error($locale->text('Cannot create Lock!'));
-  close(FH);
-
-  $form->{callback} = "admin.pl?action=list_users";
-
-  $form->redirect($locale->text('Lockfile created!'));
-
-}
-
-sub yes {
-  call_sub($main::form->{yes_nextsub});
-}
-
-sub no {
-  call_sub($main::form->{no_nextsub});
-}
-
-sub add {
-  call_sub($main::form->{add_nextsub});
-}
-
-sub edit {
-  my $form = $main::form;
-
-  $form->{edit_nextsub} ||= 'edit_user';
-
-  call_sub($form->{edit_nextsub});
-}
-
-sub delete {
-  my $form     = $main::form;
-
-  $form->{delete_nextsub} ||= 'delete_user';
-
-  call_sub($form->{delete_nextsub});
-}
-
-sub save {
-  my $form = $main::form;
-
-  $form->{save_nextsub} ||= 'save_user';
-
-  call_sub($form->{save_nextsub});
-}
-
-sub back {
-  call_sub($main::form->{back_nextsub});
-}
-
-sub dispatcher {
-  my $form   = $main::form;
-  my $locale = $main::locale;
-
-  foreach my $action (qw(create_standard_group dont_create_standard_group
-                         save_user delete_user save_user_as_new)) {
-    if ($form->{"action_${action}"}) {
-      call_sub($action);
-      return;
-    }
-  }
-
-  call_sub($form->{default_action}) if ($form->{default_action});
-
-  $form->error($locale->text('No action defined.'));
-}
-
-sub _apply_dbupgrade_scripts {
-  ::end_of_request() if SL::DBUpgrade2->new(form => $::form, dbdriver => 'Pg', auth => 1)->apply_admin_dbupgrade_scripts(1);
-}
-
-sub _nologin_file_name {
-  return $::lx_office_conf{paths}->{userspath} . '/nologin';
-}
-
-sub _search_templates {
-  my %templates = SL::Template->available_templates;
-
-  return ($templates{print_templates}, $templates{master_templates});
-}
-
 1;
diff --git a/bin/mozilla/admin_groups.pl b/bin/mozilla/admin_groups.pl
deleted file mode 100644 (file)
index 1444332..0000000
+++ /dev/null
@@ -1,244 +0,0 @@
-#=====================================================================
-# LX-Office ERP
-# Copyright (C) 2004
-# Based on SQL-Ledger Version 2.1.9
-# Web http://www.lx-office.org
-#
-#=====================================================================
-# SQL-Ledger Accounting
-# Copyright (c) 2002
-#
-#  Author: Moritz Bunkus
-#   Email: mbunkus@linet-services.de
-#     Web: www.linet-services.de
-#
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-#======================================================================
-#
-# group administration module
-# add/edit/delete user groups
-#
-#======================================================================
-
-use List::MoreUtils qw(uniq);
-
-use strict;
-
-sub edit_groups {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-
-  my @groups = sort { lc $a->{name} cmp lc $b->{name} } values %{ $main::auth->read_groups() };
-
-  $form->header();
-  print $form->parse_html_template("admin/edit_groups", { 'GROUPS'     => \@groups,
-                                                          'num_groups' => scalar @groups });
-
-  $main::lxdebug->leave_sub();
-}
-
-sub add_group {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  delete $form->{group_id};
-  $form->{message} = $locale->text("The group has been added.");
-
-  save_group();
-
-  $main::lxdebug->leave_sub();
-}
-
-sub save_group {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  $form->isblank('name', $locale->text('The group name is missing.'));
-
-  my $groups = $main::auth->read_groups();
-  my %users  = map { ( $_->{id} => 1 ) } values %{ { $::auth->read_all_users() } };
-
-  foreach my $group (values %{$groups}) {
-    if (($form->{group_id} != $group->{id})
-        && ($form->{name} eq $group->{name})) {
-      $form->show_generic_error($locale->text("A group with that name does already exist."));
-    }
-  }
-
-  my $group;
-
-  if ($form->{group_id} && $groups->{$form->{group_id}}) {
-    $group = $groups->{$form->{group_id}};
-
-  } else {
-    $group = { };
-  }
-
-  $group->{name}        = $form->{name};
-  $group->{description} = $form->{description};
-  $group->{rights}      = { map { ( $_ => $form->{"${_}_granted"} ? 1 : 0 ) } SL::Auth::all_rights() };
-  $group->{members}     = [ grep { $users{$_} } @{ $form->{user_ids} || [] } ];
-
-  my $is_new = !$form->{group_id};
-
-  $main::auth->save_group($group);
-
-  $form->{message} ||= $locale->text('The group has been saved.');
-
-  if ($is_new) {
-    edit_groups();
-
-  } else {
-    edit_group();
-  }
-
-  $main::lxdebug->leave_sub();
-}
-
-sub edit_group {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  my $groups = $main::auth->read_groups();
-
-  if (!$form->{group_id} || !$groups->{$form->{group_id}}) {
-    $form->show_generic_error($locale->text("No group has been selected, or the group does not exist anymore."));
-  }
-
-  my $group     = $groups->{$form->{group_id}};
-  my %all_users = $main::auth->read_all_users();
-  my @rights    = map {
-    { "right"       => $_->[0],
-      "description" => $_->[1],
-      "is_section"  => '--' eq substr($_->[0], 0, 2),
-      "granted"     => defined $group->{rights}->{$_->[0]} ? $group->{rights}->{$_->[0]} : 0,
-    }
-  } SL::Auth::all_rights_full();
-
-  $form->header();
-  print $form->parse_html_template("admin/edit_group", { ALL_USERS            => [ values %all_users ],
-                                                         USER_IDS_IN_GROUP    => $group->{members},
-                                                         "RIGHTS"             => \@rights,
-                                                         "name"               => $group->{name},
-                                                         "description"        => $group->{description} });
-
-  $main::lxdebug->leave_sub();
-}
-
-sub delete_group {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  my $groups = $main::auth->read_groups();
-
-  if (!$form->{group_id} || !$groups->{$form->{group_id}}) {
-    $form->show_generic_error($locale->text("No group has been selected, or the group does not exist anymore."));
-  }
-
-  if ($form->{confirmed}) {
-    $main::auth->delete_group($form->{"group_id"});
-
-    $form->{message} = $locale->text("The group has been deleted.");
-    edit_groups();
-
-  } else {
-
-    $form->header();
-    print $form->parse_html_template("admin/delete_group_confirm", $groups->{$form->{group_id}});
-  }
-
-  $main::lxdebug->leave_sub();
-}
-
-sub edit_group_membership {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  my %users  = $main::auth->read_all_users();
-  my $groups = $main::auth->read_groups();
-  $groups    = [ sort { lc $a->{name} cmp lc $b->{name} } values %{ $groups } ];
-
-  my @headings = map { { 'title' => $_ } } map { $_->{name} } @{ $groups };
-
-  foreach my $group (@{ $groups }) {
-    $group->{members_h} = { map { $_ => 1 } @{ $group->{members} } };
-  }
-
-  my @rows;
-
-  foreach my $user (sort { lc $a->{login} cmp lc $b->{login} } values %users) {
-    my $row = {
-      'id'              => $user->{id},
-      'login'           => $user->{login},
-      'name'            => $user->{name},
-      'repeat_headings' => (scalar(@rows) % 20) == 0,
-      'GROUPS'          => [],
-    };
-
-    foreach my $group (@{ $groups }) {
-      push @{ $row->{GROUPS} }, {
-        'id'        => $group->{id},
-        'is_member' => $group->{members_h}->{$user->{id}},
-      };
-    }
-
-    push @rows, $row;
-  }
-
-  $form->{title} = $locale->text('Edit group membership');
-  $form->header();
-  print $form->parse_html_template('admin/edit_group_membership', { 'HEADINGS' => \@headings, 'USERS' => \@rows });
-
-  $main::lxdebug->leave_sub();
-}
-
-sub save_group_membership {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  my %users  = $main::auth->read_all_users();
-  my $groups = $main::auth->read_groups();
-
-  foreach my $group (values %{ $groups }) {
-    $group->{members} = [ ];
-
-    foreach my $user (values %users) {
-      push @{ $group->{members} }, $user->{id} if ($form->{"u_$user->{id}_g_$group->{id}"});
-    }
-
-    $main::auth->save_group($group);
-  }
-
-  $form->{message} = $locale->text('The group memberships have been saved.');
-
-  edit_groups();
-
-  $main::lxdebug->leave_sub();
-}
-
-1;
diff --git a/bin/mozilla/admin_printer.pl b/bin/mozilla/admin_printer.pl
deleted file mode 100644 (file)
index e397868..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-use strict;
-
-use SL::Printer;
-
-sub get_login {
-  unless ($::form->{login}) {
-    get_login_form();
-    ::end_of_request();
-  }
-  return $::form->{login};
-}
-
-sub get_login_form {
-  my %users = $::auth->read_all_users;
-
-  $::form->header;
-  print $::form->parse_html_template('admin_printer/login', {
-    users => [ values %users ],
-  });
-}
-
-sub printer_dispatcher {
-  for my $action (qw(get_login_form list_printers add_printer edit_printer save_printer delete_printer list_users)) {
-    if ($::form->{$action}) {
-      ::call_sub($::locale->findsub($action));
-      ::end_of_request()
-    }
-  }
-  die "cannot find sub";
-}
-
-sub printer_management {
-  &get_login_form;
-}
-
-sub add_printer {
-  $::lxdebug->enter_sub;
-
-  my $login   = get_login();
-  my %users   = $::auth->read_all_users;
-
-  $::form->header;
-  print $::form->parse_html_template('admin_printer/edit', {
-    title   => $::locale->text("Add Printer"),
-    printer => { },
-    users   => [ values %users ],
-  });
-
-  $::lxdebug->leave_sub
-}
-
-sub edit_printer {
-  $::lxdebug->enter_sub;
-
-  my $login = get_login();
-  my $id    = $::form->{id} or $::form->{printer}{id} or &add_printer;
-  my %users   = $::auth->read_all_users;
-
-  my $printer = SL::Printer->get_printer(id => $id, login => $login);
-
-  $::form->header;
-  print $::form->parse_html_template('admin_printer/edit', {
-    title   => $::locale->text("Edit Printer"),
-    printer => $printer,
-    users   => [ values %users ],
-  });
-
-  $::lxdebug->leave_sub;
-}
-
-sub list_printers {
-  $::lxdebug->enter_sub;
-
-  my $login    = get_login();
-  my $printers = SL::Printer->all_printers(login => $login);
-  my %users   = $::auth->read_all_users;
-
-  $::form->header;
-  print $::form->parse_html_template('admin_printer/list', {
-    title        => $::locale->text('Printer'),
-    all_printers => $printers,
-    edit_link    => build_std_url("login=$login", 'action=edit_printer', 'id='),
-    users        => [ values %users ],
-  });
-
-  $::lxdebug->leave_sub;
-}
-
-
-sub save_printer {
-  $::lxdebug->enter_sub;
-
-  my $login   = get_login();
-  my $printer = $::form->{printer} || die 'no printer to save';
-
-  $::form->error($::locale->text('Description missing!'))     unless $printer->{printer_description};
-  $::form->error($::locale->text('Printer Command missing!')) unless $printer->{printer_command};
-
-  SL::Printer->save_printer(%$::form);
-
-  list_printers();
-  $::lxdebug->leave_sub;
-}
-
-sub delete_printer {
-  $::lxdebug->enter_sub;
-
-  my $login   = get_login();
-  my $printer = $::form->{printer} || die 'no printer to delete';
-
-  SL::Printer->delete_printer(%$::form);
-  list_printers();
-
-  $::lxdebug->leave_sub;
-}
-
-1;
index ebe7ed1..ad6248f 100644 (file)
@@ -42,7 +42,7 @@ use SL::User;
 use SL::USTVA;
 use SL::Iconv;
 use SL::TODO;
-use SL::Printer;
+use SL::DB::Printer;
 use CGI;
 
 require "bin/mozilla/common.pl";
@@ -935,60 +935,6 @@ sub swap_buchungsgruppen {
   $main::lxdebug->leave_sub();
 }
 
-sub edit_defaults {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  # get defaults for account numbers and last numbers
-  AM->defaultaccounts(\%myconfig, \%$form);
-  $form->{ALL_UNITS} = AM->convertible_units(AM->retrieve_all_units(), 'g');
-
-  map { $form->{"defaults_${_}"} = $form->{defaults}->{$_} } keys %{ $form->{defaults} };
-
-  # default language
-  my $all_languages = SL::DB::Manager::Language->get_all;
-
-# cash = IST-Versteuerung, accrual = SOLL-Versteuerung
-
-  foreach my $key (keys %{ $form->{IC} }) {
-    foreach my $accno (sort keys %{ $form->{IC}->{$key} }) {
-      my $array = "ACCNOS_" . uc($key);
-      $form->{$array} ||= [];
-
-      my $value = "${accno}--" . $form->{IC}->{$key}->{$accno}->{description};
-      push @{ $form->{$array} }, {
-        'name'     => $value,
-        'value'    => $value,
-        'selected' => $form->{IC}->{$key}->{$accno}->{id} == $form->{defaults}->{$key},
-      };
-    }
-  }
-
-  $form->{title} = $locale->text('Ranges of numbers and default accounts');
-
-  $form->header();
-  print $form->parse_html_template('am/edit_defaults',
-                                   { ALL_LANGUAGES => $all_languages, });
-
-  $main::lxdebug->leave_sub();
-}
-
-sub save_defaults {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  AM->save_defaults();
-
-  $form->redirect($locale->text('Defaults saved.'));
-
-  $main::lxdebug->leave_sub();
-}
-
 sub _build_cfg_options {
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -1059,7 +1005,7 @@ sub config {
     { 'name' => $locale->text('Queue'),   'value' => 'queue',   'selected' => $selected{queue}, },
     ];
 
-  $form->{PRINTERS} = [ SL::Printer->all_printers(%::myconfig) ];
+  $form->{PRINTERS} = SL::DB::Manager::Printer->get_all_sorted;
 
   my %countrycodes = User->country_codes;
 
@@ -1104,7 +1050,7 @@ sub save_preferences {
 
   TODO->save_user_config('login' => $form->{login}, %{ $form->{todo_cfg} || { } });
 
-  if (AM->save_preferences(\%myconfig, $form)) {
+  if (AM->save_preferences($form)) {
     if ($::auth->can_change_password()
         && defined $form->{new_password}
         && ($form->{new_password} ne '********')) {
@@ -1116,10 +1062,6 @@ sub save_preferences {
       }
 
       $::auth->change_password($form->{login}, $form->{new_password});
-
-      $form->{password} = $form->{new_password};
-      $::auth->set_session_value('password', $form->{password});
-      $::auth->create_or_refresh_session();
     }
 
     $form->redirect($locale->text('Preferences saved!'));
index 9f21e22..6d7e399 100644 (file)
@@ -33,6 +33,7 @@
 
 use File::Find;
 
+use SL::DB::Default;
 use SL::AM;
 use SL::Form;
 
@@ -115,6 +116,9 @@ sub display_template_form {
 
   $main::auth->assert('admin');
 
+  my $defaults = SL::DB::Default->get;
+  $form->error($::locale->text('No print templates have been created for this client yet. Please do so in the client configuration.')) if !$defaults->templates;
+
   if ($form->{"formname"} =~ m|\.\.| || $form->{"formname"} =~ m|^/|) {
     $form->{"formname"} =~ s|.*/||;
   }
@@ -205,13 +209,13 @@ sub display_template_form {
 
           my $fname = $File::Find::name;
           # remove template dir from name
-          $fname =~ s|^$myconfig{templates}/||;
+          $fname =~ s|^templates/[^/+]/||;
           # remove .tex from name
           $fname =~ s|.tex$||;
 
           push(@all_files, $fname);
 
-          }, $myconfig{templates});
+          }, $defaults->templates);
 
       # filter all files already set up (i.e. not already in @values)
       my @other_files = grep { my $a=$_; not grep {$a eq $_->{value}} @values } @all_files;
index da3bfb0..c2fd6a2 100644 (file)
@@ -35,6 +35,7 @@
 use POSIX qw(strftime);
 
 use SL::CA;
+use SL::DB::Default;
 use SL::ReportGenerator;
 
 require "bin/mozilla/reportgenerator.pl";
@@ -186,6 +187,7 @@ sub list_transactions {
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
+  my $defaults = SL::DB::Default->get;
 
   $main::auth->assert('report');
 
@@ -331,7 +333,7 @@ sub list_transactions {
   $form->{print_date} = $locale->text('Create Date') . " " . $locale->date(\%myconfig, $form->current_date(\%myconfig), 0);
   push (@options, $form->{print_date});
 
-  $form->{company} = $locale->text('Company') . " " . $myconfig{company};
+  $form->{company} = $locale->text('Company') . " " . $defaults->company;
   push (@options, $form->{company});
 
   my @columns     = qw(transdate reference description gegenkonto debit credit ustkonto ustrate balance);
index 99b61f3..e67bbbb 100644 (file)
@@ -47,6 +47,7 @@ use SL::CT;
 use SL::IC;
 use SL::IO;
 
+use SL::DB::Default;
 use SL::DB::Language;
 use SL::DB::Printer;
 use SL::Helper::Flash;
@@ -1224,6 +1225,10 @@ sub print_form {
 
   _check_io_auth();
 
+  my $defaults = SL::DB::Default->get;
+  $form->error($::locale->text('No print templates have been created for this client yet. Please do so in the client configuration.')) if !$defaults->templates;
+  $form->{templates} = $defaults->templates;
+
   my ($old_form) = @_;
 
   my $inv       = "inv";
@@ -1438,8 +1443,8 @@ sub print_form {
   if ($shipto) {
     if (   $form->{formname} eq 'purchase_order'
         || $form->{formname} eq 'request_quotation') {
-      $form->{shiptoname}   = $myconfig{company};
-      $form->{shiptostreet} = $myconfig{address};
+      $form->{shiptoname}   = $defaults->company;
+      $form->{shiptostreet} = $defaults->address;
     } else {
       map { $form->{"shipto$_"} = $form->{$_} } @a;
     }
@@ -1447,8 +1452,6 @@ sub print_form {
 
   $form->{notes} =~ s/^\s+//g;
 
-  $form->{templates} = "$myconfig{templates}";
-
   delete $form->{printer_command};
 
   $form->{language} = $form->get_template_language(\%myconfig);
@@ -1528,7 +1531,7 @@ sub print_form {
   push @template_files, "$form->{formname}.$extension";
   push @template_files, "default.$extension";
   @template_files = uniq @template_files;
-  $form->{IN}     = first { -f "$myconfig{templates}/$_" } @template_files;
+  $form->{IN}     = first { -f ($defaults->templates . "/$_") } @template_files;
 
   if (!defined $form->{IN}) {
     $::form->error($::locale->text('Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.', join ', ', map { "'$_'"} @template_files));
index 7971c2d..25379dd 100644 (file)
@@ -27,9 +27,7 @@
 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 #######################################################################
 
-use DBI;
-use SL::Auth;
-use SL::User;
+use SL::DB::Default;
 use SL::Form;
 use SL::Git;
 
@@ -51,6 +49,8 @@ sub company_logo {
   $form->{stylesheet} =  $myconfig{stylesheet};
   $form->{title}      =  $::locale->text('kivitendo');
   $form->{interface}  = $::dispatcher->interface_type;
+  $form->{client}     = $::auth->client;
+  $form->{defaults}   = SL::DB::Default->get;
 
   my $git             = SL::Git->new;
   ($form->{git_head}) = $git->get_log(since => 'HEAD~1', until => 'HEAD') if $git->is_git_installation;
index d455d5a..8062dad 100644 (file)
@@ -37,6 +37,7 @@
 
 use POSIX qw(strftime);
 
+use SL::DB::Default;
 use SL::DB::Project;
 use SL::PE;
 use SL::RP;
@@ -220,6 +221,10 @@ sub generate_income_statement {
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
+  my $defaults = SL::DB::Default->get;
+  $form->error($::locale->text('No print templates have been created for this client yet. Please do so in the client configuration.')) if !$defaults->templates;
+  $form->{templates} = $defaults->templates;
+
   $form->{padding} = "&nbsp;&nbsp;";
   $form->{bold}    = "<b>";
   $form->{endbold} = "</b>";
@@ -370,12 +375,6 @@ sub generate_income_statement {
       . qq| $longcomparetodate|;
   }
 
-  # setup variables for the form
-  my @a = qw(company address businessnumber);
-  map { $form->{$_} = $myconfig{$_} } @a;
-
-  $form->{templates} = $myconfig{templates};
-
   $form->{IN} = "income_statement.html";
 
   $form->parse_template;
@@ -387,6 +386,9 @@ sub generate_balance_sheet {
   $::lxdebug->enter_sub;
   $::auth->assert('report');
 
+  my $defaults = SL::DB::Default->get;
+  $::form->error($::locale->text('No print templates have been created for this client yet. Please do so in the client configuration.')) if !$defaults->templates;
+  $::form->{templates}     = $defaults->templates;
   $::form->{decimalplaces} = $::form->{decimalplaces} * 1 || 2;
   $::form->{padding}       = "&nbsp;&nbsp;";
   $::form->{bold}          = "<b>";
@@ -409,11 +411,6 @@ sub generate_balance_sheet {
 
 #  $::form->{IN} = "balance_sheet.html";
 
-  # setup company variables for the form
-  map { $::form->{$_} = $::myconfig{$_} } qw(company address businessnumber nativecurr);
-
-  $::form->{templates} = $::myconfig{templates};
-
   $::form->header;
   print $::form->parse_html_template('rp/balance_sheet', $data);
 
@@ -455,6 +452,7 @@ sub generate_trial_balance {
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
+  my $defaults = SL::DB::Default->get;
 
   if ($form->{reporttype} eq "custom") {
 
@@ -616,7 +614,7 @@ sub generate_trial_balance {
   $form->{print_date} = $locale->text('Create Date') . " " . $locale->date(\%myconfig, $form->current_date(\%myconfig), 0);
   push (@options, $form->{print_date});
 
-  $form->{company} = $locale->text('Company') . " " . $myconfig{company};
+  $form->{company} = $locale->text('Company') . " " . $defaults->company;
   push (@options, $form->{company});
 
 
@@ -1195,9 +1193,11 @@ sub print_form {
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
-  $form->{statementdate} = $locale->date(\%myconfig, $form->{todate}, 1);
+  my $defaults = SL::DB::Default->get;
+  $form->error($::locale->text('No print templates have been created for this client yet. Please do so in the client configuration.')) if !$defaults->templates;
+  $form->{templates} = $defaults->templates;
 
-  $form->{templates} = "$myconfig{templates}";
+  $form->{statementdate} = $locale->date(\%myconfig, $form->{todate}, 1);
 
   my $suffix = "html";
   my $attachment_suffix = "html";
@@ -1631,6 +1631,10 @@ sub generate_bwa {
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
+  my $defaults = SL::DB::Default->get;
+  $form->error($::locale->text('No print templates have been created for this client yet. Please do so in the client configuration.')) if !$defaults->templates;
+  $form->{templates} = $defaults->templates;
+
   $form->{padding} = "&nbsp;&nbsp;";
   $form->{bold}    = "<b>";
   $form->{endbold} = "</b>";
@@ -1818,11 +1822,6 @@ sub generate_bwa {
       . qq| $longtodate|;
   }
 
-  # setup variables for the form
-  my @a = qw(company address businessnumber);
-  map { $form->{$_} = $myconfig{$_} } @a;
-  $form->{templates} = $myconfig{templates};
-
   $form->{IN} = "bwa.html";
 
   $form->parse_template;
index 18f48b2..edb6eda 100755 (executable)
@@ -452,13 +452,14 @@ sub bank_transfer_download_sepa_xml {
   my $locale   =  $main::locale;
   my $cgi      =  $::request->{cgi};
   my $vc       = $form->{vc} eq 'customer' ? 'customer' : 'vendor';
+  my $defaults = SL::DB::Default->get;
 
-  if (!$myconfig->{company}) {
-    $form->show_generic_error($locale->text('You have to enter a company name in your user preferences (see the "Program" menu, "Preferences").'), 'back_button' => 1);
+  if (!$defaults->company) {
+    $form->show_generic_error($locale->text('You have to enter a company name in the client configuration.'), 'back_button' => 1);
   }
 
-  if (($vc eq 'customer') && !$myconfig->{sepa_creditor_id}) {
-    $form->show_generic_error($locale->text('You have to enter the SEPA creditor ID in your user preferences (see the "Program" menu, "Preferences").'), 'back_button' => 1);
+  if (($vc eq 'customer') && !$defaults->sepa_creditor_id) {
+    $form->show_generic_error($locale->text('You have to enter the SEPA creditor ID in the client configuration.'), 'back_button' => 1);
   }
 
   my @ids;
@@ -486,8 +487,8 @@ sub bank_transfer_download_sepa_xml {
 
   my $message_id = strftime('MSG%Y%m%d%H%M%S', localtime) . sprintf('%06d', $$);
 
-  my $sepa_xml   = SL::SEPA::XML->new('company'     => $myconfig->{company},
-                                      'creditor_id' => $myconfig->{sepa_creditor_id},
+  my $sepa_xml   = SL::SEPA::XML->new('company'     => $defaults->company,
+                                      'creditor_id' => $defaults->sepa_creditor_id,
                                       'src_charset' => $::lx_office_conf{system}->{dbcharset} || 'ISO-8859-15',
                                       'message_id'  => $message_id,
                                       'grouped'     => 1,
index 8d77a31..06258d6 100644 (file)
@@ -38,6 +38,7 @@ require "bin/mozilla/common.pl";
 
 use List::Util qw(first);
 
+use SL::DB::Default;
 use SL::PE;
 use SL::RP;
 use SL::USTVA;
@@ -83,6 +84,7 @@ sub report {
 
   $::auth->assert('advance_turnover_tax_return');
 
+  my $defaults   = SL::DB::Default->get;
   $form->{title} = $locale->text('UStVA');
   $form->{kz10}  = '';                       #Berichtigte Anmeldung? Ja =1 Nein=0
 
@@ -100,19 +102,20 @@ sub report {
   # Hier Einlesen der user-config
   # steuernummer entfernt für prerelease
   my @a = qw(
-    signature      name          company       address        businessnumber
+    signature      name
     tel            fax           email         co_chief       co_department
     co_custom1     co_custom2    co_custom3    co_custom4     co_custom5
     co_name1       co_name2      co_street     co_street1     co_zip
     co_city        co_city1      co_country    co_tel         co_tel1
     co_tel2        co_fax        co_fax1       co_email       co_email1
-    co_url         co_url1       ustid         duns           co_bankname
+    co_url         co_url1       co_bankname
     co_bankname1   co_bankname2  co_bankname3  co_blz         co_blz1
     co_blz2        co_blz3       co_accountnr  co_accountnr1  co_accountnr2
     co_accountnr3
   );
 
-  map { $form->{$_} = $myconfig{$_} } @a;
+  $form->{$_} = $myconfig{$_} for @a;
+  $form->{$_} = $defaults->$_ for qw(company address co_ustid duns);
 
   my $openings = $form->{FA_Oeffnungszeiten};
   $openings =~ s/\\\\n/<br>/g;
@@ -199,7 +202,7 @@ sub report {
     company_given    => $company_given,
     address_given    => $address_given,
     taxnumber_given  => $taxnumber_given,
-    taxnumber        => $myconfig{taxnumber},
+    taxnumber        => $defaults->taxnumber,
     select_year      => $select_year,
     period_local     => $period_local,
     method_local     => $method_local,
@@ -225,14 +228,12 @@ sub help {
   $::auth->assert('advance_turnover_tax_return');
 
   # parse help documents under doc
-  my $tmp = $::form->{templates};
   $::form->{templates} = 'doc';
   $::form->{help}      = 'ustva';
   $::form->{type}      = 'help';
   $::form->{format}    = 'html';
   generate_ustva();
 
-  #$form->{templates} = $tmp;
   $::lxdebug->leave_sub();
 }
 
@@ -543,6 +544,10 @@ sub generate_ustva {
 
   $::auth->assert('advance_turnover_tax_return');
 
+  my $defaults = SL::DB::Default->get;
+  $form->error($::locale->text('No print templates have been created for this client yet. Please do so in the client configuration.')) if !$defaults->templates;
+  $form->{templates} = $defaults->templates;
+
   # Aufruf von get_config zum Einlesen der Finanzamtdaten aus finanzamt.ini
 
   my $ustva = USTVA->new();
@@ -738,14 +743,15 @@ sub generate_ustva {
     $locale->date(\%myconfig, $form->current_date(\%myconfig), 0, 0, 0);
 
   # setup variables for the form
-  my @a = qw(company businessnumber tel fax email
+  my @a = qw(tel fax email
     co_chief co_department co_custom1 co_custom2 co_custom3 co_custom4 co_custom5
     co_name1 co_name2  co_street co_street1 co_zip co_city co_city1 co_country co_tel co_tel1 co_tel2
-    co_fax co_fax1 co_email co_email1 co_url co_url1 ustid duns
+    co_fax co_fax1 co_email co_email1 co_url co_url1
     co_bankname co_bankname1 co_bankname2 co_bankname3 co_blz co_blz1
     co_blz2 co_blz3 co_accountnr co_accountnr1 co_accountnr2 co_accountnr3);
 
-  map { $form->{$_} = $myconfig{$_} } @a;
+  $form->{$_} = $myconfig{$_} for @a;
+  $form->{$_} = $defaults->$_ for qw(company address co_ustid duns);
 
   if ($form->{address} ne '') {
     my $temp = $form->{address};
@@ -1060,7 +1066,6 @@ sub generate_ustva {
       . '!');
   }
 
-  $form->{templates} = $myconfig{templates};
   $form->{templates} = "doc" if ( $form->{type} eq 'help' );
 
   if ($form->{format} eq 'generic'){
@@ -1068,7 +1073,7 @@ sub generate_ustva {
     $form->header();
 
     my $template_ref = {
-        taxnumber => $myconfig{taxnumber},
+        taxnumber => $defaults->taxnumber,
     };
 
     print($form->parse_html_template('ustva/generic_taxreport', $template_ref));
index 382e565..0352b43 100644 (file)
@@ -166,7 +166,11 @@ openofficeorg_daemon = 1
 openofficeorg_daemon_port = 2002
 
 [task_server]
-# User name to use for database access
+# kivitendo client (either its name or its database ID) for database
+# access (both 'client' and 'login' are required)
+client =
+# kivitendo user (login) name to use for certain jobs (both 'client'
+# and 'login' are required)
 login =
 # Set to 1 for debug messages in /tmp/kivitendo-debug.log
 debug = 0
@@ -208,7 +212,10 @@ email_subject  = kivitendo self test report
 email_template = templates/mail/self_test/status_mail.txt
 
 [console]
-# autologin to use if none is given
+# Automatic login will only work if both "client" and "login" are
+# given.  "client" can be a client's database ID or its name. "login"
+# is simply a user's login name.
+client =
 login =
 
 # autorun lines will be executed after autologin.
@@ -226,9 +233,19 @@ history_file = users/console_history
 log_file = /tmp/kivitendo_console_debug.log
 
 [testing]
+# Automatic login will only work if both "client" and "login" are
+# given.  "client" can be a client's database ID or its name. "login"
+# is simply a user's login name.
+client =
+login =
+
+[devel]
+# Several settings related to the development of kivitendo.
 
-# autologin to use if none is given
-login = myxplace_standard
+# "client" is used by several scripts (e.g. rose_auto_create_model.pl)
+# when they need access to the database. It can be either a client's
+# database ID or its name.
+client =
 
 [debug]
 # Use DBIx::Log4perl for logging DBI calls. The string LXDEBUGFILE
index 3653c52..c2ef521 100644 (file)
@@ -110,6 +110,7 @@ body.login {
 table.login {
        background-color: #FFFFE0;
        padding: 20px;
+  width: 500px;
 }
 td.login {
        text-align: center;
@@ -370,3 +371,8 @@ label {
   color: #ccc;
   font-style: italic;
 }
+
+.link_separator {
+  margin-left: 6px;
+  margin-right: 6px;
+}
index ced4d98..53352fc 100644 (file)
@@ -1,3 +1,4 @@
+/* multiselect2side plugin */
 .ms2side__div {
        clear: left;
        width: 100%;
@@ -51,6 +52,6 @@
 }
 
 .ms2side__div select {
-       width: 220px;
+       width: 400px;
        float: left;
 }
index 799cbab..27a3aa2 100644 (file)
@@ -423,3 +423,8 @@ label {
   color: #aaa;
   font-style: italic;
 }
+
+.link_separator {
+  margin-left: 6px;
+  margin-right: 6px;
+}
index 1bc3a76..b2af8a3 100644 (file)
@@ -5,6 +5,44 @@ Wichtige Hinweise zum Upgrade von Ã¤lteren Versionen
 ** BITTE FERTIGEN SIE VOR DEM UPGRADE EIN BACKUP IHRER DATENBANK(EN) AN! **
 
 
+Upgrade auf v3.x.0
+==================
+
+* Neue Abhängigkeiten
+
+  * File::Copy::Recursive
+
+  Wie immer bitte vor dem ersten Aufrufen einmal die Pakete Ã¼berprüfen:
+
+  $ scripts/installation_check.pl -ro
+
+* Perl v5.10.1 oder neuer wird zwingend vorausgesetzt.
+
+* Einführung von Mandanten. Früher war die Konfiguration der
+  Datenbanken für jeden Benutzer getrennt vorzunehmen. Mit diesem
+  Release wurden Mandanten eingeführt: ein Mandant bekommt einen Namen
+  sowie die Datenbankkonfiguration, und Benutzer bekommen
+  Zugriffsrechte auf einen oder mehrere Mandanten.
+
+  Um dieses Update durchzuführen, müssen Sie sich einmal im
+  Administrationsbereich anmelden. Vorher ist kein Login im
+  Benutzerbereich möglich.
+
+  Die neue Struktur bedingt, dass viele Scripte, die bisher zur
+  Konfiguration nur einen Benutzer verlangt haben, jetzt auch einen
+  Mandanten verlangen. Die Konfiguration dieser Scripte ist deshalb
+  manuell anzupassen. Dazu gehören:
+
+  - der Task-Server (config/kivitendo.conf)
+  - CSV-Import von der Shell aus (scripts/csv-import-from-shell.sh)
+
+  Die folgenden Scripte sind ebenfalls betroffen, allerdings nur für
+  Entwickler interessant:
+
+  - scripts/dbupgrade2_tool.pl
+  - scripts/rose_auto_create_model.pl
+
+
 Upgrade auf v3.0.0
 ==================
 
index c89aa6d..b87d519 100644 (file)
 
           <listitem><para><literal>FCGI</literal> (nicht Versionen 0.68 bis 0.71 inklusive; siehe <xref linkend="Apache-Konfiguration.FCGI.WebserverUndPlugin"/>)</para></listitem>
 
+          <listitem><para><literal>File::Copy::Recursive</literal></para></listitem>
+
           <listitem><para><literal>JSON</literal></para></listitem>
 
           <listitem><para><literal>List::MoreUtils</literal></para></listitem>
   librose-db-perl librose-object-perl libsort-naturally-perl \
   libstring-shellquote-perl libtemplate-perl libtext-csv-xs-perl \
   libtext-iconv-perl liburi-perl libxml-writer-perl libyaml-perl \
-  postgresql</programlisting>
+  libfile-copy-recursive-perl postgresql</programlisting>
         </sect3>
 
         <sect3>
 
           <programlisting>yum install httpd perl-Archive-Zip perl-Clone perl-DBD-Pg \
   perl-DBI perl-DateTime perl-Email-Address perl-Email-MIME perl-FCGI \
-  perl-JSON perl-List-MoreUtils perl-Net-SMTP-SSL perl-Net-SSLGlue \
+  perl-File-Copy-Recursive perl-JSON perl-List-MoreUtils perl-Net-SMTP-SSL perl-Net-SSLGlue \
   perl-PDF-API2 perl-Params-Validate perl-Rose-DB perl-Rose-DB-Object \
   perl-Rose-Object perl-Sort-Naturally perl-String-ShellQuote \
   perl-Template-Toolkit perl-Text-CSV_XS perl-Text-Iconv perl-URI \
@@ -242,7 +244,7 @@ cpan Config::Std</programlisting>
 
           <programlisting>zypper install apache2 perl-Archive-Zip perl-Clone \
   perl-Config-Std perl-DBD-Pg perl-DBI perl-DateTime perl-Email-Address \
-  perl-Email-MIME perl-FastCGI perl-JSON perl-List-MoreUtils \
+  perl-Email-MIME perl-FastCGI perl-File-Copy-Recursive perl-JSON perl-List-MoreUtils \
   perl-Net-SMTP-SSL perl-Net-SSLGlue perl-PDF-API2 perl-Params-Validate \
   perl-Sort-Naturally perl-Template-Toolkit perl-Text-CSV_XS perl-Text-Iconv \
   perl-URI perl-XML-Writer perl-YAML postgresql-server</programlisting>
index d2594a2..8e3e5a1 100644 (file)
@@ -162,25 +162,26 @@ function focus_by_name(name){
 
 function open_jqm_window(params) {
   params = params || { };
-  var url = params.url;
-  var id  = params.id ? params.id : 'jqm_popup_dialog';
-
-  if (params.data) {
-    var data  = typeof params.data === "string" ? params.data : $.param(params.data);
-    url      += (/\?/.exec(url) ? "&" : "?") + data;
-  }
+  var id = params.id ? params.id : 'jqm_popup_dialog';
 
   $('#' + id).remove();
   var div     = $('<div id="' + id + '" class="jqmWindow jqModal_overlay ' + (params.class || '') + '"></div>').hide().appendTo('body');
   var close   = $('<div class="close"></div>').appendTo(div);
   var content = $('<div class="overlay_content"></div>').appendTo(div);
+
   div.jqm({ modal: true });
   div.jqmShow();
-  $.ajax({ url: url, success: function(new_html) { $(content).html(new_html); } });
   $(close).click(function() {
     div.jqmClose();
   });
 
+  $.ajax({
+    url:     params.url,
+    data:    params.data,
+    type:    params.type,
+    success: function(new_html) { $(content).html(new_html); }
+  });
+
   return true;
 }
 
index c775a07..f073083 100755 (executable)
@@ -42,12 +42,10 @@ $self->{texts} = {
   '<b>What</b> do you want to look for?' => '<b>Wonach</b> wollen Sie suchen?',
   'A Buchungsgruppe consists of a descriptive name and the account numbers for the income and expense accounts for those four tax zones as well as the inventory account number.' => 'Eine Buchungsgruppe besteht aus einem deskriptiven Namen, den Erl&ouml;s- und Aufwandskonten f&uuml;r diese vier Steuerzonen sowie aus einem Inventarkonto.',
   'A digit is required.'        => 'Eine Ziffer ist vorgeschrieben.',
-  'A group with that name does already exist.' => 'Eine Gruppe mit diesem Namen gibt es bereits.',
   'A lot of the usability of kivitendo has been enhanced with javascript. Although it is currently possible to use every aspect of kivitendo without javascript, we strongly recommend it. In a future version this may change and javascript may be necessary to access advanced features.' => 'Die Bedienung von kivitendo wurde an vielen Stellen mit Javascript verbessert. Obwohl es derzeit möglich ist, jeden Aspekt von kivitendo auch ohne Javascript zu benutzen, empfehlen wir es. In einer zukünftigen Version wird Javascript eventuell notwendig sein um weitergehende Features zu benutzen.',
   'A lower-case character is required.' => 'Ein Kleinbuchstabe ist vorgeschrieben.',
   'A special character is required (valid characters: #1).' => 'Ein Sonderzeichen ist vorgeschrieben (gültige Zeichen: #1).',
   'A temporary directory could not be created:' => 'Ein tempor&auml;res Verzeichnis konnte nicht erstellt werden:',
-  'A temporary file could not be created. Please verify that the directory "#1" is writeable by the webserver.' => 'Eine temporäre Datei konnte nicht angelegt werden. Bitte stellen Sie sicher, dass das Verzeichnis "#1" vom Webserver beschrieben werden darf.',
   'A temporary file could not be created:' => 'Eine tempor&auml;re Datei konnte nicht erstellt werden:',
   'A unit with this name does already exist.' => 'Eine Einheit mit diesem Namen existiert bereits.',
   'A valid taxkey is missing!'  => 'Einen gültiger Steuerschlüssel fehlt!',
@@ -75,6 +73,8 @@ $self->{texts} = {
   'Abort'                       => 'Abbrechen',
   'Abrechnungsnummer'           => 'Abrechnungsnummer',
   'Abteilung'                   => 'Abteilung',
+  'Access rights'               => 'Zugriffsrechte',
+  'Access to clients'           => 'Zugriff auf Mandanten',
   'Account'                     => 'Konto',
   'Account Category A'          => 'Aktiva/Mittelverwendung',
   'Account Category C'          => 'Kosten',
@@ -116,6 +116,7 @@ $self->{texts} = {
   'Accounting Group saved!'     => 'Buchungsgruppe gespeichert!',
   'Accounting method'           => 'Versteuerungsart',
   'Accrual'                     => 'Soll-Versteuerung',
+  'Actions'                     => 'Aktionen',
   'Active'                      => 'Aktiv',
   'Active?'                     => 'Aktiviert?',
   'Add'                         => 'Erfassen',
@@ -127,6 +128,7 @@ $self->{texts} = {
   'Add Accounts Receivables Transaction' => 'Debitorenbuchung erfassen',
   'Add Assembly'                => 'Erzeugnis erfassen',
   'Add Buchungsgruppe'          => 'Buchungsgruppe erfassen',
+  'Add Client'                  => 'Neuer Mandant',
   'Add Credit Note'             => 'Gutschrift erfassen',
   'Add Customer'                => 'Kunde erfassen',
   'Add Delivery Note'           => 'Lieferschein erfassen',
@@ -142,7 +144,6 @@ $self->{texts} = {
   'Add Part'                    => 'Ware erfassen',
   'Add Price Factor'            => 'Preisfaktor erfassen',
   'Add Pricegroup'              => 'Preisgruppe erfassen',
-  'Add Printer'                 => 'Drucker hinzufügen',
   'Add Project'                 => 'Projekt erfassen',
   'Add Purchase Delivery Order' => 'Lieferschein (Einkauf) erfassen',
   'Add Purchase Order'          => 'Lieferantenauftrag erfassen',
@@ -156,16 +157,18 @@ $self->{texts} = {
   'Add Storno Credit Note'      => 'Gutschrift Storno hinzufügen',
   'Add Transaction'             => 'Dialogbuchen',
   'Add User'                    => 'Neuer Benutzer',
+  'Add User Group'              => 'Neue Benutzergruppe',
   'Add Vendor'                  => 'Lieferant erfassen',
   'Add Vendor Invoice'          => 'Einkaufsrechnung erfassen',
   'Add Warehouse'               => 'Lager erfassen',
-  'Add a new group'             => 'Neue Gruppe erfassen',
   'Add and edit units'          => 'Einheiten erfassen und bearbeiten',
   'Add bank account'            => 'Bankkonto erfassen',
   'Add custom variable'         => 'Benutzerdefinierte Variable erfassen',
   'Add link: select records to link with' => 'Verknüpfungen hinzufügen: zu verknüpfende Belege auswählen',
   'Add links'                   => 'Verknüpfungen hinzufügen',
+  'Add new currency'            => 'Neue Währung hinzufügen',
   'Add note'                    => 'Notiz erfassen',
+  'Add printer'                 => 'Drucker hinzufügen',
   'Add unit'                    => 'Einheit hinzuf&uuml;gen',
   'Address'                     => 'Adresse',
   'Administration'              => 'Administration',
@@ -177,10 +180,13 @@ $self->{texts} = {
   'All Accounts'                => 'Alle Konten',
   'All Datasets up to date!'    => 'Alle Datenbanken sind auf aktuellem Stand.',
   'All changes in that file have been reverted.' => 'Alle &Auml;nderungen in dieser Datei wurden r&uuml;ckg&auml;ngig gemacht.',
+  'All clients'                 => 'Alle Mandanten',
   'All database upgrades have been applied.' => 'Alle Datenbankupdates wurden eingespielt.',
   'All general ledger entries'  => 'Alle Hauptbucheinträge',
+  'All groups'                  => 'Alle Gruppen',
   'All of the exports you have selected were already closed.' => 'Alle von Ihnen ausgewählten Exporte sind bereits abgeschlossen.',
   'All reports'                 => 'Alle Berichte (Konten&uuml;bersicht, Summen- u. Saldenliste, GuV, BWA, Bilanz, Projektbuchungen)',
+  'All the other clients will start with an empty set of WebDAV folders.' => 'Alle anderen Mandanten werden mit einem leeren Satz von WebDAV-Ordnern ausgestattet.',
   'All the selected exports have already been closed, or all of their items have already been executed.' => 'Alle ausgewählten Exporte sind als abgeschlossen markiert, oder für alle Einträge wurden bereits Zahlungen verbucht.',
   'All units have either no or exactly one base unit of which they are multiples.' => 'Einheiten haben entweder keine oder genau eine Basiseinheit, von der sie ein Vielfaches sind.',
   'All users'                   => 'Alle BenutzerInnen',
@@ -198,7 +204,6 @@ $self->{texts} = {
   'An invalid character was used (valid characters: #1).' => 'Ein ungültiges Zeichen wurde benutzt (gültige Zeichen: #1).',
   'An upper-case character is required.' => 'Ein Großbuchstabe ist vorgeschrieben.',
   'Annotations'                 => 'Anmerkungen',
-  'Another user with the login #1 does already exist.' => 'Es existiert bereits ein anderer Benutzer mit diesem Login.',
   'Any stock contents containing a best before date will be impossible to stock out otherwise.' => 'Sonst können Artikel, bei denen ein Mindesthaltbarkeitsdatum gesetzt ist, nicht mehr ausgelagert werden.',
   'Ap aging on %s'              => 'Offene Verbindlichkeiten zum %s',
   'Application Error. No Format given' => 'Fehler in der Anwendung. Das Ausgabeformat fehlt.',
@@ -367,7 +372,6 @@ $self->{texts} = {
   'Cancel'                      => 'Abbrechen',
   'Cancel Accounts Payables Transaction' => 'Kreditorenbuchung stornieren',
   'Cancel Accounts Receivables Transaction' => 'Debitorenbuchung stornieren',
-  'Cannot create Lock!'         => 'System kann nicht gesperrt werden!',
   'Cannot delete account!'      => 'Konto kann nicht gelöscht werden!',
   'Cannot delete customer!'     => 'Kunde kann nicht gelöscht werden!',
   'Cannot delete default account!' => 'Das Standard-Konto kann nicht gelöscht werden!',
@@ -434,14 +438,21 @@ $self->{texts} = {
   'City'                        => 'Stadt',
   'Cleared Balance'             => 'abgeschlossen',
   'Clearing Tax Received (No 71)' => 'Verrechnung des Erstattungsbetrages erwünscht (Zeile 71)',
-  'Click on login name to edit!' => 'Zum Bearbeiten den Benutzernamen anklicken!',
+  'Client'                      => 'Mandant',
+  'Client #1'                   => 'Mandant #1',
   'Client Configuration'        => 'Mandantenkonfiguration',
   'Client Configuration saved!' => 'Mandantenkonfiguration gespeichert!',
+  'Client list'                 => 'Mandantenliste',
+  'Client name'                 => 'Mandantenname',
+  'Client to assign the existing WebDAV folders to' => 'Mandant, dem bestehende WebDAV-Ordner zugewiesen werden',
+  'Client to configure the printers for' => 'Mandant, für den Drucker konfiguriert werden',
+  'Clients this Group is valid for' => 'Mandanten, für die diese Gruppe gültig ist',
+  'Clients this user has access to' => 'Mandaten, auf die Benutzer Zugriff hat',
   'Close'                       => 'Ãœbernehmen',
   'Close Books up to'           => 'Die Bücher abschließen bis zum',
   'Close Flash'                 => 'Schließen',
   'Close SEPA exports'          => 'SEPA-Export abschließen',
-  'Close Window'                => 'Fenster Schlie&szlig;en',
+  'Close Window'                => 'Fenster Schließen',
   'Close window'                => 'Fenster schließen',
   'Closed'                      => 'Geschlossen',
   'Collective Orders only work for orders from one customer!' => 'Sammelaufträge funktionieren nur für Aufträge von einem Kunden!',
@@ -450,8 +461,8 @@ $self->{texts} = {
   'Comment'                     => 'Kommentar',
   'Company'                     => 'Firma',
   'Company Name'                => 'Firmenname',
+  'Company name'                => 'Firmenname',
   'Compare to'                  => 'Gegenüberstellen zu',
-  'Configuration'               => 'Einstellungen',
   'Configuration of individual TODO items' => 'Konfiguration f&uuml;r die einzelnen Aufgabenlistenpunkte',
   'Configure'                   => 'Konfigurieren',
   'Confirm'                     => 'Best&auml;tigen',
@@ -488,9 +499,14 @@ $self->{texts} = {
   'Create Date'                 => 'Erstelldatum',
   'Create a new background job' => 'Einen neuen Hintergrund-Job anlegen',
   'Create a new business'       => 'Einen neuen Kunden-/Lieferantentyp erfassen',
+  'Create a new client'         => 'Einen neuen Mandanten anlegen',
   'Create a new department'     => 'Eine neue Abteilung erfassen',
+  'Create a new group'          => 'Neue Benutzergruppe erfassen',
   'Create a new payment term'   => 'Neue Zahlungsbedingungen anlegen',
+  'Create a new printer'        => 'Einen neuen Drucker anlegen',
   'Create a new project'        => 'Neues Projekt anlegen',
+  'Create a new user'           => 'Einen neuen Benutzer anlegen',
+  'Create a new user group'     => 'Eine neue Benutzergruppe erfassen',
   'Create and edit RFQs'        => 'Lieferantenanfragen erfassen und bearbeiten',
   'Create and edit dunnings'    => 'Mahnungen erfassen und bearbeiten',
   'Create and edit invoices and credit notes' => 'Rechnungen und Gutschriften erfassen und bearbeiten',
@@ -512,6 +528,7 @@ $self->{texts} = {
   'Create new'                  => 'Neu erfassen',
   'Create new background job'   => 'Neuen Hintergrund-Job anlegen',
   'Create new business'         => 'Kunden-/Lieferantentyp erfassen',
+  'Create new client #1'        => 'Neuen Mandanten #1 anlegen',
   'Create new department'       => 'Neue Abteilung erfassen',
   'Create new payment term'     => 'Neue Zahlungsbedingung anlegen',
   'Create tables'               => 'Tabellen anlegen',
@@ -535,6 +552,9 @@ $self->{texts} = {
   'Currencies'                  => 'W&auml;hrungen',
   'Currency'                    => 'Währung',
   'Currency (database ID)'      => 'Währung (Datenbank-ID)',
+  'Currency name'               => 'Währungsname',
+  'Currency names must be unique.' => 'Währungsnamen müssen eindeutig sein.',
+  'Currency names must not be empty.' => 'Währungsnamen dürfen nicht leer sein.',
   'Current / Next Level'        => 'Aktuelles / Nächstes Mahnlevel',
   'Current Earnings'            => 'Gewinn',
   'Current assets account'      => 'Konto für Umlaufvermögen',
@@ -578,18 +598,18 @@ $self->{texts} = {
   'DUNNING STARTED'             => 'Mahnprozess gestartet',
   'DUNS-Nr'                     => 'DUNS-Nr.',
   'Data'                        => 'Daten',
-  'Database'                    => 'Datenbank',
   'Database Administration'     => 'Datenbankadministration',
   'Database Connection Test'    => 'Test der Datenbankverbindung',
   'Database Host'               => 'Datenbankcomputer',
   'Database ID'                 => 'Datenbank-ID',
   'Database User'               => 'Datenbankbenutzer',
-  'Database User missing!'      => 'Datenbankbenutzer fehlt!',
   'Database backups and restorations are disabled in the configuration.' => 'Datenbanksicherungen und -wiederherstellungen sind in der Konfiguration deaktiviert.',
+  'Database host and port'      => 'Datenbankhost und -port',
   'Database name'               => 'Datenbankname',
+  'Database settings'           => 'Datenbankeinstellungen',
   'Database template'           => 'Datenbankvorlage',
   'Database update error:'      => 'Fehler beim Datenbankupgrade:',
-  'Dataset'                     => 'Datenbank',
+  'Database user and password'  => 'Datebankbenutzer und -passwort',
   'Dataset missing!'            => 'Datenbank fehlt!',
   'Dataset name'                => 'Datenbankname',
   'Dataset upgrade'             => 'Datenbankaktualisierung',
@@ -617,6 +637,7 @@ $self->{texts} = {
   'Default Accounts'            => 'Standardkonten',
   'Default Bin'                 => 'Standard-Lagerplatz',
   'Default Bin with ignoring onhand' => 'Standard-Lagerplatz für Lagerbewegungen ohne Ãœberprüfung auf verfügbare Menge ',
+  'Default Client (unconfigured)' => 'Standardmandant (unkonfiguriert)',
   'Default Customer/Vendor Language' => 'Standard-Kunden-/Lieferantensprache',
   'Default Transfer'            => 'Ein- / Auslagern Ã¼ber Standardlagerplätze',
   'Default Transfer Out always succeed. The current part onhand is ignored and the inventory can have negative stocks (not recommended).' => 'Auslagern Ã¼ber Standardlagerplatz funktioniert immer (verfügbare Menge wird nicht geprüft). Die Lagerbewegung wird auf den unten konfigurierten Lagerplatz gebucht (nicht empfohlen).',
@@ -625,6 +646,7 @@ $self->{texts} = {
   'Default Warehouse'           => 'Standard-Lager',
   'Default Warehouse with ignoring on hand' => 'Standardlager für Auslagern ohne Prüfung auf Bestand',
   'Default buchungsgruppe'      => 'Standardbuchungsgruppe',
+  'Default client'              => 'Standardmandant',
   'Default currency'            => 'Standardwährung',
   'Default currency missing!'   => 'Standardwährung fehlt!',
   'Default output medium'       => 'Standardausgabekanal',
@@ -632,7 +654,6 @@ $self->{texts} = {
   'Default template format'     => 'Standardvorlagenformat',
   'Default unit'                => 'Standardeinheit',
   'Default value'               => 'Standardwert',
-  'Defaults saved.'             => 'Die Standardeinstellungen wurden gespeichert.',
   'Delete'                      => 'Löschen',
   'Delete Account'              => 'Konto löschen',
   'Delete Contact'              => 'Ansprechperson löschen',
@@ -640,7 +661,6 @@ $self->{texts} = {
   'Delete Shipto'               => 'Lieferadresse löschen',
   'Delete delivery order'       => 'Lieferschein l&ouml;schen',
   'Delete drafts'               => 'Entwürfe löschen',
-  'Delete group'                => 'Gruppe l&ouml;schen',
   'Delete links'                => 'Verknüpfungen löschen',
   'Delete profile'              => 'Profil löschen',
   'Delete transaction'          => 'Buchung löschen',
@@ -692,7 +712,6 @@ $self->{texts} = {
   'Do you really want to delete AR transaction #1?' => 'Wollen Sie wirklich die Debitorenbuchung #1 löschen?',
   'Do you really want to delete GL transaction #1?' => 'Wollen Sie wirklich die Dialogbuchung #1 löschen?',
   'Do you really want to delete the selected links?' => 'Wollen Sie wirklich die ausgewählten Verknüpfungen löschen?',
-  'Do you really want to delete this group?' => 'Gruppe wirklich l&ouml;schen?',
   'Do you really want to delete this object?' => 'Wollen Sie dieses Objekt wirklich löschen?',
   'Do you really want to delete this warehouse?' => 'Wollen Sie dieses Lager wirklich l&ouml;schen?',
   'Do you want to <b>limit</b> your search?' => 'Wollen Sie Ihre Suche <b>spezialisieren</b>?',
@@ -761,6 +780,7 @@ $self->{texts} = {
   'Edit Assembly'               => 'Erzeugnis bearbeiten',
   'Edit Bins'                   => 'Lagerpl&auml;tze bearbeiten',
   'Edit Buchungsgruppe'         => 'Buchungsgruppe bearbeiten',
+  'Edit Client'                 => 'Mandanten bearbeiten',
   'Edit Credit Note'            => 'Gutschrift bearbeiten',
   'Edit Customer'               => 'Kunde editieren',
   'Edit Dunning'                => 'Mahnungen konfigurieren',
@@ -789,10 +809,10 @@ $self->{texts} = {
   'Edit Storno Credit Note'     => 'Storno Gutschrift bearbeiten',
   'Edit Storno Invoice'         => 'Stornorechnung bearbeiten',
   'Edit User'                   => 'Benutzerdaten bearbeiten',
+  'Edit User Group'             => 'Benutzergruppe bearbeiten',
   'Edit Vendor'                 => 'Lieferant editieren',
   'Edit Vendor Invoice'         => 'Einkaufsrechnung bearbeiten',
   'Edit Warehouse'              => 'Lager bearbeiten',
-  'Edit and delete a group'     => 'Gruppen bearbeiten und l&ouml;schen',
   'Edit background job'         => 'Hintergrund-Job bearbeiten',
   'Edit bank account'           => 'Bankkonto bearbeiten',
   'Edit business'               => 'Kunden-/Lieferantentyp bearbeiten',
@@ -800,20 +820,15 @@ $self->{texts} = {
   'Edit department'             => 'Abteilung bearbeiten',
   'Edit file'                   => 'Datei bearbeiten',
   'Edit greetings'              => 'Anreden bearbeiten',
-  'Edit group '                 => 'Gruppe bearbeiten',
-  'Edit group membership'       => 'Gruppenmitgliedschaften bearbeiten',
-  'Edit groups'                 => 'Gruppen bearbeiten',
-  'Edit membership'             => 'Mitgliedschaft bearbeiten',
   'Edit note'                   => 'Notiz bearbeiten',
   'Edit payment term'           => 'Zahlungsbedingungen bearbeiten',
   'Edit prices and discount (if not used, textfield is ONLY set readonly)' => 'Preise und Rabatt in Formularen frei anpassen (falls deaktiviert, wird allerdings NUR das textfield auf READONLY gesetzt / kann je nach Browserversion und technischen Fähigkeiten des Anwenders noch umgangen werden)',
   'Edit project'                => 'Projekt bearbeiten',
   'Edit project #1'             => 'Projekt #1 bearbeiten',
-  'Edit rights'                 => 'Rechte bearbeiten',
   'Edit templates'              => 'Vorlagen bearbeiten',
   'Edit the Delivery Order'     => 'Lieferschein bearbeiten',
   'Edit the configuration for periodic invoices' => 'Konfiguration für wiederkehrende Rechnungen bearbeiten',
-  'Edit the membership of all users in all groups:' => 'Bearbeiten der Mitgliedschaft aller Benutzer in allen Gruppen:',
+  'Edit the currency names in order to rename them.' => 'Bearbeiten Sie den Namen, um eine Währung umzubennen.',
   'Edit the purchase_order'     => 'Bearbeiten des Lieferantenauftrags',
   'Edit the request_quotation'  => 'Bearbeiten der Preisanfrage',
   'Edit the sales_order'        => 'Bearbeiten des Auftrags',
@@ -841,6 +856,7 @@ $self->{texts} = {
   'Error in position #1: You must either assign no transfer at all or the full quantity of #2 #3.' => 'Fehler in Position #1: Sie m&uuml;ssen einer Position entweder gar keinen Lagerausgang oder die vollst&auml;ndige im Lieferschein vermerkte Menge von #2 #3 zuweisen.',
   'Error in row #1: The quantity you entered is bigger than the stocked quantity.' => 'Fehler in Zeile #1: Die angegebene Menge ist gr&ouml;&szlig;er als die vorhandene Menge.',
   'Error message from the database driver:' => 'Fehlermeldung des Datenbanktreibers:',
+  'Error message from the database: #1' => 'Fehlermeldung der Datenbank: #1',
   'Error when saving: #1'       => 'Fehler beim Speichern: #1',
   'Error!'                      => 'Fehler!',
   'Error: Buchungsgruppe missing or invalid' => 'Fehler: Buchungsgruppe fehlt oder ungültig',
@@ -969,6 +985,7 @@ $self->{texts} = {
   'General Ledger Transaction'  => 'Dialogbuchung',
   'General ledger and cash'     => 'Finanzbuchhaltung und Zahlungsverkehr',
   'General ledger corrections'  => 'Korrekturen im Hauptbuch',
+  'General settings'            => 'Allgemeine Einstellungen',
   'Generic Tax Report'          => 'USTVA Bericht',
   'Git revision: #1, #2 #3'     => 'Git-Revision: #1, #2 #3',
   'Given Name'                  => 'Vorname',
@@ -979,13 +996,19 @@ $self->{texts} = {
   'Group'                       => 'Warengruppe',
   'Group Invoices'              => 'Rechnungen zusammenfassen',
   'Group Items'                 => 'Waren gruppieren',
+  'Group assignment'            => 'Gruppenzuordnung',
   'Group deleted!'              => 'Warengruppe gelöscht!',
+  'Group list'                  => 'Gruppenliste',
   'Group membership'            => 'Gruppenzugehörigkeit',
   'Group missing!'              => 'Warengruppe fehlt!',
   'Group saved!'                => 'Warengruppe gespeichert!',
   'Groups'                      => 'Warengruppen',
+  'Groups that are valid for this client for access rights' => 'Gruppen, die für diesen Mandanten gültig sind',
+  'Groups this user is a member in' => 'Gruppen, in denen Benutzer Mitglied ist',
+  'Groups valid for this client' => 'Für Mandanten gültige Gruppen',
   'HTML'                        => 'HTML',
   'HTML Templates'              => 'HTML-Vorlagen',
+  'Handling of WebDAV'          => 'Behandlung von WebDAV',
   'Hardcopy'                    => 'Seite drucken',
   'Has serial number'           => 'Hat eine Serienummer',
   'Heading'                     => 'Ãœberschrift',
@@ -998,6 +1021,7 @@ $self->{texts} = {
   'Hide by default'             => 'Standardm&auml;&szlig;ig verstecken',
   'Hide help text'              => 'Hilfetext verbergen',
   'Hide settings'               => 'Einstellungen verbergen',
+  'Hints'                       => 'Hinweise',
   'History'                     => 'Historie',
   'History Search'              => 'Historien Suche',
   'History Search Engine'       => 'Historien Suchmaschine',
@@ -1021,7 +1045,7 @@ $self->{texts} = {
   'If you have not chosen for example the category revenue for a tax and you choose an revenue account to create a transfer in the general ledger, this tax will not be displayed in the tax dropdown.' => 'Wenn Sie z.B. die Kategory Erlös für eine Steuer nicht gewählt haben und ein Erlöskonto beim Erstellen einer Dialogbuchung wählen, wird diese Steuer auch nicht im Dropdown-Menü für die Steuern angezeigt.',
   'If you see this message, you most likely just setup your LX-Office and haven\'t added any entry types. If this is the case, the option is accessible for administrators in the System menu.' => 'Wenn Sie diese Meldung sehen haben Sie wahrscheinlich ein frisches LX-Office Setup und noch keine Buchungsgruppen eingerichtet. Ein Administrator kann dies im Systemmen&uuml; erledigen.',
   'If you select a base unit then you also have to enter a factor.' => 'Wenn Sie eine Basiseinheit auswählen, dann müssen Sie auch einen Faktor eingeben.',
-  'If you want to change any of these parameters then press the &quot;Back&quot; button, edit the file &quot;config/kivitendo.conf&quot; and login into the admin module again.' => 'Wenn Sie einen der Parameter &auml;ndern wollen, so dr&uuml;cken Sie auf den &quot;Zur&uuml;ck&quot;-Button, bearbeiten Sie die Datei &quot;config/kivitendo.conf&quot;, und melden Sie sich erneut im Administrationsbereich an.',
+  'If you want to change any of these parameters then press the "Back" button, edit the file "config/kivitendo.conf" and login into the admin module again.' => 'Wenn Sie einen der Parameter Ã¤ndern wollen, so drücken Sie auf den "Zurück"-Button, bearbeiten Sie die Datei "config/kivitendo.conf", und melden Sie sich erneut im Administrationsbereich an.',
   'If you want to delete such a dataset you have to edit the user(s) that are using the dataset in question and have them use another dataset.' => 'Wenn Sie eine solche Datenbank l&ouml;schen wollen, so m&uuml;ssen Sie zuerst die Benutzer bearbeiten, die die fragliche Datenbank benutzen, und sie so &auml;ndern, dass sie eine andere Datenbank benutzen.',
   'If you want to set up the authentication database yourself then log in to the administration panel. kivitendo will then create the database and tables for you.' => 'Wenn Sie die Authentifizierungs-Datenbank selber einrichten wollen, so melden Sie sich im Administrationsbereich an. kivitendo wird dann die Datenbank und die erforderlichen Tabellen für Sie anlegen.',
   'If your old bins match exactly Bins in the Warehouse CLICK on <b>AUTOMATICALLY MATCH BINS</b>.' => 'Falls die alte Lagerplatz-Beschreibung in Stammdaten genau mit einem Lagerplatz in einem vorhandenem Lager Ã¼bereinstimmt, KLICK auf <b>LAGERPLÄTZE AUTOMATISCH ZUWEISEN</b>',
@@ -1037,6 +1061,8 @@ $self->{texts} = {
   'Import result'               => 'Import-Ergebnis',
   'Import summary'              => 'Import-Zusammenfassung',
   'In order to do that hit the button "Delete transaction".' => 'Drücken Sie dafür auf den Button "Buchung löschen".',
+  'In order to migrate the old folder structure into the new structure you have to chose which client the old structure will be assigned to.' => 'Um die alte Ordnerstruktur in die neue Struktur zu migrieren, müssen Sie festlegen, welchem Mandanten die bisherige Struktur zugewiesen wird.',
+  'In order to use kivitendo you have to create at least a client, a user and a group.' => 'Um kivitendo zu nutzen, müssen Sie mindestens einen Mandanten, einen Benutzer und eine Gruppe anlegen.',
   'In the latter case the tables needed by kivitendo will be created in that database.' => 'In letzterem Fall werden die von kivitendo benötigten Tabellen in dieser existierenden Datenbank angelegt.',
   'In version 2.4.0 the administrator has to enter a list of units in the administrative section.' => 'Vor Version 2.4.0 musste der Benutzer die Konten bei jeder Ware und jeder Dienstleistung einzeln ausw&auml;hlen.',
   'In-line'                     => 'im Text',
@@ -1054,9 +1080,8 @@ $self->{texts} = {
   'Incoming Payments'           => 'Zahlungseingänge',
   'Incoming invoice number'     => 'Eingangsrechnungsnummer',
   'Inconsistency in database'   => 'Unstimmigkeiten in der Datenbank',
-  'Incorrect Password!'         => 'Ungültiges Passwort!',
   'Incorrect password!'         => 'Ungültiges Passwort!',
-  'Incorrect username or password!' => 'Ungültiger Benutzername oder falsches Passwort!',
+  'Incorrect username or password or no access to selected client!' => 'Ungültiger Benutzername oder Passwort oder kein Zugriff auf den ausgewählten Mandanten!',
   'Increase'                    => 'Erhöhen',
   'Individual Items'            => 'Einzelteile',
   'Information'                 => 'Information',
@@ -1069,6 +1094,7 @@ $self->{texts} = {
   'International'               => 'Ausland',
   'Internet'                    => 'Internet',
   'Introduction of Buchungsgruppen' => 'Einf&uuml;hrung von Buchungsgruppen',
+  'Introduction of clients'     => 'Einführung von Mandanten',
   'Introduction of units'       => 'Einf&uuml;hrung von Einheiten',
   'Inv. Duedate'                => 'Rg. Fälligkeit',
   'Invalid'                     => 'Ungültig',
@@ -1104,6 +1130,7 @@ $self->{texts} = {
   'Invoices, Credit Notes & AR Transactions' => 'Rechnungen, Gutschriften & Debitorenbuchungen',
   'Is Searchable'               => 'Durchsuchbar',
   'Is this a summary account to record' => 'Sammelkonto für',
+  'It can be changed later but must be unique within the installation.' => 'Er ist nachträglich Ã¤nderbar, muss aber im System eindeutig sein.',
   'It is not allowed that a summary account occurs in a drop-down menu!' => 'Ein Sammelkonto darf nicht in Aufklappmenüs aufgenommen werden!',
   'It is possible that even after such a correction there is something wrong with this transaction (e.g. taxes that don\'t match the selected taxkey). Therefore you should re-run the general ledger analysis.' => 'Auch nach einer Korrektur kann es mit dieser Buchung noch weitere Probleme geben (z.B. nicht zum Steuerschlüssel passende Steuern), weshalb ein erneutes Ausführen der Hauptbuchanalyse empfohlen wird.',
   'It is possible to do this automatically for some Buchungsgruppen, but not for all.' => 'Es ist m&ouml;glich, dies f&uuml;r einige, aber nicht f&uuml;r alle Buchungsgruppen automatisch zu erledigen.',
@@ -1143,7 +1170,6 @@ $self->{texts} = {
   'Language saved!'             => 'Sprache gespeichert!',
   'Languages'                   => 'Sprachen',
   'Languages and translations'  => 'Sprachen und Ãœbersetzungen',
-  'Last Action'                 => 'Letzte Aktivität',
   'Last Article Number'         => 'Letzte Artikelnummer',
   'Last Assembly Number'        => 'Letzte Erzeugnisnummer',
   'Last Cost'                   => 'Einkaufspreis',
@@ -1190,11 +1216,11 @@ $self->{texts} = {
   'Loading...'                  => 'Wird geladen...',
   'Local Tax Office Preferences' => 'Angaben zum Finanzamt',
   'Lock System'                 => 'System sperren',
+  'Lock file handling failed. Please verify that the directory "#1" is writeable by the webserver.' => 'Die Lockdateibehandlung schlug fehl. Bitte stellen Sie sicher, dass der Webserver das Verzeichnis "#1" beschreiben darf.',
   'Lockfile created!'           => 'System gesperrt!',
   'Lockfile removed!'           => 'System entsperrt!',
   'Login'                       => 'Anmelden',
   'Login Name'                  => 'Benutzer',
-  'Login name missing!'         => 'Benutzer - Feld darf nicht leer sein!',
   'Login of User'               => 'Login',
   'Logout'                      => 'Abmelden',
   'Logout now'                  => 'kivitendo jetzt verlassen',
@@ -1247,7 +1273,6 @@ $self->{texts} = {
   'Missing parameter #1 in call to sub #2.' => 'Fehlender Parameter \'#1\' in Funktionsaufruf \'#2\'.',
   'Missing parameter (at least one of #1) in call to sub #2.' => 'Fehlernder Parameter (mindestens einer aus \'#1\') in Funktionsaufruf \'#2\'.',
   'Missing taxkeys in invoices with taxes.' => 'Fehlende Steuerschl&uuml;ssel in Rechnungen mit Steuern',
-  'Missing user id!'            => 'Benutzer ID fehlt!',
   'Mitarbeiter'                 => 'Mitarbeiter',
   'Mixed (requires column "type")' => 'Gemischt (erfordert Spalte "type")',
   'Mobile'                      => 'Mobiltelefon',
@@ -1275,9 +1300,11 @@ $self->{texts} = {
   'Net amount'                  => 'Nettobetrag',
   'Netto Terms'                 => 'Zahlungsziel netto',
   'New Buchungsgruppe #1'       => 'Neue Buchungsgruppe #1',
-  'New Templates'               => 'Erzeuge Vorlagen, Name',
+  'New Password'                => 'Neues Passwort',
   'New assembly'                => 'Neues Erzeugnis',
   'New bank account'            => 'Neues Bankkonto',
+  'New client #1: The database configuration fields "host", "port", "name" and "user" must not be empty.' => 'Neuer Mandant #1: Die Datenbankkonfigurationsfelder "Host", "Port" und "Name" dürfen nicht leer sein.',
+  'New client #1: The name must be unique and not empty.' => 'Neuer Mandant #1: Der Name darf nicht leer und muss eindeutig sein.',
   'New contact'                 => 'Neue Ansprechperson',
   'New customer'                => 'Neuer Kunde',
   'New filter for tax accounts' => 'Neue Filter für Steuerkonten',
@@ -1296,7 +1323,6 @@ $self->{texts} = {
   'No Company Address given'    => 'Keine Firmenadresse hinterlegt!',
   'No Company Name given'       => 'Kein Firmenname hinterlegt!',
   'No Customer was found matching the search parameters.' => 'Zu dem Suchbegriff wurde kein Endkunde gefunden',
-  'No Database Drivers available!' => 'Kein Datenbanktreiber verfügbar!',
   'No Dataset selected!'        => 'Keine Datenbank ausgewählt!',
   'No Vendor was found matching the search parameters.' => 'Zu dem Suchbegriff wurde kein Händler gefunden',
   'No action defined.'          => 'Keine Aktion definiert.',
@@ -1306,6 +1332,7 @@ $self->{texts} = {
   'No bank information has been entered in this vendor\'s master data entry. You cannot create bank transfers unless you enter bank information.' => 'Für diesen Lieferanten wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Ãœberweisungen für den Lieferanten anlegen.',
   'No bins have been added to this warehouse yet.' => 'Es wurden zu diesem Lager noch keine Lagerpl&auml;tze angelegt.',
   'No business has been created yet.' => 'Es wurden noch kein Kunden-/Lieferantentyp erfasst.',
+  'No clients have been created yet.' => 'Es wurden noch keine Mandanten angelegt.',
   'No contact selected to delete' => 'Keine Ansprechperson zum Löschen ausgewählt',
   'No customer has been selected yet.' => 'Es wurde noch kein Kunde ausgewählt.',
   'No data was found.'          => 'Es wurden keine Daten gefunden.',
@@ -1316,12 +1343,12 @@ $self->{texts} = {
   'No dunnings have been selected for printing.' => 'Es wurden keine Mahnungen zum Drucken ausgew&auml;hlt.',
   'No entries were found which had no unit assigned to them.' => 'Es wurden keine Eintr&auml;ge gefunden, denen keine Einheit zugeordnet war.',
   'No file has been uploaded yet.' => 'Es wurde noch keine Datei hochgeladen.',
-  'No group has been selected, or the group does not exist anymore.' => 'Es wurde keine Gruppe ausgew&auml;hlt, oder die Gruppe wurde in der Zwischenzeit gel&ouml;scht.',
-  'No groups have been added yet.' => 'Es wurden noch keine Gruppen angelegt.',
+  'No groups have been created yet.' => 'Es wurden noch keine Gruppen angelegt.',
   'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => 'Es wurde kein oder ein unbekanntes Authentifizierungsmodul in "config/kivitendo.conf" angegeben.',
   'No part was found matching the search parameters.' => 'Es wurde kein Artikel gefunden, auf den die Suchparameter zutreffen.',
   'No payment term has been created yet.' => 'Es wurden noch keine Zahlungsbedingungen angelegt.',
   'No prices will be updated because no prices have been entered.' => 'Es werden keine Preise aktualisiert, weil keine gültigen Preisänderungen eingegeben wurden.',
+  'No printers have been created yet.' => 'Es wurden noch keine Drucker angelegt.',
   'No problems were recognized.' => 'Es wurden keine Probleme gefunden.',
   'No report with id #1'        => 'Es gibt keinen Report mit der Id #1',
   'No shipto selected to delete' => 'Keine Lieferadresse zum Löschen ausgewählt',
@@ -1329,6 +1356,7 @@ $self->{texts} = {
   'No transaction selected!'    => 'Keine Transaktion ausgewählt',
   'No transfers were executed in this export.' => 'In diesem SEPA-Export wurden keine Ãœberweisungen ausgeführt.',
   'No unknown units where found.' => 'Es wurden keine unbekannten Einheiten gefunden.',
+  'No users have been created yet.' => 'Es wurden noch keine Benutzer anleget.',
   'No valid number entered for pricegroup "#1".' => 'Für Preisgruppe "#1" wurde keine gültige Nummer eingegeben.',
   'No vendor has been selected yet.' => 'Es wurde noch kein Lieferant ausgewählt.',
   'No warehouse has been created yet or the quantity of the bins is not configured yet.' => 'Es wurde noch kein Lager angelegt, bzw. die dazugehörigen Lagerplätze sind noch nicht konfiguriert.',
@@ -1375,6 +1403,7 @@ $self->{texts} = {
   'One or more Perl modules missing' => 'Ein oder mehr Perl-Module fehlen',
   'Only Warnings and Errors'    => 'Nur Warnungen und Fehler',
   'Only due follow-ups'         => 'Nur f&auml;llige Wiedervorlagen',
+  'Only groups that have been configured for the client the user logs in to will be considered.' => 'Allerdings werden nur diejenigen Gruppen herangezogen, die für den Mandanten konfiguriert sind.',
   'Only shown in item mode'     => 'werden nur im Artikelmodus angezeigt',
   'Oops. No valid action found to dispatch. Please report this case to the kivitendo team.' => 'Ups. Es wurde keine gültige Funktion zum Aufrufen gefunden. Bitte berichten Sie diesen Fall den kivitendo-Entwicklern.',
   'Open'                        => 'Offen',
@@ -1480,10 +1509,14 @@ $self->{texts} = {
   'Please choose for which categories the taxes should be displayed (otherwise remove the ticks):' => 'Bitte wählen Sie für welche Kontoart die Steuer angezeigt werden soll (ansonsten einfach die Häkchen entfernen)',
   'Please contact your administrator or a service provider.' => 'Bitte kontaktieren Sie Ihren Administrator oder einen Dienstleister.',
   'Please contact your administrator.' => 'Bitte wenden Sie sich an Ihren Administrator.',
+  'Please correct the settings and try again or deactivate that client.' => 'Bitte korrigieren Sie die Einstellungen und versuchen Sie es erneut, oder deaktivieren Sie diesen Mandanten.',
   'Please define a taxkey for the following taxes and run the update again:' => 'Bitte definieren Sie einen Steuerschlüssel für die folgenden Steuern und starten Sie dann das Update erneut:',
+  'Please do so in the administration area.' => 'Bitte erleidgen Sie dies im Administrationsbereich.',
   'Please enter a profile name.' => 'Bitte geben Sie einen Profilnamen an.',
   'Please enter the currency you are working with.' => 'Bitte geben Sie die Währung an, mit der Sie arbeiten.',
   'Please enter the login for the new user.' => 'Bitte geben Sie das Login für den neuen Benutzer ein.',
+  'Please enter the name for the new client.' => 'Bitte geben Sie einen Namen für den neuen Mandanten ein.',
+  'Please enter the name for the new group.' => 'Bitte geben Sie den Namen für die neue Gruppe ein.',
   'Please enter the name of the database that will be used as the template for the new database:' => 'Bitte geben Sie den Namen der Datenbank an, die als Vorlage f&uuml;r die neue Datenbank benutzt wird:',
   'Please enter the name of the dataset you want to restore the backup in.' => 'Bitte geben Sie den Namen der Datenbank ein, in der Sie die Sicherung wiederherstellen wollen.',
   'Please enter the sales tax identification number.' => 'Bitte geben Sie die Umsatzsteueridentifikationsnummer an.',
@@ -1498,12 +1531,12 @@ $self->{texts} = {
   'Please read the file'        => 'Bitte lesen Sie die Datei',
   'Please select a customer from the list below.' => 'Bitte einen Endkunden aus der Liste auswählen',
   'Please select a part from the list below.' => 'Bitte w&auml;hlen Sie einen Artikel aus der Liste aus.',
-  'Please select a user'        => 'Bitte wählen Sie einen Benutzer aus',
   'Please select a vendor from the list below.' => 'Bitte einen Händler aus der Liste auswählen',
   'Please select the chart of accounts this installation is using from the list below.' => 'Bitte w&auml;hlen Sie den Kontenrahmen aus, der bei dieser Installation verwendet wird.',
   'Please select the database you want to backup' => 'Bitte w&auml;hlen Sie die zu sichernde Datenbank gefunden',
   'Please select the destination bank account for the collections:' => 'Bitte wählen Sie das Bankkonto als Ziel für die Einzüge aus:',
   'Please select the source bank account for the transfers:' => 'Bitte wählen Sie das Bankkonto als Quelle für die Ãœberweisungen aus:',
+  'Please select which client configurations you want to create.' => 'Bitte wählen Sie aus, welche Mandanten mit welchen Einstellungen angelegt werden sollen.',
   'Please seletct the dataset you want to delete:' => 'Bitte w&auml;hlen Sie die zu l&ouml;schende Datenbank aus:',
   'Please set another taxnumber for the following taxes and run the update again:' => 'Bitte wählen Sie ein anderes Steuerautomatik-Konto für die folgenden Steuern aus uns starten Sie dann das Update erneut.',
   'Please specify a description for the warehouse designated for these goods.' => 'Bitte geben Sie den Namen des Ziellagers f&uuml;r die &uuml;bernommenen Daten ein.',
@@ -1547,11 +1580,12 @@ $self->{texts} = {
   'Print dunnings'              => 'Mahnungen drucken',
   'Print list'                  => 'Liste ausdrucken',
   'Print options'               => 'Druckoptionen',
+  'Print templates'             => 'Druckvorlagen',
   'Printer'                     => 'Drucker',
   'Printer Command'             => 'Druckbefehl',
-  'Printer Command missing!'    => 'Druckbefehl fehlt',
+  'Printer Description'         => 'Druckerbeschreibung',
   'Printer Management'          => 'Druckeradministration',
-  'Printers are created for a user database. Please select a user. The associated database will be edited.' => 'Drucker werden für eine Benutzerdatenbank erzeugt. Bitte wählen Sie einen Benutzer aus. Die Drucker werden in der verknüpften Datenbank angelegt.',
+  'Printer management'          => 'Druckerverwaltung',
   'Printing ... '               => 'Es wird gedruckt.',
   'Prior to version v2.4.0 the user could enter arbitrary strings as units for parts, services and in invoices, sales quotations etc.' => 'Vor Version v2.4.0 konnten die Benutzer sowohl in den Artikelstammdaten als auch in Belegen wie Rechnungen, Angeboten etc. beliebige Dinge als Einheit eintragen.',
   'Prior to version v2.4.0 the user had to chose the accounts for each part and service.' => 'Vor Version v2.4.0 mussten Benutzer für Artikel und Dienstleistungen jedes einzelne Buchungskonto auswählen.',
@@ -1614,7 +1648,6 @@ $self->{texts} = {
   'RFQs'                        => 'Preisanfragen',
   'ROP'                         => 'Mindestlagerbestand',
   'Ranges of numbers'           => 'Nummernkreise',
-  'Ranges of numbers and default accounts' => 'Nummernkreise und Standardkonten',
   'Re-run analysis'             => 'Analyse wiederholen',
   'Receipt'                     => 'Zahlungseingang',
   'Receipt posted!'             => 'Beleg gebucht!',
@@ -1643,7 +1676,6 @@ $self->{texts} = {
   'Remove draft when posting'   => 'Entwurf beim Buchen l&ouml;schen',
   'Removed spoolfiles!'         => 'Druckdateien entfernt!',
   'Removing marked entries from queue ...' => 'Markierte Einträge werden von der Warteschlange entfernt ...',
-  'Rename the group'            => 'Gruppe umbenennen',
   'Replace the orphaned currencies by other not orphaned currencies. To do so, please delete the currency in the textfields above and replace it by another currency. You could loose or change unintentionally exchangerates. Go on very carefully since you could destroy transactions.' => 'Ersetze die Währungen durch andere gültige Währungen. Wenn Sie sich hierfür entscheiden, ersetzen Sie bitte alle Währungen, die oben angegeben sind, durch Währungen, die in Ihrem System ordnungsgemäß eingetragen sind. Alle eingetragenen Wechselkurse für die verwaiste Währung werden dabei gelöscht. Bitte gehen Sie sehr vorsichtig vor, denn die betroffenen Buchungen können unter Umständen kaputt gehen.',
   'Report Positions'            => 'Berichte',
   'Report about warehouse contents' => 'Lagerbestand anzeigen',
@@ -1740,6 +1772,7 @@ $self->{texts} = {
   'Search term'                 => 'Suchbegriff',
   'Searchable'                  => 'Durchsuchbar',
   'Secondary sorting'           => 'Untersortierung',
+  'Section "#1"'                => 'Abschnitt "#1"',
   'Select'                      => 'auswählen',
   'Select a Customer'           => 'Endkunde auswählen',
   'Select a customer'           => 'Einen Kunden ausw&auml;hlen',
@@ -1755,7 +1788,6 @@ $self->{texts} = {
   'Select postscript or PDF!'   => 'Postscript oder PDF auswählen!',
   'Select tax office...'        => 'Finanzamt auswählen...',
   'Select the chart of accounts in use' => 'Benutzten Kontenrahmen ausw&auml;hlen',
-  'Select the checkboxes that match users to the groups they should belong to.' => 'W&auml;hlen Sie diejenigen Checkboxen aus, die die Benutzer zu den gew&uuml;schten Gruppen zuordnen.',
   'Select type of removal'      => 'Grund der Entnahme ausw&auml;hlen',
   'Select type of transfer'     => 'Grund der Umlagerung ausw&auml;hlen',
   'Selected'                    => 'Ausgewählt',
@@ -1784,7 +1816,6 @@ $self->{texts} = {
   'Set eMail text'              => 'eMail Text eingeben',
   'Settings'                    => 'Einstellungen',
   'Setup Menu'                  => 'Menü-Variante',
-  'Setup Templates'             => 'Vorlagen auswählen',
   'Ship to'                     => 'Lieferadresse',
   'Ship via'                    => 'Transportmittel',
   'Shipping Address'            => 'Lieferadresse',
@@ -1953,13 +1984,14 @@ $self->{texts} = {
   'Templates'                   => 'Vorlagen',
   'Terms missing in row '       => '+Tage fehlen in Zeile ',
   'Test and preview'            => 'Test und Vorschau',
-  'Test connection'             => 'Verbindung testen',
+  'Test database connectivity'  => 'Datenbankverbindung testen',
   'Text field'                  => 'Textfeld',
   'Text field variables: \'WIDTH=w HEIGHT=h\' sets the width and height of the text field. They default to 30 and 5 respectively.' => 'Textfelder: \'WIDTH=w HEIGHT=h\' setzen die Breite und die H&ouml;he des Textfeldes. Wenn nicht anders angegeben, so werden sie 30 Zeichen breit und f&uuml;nf Zeichen hoch dargestellt.',
   'Text variables: \'MAXLENGTH=n\' sets the maximum entry length to \'n\'.' => 'Textzeilen: \'MAXLENGTH=n\' setzt eine Maximall&auml;nge von n Zeichen.',
   'Text, text field and number variables: The default value will be used as-is.' => 'Textzeilen, Textfelder und Zahlenvariablen: Der Standardwert wird so wie er ist &uuml;bernommen.',
   'That export does not exist.' => 'Dieser Export existiert nicht.',
   'That is why kivitendo could not find a default currency.' => 'Daher konnte kivitendo keine Standardwährung finden.',
+  'The \'name\' is the field shown to the user during login.' => 'Der \'Name\' ist derjenige, der dem Benutzer beim Login angezeigt wird.',
   'The \'tag\' field must only consist of alphanumeric characters or the carachters - _ ( )' => 'Das Feld \'tag\' darf nur aus alphanumerischen Zeichen und den Zeichen - _ ( ) bestehen.',
   'The AP transaction #1 has been deleted.' => 'Die Kreditorenbuchung #1 wurde gelöscht.',
   'The AR transaction #1 has been deleted.' => 'Die Debitorenbuchung #1 wurde gelöscht.',
@@ -1969,6 +2001,8 @@ $self->{texts} = {
   'The LDAP server "#1:#2" is unreachable. Please check config/kivitendo.conf.' => 'Der LDAP-Server "#1:#2" ist nicht erreichbar. Bitte &uuml;berpr&uuml;fen Sie die Angaben in config/kivitendo.conf.',
   'The SEPA export has been created.' => 'Der SEPA-Export wurde erstellt',
   'The SEPA strings have been saved.' => 'Die bei SEPA-Ãœberweisungen verwendeten Begriffe wurden gespeichert.',
+  'The WebDAV feature is activated.' => 'Das WebDAV-Feature ist aktiviert.',
+  'The access rights a user has within a client instance is still governed by his group membership.' => 'Welche Zugriffsrechte ein Benutzer innerhalb eines Mandanten hat, wird weiterhin Ã¼ber Gruppenmitgliedschaften geregelt.',
   'The access rights have been saved.' => 'Die Zugriffsrechte wurden gespeichert.',
   'The account 3804 already exists, the update will be skipped.' => 'Das Konto 3804 existiert schon, das Update wird Ã¼bersprungen.',
   'The account 3804 will not be added automatically.' => 'Das Konto 3804 wird nicht automatisch hinzugefügt.',
@@ -1993,11 +2027,18 @@ $self->{texts} = {
   'The business has been saved.' => 'Der Kunden-/Lieferantentyp wurde gespeichert.',
   'The business is in use and cannot be deleted.' => 'Der Kunden-/Lieferantentyp wird benutzt und kann nicht gelöscht werden.',
   'The changing of tax-o-matic account is NOT recommended, but if you do so please also (re)configure buchungsgruppen and reconfigure ALL charts which point to this tax-o-matic account. ' => 'Es wird nicht empfohlen Steuerkonten (Umsatzsteuer oder Vorsteuer) "umzuhängen", aber falls es gemacht wird, bitte auch entsprechend konsequent die Buchungsgruppen und die Konten die mit dieser Steuer verknüpft sind umkonfigurieren.',
+  'The client could not be deleted.' => 'Der Mandant konnte nicht gelöscht werden.',
+  'The client has been created.' => 'Der Mandant wurde angelegt.',
+  'The client has been deleted.' => 'Der Mandant wurde gelöscht.',
+  'The client has been saved.'  => 'Der Mandant wurde gespeichert.',
   'The column "make_X" can contain either a vendor\'s database ID, a vendor number or a vendor\'s name.' => 'Die Spalte "make_X" can entweder die Datenbank-ID des Lieferanten, eine Lieferantennummer oder einen Lieferantennamen enthalten.',
   'The column triplets can occur multiple times with different numbers "X" each time (e.g. "make_1", "model_1", "lastcost_1", "make_2", "model_2", "lastcost_2", "make_3", "model_3", "lastcost_3" etc).' => 'Die Spalten-Dreiergruppen können mehrfach auftreten, sofern sie unterschiedliche Nummern "X" verwenden (z.B. "make_1", "model_1", "lastcost_1", "make_2", "model_2", "lastcost_2", "make_3", "model_3", "lastcost_3" etc).',
   'The columns &quot;Dunning Duedate&quot;, &quot;Total Fees&quot; and &quot;Interest&quot; show data for the previous dunning created for this invoice.' => 'Die Spalten &quot;Zahlbar bis&quot;, &quot;Kumulierte Geb&uuml;hren&quot; und &quot;Zinsen&quot; zeigen Daten der letzten f&uuml;r diese Rechnung erzeugten Mahnung.',
+  'The combination of database host, port and name is not unique.' => 'Die Kombination aus Datenbankhost, -port und -name ist nicht eindeutig.',
+  'The command is missing.'     => 'Der Befehl fehlt.',
   'The connection to the LDAP server cannot be encrypted (SSL/TLS startup failure). Please check config/kivitendo.conf.' => 'Die Verbindung zum LDAP-Server kann nicht verschl&uuml;sselt werden (Fehler bei SSL/TLS-Initialisierung). Bitte &uuml;berpr&uuml;fen Sie die Angaben in config/kivitendo.conf.',
   'The connection to the authentication database failed:' => 'Die Verbindung zur Authentifizierungsdatenbank schlug fehl:',
+  'The connection to the configured client database "#1" on host "#2:#3" failed.' => 'Die Verbindung zur konfigurierten Datenbank "#1" auf Host "#2:#3" schlug fehl.',
   'The connection to the database could not be established.' => 'Die Verbindung zur Datenbank konnte nicht hergestellt werden.',
   'The connection to the template database failed:' => 'Die Verbindung zur Vorlagendatenbank schlug fehl:',
   'The connection was established successfully.' => 'Die Verbindung zur Datenbank wurde erfolgreich hergestellt.',
@@ -2007,9 +2048,13 @@ $self->{texts} = {
   'The custom variable has been saved.' => 'Die benutzerdefinierte Variable wurde gespeichert.',
   'The database #1 has been successfully deleted.' => 'Die Datenbank #1 wurde erfolgreich gelöscht.',
   'The database for user management and authentication does not exist. You can create let kivitendo create it with the following parameters:' => 'Die Datenbank für die Benutzeranmeldung existiert nicht. Sie können Sie von kivitendo automatisch mit den folgenden Parametern anlegen lassen:',
+  'The database host is missing.' => 'Der Datenbankhost fehlt.',
+  'The database name is missing.' => 'Der Datenbankname fehlt.',
+  'The database port is missing.' => 'Der Datenbankport fehlt.',
   'The database update/creation did not succeed. The file #1 contained the following error:' => 'Die Datenbankaktualisierung/erstellung schlug fehl. Die Datei #1 enthielt den folgenden Fehler:',
   'The database upgrade for the introduction of Buchungsgruppen is now complete.' => 'Das Datenbankupgrade f&uuml;r die Einf&uuml;hrung von Buchungsgruppen ist jetzt beendet.',
   'The database upgrade for the introduction of units is now complete.' => 'Das Datenbankupgrade zwecks Einf&uuml;hrung von Einheiten ist nun beendet.',
+  'The database user is missing.' => 'Der Datenbankbenutzer fehlt.',
   'The dataset #1 has been successfully created.' => 'Die Datenbank #1 wurde erfolgreich angelegt.',
   'The dataset backup has been sent via email to #1.' => 'Die Datenbanksicherung wurde per Email an #1 verschickt.',
   'The dataset has to exist before a restoration can be started.' => 'Die Datenbank muss vor der Wiederherstellung bereits angelegt worden sein.',
@@ -2022,6 +2067,7 @@ $self->{texts} = {
   'The department has been saved.' => 'Die abteilung wurde gespeichert.',
   'The department is in use and cannot be deleted.' => 'Die Abteilung wird benutzt und kann nicht gelöscht werden.',
   'The description is missing.' => 'Die Beschreibung fehlt.',
+  'The description is not unique.' => 'Die Beschreibung ist nicht eindeutig.',
   'The description is shown on the form. Chose something short and descriptive.' => 'Die Beschreibung wird in der jeweiligen Maske angezeigt. Sie sollte kurz und pr&auml;gnant sein.',
   'The directory %s does not exist.' => 'Das Verzeichnis %s existiert nicht.',
   'The discount in percent'     => 'Der prozentuale Rabatt',
@@ -2042,27 +2088,29 @@ $self->{texts} = {
   'The following Datasets need to be updated' => 'Folgende Datenbanken müssen aktualisiert werden',
   'The following currencies have been used, but they are not defined:' => 'Die folgenden Währungen wurden benutzt, sind aber nicht ordnungsgemäß in der Datenbank eingetragen:',
   'The following drafts have been saved and can be loaded.' => 'Die folgenden Entw&uuml;rfe wurden gespeichert und k&ouml;nnen geladen werden.',
+  'The following groups are valid for this client' => 'Die folgenden Gruppen sind für diesen Mandanten gültig',
+  'The following list has been generated automatically from existing users collapsing users with identical settings into a single entry.' => 'Die folgende Liste wurde automatisch aus den im System vorhandenen Benutzern zusammengestellt, wobei identische Einstellungen zu einem Eintrag zusammengefasst wurden.',
   'The following old files whose settings have to be merged manually into the new configuration file "config/kivitendo.conf" still exist:' => 'Es existieren noch die folgenden alten Dateien, deren Einstellungen manuell in die neue Konfiguratsdatei "config/kivitendo.conf" migriert werden müssen:',
   'The following transaction contains wrong taxes:' => 'Die folgende Buchung enthält falsche Steuern:',
   'The following transaction contains wrong taxkeys:' => 'Die folgende Buchung enthält falsche Steuerschlüssel:',
   'The following transactions are concerned:' => 'Die folgenden Buchungen sind betroffen:',
   'The following units are unknown.' => 'Die folgenden Einheiten sind unbekannt.',
   'The following units exist already:' => 'Die folgenden Einheiten existieren bereits:',
+  'The following users are a member of this group' => 'Die folgenden Benutzer sind Mitglieder dieser Gruppe',
+  'The following users will have access to this client' => 'Die folgenden Benutzer werden auf diesen Mandanten Zugriff haben',
   'The following warnings occured during an upgrade to the document templates:' => 'Die folgenden Warnungen traten w&auml;hrend einer Aktualisierung der Dokumentenvorlagen auf:',
   'The formula needs the following syntax:<br>For regular article:<br>Variablename= Variable Unit;<br>Variablename2= Variable2 Unit2;<br>...<br>###<br>Variable + ( Variable2 / Variable )<br><b>Please be beware of the spaces in the formula</b><br>' => 'Die Formeln m&uuml;ssen in der folgenden Syntax eingegeben werden:<br>Bei normalen Artikeln:<br>Variablenname = Variable Einheit;<br>Variablenname2 = Variable2 Einheit2;<br>...<br>###<br>Variable + Variable2 * ( Variable - Variable2 )<br>Variablennamen und Einheiten dürfen nur aus alphanumerischen Zeichen bestehen.<br>Es muss jeweils die Gesamte Zeile eingegeben werden',
   'The greetings have been saved.' => 'Die Anreden wurden gespeichert',
-  'The group has been added.'   => 'Die Gruppe wurde erfasst.',
-  'The group has been deleted.' => 'Die Gruppe wurde gel&ouml;scht.',
-  'The group has been saved.'   => 'Die Gruppe wurde gespeichert.',
-  'The group memberships have been saved.' => 'Die Gruppenmitgliedschaften wurden gespeichert.',
-  'The group name is missing.'  => 'Der Gruppenname fehlt.',
   'The items are imported accoring do their number "X" regardless of the column order inside the file.' => 'Die Einträge werden in der Reihenfolge ihrer Indizes "X" unabhängig von der Spaltenreihenfolge in der Datei importiert.',
   'The link target to add has been created from the existing record.' => 'Das auszuwählende Verknüpfungsziel wurde aus dem bestehenden Beleg erstellt.',
   'The list has been printed.'  => 'Die Liste wurde ausgedruckt.',
+  'The login is missing.'       => 'Der Loginname fehlt.',
+  'The login is not unique.'    => 'Der Loginname ist nicht eindeutig.',
   'The long description is missing.' => 'Der Langtext fehlt.',
   'The name in row %d has already been used before.' => 'Der Name in Zeile %d wurde vorher bereits benutzt.',
   'The name is missing in row %d.' => 'Der Name fehlt in Zeile %d.',
   'The name is missing.'        => 'Der Name fehlt.',
+  'The name is not unique.'     => 'Der Name ist nicht eindeutig.',
   'The name must only consist of letters, numbers and underscores and start with a letter.' => 'Der Name darf nur aus Buchstaben (keine Umlaute), Ziffern und Unterstrichen bestehen und muss mit einem Buchstaben beginnen.',
   'The number of days for full payment' => 'Die Anzahl Tage, bis die Rechnung in voller Höhe bezahlt werden muss',
   'The option field is empty.'  => 'Das Optionsfeld ist leer.',
@@ -2083,6 +2131,10 @@ $self->{texts} = {
   'The pg_dump process could not be started.' => 'Der pg_dump-Prozess konnte nicht gestartet werden.',
   'The pg_restore process could not be started.' => 'Der pg_restore-Prozess konnte nicht gestartet werden.',
   'The preferred one is to install packages provided by your operating system distribution (e.g. Debian or RPM packages).' => 'Die bevorzugte Art, ein Perl-Modul zu installieren, ist durch Installation eines von Ihrem Betriebssystem zur Verf&uuml;gung gestellten Paketes (z.B. Debian-Pakete oder RPM).',
+  'The printer could not be deleted.' => 'Der Drucker konnte nicht gelöscht werden.',
+  'The printer has been created.' => 'Der Drucker wurde angelegt.',
+  'The printer has been deleted.' => 'Der Drucker wurde entfernt.',
+  'The printer has been saved.' => 'Der Drucker wurde gespeichert.',
   'The profile \'#1\' has been deleted.' => 'Das Profil \'#1\' wurde gelöscht.',
   'The profile has been saved under the name \'#1\'.' => 'Das Profil wurde unter dem Namen \'#1\' gespeichert.',
   'The program\'s exit code was #1 (&quot;0&quot; usually means that everything went OK).' => 'Der Exitcode des Programms war #1 (&quot;0&quot; bedeutet normalerweise, dass die Wiederherstellung erfolgreich war).',
@@ -2123,7 +2175,15 @@ $self->{texts} = {
   'The unit in row %d has been deleted in the meantime.' => 'Die Einheit in Zeile %d ist in der Zwischentzeit gel&ouml;scht worden.',
   'The unit in row %d has been used in the meantime and cannot be changed anymore.' => 'Die Einheit in Zeile %d wurde in der Zwischenzeit benutzt und kann nicht mehr ge&auml;ndert werden.',
   'The units have been saved.'  => 'Die Einheiten wurden gespeichert.',
-  'The user is a member in the following group(s):' => 'Der Benutzer ist Mitglied in den folgenden Gruppen:',
+  'The user can chose which client to connect to during login.' => 'Bei der Anmeldung kann der Benutzer auswählen, welchen Mandanten er benutzen möchte.',
+  'The user could not be deleted.' => 'Der Benutzer konnte nicht gelöscht werden.',
+  'The user group could not be deleted.' => 'Die Benutzergurppe konnte nicht gelöscht werden.',
+  'The user group has been created.' => 'Die Benutzergruppe wurde erstellt.',
+  'The user group has been deleted.' => 'Die Benutzergruppe wurde gelöscht.',
+  'The user group has been saved.' => 'Die Benutzergruppe wurde gespeichert.',
+  'The user has been created.'  => 'Der Benutzer wurde angelegt.',
+  'The user has been deleted.'  => 'Der Benutzer wurde gelöscht.',
+  'The user has been saved.'    => 'Der Benutzer wurde gespeichert.',
   'The variable name must only consist of letters, numbers and underscores. It must begin with a letter. Example: send_christmas_present' => 'Der Variablenname darf nur aus Zeichen (keine Umlaute), Ziffern und Unterstrichen bestehen. Er muss mit einem Buchstaben beginnen. Beispiel: weihnachtsgruss_verschicken',
   'The warehouse could not be deleted because it has already been used.' => 'Das Lager konnte nicht gel&ouml;scht werden, da es bereits in Benutzung war.',
   'The warehouse does not contain any bins.' => 'Das Lager enth&auml;lt keine Lagerpl&auml;tze.',
@@ -2160,6 +2220,7 @@ $self->{texts} = {
   'There is nothing to do in this step.' => 'In diesem Schritt gibt es nichts mehr zu tun.',
   'There was an error executing the background job.' => 'Bei der Ausführung des Hintergrund-Jobs trat ein Fehler auf.',
   'There was an error parsing the csv file: #1 in line #2.' => 'Es gab einen Fehler beim Parsen der CSV Datei: "#1" in der Zeile "#2"',
+  'Therefore several settings that had to be made for each user in the past have been consolidated into the client configuration.' => 'Dazu wurden gewisse Einstellungen, die vorher bei jedem Benutzer vorgenommen werden mussten, in die Konfiguration eines Mandanten verschoben.',
   'Therefore the definition of "kg" with the base unit "g" and a factor of 1000 is valid while defining "g" with a base unit of "kg" and a factor of "0.001" is not.' => 'So ist die Definition von "kg" mit der Basiseinheit "g" und dem Faktor 1000 zulässig, die Definition von "g" mit der Basiseinheit "kg" und dem Faktor "0,001" hingegen nicht.',
   'Therefore there\'s no need to create the same article more than once if it is sold or bought in/from another tax zone.' => 'Deswegen muss man den gleichen Artikel nicht mehr mehrmals anlegen, wenn er in verschiedenen Steuerzonen gehandelt werden soll.',
   'These units can be based on other units so that kivitendo can convert prices when the user switches from one unit to another.' => 'Einheiten können auf anderen Einheiten basieren, sodass kivitendo Preise automatisch umrechnen kann, wenn die Benutzer zwischen solchen Einheiten umschalten.',
@@ -2168,11 +2229,13 @@ $self->{texts} = {
   'This could have happened for two reasons:' => 'Dies kann aus zwei Gründen geschehen sein:',
   'This customer number is already in use.' => 'Diese Kundennummer wird bereits verwendet.',
   'This feature especially prevents mistakes by mixing up prior tax and sales tax.' => 'Dieses Feature vermeidet insbesondere Verwechslungen von Umsatz- und Vorsteuer.',
+  'This group is valid for the following clients' => 'Diese Gruppe ist für die folgenden Mandanten gültig',
   'This has been changed in this version, therefore please change the "old" bins to some real warehouse bins.' => 'Das wurde in dieser Version umgestellt, bitte Ã¤ndern Sie die Freitext-Lagerplätze auf vorhandene Lagerplätze.',
   'This has been changed in this version.' => 'Ab dieser Version ist dies nicht mehr so.',
   'This installation uses an unknown chart of accounts (&quot;#1&quot;). This database upgrade cannot create standard buchungsgruppen automatically.' => 'Diese Installation benutzt einen unbekannten Kontenrahmen (&quot;#1&quot;). Dieses Datenbankupgrade kann die Standardbuchungsgruppen nicht automatisch anlegen.',
   'This is a preliminary check for existing sources. Nothing will be created or deleted at this stage!' => 'In diesem Schritt werden bestehende Datenbanken gesucht. Es werden noch keine &Auml;nderungen vorgenommen!',
   'This is a very critical problem.' => 'Dieses Problem ist sehr schwerwiegend.',
+  'This is the client to be selected by default on the login screen.' => 'Dies ist derjenige Mandant, der im Loginbildschirm standardmäßig ausgewählt sein wird.',
   'This is the default bin for ignoring onhand' => 'Standardlagerplatz für Auslagern ohne Bestandsprüfung',
   'This is the default bin for parts' => 'Standard-Lagerplatz für Stammdaten/Waren',
   'This list is capped at 15 items to keep it fast. If you need a full list, please use reports.' => 'Diese Liste ist auf 15 Zeilen begrenzt. Wenn Sie eine vollständige Liste benötigen, erstellen Sie bitte einen Bericht.',
@@ -2187,6 +2250,8 @@ $self->{texts} = {
   'This update will change the nature the onhand of goods is tracked.' => 'Dieses update &auml;ndert die Art und Weise wie Lagermengen gez&auml;lt werden.',
   'This upgrade script tries to map all existing parts in the database to the newly created Buchungsgruppen.' => 'Dieses Upgradescript versucht, bei allen bestehenden Artikeln neu erstellte Buchungsgruppen zuzuordnen.',
   'This upgrade script tries to map all existing units in the database to the newly created units.' => 'Dieses Update-Script versucht, alle bestehenden Einheiten automatisch in die neuen Einheiten umzuwandeln.',
+  'This user is a member in the following groups' => 'Dieser Benutzer ist Mitglied in den folgenden Gruppen',
+  'This user will have access to the following clients' => 'Dieser Benutzer wird Zugriff auf die folgenden Mandanten haben',
   'This vendor number is already in use.' => 'Diese Lieferantennummer wird bereits verwendet.',
   'Three Options:'              => 'Drei Optionen:',
   'Time period for the analysis:' => 'Analysezeitraum:',
@@ -2196,8 +2261,8 @@ $self->{texts} = {
   'To (email)'                  => 'An',
   'To (time)'                   => 'Bis',
   'To Date'                     => 'Bis',
-  'To add a user to a group edit a name, change the login name and save.  A new user with the same variables will then be saved under the new login name.' => 'Um einer Gruppe einen neuen Benutzer hinzuzufügen, Ã¤ndern und speichern Sie am einfachsten einen bestehenden Benutzernamen. Unter dem neuen Namen wird dann ein Benutzer mit denselben Einstellungen angelegt.',
   'To continue please change the taxkey 0 to another value.' => 'Um fortzufahren, Ã¤ndern Sie bitte den Steuerschlüssel 0 auf einen anderen Wert.',
+  'To user login'               => 'Zum Benutzerlogin',
   'Top'                         => 'Oben',
   'Top (CSS)'                   => 'Oben (mit CSS)',
   'Top (Javascript)'            => 'Oben (mit Javascript)',
@@ -2287,20 +2352,22 @@ $self->{texts} = {
   'Updated'                     => 'Erneuert am',
   'Updating existing entry in database' => 'Existierenden Eintrag in Datenbank aktualisieren',
   'Updating prices of existing entry in database' => 'Preis des Eintrags in der Datenbank wird aktualisiert',
+  'Updating the client fields in the database "#1" on host "#2:#3" failed.' => 'Die Aktualisierung der Mandantenfelder in der Datenbank "#1" auf Host "#2:#3" schlug fehl.',
   'Uploaded on #1, size #2 kB'  => 'Am #1 hochgeladen, Größe #2 kB',
   'Use As New'                  => 'Als neu verwenden',
-  'Use Templates'               => 'Benutze Vorlagen',
   'Use master default bin for Default Transfer, if no default bin for the part is configured' => 'Standardlagerplatz für Ein- / Auslagern Ã¼ber Standard-Lagerplatz, falls für die Ware kein expliziter Lagerplatz konfiguriert ist',
   'User'                        => 'Benutzer',
   'User Config'                 => 'Einstellungen',
-  'User Login'                  => 'Als Benutzer anmelden',
-  'User deleted!'               => 'Benutzer gelöscht!',
+  'User access'                 => 'Benutzerzugriff',
+  'User list'                   => 'Benutzerliste',
   'User login'                  => 'Benutzeranmeldung',
   'User name'                   => 'Benutzername',
-  'User saved!'                 => 'Benutzer gespeichert!',
   'Username'                    => 'Benutzername',
-  'Users in this group'         => 'BenutzerInnen in dieser Gruppe',
-  'Ust-IDNr'                    => 'USt-IdNr.',
+  'Users that are a member in this group' => 'Gruppenmitglieder',
+  'Users that have access to this client' => 'Benutzer mit Zugriff auf diesen Mandaten',
+  'Users with access'           => 'Benutzer mit Zugriff',
+  'Users with access to this client' => 'Benutzer mit Zugriff auf diesen Mandanten',
+  'VAT ID'                      => 'UStdID-Nr',
   'Valid'                       => 'Gültig',
   'Valid from'                  => 'Gültig ab',
   'Valid until'                 => 'gültig bis',
@@ -2355,6 +2422,7 @@ $self->{texts} = {
   'What type of item is this?'  => 'Was ist dieser Artikel?',
   'Which is located at doc/kivitendo-Dokumentation.pdf. Click here: ' => 'Diese befindet sich unter doc/kivitendo-Dokumentation.pdf. Klicken Sie hier:',
   'With Extension Of Time'      => 'mit Dauerfristverlängerung',
+  'With the introduction of clients each client gets its own WebDAV folder.' => 'Mit der Einführung von Mandanten erhält jeder Mandant sein eigenes WebDAV-Verzeichnis.',
   'Workflow Delivery Order'     => 'Workflow Lieferschein',
   'Workflow purchase_order'     => 'Workflow Lieferantenauftrag',
   'Workflow request_quotation'  => 'Workflow Preisanfrage',
@@ -2403,6 +2471,7 @@ $self->{texts} = {
   'You have to enter a company name in your user preferences (see the "Program" menu, "Preferences").' => 'Sie müssen einen Firmennamen in Ihren Einstellungen angeben (siehe Menü "Programm", "Einstellungen").',
   'You have to enter the SEPA creditor ID in your user preferences (see the "Program" menu, "Preferences").' => 'Sie müssen die SEPA-Kreditoren-Identifikation in Ihren Einstellungen angeben (siehe Menü "Programm", "Einstellungen").',
   'You have to fill in at least an account number, the bank code, the IBAN and the BIC.' => 'Sie müssen zumindest die Kontonummer, die Bankleitzahl, die IBAN und den BIC angeben.',
+  'You have to grant users access to one or more clients.' => 'Benutzern muss dann Zugriff auf einzelne Mandanten gewährt werden.',
   'You have to specify a department.' => 'Sie müssen eine Abteilung wählen.',
   'You have to specify an execution date for each antry.' => 'Sie müssen für jeden zu buchenden Eintrag ein Ausführungsdatum angeben.',
   'You must chose a user.'      => 'Sie m&uuml;ssen einen Benutzer ausw&auml;hlen.',
@@ -2504,12 +2573,13 @@ $self->{texts} = {
   'invoice_list'                => 'debitorenbuchungsliste',
   'kivitendo'                   => 'kivitendo',
   'kivitendo Homepage'          => 'Infos zu kivitendo',
-  'kivitendo administration'    => 'kivitendo Administration',
   'kivitendo can fix these problems automatically.' => 'kivitendo kann solche Probleme automatisch beheben.',
+  'kivitendo has been extended to handle multiple clients within a single installation.' => 'kivitendo wurde um Mandantenfähigkeit erweitert.',
   'kivitendo has found one or more problems in the general ledger.' => 'kivitendo hat ein oder mehrere Probleme im Hauptbuch gefunden.',
   'kivitendo is about to update the database [ #1 ].' => 'kivitendo wird gleich die Datenbank [ #1 ] aktualisieren.',
   'kivitendo is now able to manage warehouses instead of just tracking the amount of goods in your system.' => 'kivitendo enth&auml;lt jetzt auch echte Lagerverwaultung anstatt reiner Mengenz&auml;hlung.',
   'kivitendo needs to update the authentication database before you can proceed.' => 'kivitendo muss die Authentifizierungsdatenbank aktualisieren, bevor Sie fortfahren können.',
+  'kivitendo v#1 administration' => 'kivitendo v#1 Administration',
   'kivitendo website (external)' => 'kivitendo-Webseite (extern)',
   'kivitendo will then update the database automatically.' => 'kivitendo wird die Datenbank daraufhin automatisch aktualisieren.',
   'lead deleted!'               => 'Kundenquelle gelöscht',
@@ -2534,7 +2604,6 @@ $self->{texts} = {
   'not configured'              => 'nicht konfiguriert',
   'not delivered'               => 'nicht geliefert',
   'not executed'                => 'nicht ausgeführt',
-  'not logged in'               => 'nicht eingeloggt',
   'not running'                 => 'läuft nicht',
   'not set'                     => 'nicht gesetzt',
   'not shipped'                 => 'nicht geliefert',
@@ -2614,6 +2683,7 @@ $self->{texts} = {
   'transferred in'              => 'eingelagert',
   'transferred out'             => 'ausgelagert',
   'trial_balance'               => 'susa',
+  'unconfigured'                => 'unkonfiguriert',
   'up'                          => 'hoch',
   'use program settings'        => 'benutze Programmeinstellungen',
   'use user config'             => 'Verwende Benutzereinstellung',
index f066039..3f69ac7 100644 (file)
--- a/menu.ini
+++ b/menu.ini
@@ -503,9 +503,10 @@ action=search
 [System]
 ACCESS=config
 
-[System--Ranges of numbers and default accounts]
-module=am.pl
-action=edit_defaults
+[System--Client Configuration]
+ACCESS=admin
+module=controller.pl
+action=ClientConfig/edit
 
 [System--UStVa Einstellungen]
 module=ustva.pl
@@ -688,11 +689,6 @@ action=audit_control
 module=am.pl
 action=show_history_search
 
-[System--Client Configuration]
-ACCESS=admin
-module=controller.pl
-action=ClientConfig/edit
-
 [System--Employees]
 ACCESS=admin
 module=controller.pl
index 9580e06..abc87c7 100644 (file)
@@ -43,8 +43,8 @@ sub prepare_db {
             return;
         }
 
-        # if this a dummy lx office dbh, don't try to actually prepare this.
-        if ($db->type =~ /LXOFFICE_EMPTY/) {
+        # if this a dummy kivitendo dbh, don't try to actually prepare this.
+        if ($db->type =~ /KIVITENDO_EMPTY/) {
           return;
         }
 
index 32bebab..8ce9783 100755 (executable)
@@ -19,6 +19,7 @@ use Term::ReadLine::Perl::Bind;     # use sane key binding for rxvt users
 use SL::LxOfficeConf;
 SL::LxOfficeConf->read;
 
+my $client       = $::lx_office_conf{console}{client};
 my $login        = $::lx_office_conf{console}{login}        || 'demo';
 my $history_file = $::lx_office_conf{console}{history_file} || '/tmp/kivitendo_console_history.log'; # fallback if users is not writable
 my $debug_file   = $::lx_office_conf{console}{log_file}     || '/tmp/kivitendo_console_debug.log';
@@ -27,6 +28,7 @@ my ($execute_code, $execute_file, $help, $man);
 
 my $result = GetOptions(
   "login|l=s"        => \$login,
+  "client|c=s"       => \$client,
   "history-file|i=s" => \$history_file,
   "log-file|o=s"     => \$debug_file,
   "execute|e=s"      => \$execute_code,
@@ -61,8 +63,8 @@ my $repl = Devel::REPL->new;
 $repl->load_plugin($_) for @plugins;
 $repl->load_history($history_file);
 $repl->eval('help');
-$repl->print("trying to auto login as '$login'...");
-$repl->print($repl->eval("lxinit '$login'"));
+$repl->print("trying to auto login into client '$client' with login '$login'...\n");
+execute_code($repl, "lxinit '$client', '$login'");
 
 my @code_to_execute = grep { $_ } ($autorun, $execute_code, $execute_file ? join('', read_file($execute_file)) : undef);
 execute_code($repl, $_) || exit 1 for @code_to_execute;
@@ -89,9 +91,9 @@ use List::Util qw(max);
 # it is assumed that anyone with physical access and execution rights on this script
 # won't be hindered by authentication anyway.
 sub lxinit {
-  my $login = shift;
+  my ($client, $login) = @_;
 
-  die 'need login' unless $login;
+  die 'need client and login' unless $client && $login;
 
   package main;
 
@@ -99,6 +101,8 @@ sub lxinit {
   $::locale        = Locale->new($::lx_office_conf{system}->{language});
   $::form          = Form->new;
   $::auth          = SL::Auth->new;
+  die "Cannot find client with ID or name '$client'" if !$::auth->set_client($client);
+
   $::instance_conf = SL::InstanceConfiguration->new;
   $::request       = SL::Request->new(
     cgi    => CGI->new({}),
index 91aac28..3b9faf9 100755 (executable)
@@ -42,7 +42,7 @@ use SL::Dispatcher;
 #######
 
 my ($opt_list, $opt_tree, $opt_rtree, $opt_nodeps, $opt_graphviz, $opt_help);
-my ($opt_user, $opt_apply, $opt_applied, $opt_unapplied, $opt_format, $opt_test_utf8);
+my ($opt_user, $opt_client, $opt_apply, $opt_applied, $opt_unapplied, $opt_format, $opt_test_utf8);
 my ($opt_dbhost, $opt_dbport, $opt_dbname, $opt_dbuser, $opt_dbpassword, $opt_create, $opt_type);
 my ($opt_description, $opt_encoding, @opt_depends, $opt_auth_db);
 
@@ -103,8 +103,12 @@ dbupgrade2_tool.pl [options]
     --help               Show this help and exit.
 
   General Options:
+    --client=id-or-name  The name (or database ID) of the client to use for
+                         database connectivity. You must provide both a client
+                         and a user.
     --user=name          The name of the user configuration to use for
-                         database connectivity.
+                         database connectivity. You must provide both a client
+                         and a user.
     --auth-db            Work on the authentication database instead of a
                          user database.
     --dbname=name        Database connection options for the UTF-8
@@ -356,9 +360,9 @@ sub apply_upgrade {
     print "Applying upgrade $control->{file}\n";
 
     if ($file_type eq "sql") {
-      $dbupgrader->process_query($dbh, "sql/$form->{dbdriver}-upgrade2/$control->{file}", $control, $db_charset);
+      $dbupgrader->process_query($dbh, "sql/Pg-upgrade2/$control->{file}", $control, $db_charset);
     } else {
-      $dbupgrader->process_perl_script($dbh, "sql/$form->{dbdriver}-upgrade2/$control->{file}", $control, $db_charset);
+      $dbupgrader->process_perl_script($dbh, "sql/Pg-upgrade2/$control->{file}", $control, $db_charset);
     }
   }
 
@@ -474,6 +478,7 @@ GetOptions("list"         => \$opt_list,
            "graphviz:s"   => \$opt_graphviz,
            "format:s"     => \$opt_format,
            "user=s"       => \$opt_user,
+           "client=s"     => \$opt_client,
            "apply=s"      => \$opt_apply,
            "applied"      => \$opt_applied,
            "create=s"     => \$opt_create,
@@ -494,7 +499,7 @@ GetOptions("list"         => \$opt_list,
 
 show_help() if ($opt_help);
 
-$dbupgrader = SL::DBUpgrade2->new(form => $form, dbdriver => 'Pg', auth => $opt_auth_db);
+$dbupgrader = SL::DBUpgrade2->new(form => $form, auth => $opt_auth_db);
 $controls   = $dbupgrader->parse_dbupdate_controls->{all_controls};
 
 dump_list()                                 if ($opt_list);
@@ -510,7 +515,13 @@ create_upgrade(filename   => $opt_create,
                encoding    => $opt_encoding,
                depends     => \@opt_depends) if ($opt_create);
 
+if ($opt_client && !connect_auth()->set_client($opt_client)) {
+  $form->error($form->format_string("The client '#1' does not exist.", $opt_client));
+}
+
 if ($opt_user) {
+  $form->error("Need a client, too.") if !$auth || !$auth->client;
+
   %myconfig = connect_auth()->read_user(login => $opt_user);
 
   if (!$myconfig{login}) {
index 9e6a4ae..53c3af5 100755 (executable)
@@ -238,15 +238,20 @@ EOL
 sub parse_args {
   my ($help, $man);
 
+  my ($opt_no_c, $ignore_for_compatiblity);
+
   GetOptions(
     'no-custom-files' => \$opt_n,
-    'check-files'     => \$opt_c,
+    'check-files'     => \$ignore_for_compatiblity,
+    'no-check-files'  => \$opt_no_c,
     'verbose'         => \$opt_v,
     'help'            => \$help,
     'man'             => \$man,
     'debug'           => \$debug,
   );
 
+  $opt_c = !$opt_no_c;
+
   if ($help) {
     pod2usage(1);
     exit 0;
index 1850f45..d2b6886 100755 (executable)
@@ -60,24 +60,23 @@ sub setup {
 
   SL::LxOfficeConf->read;
 
-  my $login     = $config{login} || $::lx_office_conf{devel}{login};
+  my $client = $config{client} || $::lx_office_conf{devel}{client};
 
-  if (!$login) {
-    error("No login found in config. Please provide a login:");
+  if (!$client) {
+    error("No client found in config. Please provide a client:");
     usage();
   }
 
   $::lxdebug      = LXDebug->new();
   $::locale       = Locale->new("de");
   $::form         = new Form;
-  $::auth         = SL::Auth->new();
-  $::user         = User->new(login => $login);
-  %::myconfig     = $auth->read_user(login => $login);
-  $::request      = { cgi => CGI->new({}) };
   $form->{script} = 'rose_meta_data.pl';
-  $form->{login}  = $login;
+  $::auth         = SL::Auth->new();
 
-  map { $form->{$_} = $::myconfig{$_} } qw(stylesheet charset);
+  if (!$::auth->set_client($client)) {
+    error("No client with ID or name '$client' found in config. Please provide a client:");
+    usage();
+  }
 
   mkdir $meta_path unless -d $meta_path;
 }
@@ -92,7 +91,7 @@ sub process_table {
   my $meta_file  =  "${meta_path}/${package}.pm";
   my $file       =  "SL/DB/${package}.pm";
 
-  $schema        = <<CODE if $schema;
+  my $schema_str = $schema ? <<CODE : '';
 __PACKAGE__->meta->schema('$schema');
 CODE
 
@@ -102,7 +101,7 @@ CODE
     use base qw(SL::DB::Object);
 
     __PACKAGE__->meta->table('$table');
-    $schema
+    $schema_str
     __PACKAGE__->meta->auto_initialize;
 
     __PACKAGE__->meta->perl_class_definition(indent => 2); # , braces => 'bsd'
@@ -110,7 +109,7 @@ CODE
 
   if ($EVAL_ERROR) {
     error("Error in execution for table '$table'");
-    error("'$EVAL_ERROR'") if $config{verbose};
+    error("'$EVAL_ERROR'") unless $config{quiet};
     return;
   }
 
@@ -120,6 +119,8 @@ CODE
     $definition =~ s/( foreign_keys \s*=> \s*\[ .* ^\s+ ) ${auto_generated_name} \b/${1}${desired_name}/msx;
   }
 
+  $definition =~ s/(table\s*=>.*?\n)/$1  schema  => '${schema}',\n/ if $schema;
+
   my $full_definition = <<CODE;
 # This file has been auto-generated. Do not modify it; it will be overwritten
 # by $::script automatically.
@@ -137,7 +138,6 @@ use strict;
 use SL::DB::MetaSetup::${package};
 
 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
-$schema
 __PACKAGE__->meta->make_manager_class;
 
 1;
@@ -151,7 +151,7 @@ CODE
     my $new_size    = length $full_definition;
     my $new_md5     = md5_hex($full_definition);
     if ($old_size == $new_size && $old_md5 == $new_md5) {
-      notice("No changes in $meta_file, skipping.") if $config{verbose};
+      notice("No changes in $meta_file, skipping.") unless $config{quiet};
       return;
     }
 
@@ -178,18 +178,18 @@ CODE
 sub parse_args {
   my ($options) = @_;
   GetOptions(
-    'login|user=s'      => \ my $login,
+    'client=s'          => \ my $client,
     all                 => \ my $all,
     'no-commit|dry-run' => \ my $nocommit,
     help                => sub { pod2usage(verbose => 99, sections => 'NAME|SYNOPSIS|OPTIONS') },
-    verbose             => \ my $verbose,
+    quiet               => \ my $quiet,
     diff                => \ my $diff,
   );
 
-  $options->{login}    = $login if $login;
+  $options->{client}   = $client;
   $options->{all}      = $all;
   $options->{nocommit} = $nocommit;
-  $options->{verbose}  = $verbose;
+  $options->{quiet}    = $quiet;
 
   if ($diff) {
     if (eval { require Text::Diff; 1 }) {
@@ -222,10 +222,10 @@ sub usage {
 sub make_tables {
   my @tables;
   if ($config{all}) {
-    my $db  = SL::DB::create(undef, 'LXOFFICE');
+    my $db  = SL::DB::create(undef, 'KIVITENDO');
     @tables =
-      map { $package_names{LXOFFICE}->{$_} ? "$_=" . $package_names{LXOFFICE}->{$_} : $_ }
-      grep { my $table = $_; !any { $_ eq $table } @{ $blacklist{LXOFFICE} } }
+      map { $package_names{KIVITENDO}->{$_} ? "$_=" . $package_names{KIVITENDO}->{$_} : $_ }
+      grep { my $table = $_; !any { $_ eq $table } @{ $blacklist{KIVITENDO} } }
       $db->list_tables;
   } elsif (@ARGV) {
     @tables = @ARGV;
@@ -251,7 +251,7 @@ my @tables = make_tables();
 
 for my $table (@tables) {
   # add default model name unless model name is given or no defaults exists
-  $table .= '=' . $package_names{LXOFFICE}->{lc $table} if $table !~ /=/ && $package_names{LXOFFICE}->{lc $table};
+  $table .= '=' . $package_names{KIVITENDO}->{lc $table} if $table !~ /=/ && $package_names{KIVITENDO}->{lc $table};
 
   process_table($table);
 }
@@ -268,11 +268,11 @@ rose_auto_create_model - mana Rose::DB::Object classes for kivitendo
 
 =head1 SYNOPSIS
 
-  scripts/rose_create_model.pl --login login table1[=package1] [table2[=package2] ...]
-  scripts/rose_create_model.pl --login login [--all|-a]
+  scripts/rose_create_model.pl --client name-or-id table1[=package1] [table2[=package2] ...]
+  scripts/rose_create_model.pl --client name-or-id [--all|-a]
 
   # updates all models
-  scripts/rose_create_model.pl --login login --all
+  scripts/rose_create_model.pl --client name-or-id --all
 
   # updates only customer table, login taken from config
   scripts/rose_create_model.pl customer
@@ -281,7 +281,7 @@ rose_auto_create_model - mana Rose::DB::Object classes for kivitendo
   scripts/rose_create_model.pl parts=Part
 
   # try to update parts, but don't do it. tell what would happen in detail
-  scripts/rose_create_model.pl --no-commit --verbose parts
+  scripts/rose_create_model.pl --no-commit parts
 
 =head1 DESCRIPTION
 
@@ -327,12 +327,13 @@ created if it does not exist.
 
 =over 4
 
-=item C<--login, -l LOGIN>
+=item C<--client, -c CLIENT>
 
-=item C<--user, -u LOGIN>
+Provide a client whose database settings are used. If not present the
+client is loaded from the config key C<devel/client>. If that too is
+not found, an error is thrown.
 
-Provide a login. If not present the login is loaded from the config key
-C<devel/login>. If that too is not found, an error is thrown.
+Note that C<CLIENT> can be either a database ID or a client's name.
 
 =item C<--all, -a>
 
@@ -355,10 +356,10 @@ different. Beware, does not imply C<--no-commit>.
 
 Print this help.
 
-=item C<--verbose, -v>
+=item C<--quiet, -q>
 
-Prints extra information, such as skipped files that were not changed and
-errors where the auto initialization failed.
+Does not print extra information, such as skipped files that were not
+changed and errors where the auto initialization failed.
 
 =back
 
index 3a45dd1..c8ad935 100755 (executable)
@@ -41,7 +41,8 @@ use SL::System::TaskServer;
 our %lx_office_conf;
 
 sub lxinit {
-  my $login = $lx_office_conf{task_server}->{login};
+  my $login  = $lx_office_conf{task_server}->{login};
+  my $client = $lx_office_conf{task_server}->{client};
 
   package main;
 
@@ -49,6 +50,7 @@ sub lxinit {
   $::locale        = Locale->new($::lx_office_conf{system}->{language});
   $::form          = Form->new;
   $::auth          = SL::Auth->new;
+  die "No client configured or no client found with the name/ID '$client'" unless $::auth->set_client($client);
   $::instance_conf = SL::InstanceConfiguration->new;
   $::request       = { cgi => CGI->new({}) };
 
@@ -95,8 +97,9 @@ sub gd_preconfig {
 
   SL::LxOfficeConf->read($self->{configfile});
 
-  die "Missing section [task_server] in config file"                unless $lx_office_conf{task_server};
-  die "Missing key 'login' in section [task_server] in config file" unless $lx_office_conf{task_server}->{login};
+  die "Missing section [task_server] in config file"                 unless $lx_office_conf{task_server};
+  die "Missing key 'login' in section [task_server] in config file"  unless $lx_office_conf{task_server}->{login};
+  die "Missing key 'client' in section [task_server] in config file" unless $lx_office_conf{task_server}->{client};
 
   drop_privileges();
   lxinit();
diff --git a/sql/Pg-upgrade2-auth/clients.pl b/sql/Pg-upgrade2-auth/clients.pl
new file mode 100644 (file)
index 0000000..074a15b
--- /dev/null
@@ -0,0 +1,340 @@
+# @tag: clients
+# @description: Einführung von Mandaten
+# @depends: release_3_0_0
+# @ignore: 0
+package SL::DBUpgrade2::clients;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use List::MoreUtils qw(any all);
+use List::Util qw(first);
+
+use SL::DBConnect;
+use SL::DBUtils;
+use SL::Template;
+use SL::Helper::Flash;
+
+use Rose::Object::MakeMethods::Generic (
+  scalar                  => [ qw(clients) ],
+  'scalar --get_set_init' => [ qw(users groups templates auth_db_settings data_dbhs) ],
+);
+
+sub init_users {
+  my ($self) = @_;
+  my @users  = selectall_hashref_query($::form, $self->dbh, qq|SELECT * FROM auth."user" ORDER BY lower(login)|);
+
+  foreach my $user (@users) {
+    my @attributes = selectall_hashref_query($::form, $self->dbh, <<SQL, $user->{id});
+     SELECT cfg_key, cfg_value
+     FROM auth.user_config
+     WHERE user_id = ?
+SQL
+
+    $user->{ $_->{cfg_key} } = $_->{cfg_value} for @attributes;
+  }
+
+  return \@users;
+}
+
+sub init_groups {
+  my ($self) = @_;
+  return [ selectall_hashref_query($::form, $self->dbh, qq|SELECT * FROM auth."group" ORDER BY lower(name)|) ];
+}
+
+sub init_templates {
+  my %templates = SL::Template->available_templates;
+  return $templates{print_templates};
+}
+
+sub init_auth_db_settings {
+  my $cfg = $::lx_office_conf{'authentication/database'};
+  return {
+    dbhost => $cfg->{host} || 'localhost',
+    dbport => $cfg->{port} || 5432,
+    dbname => $cfg->{name},
+  };
+}
+
+sub init_data_dbhs {
+  return [];
+}
+
+sub _clear_field {
+  my ($text) = @_;
+
+  $text ||= '';
+  $text   =~ s/^\s+|\s+$//g;
+
+  return $text;
+}
+
+sub _group_into_clients {
+  my ($self) = @_;
+
+  my @match_fields = qw(dbhost dbport dbname);
+  my @copy_fields  = (@match_fields, qw(address company co_ustid dbuser dbpasswd duns sepa_creditor_id taxnumber templates));
+  my @clients;
+
+  # Group users into clients. Users which have identical database
+  # settings (host, port and name) will be grouped. The other fields
+  # like tax number etc. are taken from the first user and only filled
+  # from user users if they're still unset.
+  foreach my $user (@{ $self->users }) {
+    $user->{$_} = _clear_field($user->{$_}) for @copy_fields;
+
+    my $existing_client = first { my $client = $_; all { ($user->{$_} || '') eq ($client->{$_} || '') } @match_fields } @clients;
+
+    if ($existing_client) {
+      push @{ $existing_client->{users} }, $user->{id};
+      $existing_client->{$_} ||= $user->{$_} for @copy_fields;
+      next;
+    }
+
+    push @clients, {
+      map({ $_ => $user->{$_} } @copy_fields),
+      name    => $::locale->text('Client #1', scalar(@clients) + 1),
+      users   => [ $user->{id} ],
+      groups  => [ map { $_->{id} } @{ $self->groups } ],
+      enabled => 1,
+    };
+  }
+
+  # Ignore users (and therefore clients) for which no database
+  # configuration has been given.
+  @clients = grep { my $client = $_; any { $client->{$_} } @match_fields } @clients;
+
+  # If there's only one client set that one as default.
+  $clients[0]->{is_default} = 1 if scalar(@clients) == 1;
+
+  # Set a couple of defaults for database fields.
+  my $num = 0;
+  foreach my $client (@clients) {
+    $num                += 1;
+    $client->{dbhost}  ||= 'localhost';
+    $client->{dbport}  ||= 5432;
+    $client->{templates} =~ s:templates/::;
+  }
+
+  $self->clients(\@clients);
+}
+
+sub _analyze {
+  my ($self, %params) = @_;
+
+  $self->_group_into_clients;
+
+  return $self->_do_convert if !@{ $self->clients };
+
+  print $::form->parse_html_template('dbupgrade/auth/clients', { SELF => $self });
+
+  return 2;
+}
+
+sub _verify_clients {
+  my ($self) = @_;
+
+  my (%names, @errors);
+
+  my $num = 0;
+  foreach my $client (@{ $self->clients }) {
+    $num += 1;
+
+    next if !$client->{enabled};
+
+    $client->{$_} = _clear_field($client->{$_}) for qw(address co_ustid company dbhost dbname dbpasswd dbport dbuser duns sepa_creditor_id taxnumber templates);
+
+    if (!$client->{name} || $names{ $client->{name} }) {
+      push @errors, $::locale->text('New client #1: The name must be unique and not empty.', $num);
+    }
+
+    $names{ $client->{name} } = 1;
+
+    if (any { !$client->{$_} } qw(dbhost dbport dbname dbuser)) {
+      push @errors, $::locale->text('New client #1: The database configuration fields "host", "port", "name" and "user" must not be empty.', $num);
+    }
+  }
+
+  return @errors;
+}
+
+sub _alter_auth_database_structure {
+  my ($self) = @_;
+
+  my @queries = (
+    qq|CREATE TABLE auth.clients (
+         id         SERIAL  PRIMARY KEY,
+         name       TEXT    NOT NULL UNIQUE,
+         dbhost     TEXT    NOT NULL,
+         dbport     INTEGER NOT NULL DEFAULT 5432,
+         dbname     TEXT    NOT NULL,
+         dbuser     TEXT    NOT NULL,
+         dbpasswd   TEXT    NOT NULL,
+         is_default BOOLEAN NOT NULL DEFAULT FALSE,
+
+         UNIQUE (dbhost, dbport, dbname)
+       )|,
+    qq|CREATE TABLE auth.clients_users (
+         client_id INTEGER NOT NULL REFERENCES auth.clients (id),
+         user_id   INTEGER NOT NULL REFERENCES auth."user"  (id),
+
+         PRIMARY KEY (client_id, user_id)
+       )|,
+    qq|CREATE TABLE auth.clients_groups (
+         client_id INTEGER NOT NULL REFERENCES auth.clients (id),
+         group_id  INTEGER NOT NULL REFERENCES auth."group" (id),
+
+         PRIMARY KEY (client_id, group_id)
+       )|,
+  );
+
+  $self->db_query($_, may_fail => 0) for @queries;
+}
+
+sub _alter_data_database_structure {
+  my ($self, $dbh) = @_;
+
+  my @queries = (
+    qq|ALTER TABLE defaults ADD COLUMN company          TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN address          TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN taxnumber        TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN co_ustid         TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN duns             TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN sepa_creditor_id TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN templates        TEXT|,
+    qq|INSERT INTO schema_info (tag, login) VALUES ('clients', 'admin')|,
+  );
+
+  foreach my $query (@queries) {
+    $dbh->do($query) || die $self->db_errstr($dbh);
+  }
+}
+
+sub _create_clients_in_auth_database {
+  my ($self)  = @_;
+
+  my @client_columns   = qw(name dbhost dbport dbname dbuser dbpasswd is_default);
+  my $q_client         = qq|INSERT INTO auth.clients (| . join(', ', @client_columns) . qq|) VALUES (| . join(', ', ('?') x @client_columns) . qq|) RETURNING id|;
+  my $sth_client       = $self->dbh->prepare($q_client) || die $self->db_errstr;
+
+  my $q_client_user    = qq|INSERT INTO auth.clients_users (client_id, user_id) VALUES (?, ?)|;
+  my $sth_client_user  = $self->dbh->prepare($q_client_user) || die $self->db_errstr;
+
+  my $q_client_group   = qq|INSERT INTO auth.clients_groups (client_id, group_id) VALUES (?, ?)|;
+  my $sth_client_group = $self->dbh->prepare($q_client_group) || die $self->db_errstr;
+
+  foreach my $client (@{ $self->clients }) {
+    next unless $client->{enabled};
+
+    $client->{is_default} = $client->{is_default} ? 1 : 0;
+
+    $sth_client->execute(@{ $client }{ @client_columns }) || die;
+    my $client_id = $sth_client->fetch->[0];
+
+    $sth_client_user ->execute($client_id, $_) || die for @{ $client->{users}  || [] };
+    $sth_client_group->execute($client_id, $_) || die for @{ $client->{groups} || [] };
+  }
+
+  $sth_client      ->finish;
+  $sth_client_user ->finish;
+  $sth_client_group->finish;
+}
+
+sub _clean_auth_database {
+  my ($self) = @_;
+
+  my @keys_to_delete = qw(acs address admin anfragen angebote bestellungen businessnumber charset companies company co_ustid currency dbconnect dbdriver dbhost dbname dboptions dbpasswd dbport dbuser duns
+                          einkaufsrechnungen in_numberformat lieferantenbestellungen login pdonumber printer rechnungen role sdonumber sepa_creditor_id sid steuernummer taxnumber templates);
+
+  $self->dbh->do(qq|DELETE FROM auth.user_config WHERE cfg_key IN (| . join(', ', ('?') x @keys_to_delete) . qq|)|, undef, @keys_to_delete)
+    || die $self->db_errstr;
+}
+
+sub _copy_fields_to_data_database {
+  my ($self, $client) = @_;
+
+  my $dbh = SL::DBConnect->connect('dbi:Pg:dbname=' . $client->{dbname} . ';host=' . $client->{dbhost} . ';port=' . $client->{dbport},
+                                   $client->{dbuser}, $client->{dbpasswd},
+                                   SL::DBConnect->get_options(AutoCommit => 0));
+  if (!$dbh) {
+    die join("\n",
+             $::locale->text('The connection to the configured client database "#1" on host "#2:#3" failed.', $client->{dbname}, $client->{dbhost}, $client->{dbport}),
+             $::locale->text('Please correct the settings and try again or deactivate that client.'),
+             $::locale->text('Error message from the database: #1', $self->db_errstr('DBI')));
+  }
+
+  my ($has_been_applied) = $dbh->selectrow_array(qq|SELECT tag FROM schema_info WHERE tag = 'clients'|);
+
+  if (!$has_been_applied) {
+    $self->_alter_data_database_structure($dbh);
+  }
+
+  my @columns = qw(company address taxnumber co_ustid duns sepa_creditor_id templates);
+  my $query   = join ', ', map { "$_ = ?" } @columns;
+  my @values  = @{ $client }{ @columns };
+
+  if (!$dbh->do(qq|UPDATE defaults SET $query|, undef, @values)) {
+    die join("\n",
+             $::locale->text('Updating the client fields in the database "#1" on host "#2:#3" failed.', $client->{dbname}, $client->{dbhost}, $client->{dbport}),
+             $::locale->text('Please correct the settings and try again or deactivate that client.'),
+             $::locale->text('Error message from the database: #1', $self->db_errstr('DBI')));
+  }
+
+  $self->data_dbhs([ @{ $self->data_dbhs }, $dbh ]);
+}
+
+sub _commit_data_database_changes {
+  my ($self) = @_;
+
+  foreach my $dbh (@{ $self->data_dbhs }) {
+    $dbh->commit;
+    $dbh->disconnect;
+  }
+}
+
+sub _do_convert {
+  my ($self) = @_;
+
+  # Skip clients that are not enabled. Clean fields.
+  my $num = 0;
+  foreach my $client (@{ $self->clients }) {
+    $num += 1;
+
+    next if !$client->{enabled};
+
+    $client->{$_}        = _clear_field($client->{$_}) for qw(dbhost dbport dbname dbuser dbpasswd address company co_ustid dbuser dbpasswd duns sepa_creditor_id taxnumber templates);
+    $client->{templates} = 'templates/' . $client->{templates};
+  }
+
+  $self->_copy_fields_to_data_database($_) for grep { $_->{enabled} } @{ $self->clients };
+
+  $self->_alter_auth_database_structure;
+  $self->_create_clients_in_auth_database;
+  $self->_clean_auth_database;
+
+  $self->_commit_data_database_changes;
+
+  return 1;
+}
+
+sub run {
+  my ($self) = @_;
+
+  return $self->_analyze if !$::form->{clients} || !@{ $::form->{clients} };
+
+  $self->clients($::form->{clients});
+
+  my @errors = $self->_verify_clients;
+
+  return $self->_do_convert if !@errors;
+
+  flash('error', @errors);
+
+  print $::form->parse_html_template('dbupgrade/auth/clients', { SELF => $self });
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2-auth/clients_webdav.pl b/sql/Pg-upgrade2-auth/clients_webdav.pl
new file mode 100644 (file)
index 0000000..2476e8a
--- /dev/null
@@ -0,0 +1,124 @@
+# @tag: clients_webdav
+# @description: WebDAV-Migration für Mandanten
+# @depends: clients
+# @ignore: 0
+package SL::DBUpgrade2::clients_webdav;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use File::Path qw(make_path);
+use IO::Dir;
+use List::MoreUtils qw(any all);
+use List::Util qw(first);
+
+use SL::DBConnect;
+use SL::DBUtils;
+use SL::Template;
+use SL::Helper::Flash;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(clients old_folders) ],
+);
+
+sub init_clients {
+  my ($self) = @_;
+  return [ selectall_hashref_query($::form, $self->dbh, qq|SELECT * FROM auth.clients ORDER BY lower(name)|) ];
+}
+
+sub init_old_folders {
+  tie my %dir, 'IO::Dir', 'webdav';
+  return [ sort grep { -d } keys %dir ];
+}
+
+sub _unlink_old_folders {
+  my ($self, %params) = @_;
+
+  rmdir $_ for @{ $self->old_folders };
+
+  return 1;
+}
+
+sub _ensure_one_client_exists {
+  my ($self, %params) = @_;
+
+  return if 0 != scalar @{ $self->clients };
+
+  my $sql = <<SQL;
+    INSERT INTO auth.clients (name, dbhost, dbport, dbname, dbuser, dbpasswd, is_default)
+    VALUES                   (?,    ?,      5432,   ?,      ?,      ?,        true)
+SQL
+
+  $self->dbh->do($sql, undef, $::locale->text('Default Client (unconfigured)'), ($::locale->text('unconfigured')) x 4);
+
+  undef $self->{clients};
+}
+
+sub _move_files_into {
+  my ($self, $client) = @_;
+
+  tie my %dir, 'IO::Dir', 'webdav';
+  my @entries = grep { !m/^\.\.?$/ } keys %dir;
+
+  make_path('webdav/' . $client->{id});
+  rename "webdav/$_", "webdav/" . $client->{id} . "/$_" for @entries;
+}
+
+sub _create_folders {
+  my ($self, $client) = @_;
+  make_path('webdav/' . $client->{id} . "/$_") for qw(angebote bestellungen anfragen lieferantenbestellungen verkaufslieferscheine einkaufslieferscheine gutschriften rechnungen einkaufsrechnungen);
+}
+
+sub _create_symlink {
+  my ($self, $client) = @_;
+
+  my $name =  $client->{name} // '';
+  $name    =~ s:/+:_:g;
+
+  make_path('webdav/links');
+  symlink '../' . $client->{id}, "webdav/links/${name}";
+}
+
+sub run {
+  my ($self) = @_;
+
+  # WebDAV not activated? Remove old folders, and we're done.
+  return $self->_unlink_old_folders if !$::lx_office_conf{features}->{webdav};
+
+  # Ensure at least one client exists.
+  $self->_ensure_one_client_exists;
+
+  my $client_to_use;
+  if (1 == scalar @{ $self->clients }) {
+    # Exactly one client? Great, use that one without bothering the
+    # user.
+    $client_to_use = $self->clients->[0];
+
+  } else {
+    # If there's more than one client then let the user select which
+    # client to move the old files into. Maybe she already did?
+    $client_to_use = first { $_->{id} == $::form->{client_id} } @{ $self->clients } if $::form->{client_id};
+
+    if (!$client_to_use) {
+      # Nope, let's select it.
+      print $::form->parse_html_template('dbupgrade/auth/clients_webdav', { SELF => $self, default_client => (first { $_->{is_default} } @{ $self->clients }) });
+      return 2;
+    }
+  }
+
+  # Move files for the selected client.
+  $self->_move_files_into($client_to_use);
+
+  # Create the directory structures for all (even the selected client
+  # -- folders might be missing).
+  for (@{ $self->clients }) {
+    $self->_create_folders($_);
+    $self->_create_symlink($_);
+  }
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2-auth/foreign_key_constraints_on_delete.sql b/sql/Pg-upgrade2-auth/foreign_key_constraints_on_delete.sql
new file mode 100644 (file)
index 0000000..197bd8e
--- /dev/null
@@ -0,0 +1,40 @@
+-- @tag: foreign_key_constraints_on_delete
+-- @description: Ã„ndert "FOREIGN KEY" constraints auf "ON DELETE CASCADE"
+-- @depends: clients
+-- @charset: utf-8
+
+-- auth.clients_groups
+ALTER TABLE auth.clients_groups DROP CONSTRAINT clients_groups_client_id_fkey;
+ALTER TABLE auth.clients_groups DROP CONSTRAINT clients_groups_group_id_fkey;
+
+ALTER TABLE auth.clients_groups ADD FOREIGN KEY (client_id) REFERENCES auth.clients (id) ON DELETE CASCADE;
+ALTER TABLE auth.clients_groups ADD FOREIGN KEY (group_id)  REFERENCES auth."group" (id) ON DELETE CASCADE;
+
+-- auth.clients_users
+ALTER TABLE auth.clients_users DROP CONSTRAINT clients_users_client_id_fkey;
+ALTER TABLE auth.clients_users DROP CONSTRAINT clients_users_user_id_fkey;
+
+ALTER TABLE auth.clients_users ADD FOREIGN KEY (client_id) REFERENCES auth.clients (id) ON DELETE CASCADE;
+ALTER TABLE auth.clients_users ADD FOREIGN KEY (user_id)   REFERENCES auth."user"  (id) ON DELETE CASCADE;
+
+-- auth.group_rights
+ALTER TABLE auth.group_rights DROP CONSTRAINT group_rights_group_id_fkey;
+
+ALTER TABLE auth.group_rights ADD FOREIGN KEY (group_id) REFERENCES auth."group" (id) ON DELETE CASCADE;
+
+ -- auth.session_content
+ALTER TABLE auth.session_content DROP CONSTRAINT session_content_session_id_fkey;
+
+ALTER TABLE auth.session_content ADD FOREIGN KEY (session_id) REFERENCES auth.session (id) ON DELETE CASCADE;
+
+ -- auth.user_config
+ALTER TABLE auth.user_config DROP CONSTRAINT user_config_user_id_fkey;
+
+ALTER TABLE auth.user_config ADD FOREIGN KEY (user_id) REFERENCES auth."user" (id) ON DELETE CASCADE;
+
+-- auth.user_group
+ALTER TABLE auth.user_group DROP CONSTRAINT user_group_user_id_fkey;
+ALTER TABLE auth.user_group DROP CONSTRAINT user_group_group_id_fkey;
+
+ALTER TABLE auth.user_group ADD FOREIGN KEY (user_id)  REFERENCES auth."user"  (id) ON DELETE CASCADE;
+ALTER TABLE auth.user_group ADD FOREIGN KEY (group_id) REFERENCES auth."group" (id) ON DELETE CASCADE;
diff --git a/sql/Pg-upgrade2/clients.pl b/sql/Pg-upgrade2/clients.pl
new file mode 100644 (file)
index 0000000..744909d
--- /dev/null
@@ -0,0 +1,29 @@
+# @tag: clients
+# @description: Mandanten
+# @depends: release_3_0_0
+package SL::DBUpgrade2::clients;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub run {
+  my ($self) = @_;
+
+  my @queries = (
+    qq|ALTER TABLE defaults ADD COLUMN company          TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN address          TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN taxnumber        TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN co_ustid         TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN duns             TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN sepa_creditor_id TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN templates        TEXT|,
+  );
+
+  $self->db_query($_) for @queries;
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/remove_role_from_employee.sql b/sql/Pg-upgrade2/remove_role_from_employee.sql
new file mode 100644 (file)
index 0000000..0e8b481
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: remove_role_from_employee
+-- @description: Nicht benutzte Spalte employee.role entfernen
+-- @depends: clients
+-- @charset: utf-8
+ALTER TABLE employee DROP COLUMN role;
diff --git a/t/rdbo_relationship_consistency.t b/t/rdbo_relationship_consistency.t
new file mode 100644 (file)
index 0000000..db35e80
--- /dev/null
@@ -0,0 +1,38 @@
+use Test::More;
+use Test::Exception;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Data::Dumper;
+use Support::TestSetup;
+use File::Slurp;
+use IO::Dir;
+
+my %dir;
+tie %dir, 'IO::Dir', 'SL/DB';
+my @pms = grep { m/\.pm$/ } keys %dir;
+
+foreach my $pm (sort @pms) {
+  my $content = read_file("SL/DB/${pm}");
+  next unless $content =~ m/__PACKAGE__->meta->add_relationships?\((.+?)\);/s;
+  my $code = $1;
+
+  my @not_existing;
+  while ($code =~ m/\b(?:map_)?class\s*=>\s*['"]SL::DB::(.+?)['"]/g) {
+    push @not_existing, $1 unless -f "SL/DB/${1}.pm";
+  }
+
+  if (@not_existing) {
+    fail("$pm: Non-existing relationship model(s) " . join(' ', @not_existing));
+  } else {
+    pass("$pm: all relationship model(s) exist");
+  }
+}
+
+# print Dumper(\@pms);
+
+
+done_testing();
index 8cbaf29..263bef8 100644 (file)
@@ -1,20 +1,22 @@
 [%- USE T8 %]
 [% USE HTML %]
-[% USE LxERP%]
+[% USE LxERP %][%- USE L -%]
  <center>
   <table class="login" border="3" cellpadding="20">
    <tr>
     <td class="login" align="center">
      <a href="http://www.kivitendo.de" target="_top"><img src="image/kivitendo.png" border="0"></a>
-     <h1>[% 'kivitendo administration' | $T8 %] [% version %]</h1>
+     <h1>[% LxERP.t8('kivitendo v#1 administration', FORM.version) %]</h1>
 
-     [% IF error %]
-     <p><span class="message_error_login">[% error %]</span></p>
-     [% END %]
+[% IF error %]
+     <p><span class="message_error_login">[% HTML.escape(error) %]</span></p>
+[% END %]
 
      <p>
 
-      <form method="post" action="admin.pl">
+      <form method="post" action="controller.pl">
+       [%- L.hidden_tag("action",   'Admin/login') %]
+       [%- L.hidden_tag("do_login", 1) %]
 
        <table width="100%">
         <tr>
           <table>
            <tr>
             <th align="right">[% 'Password' | $T8 %]</th>
-            <td><input class="login" type="password" name="{AUTH}admin_password" id="admin_password" size="30" tabindex="1"></td>
+            <td>[%- L.input_tag("{AUTH}admin_password", '', type="password", class="login", id="admin_password", size="30") %]</td>
            </tr>
           </table>
 
           <br>
-          <input type="submit" value="[% 'Login' | $T8 %]" tabindex="2">
+
+          [% L.submit_tag('dummy', LxERP.t8('Login')) %]
 
          </td>
         </tr>
        </table>
-
-       <input type="hidden" name="action" value="login">
       </form>
 
     </td>
@@ -45,7 +46,3 @@
    &nbsp;|&nbsp;
    <a href="doc/kivitendo-Dokumentation.pdf" target="_top">[%- LxERP.t8('Documentation') %]</a>
   </p>
-
-  <script type='text/javascript'>
-    $('#admin_password').focus();
-  </script>
index 128d460..baa70e5 100644 (file)
@@ -27,7 +27,6 @@
 
   <form name="Form" method="post" action="admin.pl">
 
-   <input type="hidden" name="dbdriver" value="Pg">
    <input type="hidden" name="dbhost" value="[% HTML.escape(dbhost) %]">
    <input type="hidden" name="dbport" value="[% HTML.escape(dbport) %]">
    <input type="hidden" name="dbuser" value="[% HTML.escape(dbuser) %]">
 
    </table>
 
-   <input name="callback" type="hidden" value="admin.pl?action=list_users">
+   <input name="callback" type="hidden" value="controller.pl?action=Admin/show">
    <input type="hidden" name="nextsub" value="backup_dataset_start">
-   <input type="hidden" name="back_nextsub" value="list_users">
 
    <hr size="3" noshade>
 
    <br>
 
    <input type="submit" class="submit" name="action" value="[% 'Continue' | $T8 %]">
-   <a href="admin.pl?action=pg_database_administration">[% 'Back' | $T8 %]</a>
+   <a href="controller.pl?action=Admin/show">[% 'Back' | $T8 %]</a>
 
   </form>
 
index e1573a6..e64b2a8 100644 (file)
@@ -1,11 +1,8 @@
 [%- USE T8 %]
 [%- USE LxERP %]
-[%- USE HTML %]
+[%- USE HTML %][%- USE L -%]
  <h1>[% title %]</h1>
 
  <p>[% LxERP.t8('The dataset backup has been sent via email to #1.', to) | html %]</p>
 
- <form method="post" action="admin.pl">
-  <input type="hidden" name="nextsub" value="list_users">
-  <input type="submit" name="action" value="[% 'Continue' | $T8 %]">
- </form>
+ <p>[% L.link("controller.pl?action=Admin/show", LxERP.t8("Continue")) %]
index c40b8ec..2a3c51d 100644 (file)
@@ -1,9 +1,11 @@
 [%- USE T8 %]
-[%- USE HTML %]
+[%- USE HTML %][%- USE L -%][%- USE LxERP -%]
 
  <h1>[% title %]</h1>
 
- <form method="post" action="admin.pl">
+ <form method="post" action="controller.pl">
+  [%- L.hidden_tag("action", 'Admin/create_auth_db') %]
+  [%- L.hidden_tag("{AUTH}admin_password", LXCONFIG.authentication.admin_password) %]
 
   <p>
    [% 'The database for user management and authentication does not exist. You can create let kivitendo create it with the following parameters:' | $T8 %]
   <table border="0">
    <tr>
     <td>[% 'Host' | $T8 %]:</td>
-    <td>[% HTML.escape(db_host) %]</td>
+    <td>[% HTML.escape(SELF.db_cfg.host) %]</td>
    </tr>
    <tr>
     <td>[% 'Port' | $T8 %]:</td>
-    <td>[% HTML.escape(db_port) %]</td>
+    <td>[% HTML.escape(SELF.db_cfg.port) %]</td>
    </tr>
    <tr>
     <td>[% 'User name' | $T8 %]:</td>
-    <td>[% HTML.escape(db_user) %]</td>
+    <td>[% HTML.escape(SELF.db_cfg.user) %]</td>
    </tr>
    <tr>
     <td>[% 'Database name' | $T8 %]:</td>
-    <td>[% HTML.escape(db_db) %]</td>
+    <td>[% HTML.escape(SELF.db_cfg.db) %]</td>
    </tr>
   </table>
 
    [% 'If the database user listed above does not have the right to create a database then enter the name and password of the superuser below:' | $T8 %]
   </p>
 
-  <table border="0"
+  <table border="0">
    <tr>
     <td>[% 'Superuser name' | $T8 %]:</td>
-    <td><input name="db_superuser"></td>
+    <td>[% L.input_tag('db_superuser', '') %]</td>
    </tr>
 
    <tr>
     <td>[% 'Password' | $T8 %]:</td>
-    <td><input type="password" name="db_superuser_password"></td>
+    <td>[% L.input_tag('db_superuser_password', '', type='password') %]</td>
    </tr>
   </table>
 
-  <input type="hidden" name="{AUTH}admin_password" value="[% HTML.escape(admin_password) %]">
-  <input type="hidden" name="action" value="create_auth_db">
-
-  <input type="submit" class="submit" value="[% 'Create Dataset' | $T8 %]">
-  <input type="button" class="submit" onclick="history.back()" value="[% 'Back' | $T8 %]">
-
+  [% L.submit_tag("dummy", LxERP.t8("Create Dataset")) %]
+  [% L.button_tag("history.back()", LxERP.t8("Back")) %]
  </form>
index 0e1d80a..13a89f4 100644 (file)
@@ -1,5 +1,5 @@
 [%- USE T8 %]
-[%- USE HTML %]
+[%- USE HTML %][%- USE LxERP -%][%- USE L -%]
 
  <h1>[% title %]</h1>
 
  <table border="0">
   <tr>
    <td>[% 'Host' | $T8 %]:</td>
-   <td>[% HTML.escape(db_host) %]</td>
+   <td>[% HTML.escape(SELF.db_cfg.host) %]</td>
   </tr>
   <tr>
    <td>[% 'Port' | $T8 %]:</td>
-   <td>[% HTML.escape(db_port) %]</td>
+   <td>[% HTML.escape(SELF.db_cfg.port) %]</td>
   </tr>
   <tr>
    <td>[% 'User name' | $T8 %]:</td>
-   <td>[% HTML.escape(db_user) %]</td>
+   <td>[% HTML.escape(SELF.db_cfg.user) %]</td>
   </tr>
   <tr>
    <td>[% 'Database name' | $T8 %]:</td>
-   <td>[% HTML.escape(db_db) %]</td>
+   <td>[% HTML.escape(SELF.db_cfg.db) %]</td>
   </tr>
  </table>
 
  <p>
-  [% 'If you want to change any of these parameters then press the &quot;Back&quot; button, edit the file &quot;config/kivitendo.conf&quot; and login into the admin module again.' | $T8 %]
+  [% 'If you want to change any of these parameters then press the "Back" button, edit the file "config/kivitendo.conf" and login into the admin module again.' | $T8 %]
  </p>
 
- <form method="post" action="admin.pl">
-
-  <input type="hidden" name="{AUTH}admin_password" value="[% HTML.escape(admin_password) %]">
-  <input type="hidden" name="action" value="create_auth_tables">
-
-  <input type="submit" class="submit" value="[% 'Create tables' | $T8 %]">
-  <input type="button" class="submit" onclick="history.back()" value="[% 'Back' | $T8 %]">
+ <form method="post" action="controller.pl">
+  [%- L.hidden_tag("action", 'Admin/create_auth_tables') %]
+  [%- L.hidden_tag("{AUTH}admin_password", LXCONFIG.authentication.admin_password) %]
 
+  [% L.submit_tag("dummy", LxERP.t8("Create tables")) %]
+  [% L.button_tag("history.back()", LxERP.t8("Back")) %]
  </form>
index 991bbc9..9f8b60d 100644 (file)
     </tr>
    </table>
 
-   <input type="hidden" name="dbdriver"  value="[% HTML.escape(dbdriver) %]">
    <input type="hidden" name="dbuser"    value="[% HTML.escape(dbuser) %]">
    <input type="hidden" name="dbhost"    value="[% HTML.escape(dbhost) %]">
    <input type="hidden" name="dbport"    value="[% HTML.escape(dbport) %]">
    <input type="hidden" name="dbpasswd"  value="[% HTML.escape(dbpasswd) %]">
    <input type="hidden" name="dbdefault" value="[% HTML.escape(dbdefault) %]">
 
-   <input type="hidden" name="callback" value="admin.pl?action=list_users">
+   <input type="hidden" name="callback" value="controller.pl?action=Admin/show">
 
    <input type="hidden" name="nextsub" value="dbcreate">
 
index 23fb805..1a07dc7 100644 (file)
@@ -3,7 +3,7 @@
   <h1>[% title %]</h1>
 
   <form method="post" action="admin.pl">
-   <a href="admin.pl?action=list_users">[% 'Back' | $T8 %]</a>
+   <a href="controller.pl?action=Admin/show">[% 'Back' | $T8 %]</a>
 
    <table>
     <tr>
@@ -11,8 +11,6 @@
 
       <table>
 
-       <input type="hidden" name="dbdriver" value="[% HTML.escape(dbdriver) %]">
-
        <tr>
         <td>
          <table>
@@ -48,7 +46,7 @@
        </tr>
       </table>
 
-      <input name="callback" type="hidden" value="admin.pl?action=list_users">
+      <input name="callback" type="hidden" value="controller.pl?action=Admin/show">
 
       <br>
       <input type="submit" class="submit" name="action" value="[% 'Create Dataset' | $T8 %]">
index b42c746..814ca83 100644 (file)
@@ -1,14 +1,8 @@
 [%- USE T8 %]
 [%- USE HTML %]
-[%- USE LxERP %]
+[%- USE LxERP %][%- USE L -%]
   <h1>[% title %]</h1>
 
-  <form method="post" action="admin.pl">
+  <p>[% LxERP.t8('The dataset #1 has been successfully created.', db) | html %]</p>
 
-   <p>[% LxERP.t8('The dataset #1 has been successfully created.', db) | html %]</p>
-
-   <input type="hidden" name="nextsub" value="list_users">
-
-   <p><input type="submit" class="submit" name="action" value="[% 'Continue' | $T8 %]"></p>
-
-  </form>
+  <p>[% L.link("controller.pl?action=Admin/show", LxERP.t8("Continue")) %]</p>
index a7aa798..4821cbb 100644 (file)
@@ -1,13 +1,8 @@
 [%- USE T8 %]
 [%- USE LxERP %]
-[%- USE HTML %]
+[%- USE HTML %][%- USE L -%]
   <h1>[% title %]</h1>
 
-  <form method="post" action="admin.pl">
+  <p>[% LxERP.t8('The database #1 has been successfully deleted.', db) | html %]</p>
 
-   <p>[% LxERP.t8('The database #1 has been successfully deleted.', db) | html %]</p>
-
-   <input type="hidden" name="nextsub" value="list_users">
-
-   <p><input type="submit" class="submit" name="action" value="[% 'Continue' | $T8 %]"></p>
-  </form>
+  <p>[% L.link("controller.pl?action=Admin/show", LxERP.t8("Continue")) %]</p>
index b88757a..3d99179 100644 (file)
@@ -1,5 +1,5 @@
 [%- USE T8 %]
-[% USE HTML%]
+[% USE HTML%][%- USE LxERP -%][%- USE L -%]
 [% IF NOTHING_TO_DO %]
  <p>[% 'No datasets have been selected.' | $T8 %]</p>
 
@@ -10,9 +10,4 @@
  <p>[% 'All database upgrades have been applied.' | $T8 %]</p>
 [% END %]
 
-<form method="post" action="admin.pl">
- <input type="hidden" name="nextsub" value="list_users">
-
- <input type="submit" name="action" value="[% 'Continue' | $T8 %]">
-</form>
-
+<p>[% L.link("controller.pl?action=Admin/show", LxERP.t8("Continue")) %]</p>
index e66dadf..1de867f 100644 (file)
    <select name="db">[% FOREACH row = DBSOURCES %]<option>[% HTML.escape(row.name) %]</option>[% END %]</select>
   </p>
 
-  <input type="hidden" name="dbdriver"  value="[% HTML.escape(dbdriver) %]">
   <input type="hidden" name="dbuser"    value="[% HTML.escape(dbuser) %]">
   <input type="hidden" name="dbhost"    value="[% HTML.escape(dbhost) %]">
   <input type="hidden" name="dbport"    value="[% HTML.escape(dbport) %]">
   <input type="hidden" name="dbpasswd"  value="[% HTML.escape(dbpasswd) %]">
   <input type="hidden" name="dbdefault" value="[% HTML.escape(dbdefault) %]">
 
-  <input name="callback" type="hidden" value="admin.pl?action=list_users">
+  <input name="callback" type="hidden" value="controller.pl?action=Admin/show">
 
 
   <input type="hidden" name="nextsub" value="dbdelete">
diff --git a/templates/webpages/admin/delete_group_confirm.html b/templates/webpages/admin/delete_group_confirm.html
deleted file mode 100644 (file)
index 3b46941..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-  <h1>[% 'Delete group' | $T8 %]: [% name %]</h1>
-  <p class="message_hint">[ [% name %] ] - [% 'Do you really want to delete this group?' | $T8 %]</p>
-
-   <form name="Form" method="post" action="admin.pl">
-    <a href="admin.pl?action=edit_groups">[% 'Back' | $T8 %]</a>
-
-    <input type="hidden" name="group_id" value="[% HTML.escape(id) %]">
-    <input type="hidden" name="confirmed" value="1">
-    <input type="hidden" name="delete_nextsub" value="delete_group">
-    <input type="submit" class="submit" name="action" value="[% 'Delete' | $T8 %]">
-   </form>
diff --git a/templates/webpages/admin/edit_client.html b/templates/webpages/admin/edit_client.html
new file mode 100644 (file)
index 0000000..8b0712a
--- /dev/null
@@ -0,0 +1,129 @@
+[%- USE HTML %]
+[%- USE L %][%- USE LxERP -%]
+
+[%- INCLUDE 'common/flash.html' %]
+
+<h1>[% HTML.escape(title) %]</h1>
+
+<p>[% L.link(SELF.url_for(action="show"), LxERP.t8("Back")) %]</p>
+
+<form method="post" action="controller.pl" id="form">
+ [% L.hidden_tag("client.id", SELF.client.id) %]
+ [% L.hidden_tag("action", "") %]
+
+ <h2>[%- LxERP.t8("Settings") %]</h2>
+
+ <table>
+  <tr>
+   <th align="right">[% LxERP.t8('Client name') %]</th>
+   <td>[% L.input_tag("client.name", SELF.client.name) %]</td>
+  </tr>
+
+  <tr>
+   <th align="right">[% LxERP.t8('Default client') %]</th>
+   <td>[% L.checkbox_tag("client.is_default", label=LxERP.t8('This is the client to be selected by default on the login screen.'), checked=SELF.client.is_default) %]</td>
+  </tr>
+
+  <tr>
+   <th align="right">[% LxERP.t8('Database name') %]</th>
+   <td>[% L.input_tag("client.dbname", SELF.client.dbname, 'data-dbsettings'=1) %]</td>
+  </tr>
+
+  <tr>
+   <th align="right">[% LxERP.t8('Database host and port') %]</th>
+   <td>
+    [% L.input_tag("client.dbhost", SELF.client.dbhost, 'data-dbsettings'=1) %]
+    [% L.input_tag("client.dbport", SELF.client.dbport, 'data-dbsettings'=1, size=6) %]
+   </td>
+  </tr>
+
+  <tr>
+   <th align="right">[% LxERP.t8('Database user and password') %]</th>
+   <td>
+    [% L.input_tag("client.dbuser",   SELF.client.dbuser, 'data-dbsettings'=1) %]
+    [% L.input_tag("client.dbpasswd", SELF.client.dbpasswd, 'data-dbsettings'=1) %]
+   </td>
+  </tr>
+ </table>
+
+ <div>
+  [% L.button_tag("test_database_connectivity()", LxERP.t8("Test database connectivity")) %]
+ </div>
+
+ <h2>[%- LxERP.t8("Access to clients") %]</h2>
+
+[% IF SELF.all_users.size %]
+ <p>
+  [%- LxERP.t8("The following users will have access to this client") %]:
+ </p>
+
+ <div class="clearfix">
+  [% L.select_tag("client.users[]", SELF.all_users, id="client_users", title_key="login", default=SELF.client.users, default_value_key='id', multiple=1) %]
+  [% L.multiselect2side("client_users", labelsx => LxERP.t8("All users"), labeldx => LxERP.t8("Users that have access to this client")) %]
+ </div>
+
+[%- ELSE %]
+ <p>
+  [% LxERP.t8("No users have been created yet.") %]
+ </p>
+[%- END %]
+
+ <h2>[%- LxERP.t8("Group assignment") %]</h2>
+
+[% IF SELF.all_groups.size %]
+ <p>
+  [%- LxERP.t8("The following groups are valid for this client") %]:
+ </p>
+
+ <div class="clearfix">
+  [% L.select_tag("client.groups[]", SELF.all_groups, id="client_groups", title_key="name", default=SELF.client.groups, default_value_key='id', multiple=1) %]
+  [% L.multiselect2side("client_groups", labelsx => LxERP.t8("All groups"), labeldx => LxERP.t8("Groups valid for this client")) %]
+ </div>
+
+[%- ELSE %]
+ <p>
+  [% LxERP.t8("No groups have been created yet.") %]
+ </p>
+[%- END %]
+
+<hr size="3" noshade>
+
+<p>
+ [% L.link(SELF.url_for(action="show"), LxERP.t8("Back")) %]
+
+ [% L.button_tag("submit_with_action('save_client')", LxERP.t8("Save")) %]
+ [% IF SELF.client.id %]
+  [% L.button_tag("save_as_new()", LxERP.t8("Save as new")) %]
+  [% L.button_tag("submit_with_action('delete_client')", LxERP.t8("Delete"), confirm=LxERP.t8("Are you sure?")) %]
+ [%- END %]
+</p>
+
+</form>
+
+<script type="text/javascript">
+ <!--
+  function submit_with_action(action) {
+    $("#action").val("Admin/" + action);
+    $("#form").submit();
+  }
+
+  function save_as_new() {
+    var new_client_name = prompt("[% LxERP.t8("Please enter the name for the new client.") %]", "");
+    if (!new_client_name)
+      return;
+
+    $("#client_name").val(new_client_name);
+    $("#client_id").val("");
+    submit_with_action("save_client");
+  }
+
+  function test_database_connectivity() {
+    open_jqm_window({
+      url:  'controller.pl?action=Admin/test_database_connectivity',
+      data: $("INPUT[data-dbsettings=1]").serialize(),
+      type: 'POST'
+    });
+    return true;
+  }
+   -->
+</script>
index 4dcec1a..78e1fb6 100644 (file)
-[% USE T8 %][% USE HTML %][% USE L %][% USE LxERP -%]
- [% L.javascript_tag('jquery.selectboxes', 'jquery.multiselect2side') %]
+[%- USE HTML %]
+[%- USE L %][%- USE LxERP -%]
 
-<h1>[% 'Edit group ' | $T8 %]:  [% HTML.escape(name) %]</h1>
+[%- INCLUDE 'common/flash.html' %]
 
- <form name="Form" method="post" action="admin.pl">
-  [% IF message %]
-  <p class="message_ok">[% message %]</p>
-  [% END %]
+<h1>[% HTML.escape(title) %]</h1>
 
-  <p><a href="admin.pl?action=edit_groups">[% 'Back' | $T8 %]</a></p>
+<p>[% L.link(SELF.url_for(action="show"), LxERP.t8("Back")) %]</p>
 
-   <hr>
+<form method="post" action="controller.pl" id="form">
+ [% L.hidden_tag("group.id", SELF.group.id) %]
+ [% L.hidden_tag("action", "") %]
 
-  <h3 class="listheading">[%- LxERP.t8('Edit membership') %]</h3>
+ <h2>[%- LxERP.t8("Settings") %]</h2>
+
+ <table>
+  <tr>
+   <th align="right">[% LxERP.t8('Name') %]</th>
+   <td>[% L.input_tag("group.name", SELF.group.name) %]</td>
+  </tr>
+
+  <tr>
+   <th align="right">[% LxERP.t8('Description') %]</th>
+   <td>[% L.input_tag("group.description", SELF.group.description) %]</td>
+  </tr>
+ </table>
+
+ <h2>[% LxERP.t8("Access rights") %]</h2>
+
+ [% SET granted_rights = SELF.group.rights_map %]
+
+ [%- FOREACH section = SELF.all_rights %]
+  [% SET section_number = loop.count %]
+  [% SET num_checked = 0 %]
+  [% FOREACH right = section.rights %][% SET name = right.name %][% IF granted_rights.$name %][% SET num_checked = num_checked + 1 %][% END %][% END %]
+  <h3>[% L.checkbox_tag('dummy' _ section_number, label=LxERP.t8('Section "#1"', section.description), checkall='[data-checkallgroup=' _ section_number _ ']', checked=(num_checked == section.rights.size)) %]</h3>
 
   <div class="clearfix">
-   [% L.select_tag('user_ids[]', ALL_USERS, id='user_ids', value_key = 'id', title_key = 'login', default = USER_IDS_IN_GROUP, multiple = 1) %]
+   [% FOREACH right = section.rights %]
+    [% SET name = right.name %]
+    [% L.checkbox_tag("group.rights_map." _ name, label=right.description, checked=granted_rights.$name, 'data-checkallgroup'=section_number) %]
+    <br>
+   [%- END %]
   </div>
+ [%- END %]
+
+ <h2>[%- LxERP.t8("Group membership") %]</h2>
+
+[% IF SELF.all_users.size %]
+ <p>
+  [%- LxERP.t8("The following users are a member of this group") %]:
+ </p>
+
+ <div class="clearfix">
+  [% L.select_tag("group.users[]", SELF.all_users, id="group_users", title_key="login", default=SELF.group.users, default_value_key='id', multiple=1) %]
+  [% L.multiselect2side("group_users", labelsx => LxERP.t8("All users"), labeldx => LxERP.t8("Users that are a member in this group")) %]
+ </div>
+
+[%- ELSE %]
+ <p>
+  [% LxERP.t8("No users have been created yet.") %]
+ </p>
+[%- END %]
+
+ <h2>[%- LxERP.t8("Group assignment") %]</h2>
+
+[% IF SELF.all_clients.size %]
+ <p>
+  [%- LxERP.t8("This group is valid for the following clients") %]:
+ </p>
+
+ <div class="clearfix">
+  [% L.select_tag("group.clients[]", SELF.all_clients, id="group_clients", title_key="name", default=SELF.group.clients, default_value_key='id', multiple=1) %]
+  [% L.multiselect2side("group_clients", labelsx => LxERP.t8("All clients"), labeldx => LxERP.t8("Clients this Group is valid for")) %]
+ </div>
+
+[%- ELSE %]
+ <p>
+  [% LxERP.t8("No clients have been created yet.") %]
+ </p>
+[%- END %]
+
+<hr size="3" noshade>
+
+<p>
+ [% L.link(SELF.url_for(action="show"), LxERP.t8("Back")) %]
+
+ [% L.button_tag("submit_with_action('save_group')", LxERP.t8("Save")) %]
+ [% IF SELF.group.id %]
+  [% L.button_tag("save_as_new()", LxERP.t8("Save as new")) %]
+  [% L.button_tag("submit_with_action('delete_group')", LxERP.t8("Delete"), confirm=LxERP.t8("Are you sure?")) %]
+ [%- END %]
+</p>
+
+</form>
+
+<script type="text/javascript">
+ <!--
+  function submit_with_action(action) {
+    $("#action").val("Admin/" + action);
+    $("#form").submit();
+  }
+
+  function save_as_new() {
+    var new_group_name = prompt("[% LxERP.t8("Please enter the name for the new group.") %]", "");
+    if (!new_group_name)
+      return;
 
-  <h3 class="listheading">[% 'Edit rights' | $T8 %]</h3>
-
-  <p>
-   [% FOREACH right = RIGHTS %]
-    [% IF right.is_section %]
-     <i>[% right.description %]</i><br>
-    [% ELSE %]
-     <input type="checkbox" name="[% HTML.escape(right.right) %]_granted" id="[% HTML.escape(right.right) %]_granted" [% IF right.granted %]checked[% END %]>
-     <label for="[% HTML.escape(right.right) %]_granted">[% IF right.description %][% right.description %][% ELSE %]<i>[% HTML.escape(right.right) %]</i>[% END %]</label>
-     <br>
-    [% END %]
-   [% END %]
-  </p>
-
-  <h3 class="listheading">[% LxERP.t8('Rename the group') %]</h3>
-
-  <table>
-   <tr>
-    <td>[% 'Name' | $T8 %]:</td>
-    <td><input name="name" maxlength="50" value="[% HTML.escape(name) %]"></td>
-   </tr>
-
-   <tr>
-    <td>[% 'Description' | $T8 %]:</td>
-    <td><input name="description" value="[% HTML.escape(description) %]"></td>
-   </tr>
-  </table>
-
-  <p>
-   <input type="hidden" name="group_id" value="[% HTML.escape(group_id) %]">
-   <input type="hidden" name="action" value="save_group">
-   <input type="submit" class="submit" value="[% 'Save' | $T8 %]">
-   &nbsp;
-   <a href="admin.pl?action=edit_groups">[% 'Back' | $T8 %]</a>
-  </p>
- </form>
-
- [% L.multiselect2side('user_ids', labelsx => LxERP.t8('All users'), labeldx => LxERP.t8('Users in this group')) %]
+    $("#group_name").val(new_group_name);
+    $("#group_id").val("");
+    submit_with_action("save_group");
+  }
+   -->
+</script>
diff --git a/templates/webpages/admin/edit_group_membership.html b/templates/webpages/admin/edit_group_membership.html
deleted file mode 100644 (file)
index be0a46d..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %][%- USE LxERP %]
-
- <h1>[% 'Edit group membership' | $T8 %]</h1>
-
- <p><a href="admin.pl?action=edit_groups">[% 'Back' | $T8 %]</a></p>
- <p>[% 'Select the checkboxes that match users to the groups they should belong to.' | $T8 %]</p>
-
- <form action="admin.pl">
-
-  <p>
-   <table border="0">
-    [% FOREACH user = USERS %]
-
-    [% IF user.repeat_headings %]
-    <tr>
-     <th class="listheading" valign="bottom">[% 'Login Name' | $T8 %]</th>
-     <th class="listheading" valign="bottom">[% 'Name' | $T8 %]</th>
-     [% FOREACH column = HEADINGS %]<th class="listheading" valign="bottom" align="center">[% LxERP.turn90(column.title) %]</th>
-     [% END %]
-    </tr>
-    [% END %]
-
-    <tr class="listrow[% loop.count % 2 %]">
-     <td valign="center">[% HTML.escape(user.login) %]</td>
-     <td valign="center">[% HTML.escape(user.name) %]</td>
-     [% FOREACH group = user.GROUPS %]
-     <td valign="center" align="center">
-      <input type="checkbox" name="u_[% HTML.escape(user.id) %]_g_[% HTML.escape(group.id) %]" [% IF group.is_member %]checked[% END %]>
-     </td>
-     [% END %]
-    </tr>
-    [% END %]
-   </table>
-  </p>
-
-  <input type="hidden" name="save_nextsub" value="save_group_membership">
-  <input type="hidden" name="back_nextsub" value="edit_groups">
-
-  <p>
-   <input type="submit" class="submit" name="action" value="[% 'Save' | $T8 %]">
-  </p>
-
- </form>
diff --git a/templates/webpages/admin/edit_groups.html b/templates/webpages/admin/edit_groups.html
deleted file mode 100644 (file)
index b17539d..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-
-  <h1>[% 'Edit groups' | $T8 %]</h1>
-  [% IF message %]
-  <p class="message_ok">[% message %]</p>
-  [% END %]
-
-  <p><a href="admin.pl?action=login">[% 'Back' | $T8 %]</a></p>
-
- <h2>[% 'Add a new group' | $T8 %]</h2>
-
- <form method="post" action="admin.pl">
- <br>
-  <table border="0">
-   <tr><td>[% 'Name' | $T8 %] </td><td><input name="name" maxlength="50"></td></tr>
-   <tr><td>[% 'Description' | $T8 %] </td><td><input name="description"></td></tr>
-  </table>
-   <input type="hidden" name="add_nextsub" value="add_group">
-   <p><input type="submit" class="submit" name="action" value="[% 'Add' | $T8 %]"></p>
- </form>
-
- <h2>[% 'Edit and delete a group' | $T8 %]</h2>
-
- <form name="Form" method="post" action="admin.pl">
-  [% IF num_groups %]
-  <p>
-   <select name="group_id" size="10">
-    [% FOREACH row = GROUPS %]
-    <option value="[% HTML.escape(row.id) %]"[% ' selected' IF loop.first %]>[% HTML.escape(row.name) %][% IF row.description %] ([% HTML.escape(row.description) %])[% END %]</option>
-    [% END %]
-   </select>
-  </p>
-  [% ELSE %]
-  <p>[% 'No groups have been added yet.' | $T8 %]</p>
-  [% END %]
-
-  <p>
-   [% IF num_groups %]
-   <input type="hidden" name="edit_nextsub" value="edit_group">
-   <input type="hidden" name="delete_nextsub" value="delete_group">
-   <input type="submit" class="submit" name="action" value="[% 'Edit' | $T8 %]">
-   <input type="submit" class="submit" name="action" value="[% 'Delete' | $T8 %]">
-   [% END %]
-  </p>
- </form>
-
- <h2>[% 'Group membership' | $T8 %]</h2>
-
- <p>[% 'Edit the membership of all users in all groups:' | $T8 %]</p>
-
- <form method="post" action="admin.pl">
-  <p>
-   <input type="hidden" name="edit_nextsub" value="edit_group_membership">
-   <input type="submit" class="submit" name="action" value="[% 'Edit' | $T8 %]">
-  </p>
-
- </form>
-
- <hr size="2" noshade>
diff --git a/templates/webpages/admin/edit_printer.html b/templates/webpages/admin/edit_printer.html
new file mode 100644 (file)
index 0000000..8e2f5a2
--- /dev/null
@@ -0,0 +1,35 @@
+[%- USE LxERP -%][%- USE HTML -%][%- USE L -%]
+
+[% INCLUDE 'common/flash.html' %]
+
+<h1>[% HTML.escape(title) %]</h1>
+
+<form method="post">
+ [% L.hidden_tag("client.id", SELF.client.id) %]
+ [% L.hidden_tag("action", 'Admin/dispatch') %]
+ [% L.hidden_tag("printer.id", SELF.printer.id) %]
+
+ <table>
+  <tr>
+   <th align="right">[% LxERP.t8('Printer Description') %]</th>
+   <td>[% L.input_tag("printer.printer_description", SELF.printer.printer_description, size=30) %]</td>
+  <tr>
+  <tr>
+   <th align="right">[% LxERP.t8('Printer Command') %]</th>
+   <td>[% L.input_tag("printer.printer_command", SELF.printer.printer_command, size=30) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% LxERP.t8('Template Code') %]</th>
+   <td>[% L.input_tag("printer.template_code", SELF.printer.template_code, size=8) %]</td>
+  </tr>
+ </table>
+
+ <p>
+  <a href="[% SELF.url_for(action='list_printers', 'client.id'=SELF.client.id) %]">[% LxERP.t8("Back") %]</a>
+  [% L.submit_tag("action_save_printer", LxERP.t8("Save")) %]
+  [%- IF SELF.printer.id %]
+   [% L.submit_tag("action_delete_printer", LxERP.t8("Delete"), confirm=LxERP.t8("Are you sure?")) %]
+ [%- END %]
+ </p>
+
+</form>
index 35dabd2..f8154bf 100644 (file)
-[%- USE T8 %]
 [%- USE HTML %]
 [%- USE L %][%- USE LxERP -%]
- <script type="text/javascript">
-  <!--
-      function open_connection_test_window() {
-        // host name port user passwd
-        var url = "admin.pl?INPUT_ENCODING=UTF-8&action=test_db_connection&" +
-          "dbhost="   + encodeURIComponent(get_input_value("user.dbhost"))   + "&" +
-          "dbport="   + encodeURIComponent(get_input_value("user.dbport"))   + "&" +
-          "dbname="   + encodeURIComponent(get_input_value("user.dbname"))   + "&" +
-          "dbuser="   + encodeURIComponent(get_input_value("user.dbuser"))   + "&" +
-          "dbpasswd=" + encodeURIComponent(get_input_value("user.dbpasswd")) + "&";
-
-        var parm = centerParms(400,300) + ",width=400,height=300,status=yes,scrollbars=yes";
-
-        window.open(url, "_new_generic", parm);
-      }
-    -->
- </script>
-
- <h1>[% title %]</h1>
-
- <form name="Form" id="Form" method="post" action="admin.pl">
-  <p><a href="admin.pl?action=list_users">[% 'Back' | $T8 %]</a></p>
-
-  <table width="100%">
-   <tr valign="top">
-    <td>
-     <table>
-      <tr>
-       <th align="right">[% 'Login Name' | $T8 %]</th>
-       <td>
-        [%- IF edit %]
-         <input type="hidden" id='user.id' name="user.id" value="[% HTML.escape(user.id) %]">[% HTML.escape(user.login) %]
-        [%- ELSE %]
-         <input name="user.login" value="[% HTML.escape(user.login) %]">
-        [%- END %]
-       </td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'Password' | $T8 %]</th>
-       <td>[% IF CAN_CHANGE_PASSWORD %]<input type="password" name="new_password" size="8" value="********">[% ELSE %]********[% END %]</td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'Name' | $T8 %]</th>
-       <td><input name="user.name" size="15" value="[% HTML.escape(user.name) %]"></td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'E-mail' | $T8 %]</th>
-       <td><input name="user.email" size="30" value="[% HTML.escape(user.email) %]"></td>
-      </tr>
-
-      <tr valign="top">
-       <th align="right">[% 'Signature' | $T8 %]</th>
-       <td><textarea name="user.signature" rows="3" cols="35">[% HTML.escape(user.signature) %]</textarea></td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'Phone' | $T8 %]</th>
-       <td><input name="user.tel" size="14" value="[% HTML.escape(user.tel) %]"></td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'Fax' | $T8 %]</th>
-       <td><input name="user.fax" size="14" value="[% HTML.escape(user.fax) %]"></td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'Company' | $T8 %]</th>
-       <td><input name="user.company" size="35" value="[% HTML.escape(user.company) %]"></td>
-      </tr>
-
-      <tr valign="top">
-       <th align="right">[% 'Address' | $T8 %]</th>
-       <td><textarea name="user.address" rows="4" cols="35">[% HTML.escape(user.address) %]</textarea></td>
-      </tr>
-
-      <tr valign="top">
-       <th align="right">[% 'Tax number' | $T8 %]</th>
-       <td><input name="user.taxnumber" size="14" value="[% HTML.escape(user.taxnumber) %]"></td>
-      </tr>
-
-      <tr valign="top">
-       <th align="right">[% 'Ust-IDNr' | $T8 %]</th>
-       <td><input name="user.co_ustid" size="14" value="[% HTML.escape(user.co_ustid) %]"></td>
-      </tr>
-
-      <tr valign="top">
-       <th align="right">[% 'DUNS-Nr' | $T8 %]</th>
-       <td><input name="user.duns" size="14" value="[% HTML.escape(user.duns) %]"></td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'SEPA creditor ID' | $T8 %]</th>
-       <td><input name="user.sepa_creditor_id" size="35" maxlength="35" value="[% HTML.escape(user.sepa_creditor_id) %]"></td>
-      </tr>
-     </table>
-    </td>
-
-    <td>
-     <table>
-      <tr>
-       <th align="right">[% 'Date Format' | $T8 %]</th>
-       <td>[% L.select_tag('user.dateformat', all_dateformats, default = user.dateformat) %]</td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'Number Format' | $T8 %]</th>
-       <td>[% L.select_tag('user.numberformat', all_numberformats, default = user.numberformat) %]</td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'Dropdown Limit' | $T8 %]</th>
-       <td><input name="user.vclimit" value="[% HTML.escape(user.vclimit) %]"></td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'Language' | $T8 %]</th>
-       <td>[% L.select_tag('user.countrycode', all_countrycodes, title_key = 'title', default = user.countrycode) %]</td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'Stylesheet' | $T8 %]</th>
-       <td>[% L.select_tag('user.stylesheet', all_stylesheets, default = user.stylesheet) %]</td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'Printer' | $T8 %]</th>
-       <td><input name="user.printer" size="20" value="[% HTML.escape(user.printer) %]"></td>
-      </tr>
-      <tr>
-       <th align="right">[% 'Use Templates' | $T8 %]</th>
-       <td>[% L.select_tag('usetemplates', all_templates, default = user.templates) %]</td>
-      </tr>
-      <tr>
-       <th align="right">[% 'New Templates' | $T8 %]</th>
-       <td><input name="newtemplates"></td>
-      </tr>
-      <tr>
-       <th align="right">[% 'Setup Templates' | $T8 %]</th>
-       <td>[% L.select_tag('mastertemplates', all_master_templates, default = 'Standard') %]</td>
-      </tr>
-      <tr>
-       <th align="right">[% 'Setup Menu' | $T8 %]</th>
-       <td>[% L.select_tag('user.menustyle', all_menustyles, title_key = 'title', default = user.menustyle) %]</td>
-      </tr>
-      <tr>
-       <th align='right'>[% 'Mandatory Departments' | $T8 %]</th>
-       <td>
-        <input type='radio' name='user.mandatory_departments' value='0' [% IF !user.mandatory_departments %] checked[% END %]> [% 'No' | $T8 %]
-        <input type='radio' name='user.mandatory_departments' value='1' [% IF  user.mandatory_departments %] checked[% END %]> [% 'Yes' | $T8 %]
-       </td>
-      </tr>
-
-      <input type="hidden" name="user.templates" value="[% HTML.escape(user.templates) %]">
-     </table>
-    </td>
-   </tr>
-
-   <tr class="listheading">
-    <th colspan="2">[% 'Database' | $T8 %]</th>
-   </tr>
-
-   <tr>
-    <td colspan="2">
-     <table>
-      <tr>
-       <th align="right">[% 'Driver' | $T8 %]</th>
-       <td>PostgreSQL</td>
-       <th align="right">[% 'Host' | $T8 %]</th>
-       <td><input name="user.dbhost" size="30" value="[% HTML.escape(user.dbhost) %]"></td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'Dataset' | $T8 %]</th>
-       <td><input name="user.dbname" size="15" value="[% HTML.escape(user.dbname) %]"></td>
-       <th align="right">[% 'Port' | $T8 %]</th>
-       <td><input name="user.dbport" size="4" value="[% HTML.escape(user.dbport) %]"></td>
-      </tr>
-
-      <tr>
-       <th align="right">[% 'Database User' | $T8 %]</th>
-       <td><input name="user.dbuser" size="15" value="[% HTML.escape(user.dbuser) %]"></td>
-       <th align="right">[% 'Password' | $T8 %]</th>
-       <td><input name="user.dbpasswd" type="password" size="10" value="[% HTML.escape(user.dbpasswd) %]"></td>
-      </tr>
-
-      <tr>
-       <td colspan="2"><input type="button" class="submit" onclick="open_connection_test_window();" value="[% 'Test connection' | $T8 %]"></td>
-      </tr>
-     </table>
-    </td>
-   </tr>
-
-   [% IF edit %]
-   <tr><td colspan="2"><hr size="3" noshade></td></tr>
-
-   <tr class="listheading">
-    <th colspan="2">[% 'Group membership' | $T8 %]</th>
-   </tr>
-
-   <tr>
-    <td colspan="2">[% 'The user is a member in the following group(s):' | $T8 %]</td>
-   </tr>
-
-   <tr>
-    <td colspan="2">[% FOREACH row = GROUPS %]<a href="admin.pl?action=edit_group&group_id=[% HTML.url(row.id) %]">[% HTML.escape(row.name) %]</a>
-     [% UNLESS loop.last %] | [% END %][% END %]</td>
-   </tr>
-   [% END %]
-
-   <tr><td colspan="2"><hr size="3" noshade></td></tr>
-
-  </table>
-
-  <input name="callback" type="hidden" value="admin.pl?action=list_users">
-
-  <a href="admin.pl?action=list_users">[% 'Back' | $T8 %]</a>
-  <input type="hidden" name="action" value="dispatcher">
-  <input type="submit" class="submit" name="action_save_user" value="[% 'Save' | $T8 %]">
-
-  [% IF edit %]
-   [% FOREACH row = GROUPS %]
-    <input type="hidden" name="new_user_group_ids[]" value="[% HTML.escape(row.id) %]">
-   [% END %]
-   <input type="hidden" name="new_user_login" id="new_user_login" value="">
-   <input type="hidden" name="action_save_user_as_new" id="action_save_user_as_new" value="">
-   <input type="button" class="submit" id="save_as_new_button" value="[% 'Save as new' | $T8 %]">
-   [% L.submit_tag("action_delete_user", LxERP.t8('Delete'), confirm=LxERP.t8('Are you sure?')) %]
-   <input type="hidden" name="edit" value="1">
-  [% END %]
-
- </form>
-
- <script type="text/javascript">
-  <!--
-    $(document).ready(function() {
-      $("#save_as_new_button").click(function() {
-        var new_user_login = prompt('[% 'Please enter the login for the new user.' | $T8 %]', '');
-        if (!new_user_login || (new_user_login == ''))
-          return;
-
-        $("#action_save_user_as_new").val('1');
-        $("#new_user_login").val(new_user_login);
-        $("#user_id").val('');
-        $("#Form").submit();
-      });
-    });
-    -->
- </script>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<h1>[% HTML.escape(title) %]</h1>
+
+<p>[% L.link(SELF.url_for(action="show"), LxERP.t8("Back")) %]</p>
+
+<form method="post" action="controller.pl" id="form">
+ [% L.hidden_tag("user.id", SELF.user.id) %]
+ [% L.hidden_tag("action", "") %]
+ [%- SET props=SELF.user.config_values %]
+
+ <h2>[%- LxERP.t8("Settings") %]</h2>
+
+ <table>
+  <tr valign="top">
+   <td>
+    <table>
+     <tr>
+      <th align="right">[% LxERP.t8('Login Name') %]</th>
+      <td>[% L.input_tag("user.login", SELF.user.login) %]</td>
+     </tr>
+
+     [%- IF AUTH.can_change_password %]
+     <tr>
+      <th align="right">[% LxERP.t8("New Password") %]</th>
+      <td>[% L.input_tag("new_password", "", type="password") %]</td>
+     </tr>
+     [%- END %]
+
+     <tr>
+      <th align="right">[% LxERP.t8("Name") %]</th>
+      <td>[% L.input_tag("user.config_values.name", props.name) %]</td>
+     </tr>
+
+     <tr>
+      <th align="right">[% LxERP.t8('E-mail') %]</th>
+      <td>[% L.input_tag("user.config_values.email", props.email) %]</td>
+     </tr>
+
+     <tr valign="top">
+      <th align="right">[% LxERP.t8('Signature') %]</th>
+      <td>[% L.textarea_tag("user.config_values.signature", props.signature, rows=3, cols=35) %]</td>
+     </tr>
+
+     <tr>
+      <th align="right">[% LxERP.t8('Phone') %]</th>
+      <td>[% L.input_tag("user.config_values.tel", props.tel) %]</td>
+     </tr>
+
+     <tr>
+      <th align="right">[% LxERP.t8('Fax') %]</th>
+      <td>[% L.input_tag("user.config_values.fax", props.fax) %]</td>
+     </tr>
+    </table>
+   </td>
+
+   <td>
+    <table>
+     <tr>
+      <th align="right">[% LxERP.t8("Date Format") %]</th>
+      <td>[% L.select_tag("user.config_values.dateformat", SELF.all_dateformats, default=props.dateformat) %]</td>
+     </tr>
+
+     <tr>
+      <th align="right">[% LxERP.t8("Number Format") %]</th>
+      <td>[% L.select_tag("user.config_values.numberformat", SELF.all_numberformats, default=props.numberformat) %]</td>
+     </tr>
+
+     <tr>
+      <th align="right">[% LxERP.t8("Dropdown Limit") %]</th>
+      <td>[% L.input_tag("user.config_values.vclimit", props.vclimit) %]</td>
+     </tr>
+
+     <tr>
+      <th align="right">[% LxERP.t8("Language") %]</th>
+      <td>[% L.select_tag("user.config_values.countrycode", SELF.all_countrycodes, title_key="title", default=props.countrycode) %]</td>
+     </tr>
+
+     <tr>
+      <th align="right">[% LxERP.t8("Stylesheet") %]</th>
+      <td>[% L.select_tag("user.config_values.stylesheet", SELF.all_stylesheets, default=props.stylesheet) %]</td>
+     </tr>
+
+     <tr>
+      <th align="right">[% LxERP.t8("Setup Menu") %]</th>
+      <td>[% L.select_tag("user.config_values.menustyle", SELF.all_menustyles, title_key="title", default=props.menustyle) %]</td>
+     </tr>
+
+     <tr>
+      <th align="right">[% LxERP.t8("Mandatory Departments") %]</th>
+      <td>
+       [% L.radio_button_tag('user.config_values.mandatory_departments', value='0', id='user.config_values.mandatory_departments_0', label=LxERP.t8('No'),  checked=!props.mandatory_departments) %]
+       [% L.radio_button_tag('user.config_values.mandatory_departments', value='1', id='user.config_values.mandatory_departments_1', label=LxERP.t8('Yes'), checked= props.mandatory_departments) %]
+      </td>
+     </tr>
+    </table>
+   </td>
+  </tr>
+ </table>
+
+ <h2>[%- LxERP.t8("Access to clients") %]</h2>
+
+[% IF SELF.all_clients.size %]
+ <p>
+  [%- LxERP.t8("This user will have access to the following clients") %]:
+ </p>
+
+ <div class="clearfix">
+  [% L.select_tag("user.clients[]", SELF.all_clients, id="user_clients", title_key="name", default=SELF.user.clients, default_value_key='id', multiple=1) %]
+  [% L.multiselect2side("user_clients", labelsx => LxERP.t8("All clients"), labeldx => LxERP.t8("Clients this user has access to")) %]
+ </div>
+
+[%- ELSE %]
+ <p>
+  [% LxERP.t8("No clients have been created yet.") %]
+ </p>
+[%- END %]
+
+ <h2>[%- LxERP.t8("Group membership") %]</h2>
+
+[% IF SELF.all_groups.size %]
+ <p>
+  [%- LxERP.t8("This user is a member in the following groups") %]:
+ </p>
+
+ <div class="clearfix">
+  [% L.select_tag("user.groups[]", SELF.all_groups, id="user_groups", title_key="name", default=SELF.user.groups, default_value_key='id', multiple=1) %]
+  [% L.multiselect2side("user_groups", labelsx => LxERP.t8("All groups"), labeldx => LxERP.t8("Groups this user is a member in")) %]
+ </div>
+
+[%- ELSE %]
+ <p>
+  [% LxERP.t8("No groups have been created yet.") %]
+ </p>
+[%- END %]
+
+<hr size="3" noshade>
+
+<p>
+ [% L.link(SELF.url_for(action="show"), LxERP.t8("Back")) %]
+
+ [% L.button_tag("submit_with_action('save_user')", LxERP.t8("Save")) %]
+ [% IF SELF.user.id %]
+  [% L.button_tag("save_as_new()", LxERP.t8("Save as new")) %]
+  [% L.button_tag("submit_with_action('delete_user')", LxERP.t8("Delete"), confirm=LxERP.t8("Are you sure?")) %]
+ [%- END %]
+</p>
+
+</form>
+
+<script type="text/javascript">
+ <!--
+  function submit_with_action(action) {
+    $("#action").val("Admin/" + action);
+    $("#form").submit();
+  }
+
+  function save_as_new() {
+    var new_user_login = prompt("[% LxERP.t8("Please enter the login for the new user.") %]", "");
+    if (!new_user_login)
+      return;
+
+    $("#user_login").val(new_user_login);
+    $("#user_id").val("");
+    submit_with_action("save_user");
+  }
+   -->
+</script>
diff --git a/templates/webpages/admin/list_printers.html b/templates/webpages/admin/list_printers.html
new file mode 100644 (file)
index 0000000..4da7eff
--- /dev/null
@@ -0,0 +1,70 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%]
+
+[% INCLUDE 'common/flash.html' %]
+
+<h1>[% HTML.escape(title) %]</h1>
+
+[% IF !SELF.all_clients.size %]
+<div class="error">
+ [% LxERP.t8("Error") %]:
+ [% LxERP.t8("No clients have been created yet.") %]
+</div>
+
+<div>
+ <a href="[% SELF.url_for(action='show') %]">[% LxERP.t8("Back") %]</a>
+</div>
+
+[%- ELSE %]
+
+ <div>
+  [% LxERP.t8("Actions") %]:
+  <span class="link_separator"></span>
+  <a href="[% SELF.url_for(action='show') %]">[% LxERP.t8("Back") %]</a>
+  <span class="link_separator">|</span>
+  <a href="[% SELF.url_for(action='new_printer', 'client.id'=SELF.client.id) %]">[% LxERP.t8("Add printer") %]</a>
+ </div>
+
+ <hr>
+
+ <p>
+  [% LxERP.t8("Client to configure the printers for") %]:
+  [% L.select_tag('client.id', SELF.all_clients, id='client_id', title_key='name', default=SELF.client.id) %]
+ </p>
+
+ [%- IF !SELF.all_printers.size %]
+
+  <p>
+   [% LxERP.t8("No printers have been created yet.") %]
+  </p>
+
+ [%- ELSE %]
+
+  <table width="100%">
+   <tr class="listheading">
+    <th>[% LxERP.t8('Description') %]</th>
+    <th>[% LxERP.t8('Printer Command') %]</th>
+    <th>[% LxERP.t8('Template Code') %]</th>
+   </tr>
+
+   [%- FOREACH printer = SELF.all_printers %]
+    <tr valign="top" class="listrow">
+     <td><a href="[% SELF.url_for(action='edit_printer', 'client.id'=SELF.client.id, id=printer.id) %]">[% HTML.escape(printer.printer_description) %]</a></td>
+     <td>[% HTML.escape(printer.printer_command) %]</td>
+     <td>[% HTML.escape(printer.template_code) %]</td>
+    </tr>
+   [%- END %]
+  </table>
+
+ [%- END %]
+
+ <script type="text/javascript">
+<!--
+  $(function() {
+    $('#client_id').change(function() {
+      window.location.href = "controller.pl?action=Admin/list_printers&client.id=" + $('#client_id').val();
+    });
+  });
+-->
+ </script>
+
+[%- END %]
diff --git a/templates/webpages/admin/list_users.html b/templates/webpages/admin/list_users.html
deleted file mode 100644 (file)
index 67a2d62..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
- <h1>[% title %]</h1>
-
- <form method="post" action="admin.pl">
-
-  <p>
-   <table width="100%">
-    <tr>
-     <th class="listtop">[% 'Login Name' | $T8 %]</th>
-     <th class="listtop">[% 'Name' | $T8 %]</th>
-     <th class="listtop">[% 'Company' | $T8 %]</th>
-     <th class="listtop">[% 'Templates' | $T8 %]</th>
-     <th class="listtop">[% 'Print' | $T8 %]</th>
-     <th class="listtop">[% 'Language' | $T8 %]</th>
-     <th class="listtop">[% 'Dataset' | $T8 %]</th>
-     <th class="listtop">[% 'Host' | $T8 %]</th>
-     <th class="listtop">[% 'Last Action' | $T8 %]</th>
-<!-- <th class="listtop">[% 'Driver' | $T8 %]</th> -->
-    </tr>
-
-    [% FOREACH row = MEMBERS %]
-     <tr class="listrow[% loop.count % 2 %]">
-      <td>&nbsp;<a href="admin.pl?action=edit&user.id=[% HTML.url(row.id) %]">[% HTML.escape(row.login) %]</a></td>
-      <td>&nbsp;[% HTML.escape(row.name) %]</td>
-      <td>&nbsp;[% HTML.escape(row.company) %]</td>
-      <td>&nbsp;[% HTML.escape(row.templates) %]</td>
-      <td>&nbsp;[% HTML.escape(row.template_format) %]</td>
-      <td>&nbsp;[% HTML.escape(row.countrycode) %]</td>
-      <td>&nbsp;[% HTML.escape(row.dbname) %]</td>
-      <td>&nbsp;[% IF row.dbhost %][% HTML.escape(row.dbhost) %][% ELSE %]localhost[% END %]</td>
-      <td>&nbsp;
-        [% IF( row.last_action ) %]
-          [% HTML.escape(row.last_action) %]
-        [% ELSE %]
-          [% 'not logged in' | $T8 %]
-        [% END %]</td>
-<!--  <td>&nbsp;[% HTML.escape(row.dbdriver) %]</td> -->
-     </tr>
-    [% END %]
-
-   </table>
-  <hr size="3" noshade>
-  </p>
-
-
-  <input type="submit" class="submit" name="action" value="[% 'Add User' | $T8 %]">
-  <input type="submit" class="submit" name="action" value="[% 'Edit groups' | $T8 %]">
-  <input type="submit" class="submit" name="action" value="[% 'Pg Database Administration' | $T8 %]">
-  <input type="submit" class="submit" name="action" value="[% 'Printer Management' | $T8 %]">
-  [% IF LOCKED %]
-   <input type="submit" class="submit" name="action" value="[% 'Unlock System' | $T8 %]">
-   [% ELSE %]
-   <input type="submit" class="submit" name="action" value="[% 'Lock System' | $T8 %]">
-  [% END %]
-  <input type="submit" class="submit" name="action" value="[% 'Logout' | $T8 %]">
-
-  <div style="background-color: #FFFFDA; font-size: 12px; padding: 0.5em; max-width: 720px; margin: 1em;">
-  <p>[% 'Click on login name to edit!' | $T8 %]</p>
-  <p>[% 'To add a user to a group edit a name, change the login name and save.  A new user with the same variables will then be saved under the new login name.' | $T8 %]</p>
-  </div>
- </form>
-
- <hr>
-
- <h2>[% 'User Login' | $T8 %]</h2>
-
- <form method="post" action="controller.pl">
-  <input type="hidden" name="action" value="LoginScreen/login">
-
-  <table border="0">
-   <tr>
-    <th align="right">[% 'Login Name' | $T8 %]</th>
-    <td><input class="login" name="{AUTH}login"></td>
-    <td>&nbsp;</td>
-   </tr>
-   <tr>
-    <th align="right">[% 'Password' | $T8 %]</th>
-    <td><input class="login" type="password" name="{AUTH}password"></td>
-    <td><input type="submit" value="[% 'Login' | $T8 %]"></td>
-   </tr>
-  </table>
-
- </form>
index 6e29706..bf23926 100644 (file)
@@ -8,7 +8,6 @@
 
  <form name="Form" method="post" action="admin.pl" enctype="multipart/form-data">
 
-  <input type="hidden" name="dbdriver" value="Pg">
   <input type="hidden" name="dbhost" value="[% HTML.escape(dbhost) %]">
   <input type="hidden" name="dbport" value="[% HTML.escape(dbport) %]">
   <input type="hidden" name="dbuser" value="[% HTML.escape(dbuser) %]">
index baece7d..86ca7ae 100644 (file)
@@ -1,6 +1,6 @@
 [%- USE T8 %]
 [%- USE LxERP %]
-[% USE HTML %] </pre>
+[% USE HTML %][%- USE L -%] </pre>
 
  <hr>
 
@@ -9,7 +9,6 @@
   [%- LxERP.t8('The program\'s exit code was #1 (&quot;0&quot; usually means that everything went OK).', retval) | html %]
  </p>
 
- <form method="post" action="admin.pl">
-  <input type="hidden" name="nextsub" value="list_users">
-  <input type="submit" name="action" value="[% 'Continue' | $T8 %]">
- </form>
+ <p>
+  [% L.link("controller.pl?action=Admin/show", LxERP.t8("Continue")) %]
+ </p>
diff --git a/templates/webpages/admin/show.html b/templates/webpages/admin/show.html
new file mode 100644 (file)
index 0000000..e046518
--- /dev/null
@@ -0,0 +1,122 @@
+[%- USE HTML %][%- USE LxERP -%][%- USE L -%]
+
+[% INCLUDE 'common/flash.html' %]
+
+<h1>[% title %]</h1>
+
+<div>
+ [% LxERP.t8("Actions") %]:
+ <span class="link_separator"></span>
+ [% L.link(SELF.url_for(action="new_user"), LxERP.t8("Add User")) %]
+ <span class="link_separator">|</span>
+ [% L.link(SELF.url_for(action="new_client"), LxERP.t8("Add Client")) %]
+ <span class="link_separator">|</span>
+ [% L.link(SELF.url_for(action="new_group"), LxERP.t8("Add User Group")) %]
+ <span class="link_separator">|</span>
+ [% L.link(SELF.url_for(action="pg_database_administration", controller="admin.pl"), LxERP.t8("Pg Database Administration")) %]
+ <span class="link_separator">|</span>
+ [% L.link(SELF.url_for(action="list_printers"), LxERP.t8("Printer Management")) %]
+ <span class="link_separator">|</span>
+ [% IF SELF.is_locked %]
+  [% L.link(SELF.url_for(action="unlock_system"), LxERP.t8("Unlock System")) %]
+ [% ELSE %]
+  [% L.link(SELF.url_for(action="lock_system"), LxERP.t8("Lock System")) %]
+ [% END %]
+ <span class="link_separator">|</span>
+ [% L.link(SELF.url_for(action="logout"), LxERP.t8("Logout")) %]
+ <span class="link_separator">|</span>
+ [% L.link(SELF.url_for(controller="LoginScreen", action="user_login"), LxERP.t8("To user login")) %]
+</div>
+
+<hr>
+
+<div class="tabwidget">
+ <ul>
+  <li><a href="#user_list">[%- LxERP.t8("User list") %]</a></li>
+  <li><a href="#client_list">[%- LxERP.t8("Client list") %]</a></li>
+  <li><a href="#group_list">[%- LxERP.t8("Group list") %]</a></li>
+ </ul>
+
+ <div id="user_list">
+[%- IF !SELF.all_users.size %]
+  <p>
+   [% LxERP.t8("No users have been created yet.") %]
+   [% LxERP.t8("In order to use kivitendo you have to create at least a client, a user and a group.") %]
+  </p>
+
+[%- ELSE %]
+  <table width="100%">
+   <tr class="listheading">
+    <th>[% LxERP.t8('Login Name') %]</th>
+    <th>[% LxERP.t8('Name') %]</th>
+    <th>[% LxERP.t8('Language') %]</th>
+   </tr>
+
+[% FOREACH user = SELF.all_users %]
+[%- SET config = user.config_values %]
+   <tr class="listrow">
+    <td>[% L.link(SELF.url_for(action="edit_user", id=user.id), HTML.escape(user.login)) %]</td>
+    <td>[% HTML.escape(config.name) %]</td>
+    <td>[% HTML.escape(config.countrycode) %]</td>
+   </tr>
+[% END %]
+  </table>
+[%- END %]
+ </div>
+
+ <div id="client_list">
+[%- IF !SELF.all_clients.size %]
+  <p>
+   [% LxERP.t8("No clients have been created yet.") %]
+   [% LxERP.t8("In order to use kivitendo you have to create at least a client, a user and a group.") %]
+  </p>
+
+[%- ELSE %]
+  <table width="100%">
+   <tr class="listheading">
+    <th>[% LxERP.t8('Client name') %]</th>
+    <th>[% LxERP.t8('Database ID') %]</th>
+    <th>[% LxERP.t8('Database name') %]</th>
+    <th>[% LxERP.t8('Database Host') %]</th>
+    <th>[% LxERP.t8('Database User') %]</th>
+    <th>[% LxERP.t8('Default client') %]</th>
+   </tr>
+
+[%- FOREACH client = SELF.all_clients %]
+   <tr class="listrow">
+    <td>[% L.link(SELF.url_for(action="edit_client", id=client.id), HTML.escape(client.name)) %]</td>
+    <td>[% HTML.escape(client.id) %]</td>
+    <td>[% HTML.escape(client.dbname) %]</td>
+    <td>[% HTML.escape(client.dbhost) %][% IF client.dbport %]:[%- HTML.escape(client.dbport) %][%- END %]</td>
+    <td>[% HTML.escape(client.dbuser) %]</td>
+    <td>[% IF client.is_default %][% LxERP.t8("Yes") %][%- ELSE %][% LxERP.t8("No") %][%- END %]</td>
+   </tr>
+[%- END %]
+  </table>
+[%- END %]
+ </div>
+
+ <div id="group_list">
+[%- IF !SELF.all_groups.size %]
+  <p>
+   [% LxERP.t8("No groups have been created yet.") %]
+   [% LxERP.t8("In order to use kivitendo you have to create at least a client, a user and a group.") %]
+  </p>
+
+[%- ELSE %]
+  <table width="100%">
+   <tr class="listheading">
+    <th>[% LxERP.t8('Name') %]</th>
+    <th>[% LxERP.t8('Description') %]</th>
+   </tr>
+
+[%- FOREACH group = SELF.all_groups %]
+   <tr class="listrow">
+    <td>[% L.link(SELF.url_for(action="edit_group", id=group.id), HTML.escape(group.name)) %]</td>
+    <td>[% HTML.escape(group.description) %]</td>
+   </tr>
+[%- END %]
+  </table>
+[%- END %]
+ </div>
+</div>
index 78829da..df9a59d 100644 (file)
@@ -1,25 +1,21 @@
-[%- USE T8 %]
-[%- USE HTML %]
+[%- USE HTML %][%- USE LxERP -%][%- USE L -%]
+<h1>[% HTML.escape(title) %]</h1>
 
- <h1>[% title %]</h1>
+[%- IF ok %]
 
- [%- IF connection_ok %]
+ <p class="message_ok">[% LxERP.t8('The connection was established successfully.') %]</p>
 
- <p class="message_ok">[% 'The connection was established successfully.' | $T8 %]</p>
-
- [%- ELSE %]
+[%- ELSE %]
 
  <p class="message_error">
-  [% 'The connection to the database could not be established.' | $T8 %]
-  [% 'Error message from the database driver:' | $T8 %]
+  [% LxERP.t8('The connection to the database could not be established.') %]
+  [% LxERP.t8('Error message from the database driver:') %]
  </p>
 
- <p>[% HTML.escape(errstr) %]</p>
+ <p>[% HTML.escape(error) %]</p>
 
- [%- END %]
+[%- END %]
 
- <p>
-  <form>
-   <input type="button" class="submit" onclick="window.close()" value="[% 'Close Window' | $T8 %]">
-  </form>
- </p>
+<div>
+ [% L.button_tag("\$('#jqm_popup_dialog .close').trigger('click');", LxERP.t8("Close Window")) %]
+</div>
index 293ab88..d8fb933 100644 (file)
@@ -14,7 +14,6 @@
    <table>
     <tr>
      <th class="listtop">[% 'Update?' | $T8 %]</th>
-     <th class="listtop">[% 'Dataset' | $T8 %]</th>
      <th class="listtop">[% 'Driver' | $T8 %]</th>
      <th class="listtop">[% 'Host' | $T8 %]</th>
      <th class="listtop">[% 'Port' | $T8 %]</th>
@@ -28,7 +27,6 @@
        <input type="hidden" name="dbname_[% loop.count %]" value="[% HTML.escape(row.dbname) %]">
        <label for="update_[% loop.count %]">[% HTML.escape(row.dbname) %]</label>
       </td>
-      <td><input type="hidden" name="dbdriver_[% loop.count %]" value="Pg">PostgreSQL</td>
       <td><input type="hidden" name="dbhost_[% loop.count %]" value="[% HTML.escape(row.dbhost) %]">[% HTML.escape(row.dbhost) %]</td>
       <td><input type="hidden" name="dbport_[% loop.count %]" value="[% HTML.escape(row.dbport) %]">[% HTML.escape(row.dbport) %]</td>
       <td><input type="hidden" name="dbuser_[% loop.count %]" value="[% HTML.escape(row.dbuser) %]">[% HTML.escape(row.dbuser) %]</td>
@@ -39,7 +37,7 @@
 
    <input type="hidden" name="rowcount" value="[% NEED_UPDATES.size %]">
 
-   <input name="callback" type="hidden" value="admin.pl?action=list_users">
+   <input name="callback" type="hidden" value="controller.pl?action=Admin/show">
    <input type="hidden" name="nextsub" value="dbupdate">
 
    <hr size="3" noshade>
diff --git a/templates/webpages/admin_printer/_login_form.html b/templates/webpages/admin_printer/_login_form.html
deleted file mode 100644 (file)
index 4789d12..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-[%- USE T8 %]
-[%- USE L %]
-<p>[% 'Please select a user' | $T8 %]: [% L.select_tag('login', users, value_key = 'login', title_key = 'login', default = login) %]</p>
-
diff --git a/templates/webpages/admin_printer/edit.html b/templates/webpages/admin_printer/edit.html
deleted file mode 100644 (file)
index 7aef54d..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-[%- USE T8 %]
-
-<form method=post>
-
-<h1 class=listtop colspan=2>[% title %]</h1>
-
-[%- PROCESS 'admin_printer/_login_form.html' %]
-
-<input type=hidden name="printer.id" value="[% printer.id | html %]">
-<table width=100%>
-  <tr height="5"></tr>
-  <tr>
-    <th align=left>[% 'Printer' | $T8 %]</th>
-    <td><input name="printer.printer_description" size=30 value="[% printer.printer_description | html %]"></td>
-  <tr>
-  <tr>
-    <th align=left>[% 'Printer Command' | $T8 %]</th>
-    <td><input name="printer.printer_command" size=30 value="[% printer.printer_command | html %]"></td>
-  </tr>
-  <tr>
-    <th align=left>[% 'Template Code' | $T8 %]</th>
-    <td><input name="printer.template_code" size=5 value="[% printer.template_code | html %]"></td>
-  </tr>
-  <td colspan=2><hr size=3 noshade></td>
-  </tr>
-</table>
-
-<br>
-<input type=hidden name=action value="printer_dispatcher">
-<input type=submit class=submit name=list_printers value="[% 'Back' | $T8 %]">
-<input type=submit class=submit name=save_printer value="[% 'Save' | $T8 %]">
-
-[%- IF id %]
-<input type=submit class=submit name=delete_printer value="[% 'Delete' | $T8 %]">
-[%- END %]
-
-</form>
-
diff --git a/templates/webpages/admin_printer/list.html b/templates/webpages/admin_printer/list.html
deleted file mode 100644 (file)
index 6a413d8..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-[%- USE T8 %]
-
-<form method='post'>
-
-<h1 class=listtop>[% title %]</h1>
-
-[%- PROCESS 'admin_printer/_login_form.html' %]
-
-<table width=100%>
-  <tr>
-    <td>
-      <table width=100%>
-        <tr class=listheading>
-          <th>[% 'Description' | $T8 %]</th>
-          <th>[% 'Printer Command' | $T8 %]</th>
-          <th>[% 'Template Code' | $T8 %]</th>
-        </tr>
-[%- IF all_printers.size %]
-[%- FOREACH row = all_printers %]
-        <tr valign=top class="listrow[% loop.count % 2 %]">
-          <td>&nbsp;<a href="[% edit_link %][% row.id %]">[% row.printer_description %]</a></td>
-          <td align=left>&nbsp;[% row.printer_command | html %]</td>
-          <td align=left>&nbsp;[% row.template_code | html %]</td>
-        </tr>
-[%- END %]
-[%- ELSE %]
-        <tr><td colspan='3'><p class="message_hint">[% 'No data was found.' | $T8 %]</p></td></tr>
-[%- END %]
-      </table>
-    </td>
-  </tr>
-  <tr>
-  <td><hr size=3 noshade></td>
-  </tr>
-</table>
-
-<br>
- <input type="hidden" name="action" value="printer_dispatcher">
- <input type="submit" name='get_login_form' value="[% 'Back' | $T8 %]">
- <input type="submit" name="add_printer" value ="[% 'Add' | $T8 %]">
-</form>
diff --git a/templates/webpages/admin_printer/login.html b/templates/webpages/admin_printer/login.html
deleted file mode 100644 (file)
index 0948d1b..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-[% USE T8 %]
-[% USE L %]
-
-<h1 class=listtop>[% 'Printer Management' | $T8 %]</h1>
-<form method='post'>
-<p>[% 'Printers are created for a user database. Please select a user. The associated database will be edited.' | $T8 %]</p>
-
-[%- PROCESS 'admin_printer/_login_form.html' %]
-
-<input type='hidden' name='action' value='printer_dispatcher'>
-<p>
-<input type='submit' name='list_users' value='[% 'Back' | $T8 %]'>
-<input type='submit' name='list_printers' value='[% 'Continue' | $T8 %]'>
-</p>
-
-</form>
index 18af432..c32e178 100644 (file)
       <th align="right">[% 'Fax' | $T8 %]</th>
       <td><input name="fax" size="14" value="[% HTML.escape(myconfig_fax) %]"></td>
      </tr>
-     <tr>
-      <th align="right">[% 'Company' | $T8 %]</th>
-      <td><input name="company" size="30" value="[% HTML.escape(myconfig_company) %]"></td>
-     </tr>
-     <tr valign="top">
-      <th align="right">[% 'Address' | $T8 %]</th>
-      <td><textarea name="address" rows="4" cols="50">[% HTML.escape(myconfig_address) %]</textarea></td>
-     </tr>
-     <tr>
-      <th align="right">[% 'SEPA creditor ID' | $T8 %]</th>
-      <td><input name="sepa_creditor_id" size="30" maxlength="35" value="[% HTML.escape(myconfig_sepa_creditor_id) %]"></td>
-     </tr>
 
        <tr>
         <th align="right"q>[% 'taxincluded checked' | $T8 %]</th>
diff --git a/templates/webpages/am/edit_defaults.html b/templates/webpages/am/edit_defaults.html
deleted file mode 100644 (file)
index c8c91f6..0000000
+++ /dev/null
@@ -1,224 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-
- <div class="listtop">[% title %]</div>
-
- <form method="post" action="am.pl"name="Form">
-
-  <input type="hidden" name="type" value="defaults">
-
-  <p>
-   <table>
-    <tr class="listheading">
-     <th colspan="4">[% 'Ranges of numbers' | $T8 %]</th>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Last Invoice Number' | $T8 %]</th>
-     <td><input name="invnumber" size="10" value="[% HTML.escape(defaults_invnumber) %]"></td>
-     <th align="right" nowrap>[% 'Last Customer Number' | $T8 %]</th>
-     <td><input name="customernumber" size="10" value="[% HTML.escape(defaults_customernumber) %]"></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Last Credit Note Number' | $T8 %]</th>
-     <td><input name="cnnumber" size="10" value="[% HTML.escape(defaults_cnnumber) %]"></td>
-     <th align="right" nowrap>[% 'Last Vendor Number' | $T8 %]</th>
-     <td><input name="vendornumber" size="10" value="[% HTML.escape(defaults_vendornumber) %]"></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Last Sales Order Number' | $T8 %]</th>
-     <td><input name="sonumber" size="10" value="[% HTML.escape(defaults_sonumber) %]"></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Last Purchase Order Number' | $T8 %]</th>
-     <td><input name="ponumber" size="10" value="[% HTML.escape(defaults_ponumber) %]"></td>
-     <th align="right" nowrap>[% 'Last Article Number' | $T8 %]</th>
-     <td><input name="articlenumber" size="10" value="[% HTML.escape(defaults_articlenumber) %]"></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Last Sales Quotation Number' | $T8 %]</th>
-     <td><input name="sqnumber" size="10" value="[% HTML.escape(defaults_sqnumber) %]"></td>
-     <th align="right" nowrap>[% 'Last Service Number' | $T8 %]</th>
-     <td><input name="servicenumber" size="10" value="[% HTML.escape(defaults_servicenumber) %]"></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Last RFQ Number' | $T8 %]</th>
-     <td><input name="rfqnumber" size="10" value="[% HTML.escape(defaults_rfqnumber) %]"></td>
-     <th align="right" nowrap>[% 'Last Assembly Number' | $T8 %]</th>
-     <td><input name="assemblynumber" size="10" value="[% HTML.escape(defaults_assemblynumber) %]"></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Last Sales Delivery Order Number' | $T8 %]</th>
-     <td><input name="sdonumber" size="10" value="[% HTML.escape(defaults_sdonumber) %]"></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Last Purchase Delivery Order Number' | $T8 %]</th>
-     <td><input name="pdonumber" size="10" value="[% HTML.escape(defaults_pdonumber) %]"></td>
-    </tr>
-
-    <tr class="listheading">
-     <th colspan="4">[% 'Default Accounts' | $T8 %]</th>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Inventory Account' | $T8 %]</th>
-     <td colspan="3">
-      <select name="inventory_accno">
-       [%- FOREACH row = ACCNOS_IC %]
-       <option value="[% HTML.escape(row.value) %]"[% IF row.selected %] selected[% END %]>[% HTML.escape(row.name) %]</option>
-       [%- END %]
-      </select>
-     </td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Revenue Account' | $T8 %]</th>
-     <td colspan="3">
-      <select name="income_accno">
-       [%- FOREACH row = ACCNOS_IC_INCOME %]
-       <option value="[% HTML.escape(row.value) %]"[% IF row.selected %] selected[% END %]>[% HTML.escape(row.name) %]</option>
-       [%- END %]
-      </select>
-     </td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Expense Account' | $T8 %]</th>
-     <td colspan="3">
-      <select name="expense_accno">
-       [%- FOREACH row = ACCNOS_IC_EXPENSE %]
-       <option value="[% HTML.escape(row.value) %]"[% IF row.selected %] selected[% END %]>[% HTML.escape(row.name) %]</option>
-       [%- END %]
-      </select>
-     </td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Foreign Exchange Gain' | $T8 %]</th>
-     <td colspan="3">
-      <select name="fxgain_accno">
-       [%- FOREACH row = ACCNOS_FX_GAIN %]
-       <option value="[% HTML.escape(row.value) %]"[% IF row.selected %] selected[% END %]>[% HTML.escape(row.name) %]</option>
-       [%- END %]
-      </select>
-     </td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Foreign Exchange Loss' | $T8 %]</th>
-     <td colspan="3">
-      <select name="fxloss_accno">
-       [%- FOREACH row = ACCNOS_FX_LOSS %]
-       <option value="[% HTML.escape(row.value) %]"[% IF row.selected %] selected[% END %]>[% HTML.escape(row.name) %]</option>
-       [%- END %]
-      </select>
-     </td>
-    </tr>
-     <tr>
-     <th align="right" nowrap>[% 'Current assets account' | $T8 %]</th>
-     <td colspan="3">
-      <select name="ar_paid_accno">
-       [%- FOREACH row = ACCNOS_AR_PAID %]
-       <option value="[% HTML.escape(row.value) %]"[% IF row.selected %] selected[% END %]>[% HTML.escape(row.name) %]</option>
-       [%- END %]
-      </select>
-     </td>
-    </tr>
-
-    <tr class="listheading">
-     <th colspan="4">[% 'Miscellaneous' | $T8 %]</th>
-    </tr>
-
-    <tr>
-     <th align="right">[% 'Business Number' | $T8 %]</th>
-     <td colspan="3"><input name="businessnumber" size="25" value="[% HTML.escape(defaults_businessnumber) %]"></td>
-    </tr>
-
-    <tr>
-     <th align="right">[% 'Default currency' | $T8 %]</th>
-     <td colspan="3">[% HTML.escape(defaultcurrency) %]</td>
-    </tr>
-
-    <tr>
-     <input type="hidden" name="rowcount" value="[% CURRENCIES.size %]">
-     <th align="right">[% 'Currencies' | $T8 %]</th>
-     <td colspan="3">[% FOREACH row = CURRENCIES %]<input name="curr_[% loop.count %]" size="3" value="[% HTML.escape(row.curr) %]">
-                                                   <input type=hidden name="old_curr_[% loop.count %]" value="[% HTML.escape(row.curr) %]">
-                     [% END %]
-                     <input name="new_curr" size="3" value="">
-     </td>
-    </tr>
-
-    <tr>
-     <th align="right">[% 'Weight unit' | $T8 %]</th>
- <!--    <td colspan="3"><input name="weightunit" size="20" maxlength="5" value="[% HTML.escape(defaults_weightunit) %]"></td> -->
-     <td>
-      [%- INCLUDE 'generic/multibox.html'
-           name          = 'weightunit',
-           default       = defaults_weightunit,
-           style         = 'size:20; maxength:5',
-           DATA          = ALL_UNITS,
-           id_key        = 'name',
-           label_key     = 'name',
-           -%]
-      </td>
-    </tr>
-
-    <tr>
-     <th align="right">[% 'Default Customer/Vendor Language' | $T8 %]</th>
-     <td>
-      [%- INCLUDE 'generic/multibox.html'
-           name          = 'language_id',
-           default       = defaults_language_id,
-           style         = 'size:20; maxength:5',
-           DATA          = ALL_LANGUAGES,
-           id_key        = 'id',
-           label_key     = 'description',
-           show_empty    = 1
-           -%]
-      </td>
-    </tr>
-
-    <tr class="listheading">
-     <th colspan="4">[% 'Configuration' | $T8 %]</th>
-    </tr>
-
-    <tr>
-     <th align="right">[% 'Accounting method' | $T8 %] </th>
-     <td colspan="3"><input name="accounting_method" size="20" readonly="readonly" value="[% HTML.escape(defaults_accounting_method) | $T8 %]"></td>
-    </tr>
-
-    <tr>
-     <th align="right">[% 'Inventory system' | $T8 %] </th>
-     <td colspan="3"><input name="inventory_system" size="20" readonly="readonly" value="[% HTML.escape(defaults_inventory_system) | $T8 %]"></td>
-    </tr>
-
-    <tr>
-     <th align="right">[% 'Profit determination' | $T8 %] </th>
-     <td colspan="3"><input name="profit_determination" size="20" readonly="readonly" value="[% HTML.escape(defaults_profit_determination) | $T8 %]"></td>
-    </tr>
-
-    <tr>
-     <th align="right">[% 'Chart of accounts' | $T8 %] </th>
-     <td colspan="3"><input name="coa" size="20" readonly="readonly" value="[% HTML.escape(defaults_coa) | $T8 %]"></td>
-    </tr>
-
-
-   </table>
-  </p>
-
-  <hr height="3" noshade>
-
-  <p><input type="submit" class="submit" name="action" value="[% 'Save' | $T8 %]"></p>
-
-  <hr height="3" noshade>
-
-  </p>
- </form>
diff --git a/templates/webpages/client_config/_datev_check_configuration.html b/templates/webpages/client_config/_datev_check_configuration.html
new file mode 100644 (file)
index 0000000..e71487d
--- /dev/null
@@ -0,0 +1,33 @@
+[%- USE LxERP -%][%- USE L -%]
+<div id="datev_check_configuration">
+ <table>
+  <tr>
+   <td colspan="3">[% LxERP.t8('It is possible to make a quick DATEV export everytime you post a record to ensure things work nicely with their data requirements. This will result in a slight overhead though you can enable this for each type of record independantly.') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Check on sales invoice') %]</td>
+   <td>[% L.yes_no_tag('defaults.datev_check_on_sales_invoice', SELF.defaults.datev_check_on_sales_invoice) %]</td>
+   <td>[% LxERP.t8('Perform check when a sales invoice or a payment for a sales invoice is posted?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Check on purchase invoice') %]</td>
+   <td>[% L.yes_no_tag('defaults.datev_check_on_purchase_invoice', SELF.defaults.datev_check_on_purchase_invoice) %]</td>
+   <td>[% LxERP.t8('Perform check when a purchase invoice or a payment for a purchase invoice is posted?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Check on ar transaction') %]</td>
+   <td>[% L.yes_no_tag('defaults.datev_check_on_ar_transaction', SELF.defaults.datev_check_on_ar_transaction) %]</td>
+   <td>[% LxERP.t8('Perform check when an ar transaction is posted?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Check on ap transaction') %]</td>
+   <td>[% L.yes_no_tag('defaults.datev_check_on_ap_transaction', SELF.defaults.datev_check_on_ap_transaction) %]</td>
+   <td>[% LxERP.t8('Perform check when an ap transaction is posted?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Check on gl transaction') %]</td>
+   <td>[% L.yes_no_tag('defaults.datev_check_on_gl_transaction', SELF.defaults.datev_check_on_gl_transaction) %]</td>
+   <td>[% LxERP.t8('Perform check when a gl transaction is posted?') %]</td>
+  </tr>
+ </table>
+</div>
diff --git a/templates/webpages/client_config/_default_accounts.html b/templates/webpages/client_config/_default_accounts.html
new file mode 100644 (file)
index 0000000..f893f54
--- /dev/null
@@ -0,0 +1,35 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+[% SET style="width: 600px" %]
+<div id="default_accounts">
+ <table>
+  <tr>
+   <td align="right">[% LxERP.t8("Inventory Account") %]</td>
+   <td>[% L.select_tag('defaults.inventory_accno_id', SELF.accounts.ic, default=SELF.defaults.inventory_accno_id, title_sub=\make_chart_title, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Revenue Account") %]</td>
+   <td>[% L.select_tag('defaults.income_accno_id', SELF.accounts.ic_income, default=SELF.defaults.income_accno_id, title_sub=\make_chart_title, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Expense Account") %]</td>
+   <td>[% L.select_tag('defaults.expense_accno_id', SELF.accounts.ic_expense, default=SELF.defaults.expense_accno_id, title_sub=\make_chart_title, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Foreign Exchange Gain") %]</td>
+   <td>[% L.select_tag('defaults.fxgain_accno_id', SELF.accounts.fx_gain, default=SELF.defaults.fxgain_accno_id, title_sub=\make_chart_title, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Foreign Exchange Loss") %]</td>
+   <td>[% L.select_tag('defaults.fxloss_accno_id', SELF.accounts.fx_loss, default=SELF.defaults.fxloss_accno_id, title_sub=\make_chart_title, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Current assets account") %]</td>
+   <td>[% L.select_tag('defaults.ar_paid_accno_id', SELF.accounts.ar_paid, default=SELF.defaults.ar_paid_accno_id, title_sub=\make_chart_title, style=style) %]</td>
+  </tr>
+ </table>
+</div>
diff --git a/templates/webpages/client_config/_miscellaneous.html b/templates/webpages/client_config/_miscellaneous.html
new file mode 100644 (file)
index 0000000..82c8fd4
--- /dev/null
@@ -0,0 +1,112 @@
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
+[% SET style="width: 400px" %]
+<div id="miscellaneous">
+ <table>
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Company settings") %]</td></tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Company name") %]</td>
+   <td>[% L.input_tag('defaults.company', SELF.defaults.company, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right" valign="top">[% LxERP.t8("Address") %]</td>
+   <td valign="top">[% L.textarea_tag('defaults.address', SELF.defaults.address, style=style, rows=4) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Tax number") %]</td>
+   <td>[% L.input_tag('defaults.taxnumber', SELF.defaults.taxnumber, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Tax ID number") %]</td>
+   <td>[% L.input_tag('defaults.co_ustid', SELF.defaults.co_ustid, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("SEPA creditor ID") %]</td>
+   <td>[% L.input_tag('defaults.sepa_creditor_id', SELF.defaults.sepa_creditor_id, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Business Number") %]</td>
+   <td>[% L.input_tag('defaults.businessnumber', SELF.defaults.businessnumber, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("DUNS number") %]</td>
+   <td>[% L.input_tag('defaults.duns', SELF.defaults.duns, style=style) %]</td>
+  </tr>
+
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Language settings") %]</td></tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('Default Customer/Vendor Language') %]</td>
+   <td>[% L.select_tag('defaults.language_id', SELF.all_languages, title_key='description', default=SELF.defaults.language_id, with_empty=1, style=style) %]</td>
+  </tr>
+
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Print templates") %]</td></tr>
+
+  <tr>
+   <td align="right" valign="top">[% LxERP.t8("Print templates to use") %]</td>
+   <td colspan="3" valign="top">
+    <table>
+     <tr>
+      <td>[% L.radio_button_tag('use_templates', value='existing', id='use_templates_existing', label=LxERP.t8('Use existing templates'), checked=(FORM.use_templates == 'existing')) %]</td>
+      <td>[% L.select_tag('defaults.templates', SELF.all_templates.print_templates, default=SELF.defaults.templates, value_sub=\make_templates_value, style=style) %]</td>
+     </tr>
+
+     <tr>
+      <td>[% L.radio_button_tag('use_templates', value='new', id='use_templates_new', label=LxERP.t8('Create new templates from master templates'), checked=(FORM.use_templates == 'new')) %]</td>
+      <td>[% L.select_tag('new_master_templates', SELF.all_templates.master_templates, default=FORM.new_master_templates, style=style) %]</td>
+     </tr>
+
+     <tr>
+      <td align="right">[% LxERP.t8("New name") %]</td>
+      <td>[% L.input_tag('new_templates', FORM.new_templates, style=style) %]</td>
+     </tr>
+    </table>
+   </td>
+  </tr>
+
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Currencies") %]</td></tr>
+
+  <tr>
+   <th></th>
+   <th>[% LxERP.t8("Currency name") %]</th>
+   <th>[% LxERP.t8("Default currency") %]</th>
+   <th>[% LxERP.t8("Hints") %]</th>
+  </tr>
+
+[% FOREACH currency = SELF.all_currencies %]
+  [% L.hidden_tag("currencies[+].id", currency.id) %]
+  <tr>
+   <td align="right">[% IF loop.count == 1 %][% LxERP.t8("Currencies") %][% END %]</td>
+   <td>[% L.input_tag("currencies[].name", currency.name, style=style) %]</td>
+   <td align="center">[% L.radio_button_tag('defaults.currency_id', value=currency.id, id='defaults.currency_id_' _ currency.id, checked=(SELF.defaults.currency_id == currency.id)) %]</td>
+   <td>[% IF loop.count == 1 %][% LxERP.t8("Edit the currency names in order to rename them.") %][%- END %]</td>
+  </tr>
+[% END %]
+
+  <tr>
+   <td align="right">[% LxERP.t8("Add new currency") %]</td>
+   <td>[% L.input_tag("new_currency", FORM.new_currency, style=style) %]</td>
+   <td align="center">[% L.radio_button_tag('defaults.currency_id', value=-1, id='defaults.currency_id__1', checked=(SELF.defaults.currency_id == -1)) %]</td>
+  </tr>
+
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Weight") %]</td></tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Weight unit") %]</td>
+   <td>[% L.select_tag('defaults.weightunit', SELF.all_weightunits, default=SELF.defaults.weightunit, value_key='name', title_key='name', style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('Show weights') %]</td>
+   <td>[% L.yes_no_tag('defaults.show_weight', SELF.defaults.show_weight, style=style) %]</td>
+   <td colspan="2">[% LxERP.t8('Show the weights of articles and the total weight in orders, invoices and delivery notes?') %]</td>
+  </tr>
+ </table>
+</div>
+</div>
diff --git a/templates/webpages/client_config/_orders_deleteable.html b/templates/webpages/client_config/_orders_deleteable.html
new file mode 100644 (file)
index 0000000..0b35a72
--- /dev/null
@@ -0,0 +1,25 @@
+[%- USE LxERP -%][%- USE L -%]
+<div id="orders_deleteable">
+ <table>
+  <tr>
+   <td align="right">[% LxERP.t8('Sales Orders deleteable') %]</td>
+   <td>[% L.yes_no_tag('defaults.sales_order_show_delete', SELF.defaults.sales_order_show_delete) %]</td>
+   <td>[% LxERP.t8('Show delete button in sales orders?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Purchase Orders deleteable') %]</td>
+   <td>[% L.yes_no_tag('defaults.purchase_order_show_delete', SELF.defaults.purchase_order_show_delete) %]</td>
+   <td>[% LxERP.t8('Show delete button in purchase orders?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Sales Delivery Orders deleteable') %]</td>
+   <td>[% L.yes_no_tag('defaults.sales_delivery_order_show_delete', SELF.defaults.sales_delivery_order_show_delete) %]</td>
+   <td>[% LxERP.t8('Show delete button in sales delivery orders?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Purchase Delivery Orders deleteable') %]</td>
+   <td>[% L.yes_no_tag('defaults.purchase_delivery_order_show_delete', SELF.defaults.purchase_delivery_order_show_delete) %]</td>
+   <td>[% LxERP.t8('Show delete button in purchase delivery orders?') %]</td>
+  </tr>
+ </table>
+</div>
diff --git a/templates/webpages/client_config/_posting_configuration.html b/templates/webpages/client_config/_posting_configuration.html
new file mode 100644 (file)
index 0000000..d85fd79
--- /dev/null
@@ -0,0 +1,85 @@
+[%- USE L -%][%- USE LxERP -%]
+<div id="posting_configuration">
+ <table>
+  <tr>
+   <td align="right">[% LxERP.t8('Sales invoices changeable') %]</td>
+   <td>[% L.select_tag('defaults.is_changeable', SELF.posting_options, value_key = 'value', title_key = 'title', default = SELF.defaults.is_changeable) %]</td>
+   <td>[% LxERP.t8('Should sales invoices be and when should they be changeable or deleteable after posting?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Purchase invoices changeable') %]</td>
+   <td>[% L.select_tag('defaults.ir_changeable', SELF.posting_options, value_key = 'value', title_key = 'title', default = SELF.defaults.ir_changeable) %]</td>
+   <td>[% LxERP.t8('Should purchase invoices be and when should they be deleteable after posting?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('AR transactions changeable') %]</td>
+   <td>[% L.select_tag('defaults.ar_changeable', SELF.posting_options, value_key = 'value', title_key = 'title', default = SELF.defaults.ar_changeable) %]</td>
+   <td>[% LxERP.t8('Should ar transactions be and when should they be changeable or deleteable after posting?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('AP transactions changeable') %]</td>
+   <td>[% L.select_tag('defaults.ap_changeable', SELF.posting_options, value_key = 'value', title_key = 'title', default = SELF.defaults.ap_changeable) %]</td>
+   <td>[% LxERP.t8('Should ap transactions be and when should they be changeable or deleteable after posting?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('GL transactions changeable') %]</td>
+   <td>[% L.select_tag('defaults.gl_changeable', SELF.posting_options, value_key = 'value', title_key = 'title', default = SELF.defaults.gl_changeable) %]</td>
+   <td>[% LxERP.t8('Should gl transactions be and when should they be changeable or deleteable after posting?') %]</td>
+  </tr>
+
+  <tr> </tr>
+  <tr> </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('Payments Changeable') %]</td>
+   <td>[% L.select_tag('defaults.payments_changeable', SELF.payment_options, value_key = 'value', title_key = 'title', default = SELF.defaults.payments_changeable) %]</td>
+   <td>[% LxERP.t8('Should payments be and when should they be changeable after posting?') %]</td>
+  </tr>
+
+  <tr> </tr>
+  <tr> </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('Show "mark as paid" in sales invoices') %]</td>
+   <td>[% L.yes_no_tag('defaults.is_show_mark_as_paid', SELF.defaults.is_show_mark_as_paid) %]</td>
+   <td>[% LxERP.t8('Should the "mark as paid" button showed on sales invoices?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Show "mark as paid" in purchase invoices') %]</td>
+   <td>[% L.yes_no_tag('defaults.ir_show_mark_as_paid', SELF.defaults.ir_show_mark_as_paid) %]</td>
+   <td>[% LxERP.t8('Should the "mark as paid" button showed in purchase invoices?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Show "mark as paid" in ar transactions') %]</td>
+   <td>[% L.yes_no_tag('defaults.ar_show_mark_as_paid', SELF.defaults.ar_show_mark_as_paid) %]</td>
+   <td>[% LxERP.t8('Should the "mark as paid" button showed in ar transactions?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Show "mark as paid" in ap transactions') %]</td>
+   <td>[% L.yes_no_tag('defaults.ap_show_mark_as_paid', SELF.defaults.ap_show_mark_as_paid) %]</td>
+   <td>[% LxERP.t8('Should the "mark as paid" button showed in ap transactions?') %]</td>
+  </tr>
+
+  <tr> </tr>
+  <tr> </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('Accounting method') %]</td>
+   <td>[% L.select_tag('defaults.accounting_method', SELF.accounting_options, value_key = 'value', title_key = 'title', default = SELF.defaults.accounting_method) %]</td>
+   <td>[% LxERP.t8('This option controls the posting and calculation behavior for the accounting method.') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Inventory system') %]</td>
+   <td>[% L.select_tag('defaults.inventory_system', SELF.inventory_options, value_key = 'value', title_key = 'title', default = SELF.defaults.inventory_system) %]</td>
+   <td>
+    [% LxERP.t8('This option controls the inventory system.') %]<br>
+    [% LxERP.t8('ATTENTION! You can not simply change it from periodic to perpetual once you started posting.') %]
+   </td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Profit determination') %]</td>
+   <td>[% L.select_tag('defaults.profit_determination', SELF.profit_options, value_key = 'value', title_key = 'title', default = SELF.defaults.profit_determination) %]</td>
+   <td>[% LxERP.t8('This option controls the method used for profit determination.') %]</td>
+  </tr>
+ </table>
+</div>
diff --git a/templates/webpages/client_config/_ranges_of_numbers.html b/templates/webpages/client_config/_ranges_of_numbers.html
new file mode 100644 (file)
index 0000000..51b11b9
--- /dev/null
@@ -0,0 +1,54 @@
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
+<div id="ranges_of_numbers">
+ <table>
+  <tr>
+   <td align="right" nowrap>[% LxERP.t8('Last Invoice Number') %]</td>
+   <td>[% L.input_tag("defaults.invnumber", SELF.defaults.invnumber, size="15") %]</td>
+   <td align="right" nowrap>[% LxERP.t8('Last Customer Number') %]</td>
+   <td>[% L.input_tag("defaults.customernumber", SELF.defaults.customernumber, size="15") %]</td>
+  </tr>
+
+  <tr>
+   <td align="right" nowrap>[% LxERP.t8('Last Credit Note Number') %]</td>
+   <td>[% L.input_tag("defaults.cnnumber", SELF.defaults.cnnumber, size="15") %]</td>
+   <td align="right" nowrap>[% LxERP.t8('Last Vendor Number') %]</td>
+   <td>[% L.input_tag("defaults.vendornumber", SELF.defaults.vendornumber, size="15") %]</td>
+  </tr>
+
+  <tr>
+   <td align="right" nowrap>[% LxERP.t8('Last Sales Order Number') %]</td>
+   <td>[% L.input_tag("defaults.sonumber", SELF.defaults.sonumber, size="15") %]</td>
+  </tr>
+
+  <tr>
+   <td align="right" nowrap>[% LxERP.t8('Last Purchase Order Number') %]</td>
+   <td>[% L.input_tag("defaults.ponumber", SELF.defaults.ponumber, size="15") %]</td>
+   <td align="right" nowrap>[% LxERP.t8('Last Article Number') %]</td>
+   <td>[% L.input_tag("defaults.articlenumber", SELF.defaults.articlenumber, size="15") %]</td>
+  </tr>
+
+  <tr>
+   <td align="right" nowrap>[% LxERP.t8('Last Sales Quotation Number') %]</td>
+   <td>[% L.input_tag("defaults.sqnumber", SELF.defaults.sqnumber, size="15") %]</td>
+   <td align="right" nowrap>[% LxERP.t8('Last Service Number') %]</td>
+   <td>[% L.input_tag("defaults.servicenumber", SELF.defaults.servicenumber, size="15") %]</td>
+  </tr>
+
+  <tr>
+   <td align="right" nowrap>[% LxERP.t8('Last RFQ Number') %]</td>
+   <td>[% L.input_tag("defaults.rfqnumber", SELF.defaults.rfqnumber, size="15") %]</td>
+   <td align="right" nowrap>[% LxERP.t8('Last Assembly Number') %]</td>
+   <td>[% L.input_tag("defaults.assemblynumber", SELF.defaults.assemblynumber, size="15") %]</td>
+  </tr>
+
+  <tr>
+   <td align="right" nowrap>[% LxERP.t8('Last Sales Delivery Order Number') %]</td>
+   <td>[% L.input_tag("defaults.sdonumber", SELF.defaults.sdonumber, size="15") %]</td>
+  </tr>
+
+  <tr>
+   <td align="right" nowrap>[% LxERP.t8('Last Purchase Delivery Order Number') %]</td>
+   <td>[% L.input_tag("defaults.pdonumber", SELF.defaults.pdonumber, size="15") %]</td>
+  </tr>
+ </table>
+</div>
diff --git a/templates/webpages/client_config/_warehouse.html b/templates/webpages/client_config/_warehouse.html
new file mode 100644 (file)
index 0000000..87ff835
--- /dev/null
@@ -0,0 +1,79 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%]
+<div id="warehouse">
+ <table>
+  <tr>
+   <td align="right">[% LxERP.t8('Default Transfer') %]</td>
+   <td>
+    [% L.yes_no_tag('defaults.transfer_default', SELF.defaults.transfer_default) %]
+   </td>
+   <td>
+    [% LxERP.t8('Show Transfer via default') %]<br>
+   </td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Default Transfer with Master Bin') %]</td>
+   <td>
+    [% L.yes_no_tag('defaults.transfer_default_use_master_default_bin', SELF.defaults.transfer_default_use_master_default_bin) %]
+   </td>
+   <td>
+    [% LxERP.t8('Use master default bin for Default Transfer, if no default bin for the part is configured') %]<br>
+   </td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Default Transfer Out with negative inventory') %]</td>
+   <td>
+    [% L.yes_no_tag('defaults.transfer_default_ignore_onhand', SELF.defaults.transfer_default_ignore_onhand) %]
+   </td>
+   <td>
+    [% LxERP.t8('Default Transfer Out always succeed. The current part onhand is ignored and the inventory can have negative stocks (not recommended).') %]<br>
+   </td>
+  </tr>
+
+  <tr> </tr>
+  <tr>
+   <td align="right" nowrap="true">[% LxERP.t8('Default Warehouse') %]</td>
+   <td>
+    [% L.select_tag('defaults.warehouse_id', SELF.all_warehouses, id='warehouse_id', with_empty=1, default=SELF.defaults.warehouse_id, title_key='description',
+                    onchange="warehouse_selected(this.selectedIndex == 0 ? -1 : warehouses[this.selectedIndex - 1].id, -1)") %]
+   </td>
+   <td>
+    [% LxERP.t8('This is the default bin for parts') %]<br>
+    [% LxERP.t8('If configured this bin will be preselected for all new parts. Also this bin will be used as the master default bin, if default transfer out with master bin is activated.') %]<br>
+   </td>
+  </tr>
+  <tr>
+   <td align="right" nowrap="true">[% LxERP.t8('Default Bin') %]</td>
+   <td>[% L.select_tag('defaults.bin_id', [], id='bin_id', with_empty=1) %]</td>
+  </tr>
+  <tr>
+   <td align="right" nowrap="true">[% LxERP.t8('Default Warehouse with ignoring on hand') %]</td>
+   <td>
+    [% L.select_tag('defaults.warehouse_id_ignore_onhand', SELF.all_warehouses, id='warehouse_id_ignore_onhand', with_empty=1, default=SELF.defaults.warehouse_id_ignore_onhand, title_key='description',
+                    onchange="warehouse_selected(this.selectedIndex == 0 ? -1 : warehouses[this.selectedIndex - 1].id, -1, 'bin_id_ignore_onhand')") %]
+   </td>
+   <td>
+    [% LxERP.t8('This is the default bin for ignoring onhand') %]<br>
+    [% LxERP.t8('If the default transfer out always succeed use this bin for negative stock quantity.') %]<br>
+   </td>
+  </tr>
+  <tr>
+   <td align="right" nowrap="true">[% LxERP.t8('Default Bin with ignoring onhand') %]</td>
+   <td>[% L.select_tag('defaults.bin_id_ignore_onhand', [], id='bin_id_ignore_onhand', with_empty=1) %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Show Bestbefore') %]</td>
+   <td>
+    [% L.yes_no_tag('defaults.show_bestbefore', SELF.defaults.show_bestbefore) %]
+   </td>
+   <td>
+    [% LxERP.t8('Show fields used for the best before date?') %]<br>
+    [% LxERP.t8('ATTENTION! If you enabled this feature you can not simply turn it off again without taking care that best_before fields are emptied in the database.') %]<br>
+    [% LxERP.t8('This can be done with the following query:') %]<br>
+    <br>
+    UPDATE inventory SET bestbefore = NULL; <br>
+    <br>
+    [% LxERP.t8('Any stock contents containing a best before date will be impossible to stock out otherwise.') %]
+   </td>
+  </tr>
+ </table>
+</div>
index 16df61e..5a2a9ba 100644 (file)
@@ -1,61 +1,51 @@
-[%- USE T8 %][%- USE L %][% USE LxERP %][% USE HTML %]
-[%- USE JavaScript -%]
+[%- USE L %][% USE LxERP %][% USE HTML %][%- USE JavaScript -%]
  <script type="text/javascript" src="js/common.js"></script>
  <script type="text/javascript" src="js/parts_language_selection.js"></script>
  <script type="text/javascript">
   <!--
-      warehouses = new Array();
-      [%- USE WAREHOUSES_it = Iterator(SELF.WAREHOUSES) %][%- FOREACH warehouse = WAREHOUSES_it %]
-      warehouses[[% WAREHOUSES_it.count - 1 %]] = new Array();
-      warehouses[[% WAREHOUSES_it.count - 1 %]]['id'] = [% warehouse.id %];
-      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'] = new Array();
-      [% USE BINS_it = Iterator(warehouse.BINS) %][% FOREACH bin = BINS_it %]
-      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'][[% BINS_it.count - 1 %]] = new Array();
-      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'][[% BINS_it.count - 1 %]]['description'] = "[% JavaScript.escape(bin.description) %]";
-      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'][[% BINS_it.count - 1 %]]['id'] = [% bin.id %];
-      [% END %]
-      [% END %]
-
-      function warehouse_selected(warehouse_id, bin_id, bin_id_name) {
-
-        // bin_id_name is optional and only used in client_config.html
-        var bin_id_name = bin_id_name || 'bin_id';
-
-        var control = document.getElementById(bin_id_name);
-
-        for (var i = control.options.length - 1; i >= 0; i--) {
-          control.options[i] = null;
-        }
-
-        var warehouse_index = 0;
-
-        for (i = 0; i < warehouses.length; i++)
-          if (warehouses[i]['id'] == warehouse_id) {
-            warehouse_index = i;
-            break;
-          }
-
-        var warehouse = warehouses[warehouse_index];
-        var bin_index = 0;
-
-        for (i = 0; i < warehouse['bins'].length; i++)
-          if (warehouse['bins'][i]['id'] == bin_id) {
-            bin_index = i;
-            break;
-          }
-
-        for (i = 0; i < warehouse['bins'].length; i++) {
-          control.options[i] = new Option(warehouse['bins'][i]['description'], warehouse['bins'][i]['id']);
-        }
-
-
-        control.options[bin_index].selected = true;
-      }
-
-      $(function() {
-        warehouse_selected([% SELF.warehouse_id %], [% SELF.bin_id %], 'bin_id');
-        warehouse_selected([% SELF.warehouse_id_ignore_onhand %], [% SELF.bin_id_ignore_onhand %], 'bin_id_ignore_onhand');
-      })
+var warehouses = [
+ [%- USE warehouses_it = Iterator(SELF.all_warehouses) %][%- FOREACH warehouse = warehouses_it %]
+  { id:   [% warehouse.id %],
+    bins: [
+     [% USE bins_it = Iterator(warehouse.bins_sorted) %][% FOREACH bin = bins_it %]
+      { id: [% bin.id %], description: "[% JavaScript.escape(bin.description) %]" }[% UNLESS bins_it.last %],[% END %]
+     [% END %]
+   ] }[% UNLESS warehouses_it.last %],[% END %]
+ [% END %]
+];
+
+function warehouse_selected(warehouse_id, bin_id, bin_id_name) {
+  // bin_id_name is optional and only used in client_config.html
+  bin_id_name = bin_id_name || 'bin_id';
+
+  // Remove all bins safe for the empty entry
+  var bin_select = $('#' + bin_id_name);
+  bin_select.find('option').filter('[value!=""]').remove();
+
+  // Find selected warehouse
+  var warehouse = warehouses.filter(function(elt) { return elt.id == warehouse_id; })[0];
+  if (!warehouse)
+    return;
+
+  // Add bins as options to select.
+  $(warehouse.bins).each(function(idx, bin) {
+    bin_select.append($('<option>', { value: bin.id, selected: bin.id == bin_id }).text(bin.description));
+  });
+}
+
+function enable_template_controls() {
+  var existing = $('#use_templates_existing').prop('checked');
+  $('#defaults_templates').prop('disabled', !existing);
+  $('#new_templates,#new_master_templates').prop('disabled', existing);
+}
+
+$(function() {
+  warehouse_selected([% SELF.defaults.warehouse_id || -1 %], [% SELF.defaults.bin_id || -1 %], 'bin_id');
+  warehouse_selected([% SELF.defaults.warehouse_id_ignore_onhand || -1 %], [% SELF.defaults.bin_id_ignore_onhand || -1 %], 'bin_id_ignore_onhand');
+
+  enable_template_controls();
+  $('#use_templates_existing,#use_templates_new').change(enable_template_controls);
+})
     -->
  </script>
 <h1>[% title | html %]</h1>
 [% PROCESS 'common/flash.html' %]
 
 <form action='controller.pl' method='POST'>
-
-<table>
-
- <tr class='listheading'>
-   <th colspan="3">[% 'Posting Configuration' | $T8 %]</th>
- </tr>
-
- <tr>
-   <td align="right">[% 'Sales invoices changeable' | $T8 %]</td>
-   <td>[% L.select_tag('is_changeable', SELF.posting_options, value_key => 'value', title_key => 'title', default => SELF.is_changeable) %]</td>
-   <td>[% 'Should sales invoices be and when should they be changeable or deleteable after posting?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Purchase invoices changeable' | $T8 %]</td>
-   <td>[% L.select_tag('ir_changeable', SELF.posting_options, value_key => 'value', title_key => 'title', default => SELF.ir_changeable) %]</td>
-   <td>[% 'Should purchase invoices be and when should they be deleteable after posting?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'AR transactions changeable' | $T8 %]</td>
-   <td>[% L.select_tag('ar_changeable', SELF.posting_options, value_key => 'value', title_key => 'title', default => SELF.ar_changeable) %]</td>
-   <td>[% 'Should ar transactions be and when should they be changeable or deleteable after posting?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'AP transactions changeable' | $T8 %]</td>
-   <td>[% L.select_tag('ap_changeable', SELF.posting_options, value_key => 'value', title_key => 'title', default => SELF.ap_changeable) %]</td>
-   <td>[% 'Should ap transactions be and when should they be changeable or deleteable after posting?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'GL transactions changeable' | $T8 %]</td>
-   <td>[% L.select_tag('gl_changeable', SELF.posting_options, value_key => 'value', title_key => 'title', default => SELF.gl_changeable) %]</td>
-   <td>[% 'Should gl transactions be and when should they be changeable or deleteable after posting?' | $T8 %]</td>
- </tr>
-
- <tr> </tr>
- <tr> </tr>
-
- <tr>
-   <td align="right">[% 'Payments Changeable' | $T8 %]</td>
-   <td>[% L.select_tag('payments_changeable', SELF.payment_options, value_key => 'value', title_key => 'title', default => SELF.payments_changeable) %]</td>
-   <td>[% 'Should payments be and when should they be changeable after posting?' | $T8 %]</td>
- </tr>
-
- <tr> </tr>
- <tr> </tr>
-
- <tr>
-   <td align="right">[% 'Show "mark as paid" in sales invoices' | $T8 %]</td>
-   <td>[% L.yes_no_tag('is_show_mark_as_paid', SELF.is_show_mark_as_paid) %]</td>
-   <td>[% 'Should the "mark as paid" button showed on sales invoices?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Show "mark as paid" in purchase invoices' | $T8 %]</td>
-   <td>[% L.yes_no_tag('ir_show_mark_as_paid', SELF.ir_show_mark_as_paid) %]</td>
-   <td>[% 'Should the "mark as paid" button showed in purchase invoices?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Show "mark as paid" in ar transactions' | $T8 %]</td>
-   <td>[% L.yes_no_tag('ar_show_mark_as_paid', SELF.ar_show_mark_as_paid) %]</td>
-   <td>[% 'Should the "mark as paid" button showed in ar transactions?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Show "mark as paid" in ap transactions' | $T8 %]</td>
-   <td>[% L.yes_no_tag('ap_show_mark_as_paid', SELF.ap_show_mark_as_paid) %]</td>
-   <td>[% 'Should the "mark as paid" button showed in ap transactions?' | $T8 %]</td>
- </tr>
-
- <tr> </tr>
- <tr> </tr>
-
- <tr>
-   <td align="right">[% 'Accounting method' | $T8 %]</td>
-   <td>[% L.select_tag('accounting_method', SELF.accounting_options, value_key => 'value', title_key => 'title', default => SELF.accounting_method) %]</td>
-   <td>[% 'This option controls the posting and calculation behavior for the accounting method.' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Inventory system' | $T8 %]</td>
-   <td>[% L.select_tag('inventory_system', SELF.inventory_options, value_key => 'value', title_key => 'title', default => SELF.inventory_system) %]</td>
-   <td>
-     [% 'This option controls the inventory system.' | $T8 %]<br>
-     [% 'ATTENTION! You can not simply change it from periodic to perpetual once you started posting.' | $T8 %]
-   </td>
- </tr>
- <tr>
-   <td align="right">[% 'Profit determination' | $T8 %]</td>
-   <td>[% L.select_tag('profit_determination', SELF.profit_options, value_key => 'value', title_key => 'title', default => SELF.profit_determination) %]</td>
-   <td>[% 'This option controls the method used for profit determination.' | $T8 %]</td>
- </tr>
-
- <tr> </tr>
- <tr> </tr>
-
- <tr class='listheading'>
-   <th colspan="3">[% 'DATEV check configuration' | $T8 %]</th>
- </tr>
- <tr>
-   <td colspan="3">[% 'It is possible to make a quick DATEV export everytime you post a record to ensure things work nicely with their data requirements. This will result in a slight overhead though you can enable this for each type of record independantly.' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Check on sales invoice' | $T8 %]</td>
-   <td>[% L.yes_no_tag('datev_check_on_sales_invoice', SELF.datev_check_on_sales_invoice) %]</td>
-   <td>[% 'Perform check when a sales invoice or a payment for a sales invoice is posted?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Check on purchase invoice' | $T8 %]</td>
-   <td>[% L.yes_no_tag('datev_check_on_purchase_invoice', SELF.datev_check_on_purchase_invoice) %]</td>
-   <td>[% 'Perform check when a purchase invoice or a payment for a purchase invoice is posted?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Check on ar transaction' | $T8 %]</td>
-   <td>[% L.yes_no_tag('datev_check_on_ar_transaction', SELF.datev_check_on_ar_transaction) %]</td>
-   <td>[% 'Perform check when an ar transaction is posted?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Check on ap transaction' | $T8 %]</td>
-   <td>[% L.yes_no_tag('datev_check_on_ap_transaction', SELF.datev_check_on_ap_transaction) %]</td>
-   <td>[% 'Perform check when an ap transaction is posted?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Check on gl transaction' | $T8 %]</td>
-   <td>[% L.yes_no_tag('datev_check_on_gl_transaction', SELF.datev_check_on_gl_transaction) %]</td>
-   <td>[% 'Perform check when a gl transaction is posted?' | $T8 %]</td>
- </tr>
-
- <tr> </tr>
- <tr> </tr>
-
- <tr class='listheading'>
-   <th colspan="3">[% 'Orders / Delivery Orders deleteable' | $T8 %]</th>
- </tr>
- <tr>
-   <td align="right">[% 'Sales Orders deleteable' | $T8 %]</td>
-   <td>[% L.yes_no_tag('sales_order_show_delete', SELF.sales_order_show_delete) %]</td>
-   <td>[% 'Show delete button in sales orders?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Purchase Orders deleteable' | $T8 %]</td>
-   <td>[% L.yes_no_tag('purchase_order_show_delete', SELF.purchase_order_show_delete) %]</td>
-   <td>[% 'Show delete button in purchase orders?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Sales Delivery Orders deleteable' | $T8 %]</td>
-   <td>[% L.yes_no_tag('sales_delivery_order_show_delete', SELF.sales_delivery_order_show_delete) %]</td>
-   <td>[% 'Show delete button in sales delivery orders?' | $T8 %]</td>
- </tr>
- <tr>
-   <td align="right">[% 'Purchase Delivery Orders deleteable' | $T8 %]</td>
-   <td>[% L.yes_no_tag('purchase_delivery_order_show_delete', SELF.purchase_delivery_order_show_delete) %]</td>
-   <td>[% 'Show delete button in purchase delivery orders?' | $T8 %]</td>
- </tr>
-
- <tr> </tr>
- <tr> </tr>
-
- <tr class='listheading'>
-   <th colspan="3">[% 'Warehouse' | $T8 %]</th>
- </tr>
- <tr>
-   <td align="right">[% 'Default Transfer' | $T8 %]</td>
-   <td>
-     [% L.yes_no_tag('transfer_default', SELF.transfer_default) %]
-   </td>
-   <td>
-     [% 'Show Transfer via default' | $T8 %]<br>
-  </td>
-  </tr>
-  <tr>
-   <td align="right">[% 'Default Transfer with Master Bin' | $T8 %]</td>
-   <td>
-     [% L.yes_no_tag('transfer_default_use_master_default_bin', SELF.transfer_default_use_master_default_bin) %]
-   </td>
-   <td>
-     [% 'Use master default bin for Default Transfer, if no default bin for the part is configured' | $T8 %]<br>
-  </td>
-  </tr>
-  <tr>
-   <td align="right">[% 'Default Transfer Out with negative inventory' | $T8 %]</td>
-   <td>
-     [% L.yes_no_tag('transfer_default_ignore_onhand', SELF.transfer_default_ignore_onhand) %]
-   </td>
-   <td>
-     [% 'Default Transfer Out always succeed. The current part onhand is ignored and the inventory can have negative stocks (not recommended).' | $T8 %]<br>
-  </td>
-  </tr>
-
- <tr> </tr>
-<tr>
-<th align="right" nowrap="true">[% 'Default Warehouse' | $T8 %]</th>
-<td>
- <select name="warehouse_id" onchange="warehouse_selected(warehouses[this.selectedIndex]['id'], 0)">
-  [%- FOREACH warehouse = SELF.WAREHOUSES %]
-    <option value="[% HTML.escape(warehouse.id) %]"[% IF SELF.warehouse_id == warehouse.id %] selected[% END %]>[% warehouse.description %]</option>
-  [%- END %]
- </select>
-</td>
-<td>
-   [% 'This is the default bin for parts' | $T8 %]<br>
-   [% 'If configured this bin will be preselected for all new parts. Also this bin will be used as the master default bin, if default transfer out with master bin is activated.' | $T8 %]<br>
-</td>
-</tr>
-<tr>
-<th align="right" nowrap="true">[% 'Default Bin' | $T8 %]</th>
-<td><select id="bin_id" name="bin_id"></select></td>
-</tr>
-<tr>
-<th align="right" nowrap="true">[% 'Default Warehouse with ignoring on hand' | $T8 %]</th>
-<td>
- <select name="warehouse_id_ignore_onhand" onchange="warehouse_selected(warehouses[this.selectedIndex]['id'], 0, 'bin_id_ignore_onhand')">
-  [%- FOREACH warehouse = SELF.WAREHOUSES %]
-    <option value="[% HTML.escape(warehouse.id) %]"[% IF SELF.warehouse_id_ignore_onhand == warehouse.id %] selected[% END %]>[% warehouse.description %]</option>
-  [%- END %]
- </select>
-</td>
-<td>
-   [% 'This is the default bin for ignoring onhand' | $T8 %]<br>
-   [% 'If the default transfer out always succeed use this bin for negative stock quantity.' | $T8 %]<br>
-</td>
-</tr>
-<tr>
-<th align="right" nowrap="true">[% 'Default Bin with ignoring onhand' | $T8 %]</th>
-<td><select id="bin_id_ignore_onhand" name="bin_id_ignore_onhand"></select></td>
-</tr>
-<tr>
-   <td align="right">[% 'Show Bestbefore' | $T8 %]</td>
-   <td>
-     [% L.yes_no_tag('show_bestbefore', SELF.show_bestbefore) %]
-   </td>
-   <td>
-     [% 'Show fields used for the best before date?' | $T8 %]<br>
-     [% 'ATTENTION! If you enabled this feature you can not simply turn it off again without taking care that best_before fields are emptied in the database.' | $T8 %]<br>
-     [% 'This can be done with the following query:' | $T8 %]<br>
-     <br>
-     UPDATE inventory SET bestbefore = NULL; <br>
-     <br>
-     [% 'Any stock contents containing a best before date will be impossible to stock out otherwise.' | $T8 %]
-   </td>
- </tr>
- <tr class='listheading'>
-   <th colspan="3">[% 'Weight' | $T8 %]</th>
- </tr>
- <tr>
-   <td align="right">[% 'Show weights' | $T8 %]</td>
-   <td>
-     [% L.yes_no_tag('show_weight', SELF.show_weight) %]
-   </td>
-   <td>
-     [% 'Show the weights of articles and the total weight in orders, invoices and delivery notes?' | $T8 %]<br>
-   </td>
- </tr>
-</table>
-
-<br>
-
-[%- L.hidden_tag('action',  'ClientConfig/dispatch')  %]
-[%- L.submit_tag('action_save',  LxERP.t8('Save'))  %]
-
-<br><br>
+ <div class="tabwidget">
+  <ul>
+   <li><a href="#miscellaneous">[% LxERP.t8('Miscellaneous') %]</a></li>
+   <li><a href="#ranges_of_numbers">[% LxERP.t8('Ranges of numbers') %]</a></li>
+   <li><a href="#default_accounts">[% LxERP.t8('Default Accounts') %]</a></li>
+   <li><a href="#posting_configuration">[% LxERP.t8('Posting Configuration') %]</a></li>
+   <li><a href="#datev_check_configuration">[% LxERP.t8('DATEV check configuration') %]</a></li>
+   <li><a href="#orders_deleteable">[% LxERP.t8('Orders / Delivery Orders deleteable') %]</a></li>
+   <li><a href="#warehouse">[% LxERP.t8('Warehouse') %]</a></li>
+  </ul>
+
+[% PROCESS 'client_config/_ranges_of_numbers.html' %]
+[% PROCESS 'client_config/_default_accounts.html' %]
+[% PROCESS 'client_config/_posting_configuration.html' %]
+[% PROCESS 'client_config/_datev_check_configuration.html' %]
+[% PROCESS 'client_config/_orders_deleteable.html' %]
+[% PROCESS 'client_config/_warehouse.html' %]
+[% PROCESS 'client_config/_miscellaneous.html' %]
+
+ <div>
+  [%- L.hidden_tag('action',  'ClientConfig/dispatch')  %]
+  [%- L.submit_tag('action_save',  LxERP.t8('Save'))  %]
+ </div>
 
 </form>
diff --git a/templates/webpages/dbupgrade/auth/clients.html b/templates/webpages/dbupgrade/auth/clients.html
new file mode 100644 (file)
index 0000000..15ef02a
--- /dev/null
@@ -0,0 +1,124 @@
+[%- USE LxERP -%][%- USE L -%]
+
+[%- INCLUDE 'common/flash.html' %]
+
+[% L.javascript_tag('jquery.selectboxes', 'jquery.multiselect2side') %]
+
+<h1>[%- LxERP.t8("Introduction of clients") %]</h1>
+
+<p>
+ [% LxERP.t8("kivitendo has been extended to handle multiple clients within a single installation.") %]
+ [% LxERP.t8("Therefore several settings that had to be made for each user in the past have been consolidated into the client configuration.") %]
+ [% LxERP.t8("You have to grant users access to one or more clients.") %]
+ [% LxERP.t8("The user can chose which client to connect to during login.") %]
+</p>
+
+<p>
+ [% LxERP.t8("The access rights a user has within a client instance is still governed by his group membership.") %]
+ [% LxERP.t8("Only groups that have been configured for the client the user logs in to will be considered.") %]
+</p>
+
+<p>
+ [% LxERP.t8("The following list has been generated automatically from existing users collapsing users with identical settings into a single entry.") %]
+ [% LxERP.t8("Please select which client configurations you want to create.") %]
+ [% LxERP.t8("The 'name' is the field shown to the user during login.") %]
+ [% LxERP.t8("It can be changed later but must be unique within the installation.") %]
+</p>
+
+<form method="post" action="controller.pl">
+ [%- FOREACH client = SELF.clients %]
+ [%- L.hidden_tag("clients[+].dummy", 1) %]
+
+ <h2>[%- L.checkbox_tag("clients[].enabled", label=LxERP.t8("Create new client #1", loop.count), checked=client.enabled) %]</h2>
+
+ <table>
+  <tr>
+   <th colspan="6">[%- LxERP.t8("General settings") %]</th>
+  </tr>
+
+  <tr>
+   <td align="right" valign="top">[%- LxERP.t8("Client name") %]:</td>
+   <td valign="top">[%- L.input_tag("clients[].name", client.name) %]</td>
+
+   <td align="right" valign="top">[%- LxERP.t8("Company name") %]:</td>
+   <td valign="top">[%- L.input_tag("clients[].company", client.company) %]</td>
+
+   <td align="right" valign="top">[%- LxERP.t8("Address") %]:</td>
+   <td valign="top">[%- L.textarea_tag("clients[].address", client.address, rows=4, cols=40) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[%- LxERP.t8("Tax number") %]:</td>
+   <td>[%- L.input_tag("clients[].taxnumber", client.taxnumber) %]</td>
+
+   <td align="right">[%- LxERP.t8("VAT ID") %]:</td>
+   <td>[%- L.input_tag("clients[].co_ustid", client.co_ustid) %]</td>
+
+   <td align="right">[%- LxERP.t8("DUNS-Nr") %]:</td>
+   <td>[%- L.input_tag("clients[].duns", client.duns) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[%- LxERP.t8("SEPA creditor ID") %]:</td>
+   <td colspan="5">[%- L.input_tag("clients[].sepa_creditor_id", client.sepa_creditor_id) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[%- LxERP.t8("Print templates") %]:</td>
+   <td colspan="5">[%- L.select_tag("clients[].templates", SELF.templates, default=client.templates) %]</td>
+  </tr>
+
+  <tr>
+   <th colspan="6">[%- LxERP.t8("User access") %]</th>
+  </tr>
+
+  <tr>
+   <td valign="top">[%- LxERP.t8("Users with access to this client") %]:</td>
+
+   <td valign="top" colspan="6" class="clearfix">
+    [% L.select_tag('clients[].users[]', SELF.users, id='users_multi_' _ loop.count, value_key='id', title_key='login', default=client.users, multiple=1) %]
+   </td>
+  </tr>
+
+  <tr>
+   <td valign="top">[%- LxERP.t8("Groups that are valid for this client for access rights") %]:</td>
+
+   <td valign="top" colspan="6" class="clearfix">
+    [% L.select_tag('clients[].groups[]', SELF.groups, id='groups_multi_' _ loop.count, value_key='id', title_key='name', default=client.groups, multiple=1) %]
+   </td>
+  </tr>
+
+  <tr>
+   <th colspan="6">[%- LxERP.t8("Database settings") %]</th>
+  </tr>
+
+  <tr>
+   <td align="right">[%- LxERP.t8("Database Host") %]:</td>
+   <td>[%- L.input_tag("clients[].dbhost", client.dbhost) %]</td>
+
+   <td align="right">[%- LxERP.t8("Port") %]:</td>
+   <td>[%- L.input_tag("clients[].dbport", (client.dbport || 5432)) %]</td>
+
+   <td align="right">[%- LxERP.t8("Database name") %]:</td>
+   <td>[%- L.input_tag("clients[].dbname", client.dbname) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[%- LxERP.t8("User") %]:</td>
+   <td>[%- L.input_tag("clients[].dbuser", client.dbuser) %]</td>
+
+   <td align="right">[%- LxERP.t8("Password") %]:</td>
+   <td>[%- L.input_tag("clients[].dbpasswd", client.dbpasswd) %]</td>
+  </tr>
+
+ </table>
+
+ [% L.multiselect2side('users_multi_'  _ loop.count, labelsx => LxERP.t8('All users'),  labeldx => LxERP.t8('Users with access')) %]
+ [% L.multiselect2side('groups_multi_' _ loop.count, labelsx => LxERP.t8('All groups'), labeldx => LxERP.t8('Groups valid for this client')) %]
+ [%- END %]
+
+ <p>
+  [%- L.hidden_tag('action', 'Admin/apply_dbupgrade_scripts') %]
+  [% L.submit_tag('dummy', LxERP.t8('Continue')) %]
+ </p>
+</form>
diff --git a/templates/webpages/dbupgrade/auth/clients_webdav.html b/templates/webpages/dbupgrade/auth/clients_webdav.html
new file mode 100644 (file)
index 0000000..f560323
--- /dev/null
@@ -0,0 +1,24 @@
+[%- USE LxERP -%][%- USE L -%]
+
+[%- INCLUDE 'common/flash.html' %]
+
+<h1>[%- LxERP.t8("Introduction of clients") %] -- [% LxERP.t8("Handling of WebDAV") %]</h1>
+
+<p>
+ [% LxERP.t8("The WebDAV feature is activated.") %]
+ [% LxERP.t8("With the introduction of clients each client gets its own WebDAV folder.") %]
+ [% LxERP.t8("In order to migrate the old folder structure into the new structure you have to chose which client the old structure will be assigned to.") %]
+ [% LxERP.t8("All the other clients will start with an empty set of WebDAV folders.") %]
+</p>
+
+<form method="post" action="controller.pl">
+ <p>
+  [% LxERP.t8("Client to assign the existing WebDAV folders to") %]:
+  [% L.select_tag('client_id', SELF.clients, default=default_client.id, title_key='name') %]
+ </p>
+
+ <p>
+  [%- L.hidden_tag('action', 'Admin/apply_dbupgrade_scripts') %]
+  [% L.submit_tag('dummy', LxERP.t8('Continue')) %]
+ </p>
+</form>
index b1a2a50..187eed4 100644 (file)
@@ -1,10 +1,9 @@
 [%- USE T8 %]
 [% USE HTML %]<p>[% '...done' | $T8 %]</p>
 
-<form action="[% IF is_admin %]admin.pl[% ELSE %]login.pl[% END %]">
+<form action="[% IF is_admin %]controller.pl[% ELSE %]login.pl[% END %]">
 
- <input type="hidden" name="action" value="[% IF is_admin %]login[% ELSE %]company_logo[% END %]">
+ <input type="hidden" name="action" value="[% IF is_admin %]Admin/login[% ELSE %]company_logo[% END %]">
 
  <p><input type="submit" value="[% 'Continue' | $T8 %]"></p>
 </form>
-
diff --git a/templates/webpages/generic/form_info.html b/templates/webpages/generic/form_info.html
new file mode 100644 (file)
index 0000000..d56e1fc
--- /dev/null
@@ -0,0 +1,13 @@
+[%- USE P -%]
+<div class="message_ok">
+ <b>[% P.simple_format(message) %]</b>
+</div>
+
+<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?action=company_logo'",500);
+-->
+</script>
index 1939fc3..00273aa 100644 (file)
@@ -1,6 +1,5 @@
 [%- USE T8 %]
 [%- USE HTML %][%- USE LxERP %]
- [%- DEFAULT myconfig_dbhost = 'localhost' %]
    <noscript>
    [% INCLUDE 'generic/information.html'
      title_information = LxERP.t8('Your browser does not currently support Javascript.'),
@@ -19,9 +18,9 @@
   <p>[% 'companylogo_subtitle' | $T8 %]</p>
   <p>
    <b>
-    [% HTML.escape(myconfig_company) %]
+    [% HTML.escape(defaults.company) %]
     <br>
-    [% HTML.escape(myconfig_address).replace('\\\\n', '<br>').replace('\n', '<br>') %]
+    [% HTML.escape(defaults.address).replace('\\\\n', '<br>').replace('\n', '<br>') %]
    </b>
 
    <br>
      <td>[% HTML.escape(myconfig_name) %]</td>
     </tr>
     <tr>
-     <th align="left"><a href="am.pl?action=config" title="[% 'Preferences' | $T8 %]">[% 'Language' | $T8 %]</a></th>
-     <td>[% HTML.escape(myconfig_countrycode) %]</td>
-    </tr>
-    <tr>
-     <th align="left"><a href="admin.pl" title="[% 'Administration' | $T8 %]">[% 'Templates' | $T8 %]</a></th>
-     <td>[% HTML.escape(myconfig_templates) %]</td>
+     <th align="left">[% IF AUTH_RIGHTS_ADMIN %]<a href="controller.pl?action=ClientConfig/edit" title="[% 'Client Configuration' | $T8 %]">[% END %][% 'Client' | $T8 %][% IF AUTH_RIGHTS_ADMIN %]</a>[% END %]</th>
+     <td>[% HTML.escape(client.name) %]</td>
     </tr>
     <tr>
-     <th align="left"><a href="admin.pl" title="[% 'Administration' | $T8 %]">[% 'Dataset' | $T8 %]</a></th>
-     <td>[% HTML.escape(myconfig_dbname) %]</td>
-    </tr>
-    <tr>
-     <th align="left">[% 'Database Host' | $T8 %]</th>
-     <td>[% HTML.escape(myconfig_dbhost) %]</td>
+     <th align="left"><a href="am.pl?action=config" title="[% 'Preferences' | $T8 %]">[% 'Language' | $T8 %]</a></th>
+     <td>[% HTML.escape(myconfig_countrycode) %]</td>
     </tr>
     <tr>
      <th align="left">[% 'Webserver interface' | $T8 %]</th>
index d9756e3..da4d9ba 100644 (file)
@@ -1,5 +1,5 @@
 [%- USE T8 %]
-[%- USE HTML %]
+[%- USE HTML %][%- USE L -%][%- USE LxERP -%]
  <center>
   <table class="login" border="3" cellpadding="20">
    <tr>
@@ -7,12 +7,15 @@
      <a href="http://www.kivitendo.de" target="_top"><img src="image/kivitendo.png" border="0"></a>
      <h1>[% 'kivitendo' | $T8 %] [% version %]</h1>
 
-     [% IF error %]
-     <p><span class="message_error_login">[% error %]</span></p>
-     [% END %]
-
-     <p>
+[% IF error %]
+     <div class="message_error_login">[% HTML.escape(error) %]</div>
+[% END %]
+[% IF info %]
+     <div class="message_ok">[% HTML.escape(info) %]</div>
+[% END %]
 
+[% IF SELF.clients.size %]
+[%- SET style="width: 250px" %]
       <form method="post" name="loginscreen" action="controller.pl" target="_top">
 
        <input type="hidden" name="show_dbupdate_warning" value="1">
           <table>
            <tr>
             <th align="right">[% 'Login Name' | $T8 %]</th>
-            <td><input id='input_login' class="login" name="{AUTH}login" size="30" tabindex="1"></td>
+            <td>[% L.input_tag('{AUTH}login', '', id='auth_login', style=style) %]</td>
            </tr>
            <tr>
             <th align="right">[% 'Password' | $T8 %]</th>
-            <td><input class="login" type="password" name="{AUTH}password" size="30" tabindex="2"></td>
+            <td>[% L.input_tag('{AUTH}password', '', type='password', style=style) %]</td>
+           </tr>
+           <tr>
+            <th align="right">[% 'Client' | $T8 %]</th>
+            <td>[% L.select_tag('{AUTH}client_id', SELF.clients, title_key='name', default=SELF.default_client_id, style=style) %]</td>
            </tr>
           </table>
 
           <br>
           <input type="hidden" name="action" value="LoginScreen/login">
-          <input type="submit" value="[% 'Login' | $T8 %]" tabindex="3">
+          <input type="submit" value="[% 'Login' | $T8 %]">
 
          </td>
         </tr>
        </table>
 
       </form>
+[%- ELSE %]
+      <p>
+       [% LxERP.t8("No clients have been created yet.") %]
+       [% LxERP.t8("Please do so in the administration area.") %]
+      </p>
+
+      <p>
+       <a href="controller.pl?action=Admin/login">[% LxERP.t8("Administration area") %]</a>
+      </p>
+[%- END %]
 
     </td>
    </tr>
   </table>
-  <script type='text/javascript'>
-    $(function(){ $('#input_login').focus() })
-  </script>
index 0ad43f5..b798c11 100644 (file)
@@ -10,9 +10,9 @@
  </span>
 [%- END %]
  <span class="frame-header-element frame-header-right">
-  [% 'User' | $T8 %]:
-  [% MYCONFIG.login | html %]
-  [<a href="controller.pl?action=LoginScreen/logout" target="_top" title="[% 'Logout now' | $T8 %]">[% 'Logout' | $T8 %]</a>]
+  [[% 'User' | $T8 %]: [% MYCONFIG.login | html %] -
+   [% 'Client' | $T8 %]: [% AUTH.client.name | html %] -
+   <a href="controller.pl?action=LoginScreen/logout" target="_top" title="[% 'Logout now' | $T8 %]">[% 'Logout' | $T8 %]</a>]
   [% now.to_lxoffice %] -
   [% now.hms %]
  </span>
index 11e676e..55fcab8 100644 (file)
@@ -23,6 +23,7 @@ $(clockon);
   </span>
   <span class="frame-header-element frame-header-right">
    [[% 'User' | $T8 %]: [% MYCONFIG.login | html %] -
+    [% 'Client' | $T8 %]: [% AUTH.client.name | html %] -
    <a href="controller.pl?action=LoginScreen/logout" target="_top">[% 'logout' | $T8 %]</a>]
    [% date %] <span id='clock_id' style='position:relative'></span>&nbsp;
   </span>
index 8872988..a89b268 100644 (file)
@@ -22,6 +22,7 @@ $(clockon);
  </span>
  <span class="frame-header-element frame-header-right">
     [[% 'User' | $T8 %]: [% MYCONFIG.login | html %] -
+     [% 'Client' | $T8 %]: [% AUTH.client.name | html %] -
     <a href="controller.pl?action=LoginScreen/logout" target="_top">[% 'logout' | $T8 %]</a>]
     [% date %] <span id='clock_id' style='position:relative'></span>&nbsp;
  </span>
index 48a2e3e..d566dbd 100644 (file)
 [%- IF COA_Germany %]
            [% input_steuernummer %]
 [%- ELSE %]
-[% 'Please enter the taxnumber in the administration menu user preferences' | $T8 %]
+[% 'Please enter the taxnumber in the client configuration.' | $T8 %]
 [% 'Current value:' | $T8 %] [% HTML.escape(myconfig_taxnumber) %]
 [%- END %]
 
diff --git a/webdav/.gitignore b/webdav/.gitignore
new file mode 100644 (file)
index 0000000..d6b7ef3
--- /dev/null
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/webdav/anfragen/.dummy b/webdav/anfragen/.dummy
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/webdav/anfragen/.gitignore b/webdav/anfragen/.gitignore
deleted file mode 100644 (file)
index 72e8ffc..0000000
+++ /dev/null
@@ -1 +0,0 @@
-*
diff --git a/webdav/angebote/.dummy b/webdav/angebote/.dummy
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/webdav/angebote/.gitignore b/webdav/angebote/.gitignore
deleted file mode 100644 (file)
index 72e8ffc..0000000
+++ /dev/null
@@ -1 +0,0 @@
-*
diff --git a/webdav/bestellungen/.dummy b/webdav/bestellungen/.dummy
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/webdav/bestellungen/.gitignore b/webdav/bestellungen/.gitignore
deleted file mode 100644 (file)
index 72e8ffc..0000000
+++ /dev/null
@@ -1 +0,0 @@
-*
diff --git a/webdav/einkaufsrechnungen/.dummy b/webdav/einkaufsrechnungen/.dummy
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/webdav/einkaufsrechnungen/.gitignore b/webdav/einkaufsrechnungen/.gitignore
deleted file mode 100644 (file)
index 72e8ffc..0000000
+++ /dev/null
@@ -1 +0,0 @@
-*
diff --git a/webdav/gutschriften/.dummy b/webdav/gutschriften/.dummy
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/webdav/gutschriften/.gitignore b/webdav/gutschriften/.gitignore
deleted file mode 100644 (file)
index 72e8ffc..0000000
+++ /dev/null
@@ -1 +0,0 @@
-*
diff --git a/webdav/lieferantenbestellungen/.dummy b/webdav/lieferantenbestellungen/.dummy
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/webdav/lieferantenbestellungen/.gitignore b/webdav/lieferantenbestellungen/.gitignore
deleted file mode 100644 (file)
index 72e8ffc..0000000
+++ /dev/null
@@ -1 +0,0 @@
-*
diff --git a/webdav/rechnungen/.dummy b/webdav/rechnungen/.dummy
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/webdav/rechnungen/.gitignore b/webdav/rechnungen/.gitignore
deleted file mode 100644 (file)
index 72e8ffc..0000000
+++ /dev/null
@@ -1 +0,0 @@
-*