Project

General

Profile

Fehler #348

DatevExport kommt mit bestimmten Zeichen im Buchungstext nicht klar

Added by Werner Hahn over 1 year ago. Updated 7 months ago.

Status:
Erledigt
Priority:
Normal
Assignee:
-
Target version:
-
Start date:
02/15/2018
Due date:
% Done:

0%

Estimated time:

Description

Bei Dialogbuchen ist es mir beim € Zeichen aufgefallen
Bei Rechnungen mit Kundennamen z.B. Kulašić

Associated revisions

Revision 324726ac (diff)
Added by Jan Büren 7 months ago

Fixt #348 DatevExport kommt mit bestimmten Zeichen im Buchungstext nicht klar

In der Mandantenkonfiguration befindet sich jetzt eine Einstellung,
welche die Kodierung des DATEV-Exports steuert. DATEV erwartet CP1252.
kivitendo kann diese Kodierung so vom kivitendo Nutzer einfordern, alternativ nicht
vorhandenen Zeichen versuchen zu ersetzen oder die DATEV-Erwartung ignorieren
und UTF-8 liefern. Voreingestellt ist CP1252 mit Ersetzungen

History

#1 Updated by Jan Büren over 1 year ago

  • Status changed from Neu to Abgewiesen
  • Target version deleted (3.6)

Nein.
Der DATEV-Import kann nur CP-1250 (Windows Europäisch) verarbeiten.

Damit erst gar nicht in dieser fehlerhaften Form exportiert wird, hab ich eine strikte Prüfung hierfür angesetzt, bzw. diese Zeichen werden gefiltert.

Das wäre jetzt meine erste Vermutung.
Prüf mal, wie das € kodiert ist.

Es gibt auch Testfälle für dieses Verhalten, wenn Du da etwas verbessert willst / kannst.

#2 Updated by Werner Hahn over 1 year ago

Hat sich doch als Fehler erwiesen, getestet mit Jan auf den CLTs

#3 Updated by Jan Büren over 1 year ago

  • Status changed from Abgewiesen to In Bearbeitung

Hallo Werner,
ja, sorry fürs erste Abweisen, aber ich bin erstmal von internen Kodierungsfehlern ausgegangen.

Auf der anderen Seite wäre ein Feedback zu "Prüf mal wie das € kodiert ist", simpel machbar gewesen:

Das €-Zeichen ist folgendermaßen kodiert:

$ cat t
€
$ hexdump -C t
00000000  e2 82 ac 0a                                       |....|

