1 #=====================================================================
 
   6 #   Email: p.reetz@linet-services.de
 
   7 #     Web: http://www.lx-office.org
 
  10 # This program is free software; you can redistribute it and/or modify
 
  11 # it under the terms of the GNU General Public License as published by
 
  12 # the Free Software Foundation; either version 2 of the License, or
 
  13 # (at your option) any later version.
 
  15 # This program is distributed in the hope that it will be useful,
 
  16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 
  17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
  18 # GNU General Public License for more details.
 
  19 # You should have received a copy of the GNU General Public License
 
  20 # along with this program; if not, write to the Free Software
 
  21 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 
  22 #======================================================================
 
  25 #======================================================================
 
  33 use SL::DATEV::KNEFile;
 
  38 use Exporter qw(import);
 
  40 use List::Util qw(max sum);
 
  41 use Time::HiRes qw(gettimeofday);
 
  46     DATEV_ET_BUCHUNGEN => $i++,
 
  47     DATEV_ET_STAMM     => $i++,
 
  49     DATEV_FORMAT_KNE   => $i++,
 
  50     DATEV_FORMAT_OBE   => $i++,
 
  54 my @export_constants = qw(DATEV_ET_BUCHUNGEN DATEV_ET_STAMM DATEV_FORMAT_KNE DATEV_FORMAT_OBE);
 
  55 our @EXPORT_OK = (@export_constants);
 
  56 our %EXPORT_TAGS = (CONSTANTS => [ @export_constants ]);
 
  63   my $obj = bless {}, $class;
 
  65   $obj->$_($data{$_}) for keys %data;
 
  72   $self->{exporttype} = $_[0] if @_;
 
  73   return $self->{exporttype};
 
  77   defined $_[0]->{exporttype};
 
  82   $self->{format} = $_[0] if @_;
 
  83   return $self->{format};
 
  87   defined $_[0]->{format};
 
  90 sub _get_export_path {
 
  91   $main::lxdebug->enter_sub();
 
  93   my ($a, $b) = gettimeofday();
 
  94   my $path    = _get_path_for_download_token("${a}-${b}-${$}");
 
  96   mkpath($path) unless (-d $path);
 
  98   $main::lxdebug->leave_sub();
 
 103 sub _get_path_for_download_token {
 
 104   $main::lxdebug->enter_sub();
 
 106   my $token = shift || '';
 
 109   if ($token =~ m|^(\d+)-(\d+)-(\d+)$|) {
 
 110     $path = $::lx_office_conf{paths}->{userspath} . "/datev-export-${1}-${2}-${3}/";
 
 113   $main::lxdebug->leave_sub();
 
 118 sub _get_download_token_for_path {
 
 119   $main::lxdebug->enter_sub();
 
 124   if ($path =~ m|.*datev-export-(\d+)-(\d+)-(\d+)/?$|) {
 
 125     $token = "${1}-${2}-${3}";
 
 128   $main::lxdebug->leave_sub();
 
 135   $self->{download_token} = $_[0] if @_;
 
 136   return $self->{download_token} ||= _get_download_token_for_path($self->export_path);
 
 142   return  $self->{export_path} ||= _get_path_for_download_token($self->{download_token}) || _get_export_path();
 
 147   push @{ $self->{filenames} ||= [] }, @_;
 
 151   return @{ $_[0]{filenames} || [] };
 
 156   push @{ $self->{errors} ||= [] }, @_;
 
 160   return @{ $_[0]{errors} || [] };
 
 163 sub add_net_gross_differences {
 
 165   push @{ $self->{net_gross_differences} ||= [] }, @_;
 
 168 sub net_gross_differences {
 
 169   return @{ $_[0]{net_gross_differences} || [] };
 
 172 sub sum_net_gross_differences {
 
 173   return sum $_[0]->net_gross_differences;
 
 180    $self->{from} = $_[0];
 
 183  return $self->{from};
 
 200    $self->{accnofrom} = $_[0];
 
 203  return $self->{accnofrom};
 
 210    $self->{accnoto} = $_[0];
 
 213  return $self->{accnoto};
 
 221     $self->{dbh} = $_[0];
 
 222     $self->{provided_dbh} = 1;
 
 225   $self->{dbh} ||= $::form->get_standard_dbh;
 
 232 sub clean_temporary_directories {
 
 233   $::lxdebug->enter_sub;
 
 235   foreach my $path (glob($::lx_office_conf{paths}->{userspath} . "/datev-export-*")) {
 
 236     next unless -d $path;
 
 238     my $mtime = (stat($path))[9];
 
 239     next if ((time() - $mtime) < 8 * 60 * 60);
 
 244   $::lxdebug->leave_sub;
 
 248   $main::lxdebug->enter_sub();
 
 251   my $field_len = shift;
 
 252   my $fill_char = shift;
 
 253   my $alignment = shift || 'right';
 
 255   my $text_len  = length $text;
 
 257   if ($field_len < $text_len) {
 
 258     $text = substr $text, 0, $field_len;
 
 260   } elsif ($field_len > $text_len) {
 
 261     my $filler = ($fill_char) x ($field_len - $text_len);
 
 262     $text      = $alignment eq 'right' ? $filler . $text : $text . $filler;
 
 265   $main::lxdebug->leave_sub();
 
 270 sub get_datev_stamm {
 
 271   return $_[0]{stamm} ||= selectfirst_hashref_query($::form, $_[0]->dbh, 'SELECT * FROM datev');
 
 274 sub save_datev_stamm {
 
 275   my ($self, $data) = @_;
 
 277   do_query($::form, $self->dbh, 'DELETE FROM datev');
 
 279   my @columns = qw(beraternr beratername dfvkz mandantennr datentraegernr abrechnungsnr);
 
 281   my $query = "INSERT INTO datev (" . join(', ', @columns) . ") VALUES (" . join(', ', ('?') x @columns) . ")";
 
 282   do_query($::form, $self->dbh, $query, map { $data->{$_} } @columns);
 
 284   $self->dbh->commit unless $self->provided_dbh;
 
 291   die 'no format set!' unless $self->has_format;
 
 293   if ($self->format == DATEV_FORMAT_KNE) {
 
 294     $result = $self->kne_export;
 
 295   } elsif ($self->format == DATEV_FORMAT_OBE) {
 
 296     $result = $self->obe_export;
 
 298     die 'unrecognized export format';
 
 308   die 'no exporttype set!' unless $self->has_exporttype;
 
 310   if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
 
 311     $result = $self->kne_buchungsexport;
 
 312   } elsif ($self->exporttype == DATEV_ET_STAMM) {
 
 313     $result = $self->kne_stammdatenexport;
 
 315     die 'unrecognized exporttype';
 
 322   die 'not yet implemented';
 
 328   return unless $self->from && $self->to;
 
 330   return "transdate >= '" . $self->from->to_lxoffice . "' and transdate <= '" . $self->to->to_lxoffice . "'";
 
 337 sub _get_transactions {
 
 338   $main::lxdebug->enter_sub();
 
 341   my $progress_callback = shift || sub {};
 
 343   my $form     =  $main::form;
 
 347   $fromto      =~ s/transdate/ac\.transdate/g;
 
 349   my $taxkeys  = Taxkeys->new();
 
 350   my $filter   = '';            # Useful for debugging purposes
 
 352   my %all_taxchart_ids = selectall_as_map($form, $self->dbh, qq|SELECT DISTINCT chart_id, TRUE AS is_set FROM tax|, 'chart_id', 'is_set');
 
 355     qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey,
 
 356          ar.invnumber, ar.duedate, ar.amount as umsatz,
 
 358          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, c.link,
 
 361        LEFT JOIN ar          ON (ac.trans_id    = ar.id)
 
 362        LEFT JOIN customer ct ON (ar.customer_id = ct.id)
 
 363        LEFT JOIN chart c     ON (ac.chart_id    = c.id)
 
 364        WHERE (ar.id IS NOT NULL)
 
 370        SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey,
 
 371          ap.invnumber, ap.duedate, ap.amount as umsatz,
 
 373          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, c.link,
 
 376        LEFT JOIN ap        ON (ac.trans_id  = ap.id)
 
 377        LEFT JOIN vendor ct ON (ap.vendor_id = ct.id)
 
 378        LEFT JOIN chart c   ON (ac.chart_id  = c.id)
 
 379        WHERE (ap.id IS NOT NULL)
 
 385        SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey,
 
 386          gl.reference AS invnumber, gl.transdate AS duedate, ac.amount as umsatz,
 
 387          gl.description AS name,
 
 388          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, c.link,
 
 391        LEFT JOIN gl      ON (ac.trans_id  = gl.id)
 
 392        LEFT JOIN chart c ON (ac.chart_id  = c.id)
 
 393        WHERE (gl.id IS NOT NULL)
 
 397        ORDER BY trans_id, acc_trans_id|;
 
 399   my $sth = prepare_execute_query($form, $self->dbh, $query);
 
 403   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
 
 405     if (($counter % 500) == 0) {
 
 406       $progress_callback->($counter);
 
 409     my $trans    = [ $ref ];
 
 411     my $count    = $ref->{amount};
 
 414     # if the amount of a booking in a group is smaller than 0.02, any tax
 
 415     # amounts will likely be smaller than 1 cent, so go into subcent mode
 
 416     my $subcent  = abs($count) < 0.02;
 
 418     # records from acc_trans are ordered by trans_id and acc_trans_id
 
 419     # first check for unbalanced ledger inside one trans_id
 
 420     # there may be several groups inside a trans_id, e.g. the original booking and the payment
 
 421     # each group individually should be exactly balanced and each group
 
 422     # individually needs its own datev lines
 
 424     # keep fetching new acc_trans lines until the end of a balanced group is reached
 
 425     while (abs($count) > 0.01 || $firstrun || ($subcent && abs($count) > 0.005)) {
 
 426       my $ref2 = $sth->fetchrow_hashref("NAME_lc");
 
 429       # check if trans_id of current acc_trans line is still the same as the
 
 430       # trans_id of the first line in group
 
 432       if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
 
 433         $self->add_error("Unbalanced ledger! old trans_id " . $trans->[0]->{trans_id} . " new trans_id " . $ref2->{trans_id} . " count $count");
 
 437       push @{ $trans }, $ref2;
 
 439       $count    += $ref2->{amount};
 
 443     foreach my $i (0 .. scalar(@{ $trans }) - 1) {
 
 444       my $ref        = $trans->[$i];
 
 445       my $prev_ref   = 0 < $i ? $trans->[$i - 1] : undef;
 
 446       if (   $all_taxchart_ids{$ref->{id}}
 
 447           && ($ref->{link} =~ m/(?:AP_tax|AR_tax)/)
 
 448           && (   ($prev_ref && $prev_ref->{taxkey} && (_sign($ref->{amount}) == _sign($prev_ref->{amount})))
 
 449               || $ref->{invoice})) {
 
 453       if (   !$ref->{invoice}   # we have a non-invoice booking (=gl)
 
 454           &&  $ref->{is_tax}    # that has "is_tax" set
 
 455           && !($prev_ref->{is_tax})  # previous line wasn't is_tax
 
 456           &&  (_sign($ref->{amount}) == _sign($prev_ref->{amount}))) {  # and sign same as previous sign
 
 457         $trans->[$i - 1]->{tax_amount} = $ref->{amount};
 
 461     my %taxid_taxkeys = ();
 
 463     if (scalar(@{$trans}) <= 2) {
 
 464       push @{ $self->{DATEV} }, $trans;
 
 468     # determine at which array position the reference value (called absumsatz) is
 
 469     # and which amount it has
 
 471     for my $j (0 .. (scalar(@{$trans}) - 1)) {
 
 474       # 1: gl transaction (Dialogbuchung), invoice is false, no double split booking allowed
 
 476       # 2: sales or vendor invoice (Verkaufs- und Einkaufsrechnung): invoice is
 
 477       # true, instead of absumsatz use link AR/AP (there should only be one
 
 480       # 3. AR/AP transaction (Kreditoren- und Debitorenbuchung): invoice is false,
 
 481       # instead of absumsatz use link AR/AP (there should only be one, so jump
 
 482       # out of search as soon as you find it )
 
 485       # for gl-bookings no split is allowed and there is no AR/AP account, so we always use the maximum value as a reference
 
 486       # for ap/ar bookings we can always search for AR/AP in link and use that
 
 487       if ( ( not $trans->[$j]->{'invoice'} and abs($trans->[$j]->{'amount'}) > abs($absumsatz) )
 
 488          or ($trans->[$j]->{'invoice'} and ($trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP'))) {
 
 489         $absumsatz     = $trans->[$j]->{'amount'};
 
 494       # Problem: we can't distinguish between AR and AP and normal invoices via boolean "invoice"
 
 495       # for AR and AP transaction exit the loop as soon as an AR or AP account is found
 
 496       # there must be only one AR or AP chart in the booking
 
 497       if ( $trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP') {
 
 498         $notsplitindex = $j;   # position in booking with highest amount
 
 499         $absumsatz     = $trans->[$j]->{'amount'};
 
 504     my $ml             = ($trans->[0]->{'umsatz'} > 0) ? 1 : -1;
 
 505     my $rounding_error = 0;
 
 508     # go through each line and determine if it is a tax booking or not
 
 509     # skip all tax lines and notsplitindex line
 
 510     # push all other accounts (e.g. income or expense) with corresponding taxkey
 
 512     for my $j (0 .. (scalar(@{$trans}) - 1)) {
 
 513       if (   ($j != $notsplitindex)
 
 514           && !$trans->[$j]->{is_tax}
 
 515           && (   $trans->[$j]->{'taxkey'} eq ""
 
 516               || $trans->[$j]->{'taxkey'} eq "0"
 
 517               || $trans->[$j]->{'taxkey'} eq "1"
 
 518               || $trans->[$j]->{'taxkey'} eq "10"
 
 519               || $trans->[$j]->{'taxkey'} eq "11")) {
 
 521         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
 
 523         $absumsatz               += $trans->[$j]->{'amount'};
 
 524         $new_trans{'amount'}      = $trans->[$j]->{'amount'} * (-1);
 
 525         $new_trans{'umsatz'}      = abs($trans->[$j]->{'amount'}) * $ml;
 
 526         $trans->[$j]->{'umsatz'}  = abs($trans->[$j]->{'amount'}) * $ml;
 
 528         push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
 
 530       } elsif (($j != $notsplitindex) && !$trans->[$j]->{is_tax}) {
 
 531         my %tax_info = $taxkeys->get_full_tax_info('transdate' => $trans->[$j]->{transdate});
 
 534         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
 
 536         my $tax_rate              = $tax_info{taxkeys}->{ $trans->[$j]->{'taxkey'} }->{taxrate};
 
 537         $new_trans{'net_amount'}  = $trans->[$j]->{'amount'} * -1;
 
 538         $new_trans{'tax_rate'}    = 1 + $tax_rate;
 
 540         if (!$trans->[$j]->{'invoice'}) {
 
 541           $new_trans{'amount'}      = $form->round_amount(-1 * ($trans->[$j]->{amount} + $trans->[$j]->{tax_amount}), 2);
 
 542           $new_trans{'umsatz'}      = abs($new_trans{'amount'}) * $ml;
 
 543           $trans->[$j]->{'umsatz'}  = $new_trans{'umsatz'};
 
 544           $absumsatz               += -1 * $new_trans{'amount'};
 
 547           my $unrounded             = $trans->[$j]->{'amount'} * (1 + $tax_rate) * -1 + $rounding_error;
 
 548           my $rounded               = $form->round_amount($unrounded, 2);
 
 550           $rounding_error           = $unrounded - $rounded;
 
 551           $new_trans{'amount'}      = $rounded;
 
 552           $new_trans{'umsatz'}      = abs($rounded) * $ml;
 
 553           $trans->[$j]->{'umsatz'}  = $new_trans{umsatz};
 
 554           $absumsatz               -= $rounded;
 
 557         push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
 
 558         push @taxed, $self->{DATEV}->[-1];
 
 564     while ((abs($absumsatz) >= 0.01) && (abs($absumsatz) < 1.00)) {
 
 565       if ($idx >= scalar @taxed) {
 
 566         last if (!$correction);
 
 572       my $transaction = $taxed[$idx]->[0];
 
 574       my $old_amount     = $transaction->{amount};
 
 575       my $old_correction = $correction;
 
 578       if (!$transaction->{diff}) {
 
 579         @possible_diffs = (0.01, -0.01);
 
 581         @possible_diffs = ($transaction->{diff});
 
 584       foreach my $diff (@possible_diffs) {
 
 585         my $net_amount = $form->round_amount(($transaction->{amount} + $diff) / $transaction->{tax_rate}, 2);
 
 586         next if ($net_amount != $transaction->{net_amount});
 
 588         $transaction->{diff}    = $diff;
 
 589         $transaction->{amount} += $diff;
 
 590         $transaction->{umsatz} += $diff;
 
 600     $absumsatz = $form->round_amount($absumsatz, 2);
 
 601     if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
 
 602       $self->add_error("Datev-Export fehlgeschlagen! Bei Transaktion $trans->[0]->{trans_id} ($absumsatz)");
 
 604     } elsif (abs($absumsatz) >= 0.01) {
 
 605       $self->add_net_gross_differences($absumsatz);
 
 611   $::lxdebug->leave_sub;
 
 614 sub make_kne_data_header {
 
 615   $main::lxdebug->enter_sub();
 
 617   my ($self, $form) = @_;
 
 620   my $stamm = $self->get_datev_stamm;
 
 622   my $jahr = $self->from ? $self->from->year : DateTime->today->year;
 
 625   my $header  = "\x1D\x181";
 
 626   $header    .= _fill($stamm->{datentraegernr}, 3, ' ', 'left');
 
 627   $header    .= ($self->fromto) ? "11" : "13"; # Anwendungsnummer
 
 628   $header    .= _fill($stamm->{dfvkz}, 2, '0');
 
 629   $header    .= _fill($stamm->{beraternr}, 7, '0');
 
 630   $header    .= _fill($stamm->{mandantennr}, 5, '0');
 
 631   $header    .= _fill($stamm->{abrechnungsnr} . $jahr, 6, '0');
 
 633   $header .= $self->from ? $self->from->strftime('%d%m%y') : '';
 
 634   $header .= $self->to   ? $self->to->strftime('%d%m%y')   : '';
 
 638     $header .= $primanota;
 
 641   $header .= _fill($stamm->{passwort}, 4, '0');
 
 642   $header .= " " x 16;       # Anwendungsinfo
 
 643   $header .= " " x 16;       # Inputinfo
 
 647   my $versionssatz  = $self->exporttype == DATEV_ET_BUCHUNGEN ? "\xB5" . "1," : "\xB6" . "1,";
 
 649   my $query         = qq|SELECT accno FROM chart LIMIT 1|;
 
 650   my $ref           = selectfirst_hashref_query($form, $self->dbh, $query);
 
 652   $versionssatz    .= length $ref->{accno};
 
 653   $versionssatz    .= ",";
 
 654   $versionssatz    .= length $ref->{accno};
 
 655   $versionssatz    .= ",SELF" . "\x1C\x79";
 
 657   $header          .= $versionssatz;
 
 659   $main::lxdebug->leave_sub();
 
 665   $main::lxdebug->enter_sub();
 
 667   my ($date, $six) = @_;
 
 669   my ($day, $month, $year) = split(/\./, $date);
 
 672     $day = substr($day, 1, 1);
 
 674   if (length($month) < 2) {
 
 675     $month = "0" . $month;
 
 677   if (length($year) > 2) {
 
 678     $year = substr($year, -2, 2);
 
 682     $date = $day . $month . $year;
 
 684     $date = $day . $month;
 
 687   $main::lxdebug->leave_sub();
 
 692 sub trim_leading_zeroes {
 
 700 sub make_ed_versionset {
 
 701   $main::lxdebug->enter_sub();
 
 703   my ($self, $header, $filename, $blockcount) = @_;
 
 705   my $versionset  = "V" . substr($filename, 2, 5);
 
 706   $versionset    .= substr($header, 6, 22);
 
 709     $versionset .= "0000" . substr($header, 28, 19);
 
 711     my $datum = " " x 16;
 
 712     $versionset .= $datum . "001" . substr($header, 28, 4);
 
 715   $versionset .= _fill($blockcount, 5, '0');
 
 716   $versionset .= "001";
 
 718   $versionset .= substr($header, -12, 10) . "    ";
 
 719   $versionset .= " " x 53;
 
 721   $main::lxdebug->leave_sub();
 
 727   $main::lxdebug->enter_sub();
 
 729   my ($self, $form, $fileno) = @_;
 
 731   my $stamm = $self->get_datev_stamm;
 
 733   my $ev_header  = _fill($stamm->{datentraegernr}, 3, ' ', 'left');
 
 735   $ev_header    .= _fill($stamm->{beraternr}, 7, ' ', 'left');
 
 736   $ev_header    .= _fill($stamm->{beratername}, 9, ' ', 'left');
 
 738   $ev_header    .= (_fill($fileno, 5, '0')) x 2;
 
 739   $ev_header    .= " " x 95;
 
 741   $main::lxdebug->leave_sub();
 
 746 sub kne_buchungsexport {
 
 747   $main::lxdebug->enter_sub();
 
 755   my $filename    = "ED00000";
 
 760   my $fromto = $self->fromto;
 
 762   $self->_get_transactions($fromto);
 
 764   return if $self->errors;
 
 768   while (scalar(@{ $self->{DATEV} || [] })) {
 
 771     my $ed_filename = $self->export_path . $filename;
 
 772     push(@filenames, $filename);
 
 773     my $header = $self->make_kne_data_header($form);
 
 775     my $kne_file = SL::DATEV::KNEFile->new();
 
 776     $kne_file->add_block($header);
 
 778     while (scalar(@{ $self->{DATEV} }) > 0) {
 
 779       my $transaction = shift @{ $self->{DATEV} };
 
 780       my $trans_lines = scalar(@{$transaction});
 
 789       my $buchungstext   = "";
 
 791       my $datevautomatik = 0;
 
 795       my $iconv          = $::locale->{iconv_utf8};
 
 796       my %umlaute = ($iconv->convert('ä') => 'ae',
 
 797                      $iconv->convert('ö') => 'oe',
 
 798                      $iconv->convert('ü') => 'ue',
 
 799                      $iconv->convert('Ä') => 'Ae',
 
 800                      $iconv->convert('Ö') => 'Oe',
 
 801                      $iconv->convert('Ü') => 'Ue',
 
 802                      $iconv->convert('ß') => 'sz');
 
 803       for (my $i = 0; $i < $trans_lines; $i++) {
 
 804         if ($trans_lines == 2) {
 
 805           if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
 
 806             $umsatz = $transaction->[$i]->{'amount'};
 
 809           if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
 
 810             $umsatz = $transaction->[$i]->{'umsatz'};
 
 813         if ($transaction->[$i]->{'datevautomatik'}) {
 
 816         if ($transaction->[$i]->{'taxkey'}) {
 
 817           $taxkey = $transaction->[$i]->{'taxkey'};
 
 819         if ($transaction->[$i]->{'charttax'}) {
 
 820           $charttax = $transaction->[$i]->{'charttax'};
 
 822         if ($transaction->[$i]->{'amount'} > 0) {
 
 829       # Umwandlung von Umlauten und Sonderzeichen in erlaubte Zeichen bei Textfeldern
 
 830       foreach my $umlaut (keys(%umlaute)) {
 
 831         $transaction->[$haben]->{'invnumber'} =~ s/${umlaut}/${umlaute{$umlaut}}/g;
 
 832         $transaction->[$haben]->{'name'}      =~ s/${umlaut}/${umlaute{$umlaut}}/g;
 
 835       $transaction->[$haben]->{'invnumber'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
 
 836       $transaction->[$haben]->{'name'}      =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
 
 838       $transaction->[$haben]->{'invnumber'} =  substr($transaction->[$haben]->{'invnumber'}, 0, 12);
 
 839       $transaction->[$haben]->{'name'}      =  substr($transaction->[$haben]->{'name'}, 0, 30);
 
 840       $transaction->[$haben]->{'invnumber'} =~ s/\ *$//;
 
 841       $transaction->[$haben]->{'name'}      =~ s/\ *$//;
 
 843       if ($trans_lines >= 2) {
 
 845         $gegenkonto = "a" . trim_leading_zeroes($transaction->[$haben]->{'accno'});
 
 846         $konto      = "e" . trim_leading_zeroes($transaction->[$soll]->{'accno'});
 
 847         if ($transaction->[$haben]->{'invnumber'} ne "") {
 
 848           $belegfeld1 = "\xBD" . $transaction->[$haben]->{'invnumber'} . "\x1C";
 
 851         $datum .= &datetofour($transaction->[$haben]->{'transdate'}, 0);
 
 852         $waehrung = "\xB3" . "EUR" . "\x1C";
 
 853         if ($transaction->[$haben]->{'name'} ne "") {
 
 854           $buchungstext = "\x1E" . $transaction->[$haben]->{'name'} . "\x1C";
 
 856         if ($transaction->[$haben]->{'duedate'} ne "") {
 
 857           $belegfeld2 = "\xBE" . &datetofour($transaction->[$haben]->{'duedate'}, 1) . "\x1C";
 
 861       $umsatz       = $kne_file->format_amount(abs($umsatz), 0);
 
 862       $umsatzsumme += $umsatz;
 
 863       $kne_file->add_block("+" . $umsatz);
 
 865       # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
 
 866       # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
 
 867       # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
 
 868       # Skript angelegt werden.
 
 869       # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
 
 870       # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
 
 871       # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
 
 872       # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
 
 873       # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
 
 875       if (   ( $datevautomatik || $taxkey)
 
 876           && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
 
 877 #         $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4"));
 
 878         $kne_file->add_block("\x6C${taxkey}");
 
 881       $kne_file->add_block($gegenkonto);
 
 882       $kne_file->add_block($belegfeld1);
 
 883       $kne_file->add_block($belegfeld2);
 
 884       $kne_file->add_block($datum);
 
 885       $kne_file->add_block($konto);
 
 886       $kne_file->add_block($buchungstext);
 
 887       $kne_file->add_block($waehrung . "\x79");
 
 890     my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
 
 892     $kne_file->add_block($mandantenendsumme);
 
 895     open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
 
 896     print(ED $kne_file->get_data());
 
 899     $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
 
 903   #Make EV Verwaltungsdatei
 
 904   my $ev_header = $self->make_ev_header($form, $fileno);
 
 905   my $ev_filename = $self->export_path . $evfile;
 
 906   push(@filenames, $evfile);
 
 907   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
 
 908   print(EV $ev_header);
 
 910   foreach my $file (@ed_versionset) {
 
 911     print(EV $ed_versionset[$file]);
 
 916   $self->add_filenames(@filenames);
 
 918   $main::lxdebug->leave_sub();
 
 920   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
 
 923 sub kne_stammdatenexport {
 
 924   $main::lxdebug->enter_sub();
 
 929   $self->get_datev_stamm->{abrechnungsnr} = "99";
 
 933   my $filename    = "ED00000";
 
 939   my $remaining_bytes = 256;
 
 940   my $total_bytes     = 256;
 
 941   my $buchungssatz    = "";
 
 943   my $ed_filename = $self->export_path . $filename;
 
 944   push(@filenames, $filename);
 
 945   open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
 
 946   my $header = $self->make_kne_data_header($form);
 
 947   $remaining_bytes -= length($header);
 
 951   my (@where, @values) = ((), ());
 
 952   if ($self->accnofrom) {
 
 953     push @where, 'c.accno >= ?';
 
 954     push @values, $self->accnofrom;
 
 956   if ($self->accnoto) {
 
 957     push @where, 'c.accno <= ?';
 
 958     push @values, $self->accnoto;
 
 961   my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
 
 963   my $query     = qq|SELECT c.accno, c.description
 
 968   my $sth = $self->dbh->prepare($query);
 
 969   $sth->execute(@values) || $form->dberror($query);
 
 971   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
 
 972     if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
 
 973       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
 
 974       $buchungssatz .= "\x00" x $fuellzeichen;
 
 976       $total_bytes = ($blockcount) * 256;
 
 978     $buchungssatz .= "t" . $ref->{'accno'};
 
 979     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
 
 980     $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
 
 981     $ref->{'description'} = substr($ref->{'description'}, 0, 40);
 
 982     $ref->{'description'} =~ s/\ *$//;
 
 985         ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
 
 988       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
 
 989       $buchungssatz .= "\x00" x $fuellzeichen;
 
 991       $total_bytes = ($blockcount) * 256;
 
 993     $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
 
 994     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
 
 999   print(ED $buchungssatz);
 
1000   $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
 
1001   my $dateiende = "\x00" x $fuellzeichen;
 
1003   print(ED $dateiende);
 
1006   #Make EV Verwaltungsdatei
 
1008     $self->make_ed_versionset($header, $filename, $blockcount);
 
1010   my $ev_header = $self->make_ev_header($form, $fileno);
 
1011   my $ev_filename = $self->export_path . $evfile;
 
1012   push(@filenames, $evfile);
 
1013   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
 
1014   print(EV $ev_header);
 
1016   foreach my $file (@ed_versionset) {
 
1017     print(EV $ed_versionset[$file]);
 
1021   $self->add_filenames(@filenames);
 
1023   $main::lxdebug->leave_sub();
 
1025   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
 
1029   clean_temporary_directories();
 
1040 SL::DATEV - kivitendo DATEV Export module
 
1044   use SL::DATEV qw(:CONSTANTS);
 
1046   my $datev = SL::DATEV->new(
 
1047     exporttype => DATEV_ET_BUCHUNGEN,
 
1048     format     => DATEV_FORMAT_KNE,
 
1053   my $datev = SL::DATEV->new(
 
1054     exporttype => DATEV_ET_STAMM,
 
1055     format     => DATEV_FORMAT_KNE,
 
1056     accnofrom  => $start_account_number,
 
1057     accnoto    => $end_account_number,
 
1060   # get or set datev stamm
 
1061   my $hashref = $datev->get_datev_stamm;
 
1062   $datev->save_datev_stamm($hashref);
 
1064   # manually clean up temporary directories
 
1065   $datev->clean_temporary_directories;
 
1070   if ($datev->errors) {
 
1071     die join "\n", $datev->error;
 
1074   # get relevant data for saving the export:
 
1075   my $dl_token = $datev->download_token;
 
1076   my $path     = $datev->export_path;
 
1077   my @files    = $datev->filenames;
 
1079   # retrieving an export at a later time
 
1080   my $datev = SL::DATEV->new(
 
1081     download_token => $dl_token_from_user,
 
1084   my $path     = $datev->export_path;
 
1085   my @files    = glob("$path/*");
 
1089 This module implements the DATEV export standard. For usage see above.
 
1097 Generic constructor. See section attributes for information about hat to pass.
 
1099 =item get_datev_stamm
 
1101 Loads DATEV Stammdaten and returns as hashref.
 
1103 =item save_datev_stamm HASHREF
 
1105 Saves DATEV Stammdaten from provided hashref.
 
1109 See L<CONSTANTS> for possible values
 
1111 =item has_exporttype
 
1113 Returns true if an exporttype has been set. Without exporttype most report functions won't work.
 
1117 Specifies the designated format of the export. Currently only KNE export is implemented.
 
1119 See L<CONSTANTS> for possible values
 
1123 Returns true if a format has been set. Without format most report functions won't work.
 
1125 =item download_token
 
1127 Returns a download token for this DATEV object.
 
1129 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
 
1133 Returns an export_path for this DATEV object.
 
1135 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
 
1139 Returns a list of filenames generated by this DATEV object. This only works if th files were generated during it's lifetime, not if the object was created from a download_token.
 
1141 =item net_gross_differences
 
1143 If there were any net gross differences during calculation they will be collected here.
 
1145 =item sum_net_gross_differences
 
1147 Sum of all differences.
 
1149 =item clean_temporary_directories
 
1151 Forces a garbage collection on previous exports which will delete all exports that are older than 8 hours. It will be automatically called on destruction of the object, but is advised to be called manually before delivering results of an export to the user.
 
1155 Returns a list of errors that occured. If no errors occured, the export was a success.
 
1159 Exports data. You have to have set L<exporttype> and L<format> or an error will
 
1160 occur. OBE exports are currently not implemented.
 
1166 This is a list of attributes set in either the C<new> or a method of the same name.
 
1172 Set a database handle to use in the process. This allows for an export to be
 
1173 done on a transaction in progress without committing first.
 
1177 See L<CONSTANTS> for possible values. This MUST be set before export is called.
 
1181 See L<CONSTANTS> for possible values. This MUST be set before export is called.
 
1183 =item download_token
 
1185 Can be set on creation to retrieve a prior export for download.
 
1191 Set boundary dates for the export. Currently thse MUST be set for the export to work.
 
1197 Set boundary account numbers for the export. Only useful for a stammdaten export.
 
1203 =head2 Supplied to L<exporttype>
 
1207 =item DATEV_ET_BUCHUNGEN
 
1209 =item DATEV_ET_STAMM
 
1213 =head2 Supplied to L<format>.
 
1217 =item DATEV_FORMAT_KNE
 
1219 =item DATEV_FORMAT_OBE
 
1223 =head1 ERROR HANDLING
 
1225 This module will die in the following cases:
 
1231 No or unrecognized exporttype or format was provided for an export
 
1235 OBE rxport was called, which is not yet implemented.
 
1243 Errors that occur during th actual export will be collected in L<errors>. The following types can occur at the moment:
 
1249 C<Unbalanced Ledger!>. Exactly that, your ledger is unbalanced. Should never occur.
 
1253 C<Datev-Export fehlgeschlagen! Bei Transaktion %d (%f).>  This error occurs if a
 
1254 transaction could not be reliably sorted out, or had rounding errors over the acceptable threshold.
 
1258 =head1 BUGS AND CAVEATS
 
1264 Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to to get a working export.
 
1268 OBE export is currently not implemented.
 
1274 - handling of export_path and download token is a bit dodgy, clean that up.
 
1278 L<SL::DATEV::KNEFile>
 
1282 Philip Reetz E<lt>p.reetz@linet-services.deE<gt>,
 
1284 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
 
1286 Jan Büren E<lt>jan@lx-office-hosting.deE<gt>,
 
1288 Geoffrey Richardson E<lt>information@lx-office-hosting.deE<gt>,
 
1290 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,