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;
 
  37 use Exporter qw(import);
 
  39 use List::Util qw(max sum);
 
  40 use Time::HiRes qw(gettimeofday);
 
  45     DATEV_ET_BUCHUNGEN => $i++,
 
  46     DATEV_ET_STAMM     => $i++,
 
  48     DATEV_FORMAT_KNE   => $i++,
 
  49     DATEV_FORMAT_OBE   => $i++,
 
  53 my @export_constants = qw(DATEV_ET_BUCHUNGEN DATEV_ET_STAMM DATEV_FORMAT_KNE DATEV_FORMAT_OBE);
 
  54 our @EXPORT_OK = (@export_constants);
 
  55 our %EXPORT_TAGS = (CONSTANTS => [ @export_constants ]);
 
  62   my $obj = bless {}, $class;
 
  64   $obj->$_($data{$_}) for keys %data;
 
  71   $self->{exporttype} = $_[0] if @_;
 
  72   return $self->{exporttype};
 
  76   defined $_[0]->{exporttype};
 
  81   $self->{format} = $_[0] if @_;
 
  82   return $self->{format};
 
  86   defined $_[0]->{format};
 
  89 sub _get_export_path {
 
  90   $main::lxdebug->enter_sub();
 
  92   my ($a, $b) = gettimeofday();
 
  93   my $path    = _get_path_for_download_token("${a}-${b}-${$}");
 
  95   mkpath($path) unless (-d $path);
 
  97   $main::lxdebug->leave_sub();
 
 102 sub _get_path_for_download_token {
 
 103   $main::lxdebug->enter_sub();
 
 105   my $token = shift || '';
 
 108   if ($token =~ m|^(\d+)-(\d+)-(\d+)$|) {
 
 109     $path = $::lx_office_conf{paths}->{userspath} . "/datev-export-${1}-${2}-${3}/";
 
 112   $main::lxdebug->leave_sub();
 
 117 sub _get_download_token_for_path {
 
 118   $main::lxdebug->enter_sub();
 
 123   if ($path =~ m|.*datev-export-(\d+)-(\d+)-(\d+)/?$|) {
 
 124     $token = "${1}-${2}-${3}";
 
 127   $main::lxdebug->leave_sub();
 
 134   $self->{download_token} = $_[0] if @_;
 
 135   return $self->{download_token} ||= _get_download_token_for_path($self->export_path);
 
 141   return  $self->{export_path} ||= _get_path_for_download_token($self->{download_token}) || _get_export_path();
 
 146   push @{ $self->{filenames} ||= [] }, @_;
 
 150   return @{ $_[0]{filenames} || [] };
 
 155   push @{ $self->{errors} ||= [] }, @_;
 
 159   return @{ $_[0]{errors} || [] };
 
 162 sub add_net_gross_differences {
 
 164   push @{ $self->{net_gross_differences} ||= [] }, @_;
 
 167 sub net_gross_differences {
 
 168   return @{ $_[0]{net_gross_differences} || [] };
 
 171 sub sum_net_gross_differences {
 
 172   return sum $_[0]->net_gross_differences;
 
 179    $self->{from} = $_[0];
 
 182  return $self->{from};
 
 199     $self->{trans_id} = $_[0];
 
 202   return $self->{trans_id};
 
 209    $self->{accnofrom} = $_[0];
 
 212  return $self->{accnofrom};
 
 219    $self->{accnoto} = $_[0];
 
 222  return $self->{accnoto};
 
 230     $self->{dbh} = $_[0];
 
 231     $self->{provided_dbh} = 1;
 
 234   $self->{dbh} ||= $::form->get_standard_dbh;
 
 241 sub clean_temporary_directories {
 
 242   $::lxdebug->enter_sub;
 
 244   foreach my $path (glob($::lx_office_conf{paths}->{userspath} . "/datev-export-*")) {
 
 245     next unless -d $path;
 
 247     my $mtime = (stat($path))[9];
 
 248     next if ((time() - $mtime) < 8 * 60 * 60);
 
 253   $::lxdebug->leave_sub;
 
 257   $main::lxdebug->enter_sub();
 
 260   my $field_len = shift;
 
 261   my $fill_char = shift;
 
 262   my $alignment = shift || 'right';
 
 264   my $text_len  = length $text;
 
 266   if ($field_len < $text_len) {
 
 267     $text = substr $text, 0, $field_len;
 
 269   } elsif ($field_len > $text_len) {
 
 270     my $filler = ($fill_char) x ($field_len - $text_len);
 
 271     $text      = $alignment eq 'right' ? $filler . $text : $text . $filler;
 
 274   $main::lxdebug->leave_sub();
 
 279 sub get_datev_stamm {
 
 280   return $_[0]{stamm} ||= selectfirst_hashref_query($::form, $_[0]->dbh, 'SELECT * FROM datev');
 
 283 sub save_datev_stamm {
 
 284   my ($self, $data) = @_;
 
 286   do_query($::form, $self->dbh, 'DELETE FROM datev');
 
 288   my @columns = qw(beraternr beratername dfvkz mandantennr datentraegernr abrechnungsnr);
 
 290   my $query = "INSERT INTO datev (" . join(', ', @columns) . ") VALUES (" . join(', ', ('?') x @columns) . ")";
 
 291   do_query($::form, $self->dbh, $query, map { $data->{$_} } @columns);
 
 293   $self->dbh->commit unless $self->provided_dbh;
 
 300   die 'no format set!' unless $self->has_format;
 
 302   if ($self->format == DATEV_FORMAT_KNE) {
 
 303     $result = $self->kne_export;
 
 304   } elsif ($self->format == DATEV_FORMAT_OBE) {
 
 305     $result = $self->obe_export;
 
 307     die 'unrecognized export format';
 
 317   die 'no exporttype set!' unless $self->has_exporttype;
 
 319   if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
 
 320     $result = $self->kne_buchungsexport;
 
 321   } elsif ($self->exporttype == DATEV_ET_STAMM) {
 
 322     $result = $self->kne_stammdatenexport;
 
 324     die 'unrecognized exporttype';
 
 331   die 'not yet implemented';
 
 337   return unless $self->from && $self->to;
 
 339   return "transdate >= '" . $self->from->to_lxoffice . "' and transdate <= '" . $self->to->to_lxoffice . "'";
 
 346 sub _get_transactions {
 
 347   $main::lxdebug->enter_sub();
 
 350   my $progress_callback = shift || sub {};
 
 352   my $form     =  $main::form;
 
 354   my $trans_id_filter = '';
 
 356   $trans_id_filter = 'AND ac.trans_id = ' . $self->trans_id if $self->trans_id;
 
 360   $fromto      =~ s/transdate/ac\.transdate/g;
 
 362   my $filter   = '';            # Useful for debugging purposes
 
 364   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');
 
 367     qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey,
 
 368          ar.invnumber, ar.duedate, ar.amount as umsatz, ar.deliverydate,
 
 370          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
 
 374        LEFT JOIN ar          ON (ac.trans_id    = ar.id)
 
 375        LEFT JOIN customer ct ON (ar.customer_id = ct.id)
 
 376        LEFT JOIN chart c     ON (ac.chart_id    = c.id)
 
 377        LEFT JOIN tax t       ON (ac.tax_id      = t.id)
 
 378        WHERE (ar.id IS NOT NULL)
 
 385        SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey,
 
 386          ap.invnumber, ap.duedate, ap.amount as umsatz, ap.deliverydate,
 
 388          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
 
 392        LEFT JOIN ap        ON (ac.trans_id  = ap.id)
 
 393        LEFT JOIN vendor ct ON (ap.vendor_id = ct.id)
 
 394        LEFT JOIN chart c   ON (ac.chart_id  = c.id)
 
 395        LEFT JOIN tax t     ON (ac.tax_id    = t.id)
 
 396        WHERE (ap.id IS NOT NULL)
 
 403        SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey,
 
 404          gl.reference AS invnumber, gl.transdate AS duedate, ac.amount as umsatz, NULL as deliverydate,
 
 405          gl.description AS name,
 
 406          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
 
 410        LEFT JOIN gl      ON (ac.trans_id  = gl.id)
 
 411        LEFT JOIN chart c ON (ac.chart_id  = c.id)
 
 412        LEFT JOIN tax t   ON (ac.tax_id    = t.id)
 
 413        WHERE (gl.id IS NOT NULL)
 
 418        ORDER BY trans_id, acc_trans_id|;
 
 420   my $sth = prepare_execute_query($form, $self->dbh, $query);
 
 424   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
 
 426     if (($counter % 500) == 0) {
 
 427       $progress_callback->($counter);
 
 430     my $trans    = [ $ref ];
 
 432     my $count    = $ref->{amount};
 
 435     # if the amount of a booking in a group is smaller than 0.02, any tax
 
 436     # amounts will likely be smaller than 1 cent, so go into subcent mode
 
 437     my $subcent  = abs($count) < 0.02;
 
 439     # records from acc_trans are ordered by trans_id and acc_trans_id
 
 440     # first check for unbalanced ledger inside one trans_id
 
 441     # there may be several groups inside a trans_id, e.g. the original booking and the payment
 
 442     # each group individually should be exactly balanced and each group
 
 443     # individually needs its own datev lines
 
 445     # keep fetching new acc_trans lines until the end of a balanced group is reached
 
 446     while (abs($count) > 0.01 || $firstrun || ($subcent && abs($count) > 0.005)) {
 
 447       my $ref2 = $sth->fetchrow_hashref("NAME_lc");
 
 450       # check if trans_id of current acc_trans line is still the same as the
 
 451       # trans_id of the first line in group
 
 453       if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
 
 454         $self->add_error("Unbalanced ledger! old trans_id " . $trans->[0]->{trans_id} . " new trans_id " . $ref2->{trans_id} . " count $count");
 
 458       push @{ $trans }, $ref2;
 
 460       $count    += $ref2->{amount};
 
 464     foreach my $i (0 .. scalar(@{ $trans }) - 1) {
 
 465       my $ref        = $trans->[$i];
 
 466       my $prev_ref   = 0 < $i ? $trans->[$i - 1] : undef;
 
 467       if (   $all_taxchart_ids{$ref->{id}}
 
 468           && ($ref->{link} =~ m/(?:AP_tax|AR_tax)/)
 
 469           && (   ($prev_ref && $prev_ref->{taxkey} && (_sign($ref->{amount}) == _sign($prev_ref->{amount})))
 
 470               || $ref->{invoice})) {
 
 474       if (   !$ref->{invoice}   # we have a non-invoice booking (=gl)
 
 475           &&  $ref->{is_tax}    # that has "is_tax" set
 
 476           && !($prev_ref->{is_tax})  # previous line wasn't is_tax
 
 477           &&  (_sign($ref->{amount}) == _sign($prev_ref->{amount}))) {  # and sign same as previous sign
 
 478         $trans->[$i - 1]->{tax_amount} = $ref->{amount};
 
 483     if (scalar(@{$trans}) <= 2) {
 
 484       push @{ $self->{DATEV} }, $trans;
 
 488     # determine at which array position the reference value (called absumsatz) is
 
 489     # and which amount it has
 
 491     for my $j (0 .. (scalar(@{$trans}) - 1)) {
 
 494       # 1: gl transaction (Dialogbuchung), invoice is false, no double split booking allowed
 
 496       # 2: sales or vendor invoice (Verkaufs- und Einkaufsrechnung): invoice is
 
 497       # true, instead of absumsatz use link AR/AP (there should only be one
 
 500       # 3. AR/AP transaction (Kreditoren- und Debitorenbuchung): invoice is false,
 
 501       # instead of absumsatz use link AR/AP (there should only be one, so jump
 
 502       # out of search as soon as you find it )
 
 505       # for gl-bookings no split is allowed and there is no AR/AP account, so we always use the maximum value as a reference
 
 506       # for ap/ar bookings we can always search for AR/AP in link and use that
 
 507       if ( ( not $trans->[$j]->{'invoice'} and abs($trans->[$j]->{'amount'}) > abs($absumsatz) )
 
 508          or ($trans->[$j]->{'invoice'} and ($trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP'))) {
 
 509         $absumsatz     = $trans->[$j]->{'amount'};
 
 514       # Problem: we can't distinguish between AR and AP and normal invoices via boolean "invoice"
 
 515       # for AR and AP transaction exit the loop as soon as an AR or AP account is found
 
 516       # there must be only one AR or AP chart in the booking
 
 517       if ( $trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP') {
 
 518         $notsplitindex = $j;   # position in booking with highest amount
 
 519         $absumsatz     = $trans->[$j]->{'amount'};
 
 524     my $ml             = ($trans->[0]->{'umsatz'} > 0) ? 1 : -1;
 
 525     my $rounding_error = 0;
 
 528     # go through each line and determine if it is a tax booking or not
 
 529     # skip all tax lines and notsplitindex line
 
 530     # push all other accounts (e.g. income or expense) with corresponding taxkey
 
 532     for my $j (0 .. (scalar(@{$trans}) - 1)) {
 
 533       if (   ($j != $notsplitindex)
 
 534           && !$trans->[$j]->{is_tax}
 
 535           && (   $trans->[$j]->{'taxkey'} eq ""
 
 536               || $trans->[$j]->{'taxkey'} eq "0"
 
 537               || $trans->[$j]->{'taxkey'} eq "1"
 
 538               || $trans->[$j]->{'taxkey'} eq "10"
 
 539               || $trans->[$j]->{'taxkey'} eq "11")) {
 
 541         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
 
 543         $absumsatz               += $trans->[$j]->{'amount'};
 
 544         $new_trans{'amount'}      = $trans->[$j]->{'amount'} * (-1);
 
 545         $new_trans{'umsatz'}      = abs($trans->[$j]->{'amount'}) * $ml;
 
 546         $trans->[$j]->{'umsatz'}  = abs($trans->[$j]->{'amount'}) * $ml;
 
 548         push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
 
 550       } elsif (($j != $notsplitindex) && !$trans->[$j]->{is_tax}) {
 
 553         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
 
 555         my $tax_rate              = $trans->[$j]->{'taxrate'};
 
 556         $new_trans{'net_amount'}  = $trans->[$j]->{'amount'} * -1;
 
 557         $new_trans{'tax_rate'}    = 1 + $tax_rate;
 
 559         if (!$trans->[$j]->{'invoice'}) {
 
 560           $new_trans{'amount'}      = $form->round_amount(-1 * ($trans->[$j]->{amount} + $trans->[$j]->{tax_amount}), 2);
 
 561           $new_trans{'umsatz'}      = abs($new_trans{'amount'}) * $ml;
 
 562           $trans->[$j]->{'umsatz'}  = $new_trans{'umsatz'};
 
 563           $absumsatz               += -1 * $new_trans{'amount'};
 
 566           my $unrounded             = $trans->[$j]->{'amount'} * (1 + $tax_rate) * -1 + $rounding_error;
 
 567           my $rounded               = $form->round_amount($unrounded, 2);
 
 569           $rounding_error           = $unrounded - $rounded;
 
 570           $new_trans{'amount'}      = $rounded;
 
 571           $new_trans{'umsatz'}      = abs($rounded) * $ml;
 
 572           $trans->[$j]->{'umsatz'}  = $new_trans{umsatz};
 
 573           $absumsatz               -= $rounded;
 
 576         push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
 
 577         push @taxed, $self->{DATEV}->[-1];
 
 583     while ((abs($absumsatz) >= 0.01) && (abs($absumsatz) < 1.00)) {
 
 584       if ($idx >= scalar @taxed) {
 
 585         last if (!$correction);
 
 591       my $transaction = $taxed[$idx]->[0];
 
 593       my $old_amount     = $transaction->{amount};
 
 594       my $old_correction = $correction;
 
 597       if (!$transaction->{diff}) {
 
 598         @possible_diffs = (0.01, -0.01);
 
 600         @possible_diffs = ($transaction->{diff});
 
 603       foreach my $diff (@possible_diffs) {
 
 604         my $net_amount = $form->round_amount(($transaction->{amount} + $diff) / $transaction->{tax_rate}, 2);
 
 605         next if ($net_amount != $transaction->{net_amount});
 
 607         $transaction->{diff}    = $diff;
 
 608         $transaction->{amount} += $diff;
 
 609         $transaction->{umsatz} += $diff;
 
 619     $absumsatz = $form->round_amount($absumsatz, 2);
 
 620     if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
 
 621       $self->add_error("Datev-Export fehlgeschlagen! Bei Transaktion $trans->[0]->{trans_id} ($absumsatz)");
 
 623     } elsif (abs($absumsatz) >= 0.01) {
 
 624       $self->add_net_gross_differences($absumsatz);
 
 630   $::lxdebug->leave_sub;
 
 633 sub make_kne_data_header {
 
 634   $main::lxdebug->enter_sub();
 
 636   my ($self, $form) = @_;
 
 639   my $stamm = $self->get_datev_stamm;
 
 641   my $jahr = $self->from ? $self->from->year : DateTime->today->year;
 
 644   my $header  = "\x1D\x181";
 
 645   $header    .= _fill($stamm->{datentraegernr}, 3, ' ', 'left');
 
 646   $header    .= ($self->fromto) ? "11" : "13"; # Anwendungsnummer
 
 647   $header    .= _fill($stamm->{dfvkz}, 2, '0');
 
 648   $header    .= _fill($stamm->{beraternr}, 7, '0');
 
 649   $header    .= _fill($stamm->{mandantennr}, 5, '0');
 
 650   $header    .= _fill($stamm->{abrechnungsnr} . $jahr, 6, '0');
 
 652   $header .= $self->from ? $self->from->strftime('%d%m%y') : '';
 
 653   $header .= $self->to   ? $self->to->strftime('%d%m%y')   : '';
 
 657     $header .= $primanota;
 
 660   $header .= _fill($stamm->{passwort}, 4, '0');
 
 661   $header .= " " x 16;       # Anwendungsinfo
 
 662   $header .= " " x 16;       # Inputinfo
 
 666   my $versionssatz  = $self->exporttype == DATEV_ET_BUCHUNGEN ? "\xB5" . "1," : "\xB6" . "1,";
 
 668   my $query         = qq|SELECT accno FROM chart LIMIT 1|;
 
 669   my $ref           = selectfirst_hashref_query($form, $self->dbh, $query);
 
 671   $versionssatz    .= length $ref->{accno};
 
 672   $versionssatz    .= ",";
 
 673   $versionssatz    .= length $ref->{accno};
 
 674   $versionssatz    .= ",SELF" . "\x1C\x79";
 
 676   $header          .= $versionssatz;
 
 678   $main::lxdebug->leave_sub();
 
 684   $main::lxdebug->enter_sub();
 
 686   my ($date, $six) = @_;
 
 688   my ($day, $month, $year) = split(/\./, $date);
 
 691     $day = substr($day, 1, 1);
 
 693   if (length($month) < 2) {
 
 694     $month = "0" . $month;
 
 696   if (length($year) > 2) {
 
 697     $year = substr($year, -2, 2);
 
 701     $date = $day . $month . $year;
 
 703     $date = $day . $month;
 
 706   $main::lxdebug->leave_sub();
 
 711 sub trim_leading_zeroes {
 
 719 sub make_ed_versionset {
 
 720   $main::lxdebug->enter_sub();
 
 722   my ($self, $header, $filename, $blockcount) = @_;
 
 724   my $versionset  = "V" . substr($filename, 2, 5);
 
 725   $versionset    .= substr($header, 6, 22);
 
 728     $versionset .= "0000" . substr($header, 28, 19);
 
 730     my $datum = " " x 16;
 
 731     $versionset .= $datum . "001" . substr($header, 28, 4);
 
 734   $versionset .= _fill($blockcount, 5, '0');
 
 735   $versionset .= "001";
 
 737   $versionset .= substr($header, -12, 10) . "    ";
 
 738   $versionset .= " " x 53;
 
 740   $main::lxdebug->leave_sub();
 
 746   $main::lxdebug->enter_sub();
 
 748   my ($self, $form, $fileno) = @_;
 
 750   my $stamm = $self->get_datev_stamm;
 
 752   my $ev_header  = _fill($stamm->{datentraegernr}, 3, ' ', 'left');
 
 754   $ev_header    .= _fill($stamm->{beraternr}, 7, ' ', 'left');
 
 755   $ev_header    .= _fill($stamm->{beratername}, 9, ' ', 'left');
 
 757   $ev_header    .= (_fill($fileno, 5, '0')) x 2;
 
 758   $ev_header    .= " " x 95;
 
 760   $main::lxdebug->leave_sub();
 
 765 sub kne_buchungsexport {
 
 766   $main::lxdebug->enter_sub();
 
 774   my $filename    = "ED00000";
 
 779   my $fromto = $self->fromto;
 
 781   $self->_get_transactions($fromto);
 
 783   return if $self->errors;
 
 787   while (scalar(@{ $self->{DATEV} || [] })) {
 
 790     my $ed_filename = $self->export_path . $filename;
 
 791     push(@filenames, $filename);
 
 792     my $header = $self->make_kne_data_header($form);
 
 794     my $kne_file = SL::DATEV::KNEFile->new();
 
 795     $kne_file->add_block($header);
 
 797     while (scalar(@{ $self->{DATEV} }) > 0) {
 
 798       my $transaction = shift @{ $self->{DATEV} };
 
 799       my $trans_lines = scalar(@{$transaction});
 
 808       my $buchungstext   = "";
 
 810       my $datevautomatik = 0;
 
 814       my $iconv          = $::locale->{iconv_utf8};
 
 815       my %umlaute = ($iconv->convert('ä') => 'ae',
 
 816                      $iconv->convert('ö') => 'oe',
 
 817                      $iconv->convert('ü') => 'ue',
 
 818                      $iconv->convert('Ä') => 'Ae',
 
 819                      $iconv->convert('Ö') => 'Oe',
 
 820                      $iconv->convert('Ü') => 'Ue',
 
 821                      $iconv->convert('ß') => 'sz');
 
 822       for (my $i = 0; $i < $trans_lines; $i++) {
 
 823         if ($trans_lines == 2) {
 
 824           if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
 
 825             $umsatz = $transaction->[$i]->{'amount'};
 
 828           if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
 
 829             $umsatz = $transaction->[$i]->{'umsatz'};
 
 832         if ($transaction->[$i]->{'datevautomatik'}) {
 
 835         if ($transaction->[$i]->{'taxkey'}) {
 
 836           $taxkey = $transaction->[$i]->{'taxkey'};
 
 838         if ($transaction->[$i]->{'charttax'}) {
 
 839           $charttax = $transaction->[$i]->{'charttax'};
 
 841         if ($transaction->[$i]->{'amount'} > 0) {
 
 848       # Umwandlung von Umlauten und Sonderzeichen in erlaubte Zeichen bei Textfeldern
 
 849       foreach my $umlaut (keys(%umlaute)) {
 
 850         $transaction->[$haben]->{'invnumber'} =~ s/${umlaut}/${umlaute{$umlaut}}/g;
 
 851         $transaction->[$haben]->{'name'}      =~ s/${umlaut}/${umlaute{$umlaut}}/g;
 
 854       $transaction->[$haben]->{'invnumber'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
 
 855       $transaction->[$haben]->{'name'}      =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
 
 857       $transaction->[$haben]->{'invnumber'} =  substr($transaction->[$haben]->{'invnumber'}, 0, 12);
 
 858       $transaction->[$haben]->{'name'}      =  substr($transaction->[$haben]->{'name'}, 0, 30);
 
 859       $transaction->[$haben]->{'invnumber'} =~ s/\ *$//;
 
 860       $transaction->[$haben]->{'name'}      =~ s/\ *$//;
 
 862       if ($trans_lines >= 2) {
 
 864         $gegenkonto = "a" . trim_leading_zeroes($transaction->[$haben]->{'accno'});
 
 865         $konto      = "e" . trim_leading_zeroes($transaction->[$soll]->{'accno'});
 
 866         if ($transaction->[$haben]->{'invnumber'} ne "") {
 
 867           $belegfeld1 = "\xBD" . $transaction->[$haben]->{'invnumber'} . "\x1C";
 
 870         $datum .= &datetofour($transaction->[$haben]->{'transdate'}, 0);
 
 871         $waehrung = "\xB3" . "EUR" . "\x1C";
 
 872         if ($transaction->[$haben]->{'name'} ne "") {
 
 873           $buchungstext = "\x1E" . $transaction->[$haben]->{'name'} . "\x1C";
 
 875         if ($transaction->[$haben]->{'duedate'} ne "") {
 
 876           $belegfeld2 = "\xBE" . &datetofour($transaction->[$haben]->{'duedate'}, 1) . "\x1C";
 
 880       $umsatz       = $kne_file->format_amount(abs($umsatz), 0);
 
 881       $umsatzsumme += $umsatz;
 
 882       $kne_file->add_block("+" . $umsatz);
 
 884       # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
 
 885       # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
 
 886       # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
 
 887       # Skript angelegt werden.
 
 888       # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
 
 889       # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
 
 890       # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
 
 891       # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
 
 892       # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
 
 894       if (   ( $datevautomatik || $taxkey)
 
 895           && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
 
 896 #         $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4"));
 
 897         $kne_file->add_block("\x6C${taxkey}");
 
 900       $kne_file->add_block($gegenkonto);
 
 901       $kne_file->add_block($belegfeld1);
 
 902       $kne_file->add_block($belegfeld2);
 
 903       $kne_file->add_block($datum);
 
 904       $kne_file->add_block($konto);
 
 905       $kne_file->add_block($buchungstext);
 
 906       $kne_file->add_block($waehrung . "\x79");
 
 909     my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
 
 911     $kne_file->add_block($mandantenendsumme);
 
 914     open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
 
 915     print(ED $kne_file->get_data());
 
 918     $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
 
 922   #Make EV Verwaltungsdatei
 
 923   my $ev_header = $self->make_ev_header($form, $fileno);
 
 924   my $ev_filename = $self->export_path . $evfile;
 
 925   push(@filenames, $evfile);
 
 926   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
 
 927   print(EV $ev_header);
 
 929   foreach my $file (@ed_versionset) {
 
 930     print(EV $ed_versionset[$file]);
 
 935   $self->add_filenames(@filenames);
 
 937   $main::lxdebug->leave_sub();
 
 939   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
 
 942 sub kne_stammdatenexport {
 
 943   $main::lxdebug->enter_sub();
 
 948   $self->get_datev_stamm->{abrechnungsnr} = "99";
 
 952   my $filename    = "ED00000";
 
 958   my $remaining_bytes = 256;
 
 959   my $total_bytes     = 256;
 
 960   my $buchungssatz    = "";
 
 962   my $ed_filename = $self->export_path . $filename;
 
 963   push(@filenames, $filename);
 
 964   open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
 
 965   my $header = $self->make_kne_data_header($form);
 
 966   $remaining_bytes -= length($header);
 
 970   my (@where, @values) = ((), ());
 
 971   if ($self->accnofrom) {
 
 972     push @where, 'c.accno >= ?';
 
 973     push @values, $self->accnofrom;
 
 975   if ($self->accnoto) {
 
 976     push @where, 'c.accno <= ?';
 
 977     push @values, $self->accnoto;
 
 980   my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
 
 982   my $query     = qq|SELECT c.accno, c.description
 
 987   my $sth = $self->dbh->prepare($query);
 
 988   $sth->execute(@values) || $form->dberror($query);
 
 990   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
 
 991     if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
 
 992       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
 
 993       $buchungssatz .= "\x00" x $fuellzeichen;
 
 995       $total_bytes = ($blockcount) * 256;
 
 997     $buchungssatz .= "t" . $ref->{'accno'};
 
 998     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
 
 999     $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
 
1000     $ref->{'description'} = substr($ref->{'description'}, 0, 40);
 
1001     $ref->{'description'} =~ s/\ *$//;
 
1004         ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
 
1007       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
 
1008       $buchungssatz .= "\x00" x $fuellzeichen;
 
1010       $total_bytes = ($blockcount) * 256;
 
1012     $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
 
1013     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
 
1018   print(ED $buchungssatz);
 
1019   $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
 
1020   my $dateiende = "\x00" x $fuellzeichen;
 
1022   print(ED $dateiende);
 
1025   #Make EV Verwaltungsdatei
 
1027     $self->make_ed_versionset($header, $filename, $blockcount);
 
1029   my $ev_header = $self->make_ev_header($form, $fileno);
 
1030   my $ev_filename = $self->export_path . $evfile;
 
1031   push(@filenames, $evfile);
 
1032   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
 
1033   print(EV $ev_header);
 
1035   foreach my $file (@ed_versionset) {
 
1036     print(EV $ed_versionset[$file]);
 
1040   $self->add_filenames(@filenames);
 
1042   $main::lxdebug->leave_sub();
 
1044   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
 
1048   clean_temporary_directories();
 
1059 SL::DATEV - kivitendo DATEV Export module
 
1063   use SL::DATEV qw(:CONSTANTS);
 
1065   my $datev = SL::DATEV->new(
 
1066     exporttype => DATEV_ET_BUCHUNGEN,
 
1067     format     => DATEV_FORMAT_KNE,
 
1072   my $datev = SL::DATEV->new(
 
1073     exporttype => DATEV_ET_STAMM,
 
1074     format     => DATEV_FORMAT_KNE,
 
1075     accnofrom  => $start_account_number,
 
1076     accnoto    => $end_account_number,
 
1079   # get or set datev stamm
 
1080   my $hashref = $datev->get_datev_stamm;
 
1081   $datev->save_datev_stamm($hashref);
 
1083   # manually clean up temporary directories
 
1084   $datev->clean_temporary_directories;
 
1089   if ($datev->errors) {
 
1090     die join "\n", $datev->error;
 
1093   # get relevant data for saving the export:
 
1094   my $dl_token = $datev->download_token;
 
1095   my $path     = $datev->export_path;
 
1096   my @files    = $datev->filenames;
 
1098   # retrieving an export at a later time
 
1099   my $datev = SL::DATEV->new(
 
1100     download_token => $dl_token_from_user,
 
1103   my $path     = $datev->export_path;
 
1104   my @files    = glob("$path/*");
 
1108 This module implements the DATEV export standard. For usage see above.
 
1116 Generic constructor. See section attributes for information about hat to pass.
 
1118 =item get_datev_stamm
 
1120 Loads DATEV Stammdaten and returns as hashref.
 
1122 =item save_datev_stamm HASHREF
 
1124 Saves DATEV Stammdaten from provided hashref.
 
1128 See L<CONSTANTS> for possible values
 
1130 =item has_exporttype
 
1132 Returns true if an exporttype has been set. Without exporttype most report functions won't work.
 
1136 Specifies the designated format of the export. Currently only KNE export is implemented.
 
1138 See L<CONSTANTS> for possible values
 
1142 Returns true if a format has been set. Without format most report functions won't work.
 
1144 =item download_token
 
1146 Returns a download token for this DATEV object.
 
1148 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
 
1152 Returns an export_path for this DATEV object.
 
1154 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
 
1158 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.
 
1160 =item net_gross_differences
 
1162 If there were any net gross differences during calculation they will be collected here.
 
1164 =item sum_net_gross_differences
 
1166 Sum of all differences.
 
1168 =item clean_temporary_directories
 
1170 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.
 
1174 Returns a list of errors that occured. If no errors occured, the export was a success.
 
1178 Exports data. You have to have set L<exporttype> and L<format> or an error will
 
1179 occur. OBE exports are currently not implemented.
 
1185 This is a list of attributes set in either the C<new> or a method of the same name.
 
1191 Set a database handle to use in the process. This allows for an export to be
 
1192 done on a transaction in progress without committing first.
 
1196 See L<CONSTANTS> for possible values. This MUST be set before export is called.
 
1200 See L<CONSTANTS> for possible values. This MUST be set before export is called.
 
1202 =item download_token
 
1204 Can be set on creation to retrieve a prior export for download.
 
1210 Set boundary dates for the export. Currently thse MUST be set for the export to work.
 
1216 Set boundary account numbers for the export. Only useful for a stammdaten export.
 
1222 =head2 Supplied to L<exporttype>
 
1226 =item DATEV_ET_BUCHUNGEN
 
1228 =item DATEV_ET_STAMM
 
1232 =head2 Supplied to L<format>.
 
1236 =item DATEV_FORMAT_KNE
 
1238 =item DATEV_FORMAT_OBE
 
1242 =head1 ERROR HANDLING
 
1244 This module will die in the following cases:
 
1250 No or unrecognized exporttype or format was provided for an export
 
1254 OBE rxport was called, which is not yet implemented.
 
1262 Errors that occur during th actual export will be collected in L<errors>. The following types can occur at the moment:
 
1268 C<Unbalanced Ledger!>. Exactly that, your ledger is unbalanced. Should never occur.
 
1272 C<Datev-Export fehlgeschlagen! Bei Transaktion %d (%f).>  This error occurs if a
 
1273 transaction could not be reliably sorted out, or had rounding errors over the acceptable threshold.
 
1277 =head1 BUGS AND CAVEATS
 
1283 Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to to get a working export.
 
1287 OBE export is currently not implemented.
 
1293 - handling of export_path and download token is a bit dodgy, clean that up.
 
1297 L<SL::DATEV::KNEFile>
 
1301 Philip Reetz E<lt>p.reetz@linet-services.deE<gt>,
 
1303 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
 
1305 Jan Büren E<lt>jan@lx-office-hosting.deE<gt>,
 
1307 Geoffrey Richardson E<lt>information@lx-office-hosting.deE<gt>,
 
1309 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,