Ah, ok 'e2 82 ac' -> € in utf8 (s.a.: http://utf8-chartable.de/unicode-utf8-table.pl?start=8320&number=128&names=)

Das sollte sauber nach cp1252 konvertieren, nämlich so:

$ iconv -futf8 -tcp1252 t
€

Kulašić ist jetzt der Knackpunkt, hier gibt es ein Zeichen das nicht in cp1252 vorhanden ist.

$ cat kulasic                                             # utf8 terminal
Kulašić
$ iconv -futf8 -tcp1252 kulasic         # win-1252 terminal
Kulašiiconv: ungültige Eingabe-Sequenz an der Stelle 7

An der Stelle war ich bei Entwickeln unsicher, ob ich das generell ablehne oder dann nur teilweise kodiert übergeben soll.

Soweit die Zusammenfassung, ich hab jetzt einen Testfall für diese 4 Varianten und einen Fix.

S.a. Commit f2b3e089.

P.S.: Es gibt wieder steigmann unstable

#4 Updated by Moritz Bunkus over 1 year ago

Du kannst iconv (und damit Text::Iconv) mit der Option translit nutzen. Dann werden Zeichen, die im Zielzeichensatz nicht vorhanden sind, durch ähnliche Zeichen ersetzt:

[0 mbunkus@chai-latte ~] echo Kulašić | iconv -f utf-8 -t cp1252//translit | iconv -f cp1252 -t utf-8
Kulašic
[0 mbunkus@chai-latte ~] perl -MText::Iconv -Mutf8 -e 'print(Text::Iconv->new("utf-8", "cp1252//translit")->convert("Kulašić"), "\n")' | iconv -f cp1252 -t utf-8
Kulašic

In kivitendo über den Wrapper SL::Iconv.

#5 Updated by Jan Büren 7 months ago

Hi,
der folgende Code-Schnipsel tut außerhalb von kivitendo das was er soll:

use Text::Iconv; 
use strict;

open my $fh_in, '<', 'input2.txt' or die "could not open <input.txt> for reading: $!";
open my $fh_out, '>', 'output.txt' or die "could not open <output.txt> for writing: $!";

print $fh_out Text::Iconv->new("utf-8", "cp1252//translit")->convert($_) while <$fh_in>;

close $fh_in;
close $fh_out;

Ein strace ergibt:


openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/perl/5.26/DynaLoader.pm", O_RDONLY) = 4
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/perl5/5.26/auto/Text/Iconv/Iconv.so", O_RDONLY|O_CLOEXEC) = 4

Ein hexdump zeigt das c als 0x63 was in cp1252 dem c entspricht:

00000260  69 63 22  |ic" 

In kivi funzt das nicht. Sind hier noch andere Konfigurationen gesetzt, wird ein anderes Modul geladen?


      my $filename_translit = "EXTF_DATEV_kivitendo_translit" . $self->from->ymd() . '-' . $self->to->ymd() . ".csv";

      open my $fh_in,  '<:encoding(UTF-8)',  $self->export_path . '/' . $filename or die "could not open $filename for reading: $!";
      open my $fh_out, '>', $self->export_path . '/' . $filename_translit or die "could not open $filename_translit for writing: $!";

      print $fh_out Text::Iconv->new("utf-8", "cp1252//translit")->convert($_) while <$fh_in>;

      close $fh_in;
      close $fh_out;

Damit wird aus dem ć ein ?:

hexcode:

00000260  69 3f 22   |i?" 

Wen mich per strace an den fcgi-Prozess dranhänge, bekomme ich keine klare Antwort, welches Modul geladen wird.

Ideen?

#6 Updated by Jan Büren 7 months ago

Der Testfall lässt sich noch simpler beschreiben:

kivi:

$main::lxdebug->message(0, "was hier " . Text::Iconv->new("utf-8", "cp1252//translit")->convert("Kulašić"));

was hier Kulaši?

perl skript:

print Text::Iconv->new("utf-8", "cp1252//translit")->convert("Kulašić");
Kulašic

#7 Updated by Bernd Bleßmann 7 months ago

Der Unterschied scheint mir die Umgebung zu sein, speziell die Spracheinstellungen (LANG (LANGUAGE?/LOCALE?)):


bernd@specht-kivitendo:~/kivi$ perl -MText::Iconv -Mutf8 -e 'print(Text::Iconv->new("utf-8", "cp1252//translit")->convert("Kulašić"), "\n");' | iconv -f cp1252 -t utf-8
Kulašic
bernd@specht-kivitendo:~/kivi$ LANG= perl -MText::Iconv -Mutf8 -e 'print(Text::Iconv->new("utf-8", "cp1252//translit")->convert("Kulašić"), "\n");' | iconv -f cp1252 -t utf-8
Kulaši?

Beim ersten Beispiel wird das 'c' umgewandelt, beim zweiten (mit LANG=) nicht, wie in der kivi. Was das für Implikationen für den kivi-Code hat bzw. was man da machen kann/muss, weiß ich nicht.

#8 Updated by Moritz Bunkus 7 months ago

Das Problem liegt in der Tat in den Locales. iconv ist eine Funktion der glibc unter Linux, und das Modul `Text::Iconv` ist nur ein dünner Wrapper darüber. Für die glibc wird beim Start des Perlinterpreters die Locales initialisiert, u.a. die für den Zeichensatz (`LC_ALL` falls gesetzt, ansonsten `LC_CTYPE`, ansonsten aus `LANG` abgeleitet). Unter (F)CGI sind die ganzen Umgebungsvariablen, die die Locale angeben, aber leer, weshalb effektiv die Locale `C` (= POSIX-Locale mit Zeichensatz ASCII) zum Einsatz kommt.

Wird nun die Konvertierungsfunktion mit einem String wie Kulašić aufgerufen, so kann iconv wohl nicht transliteraten, wenn die `LC_CTYPE=C` gilt.

Also ist die Lösung, dafür zu sorgen, dass `LC_CTYPE` auch brauchbar gesetzt ist. Dazu hat man mehrere Optionen:

1. Man sorgt dafür, dass die Umgebungsvariablen richtig gesetzt sind, bevor Perl ausgeführt wird. Wie das geht, hängt in dem Moment also vom verwendeten Webserver ab.
2. Man könnte global (z.B. im Dispatcher, weil durch den alles läuft) einmal `setlocale("LC_CTYPE", "de_DE.UTF-8");` machen.
3. Man könnte das nur in der Funktion machen, die zu CP1252 umwandeln soll. Da Locale-Einstellungen globaler State sind, sollte die Funktion nach sich aufräumen und die Locale wieder auf den vorherigen Wert zurück stellen.

Eine Funktion, die einen String in CP1252 mit Transliteration umwandelt, könnte z.B. so aussehen (tatsächlich getestet):

```perl
  1. Oben:
    use List::Util qw(first);
    use POSIX qw(setlocale);
  1. Woanders:
    sub to_cp1252_translit {
    my ($text) = @_;

    eval {
    my $old_locale = first { setlocale('LC_ALL', $_) } qw(de_DE.UTF-8 en_US.UTF-8);
    $text = Text::Iconv->new("utf-8", "cp1252//translit")->convert($text);
    setlocale('LC_ALL', $old_locale);
    1;
    } or do {
    $::lxdebug->message(LXDebug::WARN, "to_cp1252_translit exception: " . $@);
    return '';
    };

    return $text;
    }
    ```

Es wird vorausgesetzt, dass entweder `de_DE.UTF-8` oder `en_US.UTF-8` als Locale auf dem Server vorhanden ist. Welche, ist egal; es geht hier nur um den Zeichensatz, nicht um die Sprache, das Datumsformat oder so (wir setzen ja nur `LC_CTYPE` und nicht z.B. `LC_MESSAGES`).

Mein persönlicher Favorit wäre eher Variante 2, es global zu setzen, denn dann würden alle iconv-Aufrufe davon profitieren (und evtl. andere C-Funktionen). Das könnte man dann direkt in `SL::Dispatcher::new` machen, da das nur einmal pro laufender Instanz gemacht werden muss (ebenfalls getestet):

```perl
  1. Oben ergänzen:
    use POSIX qw(setlocale);

sub new {
my ($class, $interface) = @_;

my $self           = bless {}, $class;
$self->{interface} = lc($interface || 'cgi');
$self->{auth_handler} = SL::Dispatcher::AuthHandler->new;
SL::ArchiveZipFixes->apply_fixes;
  1. Initialize character type locale to UTF-8:
    foreach my $locale (qw(de_DE.UTF-8 en_US.UTF-8)) {
    last if setlocale('LC_ALL', $locale);
    }
return $self;
}
```

Der Konvertierungsaufruf ist dann wirlich nur noch `my $new_text = Text::Iconv->new("utf-8", "cp1252//translit")->convert($old_text);`

#9 Updated by Moritz Bunkus 7 months ago

Ach fuck you, vergessen, dass Redmine ja nicht Markdown nutzt. Und bearbeiten kann ich meine Kommentare auch nicht, daher einmal komplett neu.

Das Problem liegt in der Tat in den Locales. iconv ist eine Funktion der glibc unter Linux, und das Modul Text::Iconv ist nur ein dünner Wrapper darüber. Für die glibc wird beim Start des Perlinterpreters die Locales initialisiert, u.a. die für den Zeichensatz (LC_ALL falls gesetzt, ansonsten LC_CTYPE, ansonsten aus LANG abgeleitet). Unter (F)CGI sind die ganzen Umgebungsvariablen, die die Locale angeben, aber leer, weshalb effektiv die Locale C (= POSIX-Locale mit Zeichensatz ASCII) zum Einsatz kommt.

Wird nun die Konvertierungsfunktion mit einem String wie Kulašić aufgerufen, so kann iconv wohl nicht transliteraten, wenn die LC_CTYPE=C gilt.

Also ist die Lösung, dafür zu sorgen, dass LC_CTYPE auch brauchbar gesetzt ist. Dazu hat man mehrere Optionen:

1. Man sorgt dafür, dass die Umgebungsvariablen richtig gesetzt sind, bevor Perl ausgeführt wird. Wie das geht, hängt in dem Moment also vom verwendeten Webserver ab.
2. Man könnte global (z.B. im Dispatcher, weil durch den alles läuft) einmal setlocale("LC_CTYPE", "de_DE.UTF-8"); machen.
3. Man könnte das nur in der Funktion machen, die zu CP1252 umwandeln soll. Da Locale-Einstellungen globaler State sind, sollte die Funktion nach sich aufräumen und die Locale wieder auf den vorherigen Wert zurück stellen.

Eine Funktion, die einen String in CP1252 mit Transliteration umwandelt, könnte z.B. so aussehen (tatsächlich getestet):

# Oben:
use List::Util qw(first);
use POSIX qw(setlocale);

#Woanders:
sub to_cp1252_translit {
  my ($text) = @_;
    eval {
    my $old_locale = first { setlocale('LC_ALL', $_) } qw(de_DE.UTF-8 en_US.UTF-8);
    $text = Text::Iconv->new("utf-8", "cp1252//translit")->convert($text);
    setlocale('LC_ALL', $old_locale);
    1;
  } or do {
    $::lxdebug->message(LXDebug::WARN, "to_cp1252_translit exception: " . $@);
    return '';
  };

  return $text;
}

Es wird vorausgesetzt, dass entweder de_DE.UTF-8 oder en_US.UTF-8 als Locale auf dem Server vorhanden ist. Welche, ist egal; es geht hier nur um den Zeichensatz, nicht um die Sprache, das Datumsformat oder so (wir setzen ja nur LC_CTYPE und nicht z.B. LC_MESSAGES).

Mein persönlicher Favorit wäre eher Variante 2, es global zu setzen, denn dann würden alle iconv-Aufrufe davon profitieren (und evtl. andere C-Funktionen). Das könnte man dann direkt in SL::Dispatcher::new machen, da das nur einmal pro laufender Instanz gemacht werden muss (ebenfalls getestet):

#Oben ergänzen:
use POSIX qw(setlocale);

sub new {
  my ($class, $interface) = @_;

  my $self           = bless {}, $class;
  $self->{interface} = lc($interface || 'cgi');
  $self->{auth_handler} = SL::Dispatcher::AuthHandler->new;

  SL::ArchiveZipFixes->apply_fixes;

  # Initialize character type locale to UTF-8:
  foreach my $locale (qw(de_DE.UTF-8 en_US.UTF-8)) {
    last if setlocale('LC_ALL', $locale);
  }

  return $self;
}

Der Konvertierungsaufruf ist dann wirlich nur noch my $new_text = Text::Iconv->new("utf-8", "cp1252//translit")->convert($old_text);

#10 Updated by Moritz Bunkus 7 months ago

(vergeblicher Postversuch vergeblich versucht zu entfernen)

#11 Updated by Jan Büren 7 months ago

  • Status changed from In Bearbeitung to Erledigt

mit #324726acd30b8992854a2d5

Also available in: Atom PDF