4 # optional: background jobable
 
   9 use parent qw(Rose::Object);
 
  16 use List::MoreUtils qw(any);
 
  17 use List::UtilsBy qw(partition_by sort_by);
 
  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);
 
  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 csv_headers) ],
 
  30 # name:        short name, translated
 
  31 # description: long description, translated
 
  32 # columns:     list of columns to export. export all columns if not present
 
  33 # primary_key: override primary key
 
  35   chart    => { name => t8('Charts'),    description => t8('Chart of Accounts'),    primary_key => 'accno', columns => [ qw(id accno description) ],     },
 
  36   customer => { name => t8('Customers'), description => t8('Customer Master Data'), columns => [ qw(id customernumber name department_1 department_2 street zipcode city country contact phone fax email notes taxnumber obsolete ustid) ] },
 
  37   vendor   => { name => t8('Vendors'),   description => t8('Vendor Master Data'),   columns => [ qw(id vendornumber name department_1 department_2 street zipcode city country contact phone fax email notes taxnumber obsolete ustid) ] },
 
  43      accno          => t8('Account Number'),
 
  44      description    => t8('Description'),
 
  49      department_1   => t8('Department 1'),
 
  50      department_2   => t8('Department 2'),
 
  51      street         => t8('Street'),
 
  52      zipcode        => t8('Zipcode'),
 
  54      country        => t8('Country'),
 
  55      contact        => t8('Contact'),
 
  58      email          => t8('E-mail'),
 
  60      customernumber => t8('Customer Number'),
 
  61      vendornumber   => t8('Vendor Number'),
 
  62      taxnumber      => t8('Tax Number'),
 
  63      obsolete       => t8('Obsolete'),
 
  64      ustid          => t8('Tax ID number'),
 
  67 $column_titles{$_} = $column_titles{customer_vendor} for qw(customer vendor);
 
  69 my %datev_column_defs = (
 
  70   trans_id          => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('ID'), },
 
  71   amount            => { type => 'Rose::DB::Object::Metadata::Column::Numeric', text => t8('Amount'), },
 
  72   credit_accname    => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Credit Account Name'), },
 
  73   credit_accno      => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Credit Account'), },
 
  74   credit_amount     => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Credit Amount'), },
 
  75   credit_tax        => { type => 'Rose::DB::Object::Metadata::Column::Numeric', text => t8('Credit Tax (lit)'), },
 
  76   debit_accname     => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Debit Account Name'), },
 
  77   debit_accno       => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Debit Account'), },
 
  78   debit_amount      => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Debit Amount'), },
 
  79   debit_tax         => { type => 'Rose::DB::Object::Metadata::Column::Numeric', text => t8('Debit Tax (lit)'), },
 
  80   invnumber         => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Reference'), },
 
  81   name              => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Name'), },
 
  82   notes             => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Notes'), },
 
  83   tax               => { type => 'Rose::DB::Object::Metadata::Column::Numeric', text => t8('Tax'), },
 
  84   taxdescription    => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('tax_taxdescription'), },
 
  85   taxkey            => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('Taxkey'), },
 
  86   tax_accname       => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Tax Account Name'), },
 
  87   tax_accno         => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Tax Account'), },
 
  88   transdate         => { type => 'Rose::DB::Object::Metadata::Column::Date',    text => t8('Invoice Date'), },
 
  89   vcnumber          => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Customer/Vendor Number'), },
 
  90   customer_id       => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('Customer (database ID)'), },
 
  91   vendor_id         => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('Vendor (database ID)'), },
 
  92   itime             => { type => 'Rose::DB::Object::Metadata::Column::Date',    text => t8('Create Date'), },
 
  93   gldate            => { type => 'Rose::DB::Object::Metadata::Column::Date',    text => t8('Booking Date'), },
 
  96 my @datev_columns = qw(
 
 100   transdate    invnumber      amount
 
 101   debit_accno  debit_accname debit_amount debit_tax
 
 102   credit_accno credit_accname credit_amount credit_tax
 
 104   tax_accno    tax_accname    taxkey
 
 108 # rows in this listing are tiers.
 
 109 # tables may depend on ids in a tier above them
 
 110 my @export_table_order = qw(
 
 111   ar ap gl oe delivery_orders
 
 112   invoice orderitems delivery_order_items
 
 119 # needed because the standard dbh sets datestyle german and we don't want to mess with that
 
 120 my $date_format = 'DD.MM.YYYY';
 
 121 my $number_format = '1000.00';
 
 123 my $myconfig = { numberformat => $number_format };
 
 125 # callbacks that produce the xml spec for these column types
 
 127   'Rose::DB::Object::Metadata::Column::Integer'   => sub { $_[0]->tag('Numeric') },  # see Caveats for integer issues
 
 128   'Rose::DB::Object::Metadata::Column::BigInt'    => sub { $_[0]->tag('Numeric') },  # see Caveats for integer issues
 
 129   'Rose::DB::Object::Metadata::Column::Text'      => sub { $_[0]->tag('AlphaNumeric') },
 
 130   'Rose::DB::Object::Metadata::Column::Varchar'   => sub { $_[0]->tag('AlphaNumeric') },
 
 131   'Rose::DB::Object::Metadata::Column::Character' => sub { $_[0]->tag('AlphaNumeric') },
 
 132   'Rose::DB::Object::Metadata::Column::Numeric'   => sub { $_[0]->tag('Numeric', sub { $_[0]->tag('Accuracy', 5) }) },
 
 133   'Rose::DB::Object::Metadata::Column::Date'      => sub { $_[0]->tag('Date', sub { $_[0]->tag('Format', $date_format) }) },
 
 134   'Rose::DB::Object::Metadata::Column::Timestamp' => sub { $_[0]->tag('Date', sub { $_[0]->tag('Format', $date_format) }) },
 
 135   'Rose::DB::Object::Metadata::Column::Float'     => sub { $_[0]->tag('Numeric') },
 
 136   'Rose::DB::Object::Metadata::Column::Boolean'   => sub { $_[0]
 
 137     ->tag('AlphaNumeric')
 
 138     ->tag('Map', sub { $_[0]
 
 140       ->tag('To', t8('true'))
 
 142     ->tag('Map', sub { $_[0]
 
 144       ->tag('To', t8('false'))
 
 146     ->tag('Map', sub { $_[0]
 
 148       ->tag('To', t8('false'))
 
 153 sub generate_export {
 
 157   $self->from && 'DateTime' eq ref $self->from or die 'need from date';
 
 158   $self->to   && 'DateTime' eq ref $self->to   or die 'need to date';
 
 159   $self->from <= $self->to                     or die 'from date must be earlier or equal than to date';
 
 160   $self->tables && @{ $self->tables }          or die 'need tables';
 
 161   for (@{ $self->tables }) {
 
 162     next if $known_tables{$_};
 
 163     die "unknown table '$_'";
 
 166   # get data from those tables and save to csv
 
 167   # for that we need to build queries that fetch all the columns
 
 168   for ($self->sorted_tables) {
 
 169     $self->do_csv_export($_);
 
 172   $self->do_datev_csv_export;
 
 178   $self->files->{'gdpdu-01-08-2002.dtd'} = File::Spec->catfile('users', 'gdpdu-01-08-2002.dtd');
 
 181   my ($fh, $zipfile) = File::Temp::tempfile();
 
 182   my $zip            = Archive::Zip->new;
 
 184   while (my ($name, $file) = each %{ $self->files }) {
 
 185     $zip->addFile($file, $name);
 
 188   $zip->writeToFileHandle($fh) == Archive::Zip::AZ_OK() or die 'error writing zip file';
 
 197   my ($fh, $filename) = File::Temp::tempfile();
 
 198   binmode($fh, ':utf8');
 
 200   $self->files->{'INDEX.XML'} = $filename;
 
 201   push @{ $self->tempfiles }, $filename;
 
 203   my $writer = XML::Writer->new(
 
 208   $self->writer($writer);
 
 209   $self->writer->xmlDecl('UTF-8');
 
 210   $self->writer->doctype('DataSet', undef, "gdpdu-01-08-2002.dtd");
 
 211   $self->tag('DataSet', sub { $self
 
 212     ->tag('Version', '1.0')
 
 213     ->tag('DataSupplier', sub { $self
 
 214       ->tag('Name', $self->client_name)
 
 215       ->tag('Location', $self->client_location)
 
 216       ->tag('Comment', $self->make_comment)
 
 218     ->tag('Media', sub { $self
 
 219       ->tag('Name', t8('DataSet #1', 1));
 
 220       for (reverse $self->sorted_tables) { $self  # see CAVEATS for table order
 
 223       $self->do_datev_xml_table;
 
 230   my ($self, $table) = @_;
 
 231   my $writer = $self->writer;
 
 233   $self->tag('Table', sub { $self
 
 234     ->tag('URL', "$table.csv")
 
 235     ->tag('Name', $known_tables{$table}{name})
 
 236     ->tag('Description', $known_tables{$table}{description})
 
 237     ->tag('Validity', sub { $self
 
 238       ->tag('Range', sub { $self
 
 239         ->tag('From', $self->from->to_kivitendo(dateformat => 'dd.mm.yyyy'))
 
 240         ->tag('To',   $self->to->to_kivitendo(dateformat => 'dd.mm.yyyy'))
 
 242       ->tag('Format', $date_format)
 
 245     ->tag('DecimalSymbol', '.')
 
 246     ->tag('DigitGroupingSymbol', '|')     # see CAVEATS in documentation
 
 247     ->tag('Range', sub { $self
 
 248       ->tag('From', $self->csv_headers ? 2 : 1)
 
 250     ->tag('VariableLength', sub { $self
 
 251       ->tag('ColumnDelimiter', ',')       # see CAVEATS for missing RecordDelimiter
 
 252       ->tag('TextEncapsulator', '"')
 
 254       ->foreign_keys($table)
 
 261   my $package = SL::DB::Helper::Mappings::get_package_for_table($table);
 
 264   my $use_white_list = 0;
 
 265   if ($known_tables{$table}{columns}) {
 
 267     $white_list{$_} = 1 for @{ $known_tables{$table}{columns} || [] };
 
 270   # PrimaryKeys must come before regular columns, so partition first
 
 272     $known_tables{$table}{primary_key}
 
 273       ? 1 * ($_ eq $known_tables{$table}{primary_key})
 
 274       : 1 * $_->is_primary_key_member
 
 276     $use_white_list ? $white_list{$_->name} : 1
 
 277   } $package->meta->columns;
 
 281   my ($self, $table) = @_;
 
 283   my %cols_by_primary_key = _table_columns($table);
 
 285   for my $column (@{ $cols_by_primary_key{1} }) {
 
 286     my $type = $column_types{ ref $column };
 
 288     die "unknown col type @{[ ref $column ]}" unless $type;
 
 290     $self->tag('VariablePrimaryKey', sub { $self
 
 291       ->tag('Name', $column_titles{$table}{$column->name});
 
 296   for my $column (@{ $cols_by_primary_key{0} }) {
 
 297     my $type = $column_types{ ref $column };
 
 299     die "unknown col type @{[ ref $column]}" unless $type;
 
 301     $self->tag('VariableColumn', sub { $self
 
 302       ->tag('Name', $column_titles{$table}{$column->name});
 
 311   my ($self, $table) = @_;
 
 312   my $package = SL::DB::Helper::Mappings::get_package_for_table($table);
 
 314   my %requested = map { $_ => 1 } @{ $self->tables };
 
 316   for my $rel ($package->meta->foreign_keys) {
 
 317     next unless $requested{ $rel->class->meta->table };
 
 319     # ok, now extract the columns used as foreign key
 
 320     my %key_columns = $rel->key_columns;
 
 322     if (1 != keys %key_columns) {
 
 323       die "multi keys? we don't support this currently. fix it please";
 
 326     if ($table eq $rel->class->meta->table) {
 
 327       # self referential foreign keys are a PITA to export correctly. skip!
 
 331     $self->tag('ForeignKey', sub {
 
 332       $_[0]->tag('Name',  $column_titles{$table}{$_}) for keys %key_columns;
 
 333       $_[0]->tag('References', $rel->class->meta->table);
 
 338 sub do_datev_xml_table {
 
 340   my $writer = $self->writer;
 
 342   $self->tag('Table', sub { $self
 
 343     ->tag('URL', "transactions.csv")
 
 344     ->tag('Name', t8('Transactions'))
 
 345     ->tag('Description', t8('Transactions'))
 
 346     ->tag('Validity', sub { $self
 
 347       ->tag('Range', sub { $self
 
 348         ->tag('From', $self->from->to_kivitendo(dateformat => 'dd.mm.yyyy'))
 
 349         ->tag('To',   $self->to->to_kivitendo(dateformat => 'dd.mm.yyyy'))
 
 351       ->tag('Format', $date_format)
 
 354     ->tag('DecimalSymbol', '.')
 
 355     ->tag('DigitGroupingSymbol', '|')     # see CAVEATS in documentation
 
 356     ->tag('Range', sub { $self
 
 357       ->tag('From', $self->csv_headers ? 2 : 1)
 
 359     ->tag('VariableLength', sub { $self
 
 360       ->tag('ColumnDelimiter', ',')       # see CAVEATS for missing RecordDelimiter
 
 361       ->tag('TextEncapsulator', '"')
 
 369   my ($self, $table) = @_;
 
 371   my %cols_by_primary_key = partition_by { 1 * $datev_column_defs{$_}{primary_key} } @datev_columns;
 
 373   for my $column (@{ $cols_by_primary_key{1} }) {
 
 374     my $type = $column_types{ $datev_column_defs{$column}{type} };
 
 376     die "unknown col type @{[ $column ]}" unless $type;
 
 378     $self->tag('VariablePrimaryKey', sub { $self
 
 379       ->tag('Name', $datev_column_defs{$column}{text});
 
 384   for my $column (@{ $cols_by_primary_key{0} }) {
 
 385     my $type = $column_types{ $datev_column_defs{$column}{type} };
 
 387     die "unknown col type @{[ ref $column]}" unless $type;
 
 389     $self->tag('VariableColumn', sub { $self
 
 390       ->tag('Name', $datev_column_defs{$column}{text});
 
 398 sub datev_foreign_keys {
 
 401   $self->tag('ForeignKey', sub { $_[0]
 
 402     ->tag('Name', $datev_column_defs{customer_id}{text})
 
 403     ->tag('References', 'customer')
 
 405   $self->tag('ForeignKey', sub { $_[0]
 
 406     ->tag('Name', $datev_column_defs{vendor_id}{text})
 
 407     ->tag('References', 'vendor')
 
 409   $self->tag('ForeignKey', sub { $_[0]
 
 410     ->tag('Name', $datev_column_defs{$_}{text})
 
 411     ->tag('References', 'chart')
 
 412   }) for qw(debit_accno credit_accno tax_accno);
 
 415 sub do_datev_csv_export {
 
 418   my $datev = SL::DATEV->new(from => $self->from, to => $self->to);
 
 420   $datev->generate_datev_data(from_to => $datev->fromto);
 
 422   if ($datev->errors) {
 
 423     die [ $datev->errors ];
 
 426   for my $transaction (@{ $datev->{DATEV} }) {
 
 427     for my $entry (@{ $transaction }) {
 
 428       $entry->{sortkey} = join '-', map { lc } (DateTime->from_kivitendo($entry->{transdate})->strftime('%Y%m%d'), $entry->{name}, $entry->{reference});
 
 432   my @transactions = sort_by { $_->[0]->{sortkey} } @{ $datev->{DATEV} };
 
 434   my $csv = Text::CSV_XS->new({ binary => 1, eol => "\r\n", sep_char => ",", quote_char => '"' });
 
 436   my ($fh, $filename) = File::Temp::tempfile();
 
 437   binmode($fh, ':utf8');
 
 439   $self->files->{"transactions.csv"} = $filename;
 
 440   push @{ $self->tempfiles }, $filename;
 
 442   if ($self->csv_headers) {
 
 443     $csv->print($fh, [ map { _normalize_cell($datev_column_defs{$_}{text}) } @datev_columns ]);
 
 446   for my $transaction (@transactions) {
 
 447     my $is_payment     = any { $_->{link} =~ m{A[PR]_paid} } @{ $transaction };
 
 449     my ($soll, $haben) = map { $transaction->[$_] } ($transaction->[0]->{amount} > 0 ? (1, 0) : (0, 1));
 
 450     my $tax            = defined($soll->{tax_accno}) ? $soll : defined($haben->{tax_accno}) ? $haben : {};
 
 451     my $amount         = defined($soll->{net_amount}) ? $soll : $haben;
 
 452     $haben->{notes}    = ($haben->{memo} || $soll->{memo}) if $haben->{memo} || $soll->{memo};
 
 453     $haben->{notes}  //= '';
 
 454     $haben->{notes}    =  SL::HTML::Util->strip($haben->{notes});
 
 456     my $tax_amount = defined $amount->{net_amount} ? abs($amount->{amount}) - abs($amount->{net_amount}) : 0;
 
 458     $tax = {} if abs($tax_amount) < 0.001;
 
 461       amount           => $::form->format_amount($myconfig, abs($amount->{amount}),5),
 
 462       debit_accno      => $soll->{accno},
 
 463       debit_accname    => $soll->{accname},
 
 464       debit_amount     => $::form->format_amount($myconfig, abs(-$soll->{amount}),5),
 
 465       debit_tax        => $soll->{tax_accno} ? $::form->format_amount($myconfig, $tax_amount, 5) : 0,
 
 466       credit_accno     => $haben->{accno},
 
 467       credit_accname   => $haben->{accname},
 
 468       credit_amount    => $::form->format_amount($myconfig, abs($haben->{amount}),5),,
 
 469       credit_tax       => $haben->{tax_accno} ? $::form->format_amount($myconfig, $tax_amount, 5) : 0,
 
 470       tax              => $::form->format_amount($myconfig, $tax_amount, 5),
 
 471       notes            => $haben->{notes},
 
 472       (map { ($_ => $tax->{$_})                    } qw(taxkey tax_accname tax_accno taxdescription)),
 
 473       (map { ($_ => ($haben->{$_} // $soll->{$_})) } qw(trans_id invnumber name vcnumber transdate gldate itime customer_id vendor_id)),
 
 476 #     if ($row{debit_amount} + $row{debit_tax} - ($row{credit_amount} + $row{credit_tax}) > 0.005) {
 
 477 #       $::lxdebug->dump(0,  "broken taxes", [ $transaction, \%row,  $row{debit_amount} + $row{debit_tax}, $row{credit_amount} + $row{credit_tax} ]);
 
 480     _normalize_cell($_) for values %row; # see CAVEATS
 
 482     $csv->print($fh, [ map { $row{$_} } @datev_columns ]);
 
 485   # and build xml spec for it
 
 489   my ($self, $table) = @_;
 
 491   my $csv = Text::CSV_XS->new({ binary => 1, eol => "\r\n", sep_char => ",", quote_char => '"' });
 
 493   my ($fh, $filename) = File::Temp::tempfile();
 
 494   binmode($fh, ':utf8');
 
 496   $self->files->{"$table.csv"} = $filename;
 
 497   push @{ $self->tempfiles }, $filename;
 
 499   # in the right order (primary keys first)
 
 500   my %cols_by_primary_key = _table_columns($table);
 
 501   my @columns = (@{ $cols_by_primary_key{1} }, @{ $cols_by_primary_key{0} });
 
 502   my %col_index = do { my $i = 0; map {; "$_" => $i++ } @columns };
 
 504   if ($self->csv_headers) {
 
 505     $csv->print($fh, [ map { _normalize_cell($column_titles{$table}{$_->name}) } @columns ]) or die $csv->error_diag;
 
 508   # and normalize date stuff
 
 509   my @select_tokens = map { (ref $_) =~ /Time/ ? $_->name . '::date' : $_->name } @columns;
 
 513   if ($known_tables{$table}{transdate}) {
 
 515       push @where_tokens, "$known_tables{$table}{transdate} >= ?";
 
 516       push @values, $self->from;
 
 519       push @where_tokens, "$known_tables{$table}{transdate} <= ?";
 
 520       push @values, $self->to;
 
 523   if ($known_tables{$table}{tables}) {
 
 524     my ($col, @col_specs) = @{ $known_tables{$table}{tables} };
 
 527       my ($ftable, $fkey) = split /\./, $_;
 
 528       if (!exists $self->export_ids->{$ftable}{$fkey}) {
 
 529          # check if we forgot to keep it
 
 530          if (!grep { $_ eq $fkey } @{ $known_tables{$ftable}{keep} || [] }) {
 
 531            die "unknown table spec '$_' for table $table, did you forget to keep $fkey in $ftable?"
 
 533            # hmm, most likely just an empty set.
 
 534            $self->export_ids->{$ftable}{$fkey} = {};
 
 537       $ids{$_}++ for keys %{ $self->export_ids->{$ftable}{$fkey} };
 
 540       push @where_tokens, "$col IN (@{[ join ',', ('?') x keys %ids ]})";
 
 541       push @values, keys %ids;
 
 543       push @where_tokens, '1=0';
 
 547   my $where_clause = @where_tokens ? 'WHERE ' . join ' AND ', @where_tokens : '';
 
 549   my $query = "SELECT " . join(', ', @select_tokens) . " FROM $table $where_clause";
 
 551   my $sth = $::form->get_standard_dbh->prepare($query);
 
 552   $sth->execute(@values) or $::form->dberror($query);
 
 554   while (my $row = $sth->fetch) {
 
 555     for my $keep_col (@{ $known_tables{$table}{keep} || [] }) {
 
 556       next if !$row->[$col_index{$keep_col}];
 
 557       $self->export_ids->{$table}{$keep_col} ||= {};
 
 558       $self->export_ids->{$table}{$keep_col}{$row->[$col_index{$keep_col}]}++;
 
 560     _normalize_cell($_) for @$row; # see CAVEATS
 
 562     $csv->print($fh, $row) or $csv->error_diag;
 
 568   my ($self, $tag, $content) = @_;
 
 570   $self->writer->startTag($tag);
 
 571   if ('CODE' eq ref $content) {
 
 574     $self->writer->characters($content);
 
 576   $self->writer->endTag;
 
 581   my $gobd_version  = API_VERSION();
 
 582   my $kivi_version  = $::form->read_version;
 
 583   my $person        = $::myconfig{name};
 
 584   my $contact       = join ', ',
 
 585     (t8("Email") . ": $::myconfig{email}" ) x!! $::myconfig{email},
 
 586     (t8("Tel")   . ": $::myconfig{tel}" )   x!! $::myconfig{tel},
 
 587     (t8("Fax")   . ": $::myconfig{fax}" )   x!! $::myconfig{fax};
 
 589   t8('DataSet for GoBD version #1. Created with kivitendo #2 by #3 (#4)',
 
 590     $gobd_version, $kivi_version, $person, $contact
 
 598 sub client_location {
 
 605   my %given = map { $_ => 1 } @{ $self->tables };
 
 607   grep { $given{$_} } @export_table_order;
 
 611   my ($self, $yesno) = @_;
 
 613   $self->tables(\@export_table_order) if $yesno;
 
 616 sub _normalize_cell {
 
 624 sub init_files { +{} }
 
 625 sub init_export_ids { +{} }
 
 626 sub init_tempfiles { [] }
 
 627 sub init_tables { [ grep { $known_tables{$_} } @export_table_order ] }
 
 628 sub init_csv_headers { 1 }
 
 631   DateTime->new(year => 2002, month => 8, day => 14)->to_kivitendo;
 
 635   unlink $_ for @{ $_[0]->tempfiles || [] };
 
 646 SL::GoBD - IDEA export generator
 
 654 Create new export object. C<PARAMS> may contain:
 
 660 The name of the company, needed for the supplier header
 
 664 Location of the company, needed for the supplier header
 
 670 Will only include records in the specified date range. Data pulled from other
 
 671 tables will be culled to match what is needed for these records.
 
 675 Optional. If set, will include a header line in the exported CSV files. Default true.
 
 679 Ooptional list of tables to be exported. Defaults to all tables.
 
 683 Optional alternative to C<tables>, forces all known tables.
 
 687 =item C<generate_export>
 
 689 Do the work. Will return an absolute path to a temp file where all export files
 
 696 Sigh. There are a lot of issues with the IDEA software that were found out by
 
 699 =head2 Problems in the Specification
 
 705 The specced date format is capable of only C<YY>, C<YYYY>, C<MM>,
 
 706 and C<DD>. There are no timestamps or timezones.
 
 710 Numbers have the same issue. There is not dedicated integer type, and hinting
 
 711 at an integer type by setting accuracy to 0 generates a warning for redundant
 
 714 Also the number parsing is documented to be fragile. Official docs state that
 
 715 behaviour for too low C<Accuracy> settings is undefined.
 
 719 Foreign key definition is broken. Instead of giving column maps it assumes that
 
 720 foreign keys map to the primary keys given for the target table, and in that
 
 721 order. Also the target table must be known in full before defining a foreign key.
 
 723 As a consequence any additional keys apart from primary keys are not possible.
 
 724 Self-referencing tables are also not possible.
 
 728 The spec does not support splitting data sets into smaller chunks. For data
 
 729 sets that exceed 700MB the spec helpfully suggests: "Use a bigger medium, such
 
 734 It is not possible to set an empty C<DigitGroupingSymbol> since then the import
 
 735 will just work with the default. This was asked in their forum, and the
 
 736 response actually was to use a bogus grouping symbol that is not used:
 
 738   Einfache Lösung: Definieren Sie das Tausendertrennzeichen als Komma, auch
 
 739   wenn es nicht verwendet wird. Sollten Sie das Komma bereits als Feldtrenner
 
 740   verwenden, so wählen Sie als Tausendertrennzeichen eine Alternative wie das
 
 743 L<http://www.gdpdu-portal.com/forum/index.php?mode=thread&id=1392>
 
 747 It is not possible to define a C<RecordDelimiter> with XML entities. 

 
 748 generates the error message:
 
 750   C<RecordDelimiter>-Wert (
) sollte immer aus ein oder zwei Zeichen
 
 753 Instead we just use the implicit default RecordDelimiter CRLF.
 
 757 =head2 Bugs in the IDEA software
 
 763 The CSV import library used in IDEA is not able to parse newlines (or more
 
 764 exactly RecordDelimiter) in data. So this export substites all of these with
 
 769 Neither it is able to parse escaped C<ColumnDelimiter> in data. It just splits
 
 770 on that symbol no matter what surrounds or preceeds it.
 
 774 Oh and of course C<TextEncapsulator> is also not allowed in data. It's just
 
 775 stripped at the beginning and end of data.
 
 779 And the character "!" is used internally as a warning signal and must not be
 
 780 present in the data as well.
 
 784 C<VariableLength> data is truncated on import to 512 bytes (Note: it said
 
 785 characters, but since they are mutilating data into a single byte encoding
 
 786 anyway, they most likely meant bytes). The auditor recommends splitting into
 
 791 Despite the standard specifying UTF-8 as a valid encoding the IDEA software
 
 792 will just downgrade everything to latin1.
 
 796 =head2 Problems outside of the software
 
 802 The law states that "all business related data" should be made available. In
 
 803 practice there's no definition for what makes data "business related", and
 
 804 different auditors seems to want different data.
 
 806 Currently we export most of the transactional data with supplementing
 
 807 customers, vendors and chart of accounts.
 
 811 While the standard explicitely state to provide data normalized, in practice
 
 812 autditors aren't trained database operators and can not create complex vies on
 
 813 normalized data on their own. The reason this works for other software is, that
 
 814 DATEV and SAP seem to have written import plugins for their internal formats in
 
 817 So what is really exported is not unlike a DATEV export. Each transaction gets
 
 818 splitted into chunks of 2 positions (3 with tax on one side). Those get
 
 819 denormalized into a single data row with credfit/debit/tax fields. The charts
 
 820 get denormalized into it as well, in addition to their account number serving
 
 823 Customers and vendors get denormalized into this as well, but are linked by ids
 
 824 to their tables. And the reason for this is...
 
 828 Some auditors do not have a full license of the IDEA software, and
 
 829 can't do table joins.
 
 835 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>