GDPDU: taxdescription und Übersetzungen
[kivitendo-erp.git] / SL / GDPDU.pm
1 package SL::GDPDU;
2
3 # TODO:
4 # optional: background jobable
5
6 use strict;
7 use utf8;
8
9 use parent qw(Rose::Object);
10
11 use Text::CSV_XS;
12 use XML::Writer;
13 use Archive::Zip;
14 use File::Temp ();
15 use File::Spec ();
16 use List::MoreUtils qw(any);
17 use List::UtilsBy qw(partition_by sort_by);
18
19 use SL::DB::Helper::ALL; # since we work on meta data, we need everything
20 use SL::DB::Helper::Mappings;
21 use SL::Locale::String qw(t8);
22
23 use Rose::Object::MakeMethods::Generic (
24   scalar                  => [ qw(from to writer company location) ],
25   'scalar --get_set_init' => [ qw(files tempfiles export_ids tables) ],
26 );
27
28 # in this we find:
29 # key:         table name
30 # name:        short name, translated
31 # description: long description, translated
32 # transdate:   column used to filter from/to, empty if table is filtered otherwise
33 # keep:        arrayref of columns that should be saved for further referencing
34 # tables:      arrayref with one column and one or many table.column references that were kept earlier
35 my %known_tables = (
36   chart    => { name => t8('Charts'),    description => t8('Chart of Accounts'),    primary_key => 'accno', columns => [ qw(id accno description) ],     },
37   customer => { name => t8('Customers'), description => t8('Customer Master Data'), columns => [ qw(id name department_1 department_2 street zipcode city country contact phone fax email notes customernumber taxnumber obsolete ustid) ] },
38   vendor   => { name => t8('Vendors'),   description => t8('Vendor Master Data'),   columns => [ qw(id name department_1 department_2 street zipcode city country contact phone fax email notes customernumber taxnumber obsolete ustid) ] },
39 );
40
41 my %datev_column_defs = (
42   acc_trans_id      => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('ID'), primary_key => 1 },
43   amount            => { type => 'Rose::DB::Object::Metadata::Column::Numeric', text => t8('Amount'), },
44   credit_accname    => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Credit Account Name'), },
45   credit_accno      => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Credit Account'), },
46   debit_accname     => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Debit Account Name'), },
47   debit_accno       => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Debit Account'), },
48   invnumber         => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Reference'), },
49   name              => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Name'), },
50   notes             => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Notes'), },
51   tax               => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Tax'), },
52   taxdescription    => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('tax_taxdescription'), },
53   taxkey            => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('Taxkey'), },
54   tax_accname       => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Tax Account Name'), },
55   tax_accno         => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Tax Account'), },
56   transdate         => { type => 'Rose::DB::Object::Metadata::Column::Date',    text => t8('Invoice Date'), },
57   vcnumber          => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Customer/Vendor Number'), },
58   customer_id       => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('Customer (database ID)'), },
59   vendor_id         => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('Vendor (database ID)'), },
60 );
61
62 my @datev_columns = qw(
63   acc_trans_id
64   customer_id vendor_id
65   name           vcnumber
66   transdate    invnumber      amount
67   debit_accno  debit_accname
68   credit_accno credit_accname
69   taxdescription tax
70   tax_accno    tax_accname    taxkey
71   notes
72 );
73
74 # rows in this listing are tiers.
75 # tables may depend on ids in a tier above them
76 my @export_table_order = qw(
77   ar ap gl oe delivery_orders
78   invoice orderitems delivery_order_items
79   customer vendor
80   parts
81   acc_trans
82   chart
83 );
84
85 # needed because the standard dbh sets datestyle german and we don't want to mess with that
86 my $date_format = 'DD.MM.YYYY';
87
88 # callbacks that produce the xml spec for these column types
89 my %column_types = (
90   'Rose::DB::Object::Metadata::Column::Integer'   => sub { $_[0]->tag('Numeric') },  # see Caveats for integer issues
91   'Rose::DB::Object::Metadata::Column::BigInt'    => sub { $_[0]->tag('Numeric') },  # see Caveats for integer issues
92   'Rose::DB::Object::Metadata::Column::Text'      => sub { $_[0]->tag('AlphaNumeric') },
93   'Rose::DB::Object::Metadata::Column::Varchar'   => sub { $_[0]->tag('AlphaNumeric') },
94   'Rose::DB::Object::Metadata::Column::Character' => sub { $_[0]->tag('AlphaNumeric') },
95   'Rose::DB::Object::Metadata::Column::Numeric'   => sub { $_[0]->tag('Numeric', sub { $_[0]->tag('Accuracy', 5) }) },
96   'Rose::DB::Object::Metadata::Column::Date'      => sub { $_[0]->tag('Date', sub { $_[0]->tag('Format', $date_format) }) },
97   'Rose::DB::Object::Metadata::Column::Timestamp' => sub { $_[0]->tag('Date', sub { $_[0]->tag('Format', $date_format) }) },
98   'Rose::DB::Object::Metadata::Column::Float'     => sub { $_[0]->tag('Numeric') },
99   'Rose::DB::Object::Metadata::Column::Boolean'   => sub { $_[0]
100     ->tag('AlphaNumeric')
101     ->tag('Map', sub { $_[0]
102       ->tag('From', 1)
103       ->tag('To', t8('true'))
104     })
105     ->tag('Map', sub { $_[0]
106       ->tag('From', 0)
107       ->tag('To', t8('false'))
108     })
109     ->tag('Map', sub { $_[0]
110       ->tag('From', '')
111       ->tag('To', t8('false'))
112     })
113   },
114 );
115
116 sub generate_export {
117   my ($self) = @_;
118
119   # verify data
120   $self->from && 'DateTime' eq ref $self->from or die 'need from date';
121   $self->to   && 'DateTime' eq ref $self->to   or die 'need to date';
122   $self->from <= $self->to                     or die 'from date must be earlier or equal than to date';
123   $self->tables && @{ $self->tables }          or die 'need tables';
124   for (@{ $self->tables }) {
125     next if $known_tables{$_};
126     die "unknown table '$_'";
127   }
128
129   # get data from those tables and save to csv
130   # for that we need to build queries that fetch all the columns
131   for ($self->sorted_tables) {
132     $self->do_csv_export($_);
133   }
134
135   $self->do_datev_csv_export;
136
137   # write xml file
138   $self->do_xml_file;
139
140   # add dtd
141   $self->files->{'gdpdu-01-08-2002.dtd'} = File::Spec->catfile('users', 'gdpdu-01-08-2002.dtd');
142
143   # make zip
144   my ($fh, $zipfile) = File::Temp::tempfile();
145   my $zip            = Archive::Zip->new;
146
147   while (my ($name, $file) = each %{ $self->files }) {
148     $zip->addFile($file, $name);
149   }
150
151   $zip->writeToFileHandle($fh) == Archive::Zip::AZ_OK() or die 'error writing zip file';
152   close($fh);
153
154   return $zipfile;
155 }
156
157 sub do_xml_file {
158   my ($self) = @_;
159
160   my ($fh, $filename) = File::Temp::tempfile();
161   binmode($fh, ':utf8');
162
163   $self->files->{'INDEX.XML'} = $filename;
164   push @{ $self->tempfiles }, $filename;
165
166   my $writer = XML::Writer->new(
167     OUTPUT      => $fh,
168     ENCODING    => 'UTF-8',
169   );
170
171   $self->writer($writer);
172   $self->writer->xmlDecl('UTF-8');
173   $self->writer->doctype('DataSet', undef, "gdpdu-01-08-2002.dtd");
174   $self->tag('DataSet', sub { $self
175     ->tag('Version', '1.0')
176     ->tag('DataSupplier', sub { $self
177       ->tag('Name', $self->client_name)
178       ->tag('Location', $self->client_location)
179       ->tag('Comment', $self->make_comment)
180     })
181     ->tag('Media', sub { $self
182       ->tag('Name', t8('DataSet #1', 1));
183       for (reverse $self->sorted_tables) { $self  # see CAVEATS for table order
184         ->table($_)
185       }
186       $self->do_datev_xml_table;
187     })
188   });
189   close($fh);
190 }
191
192 sub table {
193   my ($self, $table) = @_;
194   my $writer = $self->writer;
195
196   $self->tag('Table', sub { $self
197     ->tag('URL', "$table.csv")
198     ->tag('Name', $known_tables{$table}{name})
199     ->tag('Description', $known_tables{$table}{description})
200     ->tag('Validity', sub { $self
201       ->tag('Range', sub { $self
202         ->tag('From', $self->from->to_kivitendo(dateformat => 'dd.mm.yyyy'))
203         ->tag('To',   $self->to->to_kivitendo(dateformat => 'dd.mm.yyyy'))
204       })
205       ->tag('Format', $date_format)
206     })
207     ->tag('UTF8')
208     ->tag('DecimalSymbol', '.')
209     ->tag('DigitGroupingSymbol', '|')     # see CAVEATS in documentation
210     ->tag('VariableLength', sub { $self
211       ->tag('ColumnDelimiter', ',')       # see CAVEATS for missing RecordDelimiter
212       ->tag('TextEncapsulator', '"')
213       ->columns($table)
214       ->foreign_keys($table)
215     })
216   });
217 }
218
219 sub _table_columns {
220   my ($table) = @_;
221   my $package = SL::DB::Helper::Mappings::get_package_for_table($table);
222
223   my %white_list;
224   my $use_white_list = 0;
225   if ($known_tables{$table}{columns}) {
226     $use_white_list = 1;
227     $white_list{$_} = 1 for @{ $known_tables{$table}{columns} || [] };
228   }
229
230   # PrimaryKeys must come before regular columns, so partition first
231   partition_by {
232     $known_tables{$table}{primary_key}
233       ? 1 * ($_ eq $known_tables{$table}{primary_key})
234       : 1 * $_->is_primary_key_member
235   } grep {
236     $use_white_list ? $white_list{$_->name} : 1
237   } $package->meta->columns;
238 }
239
240 sub columns {
241   my ($self, $table) = @_;
242
243   my %cols_by_primary_key = _table_columns($table);
244
245   for my $column (@{ $cols_by_primary_key{1} }) {
246     my $type = $column_types{ ref $column };
247
248     die "unknown col type @{[ ref $column ]}" unless $type;
249
250     $self->tag('VariablePrimaryKey', sub { $self
251       ->tag('Name', $column->name);
252       $type->($self);
253     })
254   }
255
256   for my $column (@{ $cols_by_primary_key{0} }) {
257     my $type = $column_types{ ref $column };
258
259     die "unknown col type @{[ ref $column]}" unless $type;
260
261     $self->tag('VariableColumn', sub { $self
262       ->tag('Name', $column->name);
263       $type->($self);
264     })
265   }
266
267   $self;
268 }
269
270 sub foreign_keys {
271   my ($self, $table) = @_;
272   my $package = SL::DB::Helper::Mappings::get_package_for_table($table);
273
274   my %requested = map { $_ => 1 } @{ $self->tables };
275
276   for my $rel ($package->meta->foreign_keys) {
277     next unless $requested{ $rel->class->meta->table };
278
279     # ok, now extract the columns used as foreign key
280     my %key_columns = $rel->key_columns;
281
282     if (1 != keys %key_columns) {
283       die "multi keys? we don't support this currently. fix it please";
284     }
285
286     if ($table eq $rel->class->meta->table) {
287       # self referential foreign keys are a PITA to export correctly. skip!
288       next;
289     }
290
291     $self->tag('ForeignKey', sub {
292       $_[0]->tag('Name', $_) for keys %key_columns;
293       $_[0]->tag('References', $rel->class->meta->table);
294    });
295   }
296 }
297
298 sub do_datev_xml_table {
299   my ($self) = @_;
300   my $writer = $self->writer;
301
302   $self->tag('Table', sub { $self
303     ->tag('URL', "transaction.csv")
304     ->tag('Name', t8('Transactions'))
305     ->tag('Description', t8('Transactions'))
306     ->tag('Validity', sub { $self
307       ->tag('Range', sub { $self
308         ->tag('From', $self->from->to_kivitendo(dateformat => 'dd.mm.yyyy'))
309         ->tag('To',   $self->to->to_kivitendo(dateformat => 'dd.mm.yyyy'))
310       })
311       ->tag('Format', $date_format)
312     })
313     ->tag('UTF8')
314     ->tag('DecimalSymbol', '.')
315     ->tag('DigitGroupingSymbol', '|')     # see CAVEATS in documentation
316     ->tag('VariableLength', sub { $self
317       ->tag('ColumnDelimiter', ',')       # see CAVEATS for missing RecordDelimiter
318       ->tag('TextEncapsulator', '"')
319       ->datev_columns
320       ->datev_foreign_keys
321     })
322   });
323 }
324
325 sub datev_columns {
326   my ($self, $table) = @_;
327
328   my %cols_by_primary_key = partition_by { 1 * $datev_column_defs{$_}{primary_key} } @datev_columns;
329   $::lxdebug->dump(0,  "cols", \%cols_by_primary_key);
330
331   for my $column (@{ $cols_by_primary_key{1} }) {
332     my $type = $column_types{ $datev_column_defs{$column}{type} };
333
334     die "unknown col type @{[ $column ]}" unless $type;
335
336     $self->tag('VariablePrimaryKey', sub { $self
337       ->tag('Name', $column);
338       $type->($self);
339     })
340   }
341
342   for my $column (@{ $cols_by_primary_key{0} }) {
343     my $type = $column_types{ $datev_column_defs{$column}{type} };
344
345     die "unknown col type @{[ ref $column]}" unless $type;
346
347     $self->tag('VariableColumn', sub { $self
348       ->tag('Name', $column);
349       $type->($self);
350     })
351   }
352
353   $self;
354 }
355
356 sub datev_foreign_keys {
357   my ($self) = @_;
358   # hard code weeee
359   $self->tag('ForeignKey', sub { $_[0]
360     ->tag('Name', 'customer_id')
361     ->tag('References', 'customer')
362   });
363   $self->tag('ForeignKey', sub { $_[0]
364     ->tag('Name', 'vendor_id')
365     ->tag('References', 'vendor')
366   });
367   $self->tag('ForeignKey', sub { $_[0]
368     ->tag('Name', $_)
369     ->tag('References', 'chart')
370   }) for qw(debit_accno credit_accno tax_accno);
371 }
372
373 sub do_datev_csv_export {
374   my ($self) = @_;
375
376   my $datev = SL::DATEV->new(from => $self->from, to => $self->to);
377
378   $datev->_get_transactions(from_to => $datev->fromto);
379
380   for my $transaction (@{ $datev->{DATEV} }) {
381     for my $entry (@{ $transaction }) {
382       $entry->{sortkey} = join '-', map { lc } (DateTime->from_kivitendo($entry->{transdate})->strftime('%Y%m%d'), $entry->{name}, $entry->{reference});
383     }
384   }
385
386   my @transactions = sort_by { $_->[0]->{sortkey} } @{ $datev->{DATEV} };
387
388   my $csv = Text::CSV_XS->new({ binary => 1, eol => "\r\n", sep_char => ",", quote_char => '"' });
389
390   my ($fh, $filename) = File::Temp::tempfile();
391   binmode($fh, ':utf8');
392
393   $self->files->{"transactions.csv"} = $filename;
394   push @{ $self->tempfiles }, $filename;
395
396   for my $transaction (@transactions) {
397     my $is_payment     = any { $_->{link} =~ m{A[PR]_paid} } @{ $transaction };
398
399     my ($soll, $haben) = map { $transaction->[$_] } ($transaction->[0]->{amount} > 0 ? (1, 0) : (0, 1));
400     my $tax            = defined($soll->{tax_accno})  ? $soll : $haben;
401     my $amount         = defined($soll->{net_amount}) ? $soll : $haben;
402     $haben->{notes}    = ($haben->{memo} || $soll->{memo}) if $haben->{memo} || $soll->{memo};
403     $haben->{notes}  //= '';
404     $haben->{notes}    =  SL::HTML::Util->strip($haben->{notes});
405     $haben->{notes}    =~ s{\r}{}g;
406     $haben->{notes}    =~ s{\n+}{ }g;
407
408     my %row            = (
409       customer_id      => $soll->{customer_id} || $haben->{customer_id},
410       vendor_id        => $soll->{vendor_id} || $haben->{vendor_id},
411       amount           => abs($amount->{amount}),
412       debit_accno      => $soll->{accno},
413       debit_accname    => $soll->{accname},
414       credit_accno     => $haben->{accno},
415       credit_accname   => $haben->{accname},
416       tax              => defined $amount->{net_amount} ? abs($amount->{amount}) - abs($amount->{net_amount}) : 0,
417       taxdescription   => defined($soll->{tax_accno}) ? $soll->{taxdescription} : $haben->{taxdescription},
418       notes            => $haben->{notes},
419       (map { ($_ => $tax->{$_})                    } qw(taxkey tax_accname tax_accno)),
420       (map { ($_ => ($haben->{$_} // $soll->{$_})) } qw(acc_trans_id invnumber name vcnumber transdate)),
421     );
422
423     $csv->print($fh, [ map { $row{$_} } @datev_columns ]);
424   }
425
426   # and build xml spec for it
427 }
428
429 sub do_csv_export {
430   my ($self, $table) = @_;
431
432   my $csv = Text::CSV_XS->new({ binary => 1, eol => "\r\n", sep_char => ",", quote_char => '"' });
433
434   my ($fh, $filename) = File::Temp::tempfile();
435   binmode($fh, ':utf8');
436
437   $self->files->{"$table.csv"} = $filename;
438   push @{ $self->tempfiles }, $filename;
439
440   # in the right order (primary keys first)
441   my %cols_by_primary_key = _table_columns($table);
442   my @columns = (@{ $cols_by_primary_key{1} }, @{ $cols_by_primary_key{0} });
443   my %col_index = do { my $i = 0; map {; "$_" => $i++ } @columns };
444
445   # and normalize date stuff
446   my @select_tokens = map { (ref $_) =~ /Time/ ? $_->name . '::date' : $_->name } @columns;
447
448   my @where_tokens;
449   my @values;
450   if ($known_tables{$table}{transdate}) {
451     if ($self->from) {
452       push @where_tokens, "$known_tables{$table}{transdate} >= ?";
453       push @values, $self->from;
454     }
455     if ($self->to) {
456       push @where_tokens, "$known_tables{$table}{transdate} <= ?";
457       push @values, $self->to;
458     }
459   }
460   if ($known_tables{$table}{tables}) {
461     my ($col, @col_specs) = @{ $known_tables{$table}{tables} };
462     my %ids;
463     for (@col_specs) {
464       my ($ftable, $fkey) = split /\./, $_;
465       if (!exists $self->export_ids->{$ftable}{$fkey}) {
466          # check if we forgot to keep it
467          if (!grep { $_ eq $fkey } @{ $known_tables{$ftable}{keep} || [] }) {
468            die "unknown table spec '$_' for table $table, did you forget to keep $fkey in $ftable?"
469          } else {
470            # hmm, most likely just an empty set.
471            $self->export_ids->{$ftable}{$fkey} = {};
472          }
473       }
474       $ids{$_}++ for keys %{ $self->export_ids->{$ftable}{$fkey} };
475     }
476     if (keys %ids) {
477       push @where_tokens, "$col IN (@{[ join ',', ('?') x keys %ids ]})";
478       push @values, keys %ids;
479     } else {
480       push @where_tokens, '1=0';
481     }
482   }
483
484   my $where_clause = @where_tokens ? 'WHERE ' . join ' AND ', @where_tokens : '';
485
486   my $query = "SELECT " . join(', ', @select_tokens) . " FROM $table $where_clause";
487
488   my $sth = $::form->get_standard_dbh->prepare($query);
489   $sth->execute(@values) or die "error executing query $query: " . $sth->errstr;
490
491   while (my $row = $sth->fetch) {
492     for my $keep_col (@{ $known_tables{$table}{keep} || [] }) {
493       next if !$row->[$col_index{$keep_col}];
494       $self->export_ids->{$table}{$keep_col} ||= {};
495       $self->export_ids->{$table}{$keep_col}{$row->[$col_index{$keep_col}]}++;
496     }
497     s/\r\n/ /g for @$row; # see CAVEATS
498
499     $csv->print($fh, $row) or $csv->error_diag;
500   }
501   $sth->finish();
502 }
503
504 sub tag {
505   my ($self, $tag, $content) = @_;
506
507   $self->writer->startTag($tag);
508   if ('CODE' eq ref $content) {
509     $content->($self);
510   } else {
511     $self->writer->characters($content);
512   }
513   $self->writer->endTag;
514   return $self;
515 }
516
517 sub make_comment {
518   my $gdpdu_version = API_VERSION();
519   my $kivi_version  = $::form->read_version;
520   my $person        = $::myconfig{name};
521   my $contact       = join ', ',
522     (t8("Email") . ": $::myconfig{email}" ) x!! $::myconfig{email},
523     (t8("Tel")   . ": $::myconfig{tel}" )   x!! $::myconfig{tel},
524     (t8("Fax")   . ": $::myconfig{fax}" )   x!! $::myconfig{fax};
525
526   t8('DataSet for GDPdU version #1. Created with kivitendo #2 by #3 (#4)',
527     $gdpdu_version, $kivi_version, $person, $contact
528   );
529 }
530
531 sub client_name {
532   $_[0]->company
533 }
534
535 sub client_location {
536   $_[0]->location
537 }
538
539 sub sorted_tables {
540   my ($self) = @_;
541
542   my %given = map { $_ => 1 } @{ $self->tables };
543
544   grep { $given{$_} } @export_table_order;
545 }
546
547 sub all_tables {
548   my ($self, $yesno) = @_;
549
550   $self->tables(\@export_table_order) if $yesno;
551 }
552
553 sub init_files { +{} }
554 sub init_export_ids { +{} }
555 sub init_tempfiles { [] }
556 sub init_tables { [ grep { $known_tables{$_} } @export_table_order ] }
557
558 sub API_VERSION {
559   DateTime->new(year => 2002, month => 8, day => 14)->to_kivitendo;
560 }
561
562 sub DESTROY {
563   unlink $_ for @{ $_[0]->tempfiles || [] };
564 }
565
566 1;
567
568 __END__
569
570 =encoding utf-8
571
572 =head1 NAME
573
574 SL::GDPDU - IDEA export generator
575
576 =head1 FUNCTIONS
577
578 =over 4
579
580 =item C<new PARAMS>
581
582 Create new export object. C<PARAMS> may contain:
583
584 =over 4
585
586 =item company
587
588 The name of the company, needed for the supplier header
589
590 =item location
591
592 Location of the company, needed for the suupplier header
593
594 =item from
595
596 =item to
597
598 Will only include records in the specified date range. Data pulled from other
599 tables will be culled to match what is needed for these records.
600
601 =item tables
602
603 A list of tables to be exported.
604
605 =item all_tables
606
607 Alternative to C<tables>, enables all known tables.
608
609 =back
610
611 =item C<generate_export>
612
613 Do the work. Will return an absolut path to a temp file where all export files
614 are zipped together.
615
616 =back
617
618 =head1 CAVEATS
619
620 =over 4
621
622 =item *
623
624 Date format is shit. The official docs state that only C<YY>, C<YYYY>, C<MM>,
625 and C<DD> are supported, timestamps do not exist.
626
627 =item *
628
629 Number parsing seems to be fragile. Official docs state that behaviour for too
630 low C<Accuracy> settings is undefined. Accuracy of 0 is not taken to mean
631 Integer but instead generates a warning for redudancy.
632
633 There is no dedicated integer type.
634
635 =item *
636
637 Currently C<ar> and C<ap> have a foreign key to themself with the name
638 C<storno_id>. If this foreign key is present in the C<INDEX.XML> then the
639 storno records have to be too. Since this is extremely awkward to code and
640 confusing for the examiner as to why there are records outside of the time
641 range, this export skips all self-referential foreign keys.
642
643 =item *
644
645 Documentation for foreign keys is extremely weird. Instead of giving column
646 maps it assumes that foreign keys map to the primary keys given for the target
647 table, and in that order. Foreign keys to keys that are not primary seems to be
648 impossible. Changing type is also not allowed (which actually makes sense).
649 Hopefully there are no bugs there.
650
651 =item *
652
653 It's currently disallowed to export the whole dataset. It's not clear if this
654 is wanted.
655
656 =item *
657
658 It is not possible to set an empty C<DigiGroupingSymbol> since then the import
659 will just work with the default. This was asked in their forum, and the
660 response actually was:
661
662   Einfache Lösung: Definieren Sie das Tausendertrennzeichen als Komma, auch
663   wenn es nicht verwendet wird. Sollten Sie das Komma bereits als Feldtrenner
664   verwenden, so wählen Sie als Tausendertrennzeichen eine Alternative wie das
665   Pipe-Symbol |.
666
667 L<http://www.gdpdu-portal.com/forum/index.php?mode=thread&id=1392>
668
669 =item *
670
671 It is not possible to define a C<RecordDelimiter> with XML entities. &#x0A;
672 generates the error message:
673
674   C<RecordDelimiter>-Wert (&#x0A;) sollte immer aus ein oder zwei Zeichen
675   bestehen.
676
677 Instead we just use the implicit default RecordDelimiter CRLF.
678
679 =item *
680
681 Not confirmed yet:
682
683 Foreign keys seem only to work with previously defined tables (which would be
684 utterly insane).
685
686 =item *
687
688 The CSV import library used in IDEA is not able to parse newlines (or more
689 exactly RecordDelimiter) in data. So this export substites all of these with
690 spaces.
691
692 =back
693
694 =head1 AUTHOR
695
696 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
697
698 =cut