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