'All changes in that file have been reverted.' => 'Alle Änderungen in dieser Datei wurden rückgängig gemacht.',
'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übersicht, Summen- u. Saldenliste, GuV, BWA, Bilanz, Projektbuchungen)',
'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.',
'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 #1' => 'Mandant #1',
'Client Configuration' => 'Mandantenkonfiguration',
'Client Configuration saved!' => 'Mandantenkonfiguration gespeichert!',
+ 'Client name' => 'Mandantenname',
'Close' => 'Übernehmen',
'Close Books up to' => 'Die Bücher abschließen bis zum',
'Close Flash' => 'Schließen',
'Comment' => 'Kommentar',
'Company' => 'Firma',
'Company Name' => 'Firmenname',
+ 'Company name' => 'Firmenname',
'Compare to' => 'Gegenüberstellen zu',
'Configuration' => 'Einstellungen',
'Configuration of individual TODO items' => 'Konfiguration für die einzelnen Aufgabenlistenpunkte',
'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',
'Database User missing!' => 'Datenbankbenutzer fehlt!',
'Database backups and restorations are disabled in the configuration.' => 'Datenbanksicherungen und -wiederherstellungen sind in der Konfiguration deaktiviert.',
'Database name' => 'Datenbankname',
+ 'Database settings' => 'Datenbankeinstellungen',
'Database template' => 'Datenbankvorlage',
'Database update error:' => 'Fehler beim Datenbankupgrade:',
'Dataset' => 'Datenbank',
'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üssen einer Position entweder gar keinen Lagerausgang oder die vollstä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öß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',
'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',
'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 valid for this client' => 'Für Mandanten gültige Gruppen',
'HTML' => 'HTML',
'HTML Templates' => 'HTML-Vorlagen',
'Hardcopy' => 'Seite drucken',
'International' => 'Ausland',
'Internet' => 'Internet',
'Introduction of Buchungsgruppen' => 'Einführung von Buchungsgruppen',
+ 'Introduction of clients' => 'Einführung von Mandanten',
'Introduction of units' => 'Einführung von Einheiten',
'Inv. Duedate' => 'Rg. Fälligkeit',
'Invalid' => 'Ungültig',
'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öglich, dies für einige, aber nicht für alle Buchungsgruppen automatisch zu erledigen.',
'New Templates' => 'Erzeuge Vorlagen, Name',
'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',
'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ä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',
'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 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 select the database you want to backup' => 'Bitte wä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ählen Sie die zu lö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ür die übernommenen Daten ein.',
'Print dunnings' => 'Mahnungen drucken',
'Print list' => 'Liste ausdrucken',
'Print options' => 'Druckoptionen',
+ 'Print templates' => 'Druckvorlagen',
'Printer' => 'Drucker',
'Printer Command' => 'Druckbefehl',
'Printer Command missing!' => 'Druckbefehl fehlt',
'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 ü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.',
'The LDAP server "#1:#2" is unreachable. Please check config/kivitendo.conf.' => 'Der LDAP-Server "#1:#2" ist nicht erreichbar. Bitte überprü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 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.',
'The columns "Dunning Duedate", "Total Fees" and "Interest" show data for the previous dunning created for this invoice.' => 'Die Spalten "Zahlbar bis", "Kumulierte Gebühren" und "Zinsen" zeigen Daten der letzten für diese Rechnung erzeugten Mahnung.',
'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üsselt werden (Fehler bei SSL/TLS-Initialisierung). Bitte überprü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.',
'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ürfe wurden gespeichert und können geladen werden.',
+ '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 unit in row %d has been deleted in the meantime.' => 'Die Einheit in Zeile %d ist in der Zwischentzeit gelö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ändert werden.',
'The units have been saved.' => 'Die Einheiten wurden gespeichert.',
+ '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 is a member in the following group(s):' => 'Der Benutzer ist Mitglied in den folgenden Gruppen:',
'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öscht werden, da es bereits in Benutzung war.',
'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.',
'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',
'User' => 'Benutzer',
'User Config' => 'Einstellungen',
'User Login' => 'Als Benutzer anmelden',
+ 'User access' => 'Benutzerzugriff',
+ 'User data migration' => 'Benutzerdatenmigration',
'User deleted!' => 'Benutzer gelöscht!',
'User login' => 'Benutzeranmeldung',
'User name' => 'Benutzername',
'User saved!' => 'Benutzer gespeichert!',
'Username' => 'Benutzername',
'Users in this group' => 'BenutzerInnen in dieser Gruppe',
+ 'Users with access' => 'Benutzer mit Zugriff',
+ 'Users with access to this client' => 'Benutzer mit Zugriff auf diesen Mandanten',
'Ust-IDNr' => 'USt-IdNr.',
+ 'VAT ID' => 'UStdID-Nr',
'Valid' => 'Gültig',
'Valid from' => 'Gültig ab',
'Valid until' => 'gültig bis',
'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üssen einen Benutzer auswählen.',
'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 been switched to group-based access restrictions.' => 'kivitendo wurde auf eine gruppenbasierte Benutzerzugriffsverwaltung umgestellt.',
'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ält jetzt auch echte Lagerverwaultung anstatt reiner Mengenzählung.',
--- /dev/null
+# @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;
--- /dev/null
+[%- 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="admin.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', 'list_users') %]
+ [% L.submit_tag('dummy', LxERP.t8('Continue')) %]
+ </p>
+</form>