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->{trans_id} = $_[0];
 
 203   return $self->{trans_id};
 
 210    $self->{accnofrom} = $_[0];
 
 213  return $self->{accnofrom};
 
 220    $self->{accnoto} = $_[0];
 
 223  return $self->{accnoto};
 
 231     $self->{dbh} = $_[0];
 
 232     $self->{provided_dbh} = 1;
 
 235   $self->{dbh} ||= $::form->get_standard_dbh;
 
 242 sub clean_temporary_directories {
 
 243   $::lxdebug->enter_sub;
 
 245   foreach my $path (glob($::lx_office_conf{paths}->{userspath} . "/datev-export-*")) {
 
 246     next unless -d $path;
 
 248     my $mtime = (stat($path))[9];
 
 249     next if ((time() - $mtime) < 8 * 60 * 60);
 
 254   $::lxdebug->leave_sub;
 
 258   $main::lxdebug->enter_sub();
 
 261   my $field_len = shift;
 
 262   my $fill_char = shift;
 
 263   my $alignment = shift || 'right';
 
 265   my $text_len  = length $text;
 
 267   if ($field_len < $text_len) {
 
 268     $text = substr $text, 0, $field_len;
 
 270   } elsif ($field_len > $text_len) {
 
 271     my $filler = ($fill_char) x ($field_len - $text_len);
 
 272     $text      = $alignment eq 'right' ? $filler . $text : $text . $filler;
 
 275   $main::lxdebug->leave_sub();
 
 280 sub get_datev_stamm {
 
 281   return $_[0]{stamm} ||= selectfirst_hashref_query($::form, $_[0]->dbh, 'SELECT * FROM datev');
 
 284 sub save_datev_stamm {
 
 285   my ($self, $data) = @_;
 
 287   do_query($::form, $self->dbh, 'DELETE FROM datev');
 
 289   my @columns = qw(beraternr beratername dfvkz mandantennr datentraegernr abrechnungsnr);
 
 291   my $query = "INSERT INTO datev (" . join(', ', @columns) . ") VALUES (" . join(', ', ('?') x @columns) . ")";
 
 292   do_query($::form, $self->dbh, $query, map { $data->{$_} } @columns);
 
 294   $self->dbh->commit unless $self->provided_dbh;
 
 301   die 'no format set!' unless $self->has_format;
 
 303   if ($self->format == DATEV_FORMAT_KNE) {
 
 304     $result = $self->kne_export;
 
 305   } elsif ($self->format == DATEV_FORMAT_OBE) {
 
 306     $result = $self->obe_export;
 
 308     die 'unrecognized export format';
 
 318   die 'no exporttype set!' unless $self->has_exporttype;
 
 320   if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
 
 321     $result = $self->kne_buchungsexport;
 
 322   } elsif ($self->exporttype == DATEV_ET_STAMM) {
 
 323     $result = $self->kne_stammdatenexport;
 
 325     die 'unrecognized exporttype';
 
 332   die 'not yet implemented';
 
 338   return unless $self->from && $self->to;
 
 340   return "transdate >= '" . $self->from->to_lxoffice . "' and transdate <= '" . $self->to->to_lxoffice . "'";
 
 347 sub _get_transactions {
 
 348   $main::lxdebug->enter_sub();
 
 351   my $progress_callback = shift || sub {};
 
 353   my $form     =  $main::form;
 
 355   my $trans_id_filter = '';
 
 357   $trans_id_filter = 'AND ac.trans_id = ' . $self->trans_id if $self->trans_id;
 
 361   $fromto      =~ s/transdate/ac\.transdate/g;
 
 363   my $taxkeys  = Taxkeys->new();
 
 364   my $filter   = '';            # Useful for debugging purposes
 
 366   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');
 
 369     qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey,
 
 370          ar.invnumber, ar.duedate, ar.amount as umsatz, ar.deliverydate,
 
 372          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
 
 375        LEFT JOIN ar          ON (ac.trans_id    = ar.id)
 
 376        LEFT JOIN customer ct ON (ar.customer_id = ct.id)
 
 377        LEFT JOIN chart c     ON (ac.chart_id    = c.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,
 
 391        LEFT JOIN ap        ON (ac.trans_id  = ap.id)
 
 392        LEFT JOIN vendor ct ON (ap.vendor_id = ct.id)
 
 393        LEFT JOIN chart c   ON (ac.chart_id  = c.id)
 
 394        WHERE (ap.id IS NOT NULL)
 
 401        SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey,
 
 402          gl.reference AS invnumber, gl.transdate AS duedate, ac.amount as umsatz, NULL as deliverydate,
 
 403          gl.description AS name,
 
 404          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
 
 407        LEFT JOIN gl      ON (ac.trans_id  = gl.id)
 
 408        LEFT JOIN chart c ON (ac.chart_id  = c.id)
 
 409        WHERE (gl.id IS NOT NULL)
 
 414        ORDER BY trans_id, acc_trans_id|;
 
 416   my $sth = prepare_execute_query($form, $self->dbh, $query);
 
 420   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
 
 422     if (($counter % 500) == 0) {
 
 423       $progress_callback->($counter);
 
 426     my $trans    = [ $ref ];
 
 428     my $count    = $ref->{amount};
 
 431     # if the amount of a booking in a group is smaller than 0.02, any tax
 
 432     # amounts will likely be smaller than 1 cent, so go into subcent mode
 
 433     my $subcent  = abs($count) < 0.02;
 
 435     # records from acc_trans are ordered by trans_id and acc_trans_id
 
 436     # first check for unbalanced ledger inside one trans_id
 
 437     # there may be several groups inside a trans_id, e.g. the original booking and the payment
 
 438     # each group individually should be exactly balanced and each group
 
 439     # individually needs its own datev lines
 
 441     # keep fetching new acc_trans lines until the end of a balanced group is reached
 
 442     while (abs($count) > 0.01 || $firstrun || ($subcent && abs($count) > 0.005)) {
 
 443       my $ref2 = $sth->fetchrow_hashref("NAME_lc");
 
 446       # check if trans_id of current acc_trans line is still the same as the
 
 447       # trans_id of the first line in group
 
 449       if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
 
 450         $self->add_error("Unbalanced ledger! old trans_id " . $trans->[0]->{trans_id} . " new trans_id " . $ref2->{trans_id} . " count $count");
 
 454       push @{ $trans }, $ref2;
 
 456       $count    += $ref2->{amount};
 
 460     foreach my $i (0 .. scalar(@{ $trans }) - 1) {
 
 461       my $ref        = $trans->[$i];
 
 462       my $prev_ref   = 0 < $i ? $trans->[$i - 1] : undef;
 
 463       if (   $all_taxchart_ids{$ref->{id}}
 
 464           && ($ref->{link} =~ m/(?:AP_tax|AR_tax)/)
 
 465           && (   ($prev_ref && $prev_ref->{taxkey} && (_sign($ref->{amount}) == _sign($prev_ref->{amount})))
 
 466               || $ref->{invoice})) {
 
 470       if (   !$ref->{invoice}   # we have a non-invoice booking (=gl)
 
 471           &&  $ref->{is_tax}    # that has "is_tax" set
 
 472           && !($prev_ref->{is_tax})  # previous line wasn't is_tax
 
 473           &&  (_sign($ref->{amount}) == _sign($prev_ref->{amount}))) {  # and sign same as previous sign
 
 474         $trans->[$i - 1]->{tax_amount} = $ref->{amount};
 
 478     my %taxid_taxkeys = ();
 
 480     if (scalar(@{$trans}) <= 2) {
 
 481       push @{ $self->{DATEV} }, $trans;
 
 485     # determine at which array position the reference value (called absumsatz) is
 
 486     # and which amount it has
 
 488     for my $j (0 .. (scalar(@{$trans}) - 1)) {
 
 491       # 1: gl transaction (Dialogbuchung), invoice is false, no double split booking allowed
 
 493       # 2: sales or vendor invoice (Verkaufs- und Einkaufsrechnung): invoice is
 
 494       # true, instead of absumsatz use link AR/AP (there should only be one
 
 497       # 3. AR/AP transaction (Kreditoren- und Debitorenbuchung): invoice is false,
 
 498       # instead of absumsatz use link AR/AP (there should only be one, so jump
 
 499       # out of search as soon as you find it )
 
 502       # for gl-bookings no split is allowed and there is no AR/AP account, so we always use the maximum value as a reference
 
 503       # for ap/ar bookings we can always search for AR/AP in link and use that
 
 504       if ( ( not $trans->[$j]->{'invoice'} and abs($trans->[$j]->{'amount'}) > abs($absumsatz) )
 
 505          or ($trans->[$j]->{'invoice'} and ($trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP'))) {
 
 506         $absumsatz     = $trans->[$j]->{'amount'};
 
 511       # Problem: we can't distinguish between AR and AP and normal invoices via boolean "invoice"
 
 512       # for AR and AP transaction exit the loop as soon as an AR or AP account is found
 
 513       # there must be only one AR or AP chart in the booking
 
 514       if ( $trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP') {
 
 515         $notsplitindex = $j;   # position in booking with highest amount
 
 516         $absumsatz     = $trans->[$j]->{'amount'};
 
 521     my $ml             = ($trans->[0]->{'umsatz'} > 0) ? 1 : -1;
 
 522     my $rounding_error = 0;
 
 525     # go through each line and determine if it is a tax booking or not
 
 526     # skip all tax lines and notsplitindex line
 
 527     # push all other accounts (e.g. income or expense) with corresponding taxkey
 
 529     for my $j (0 .. (scalar(@{$trans}) - 1)) {
 
 530       if (   ($j != $notsplitindex)
 
 531           && !$trans->[$j]->{is_tax}
 
 532           && (   $trans->[$j]->{'taxkey'} eq ""
 
 533               || $trans->[$j]->{'taxkey'} eq "0"
 
 534               || $trans->[$j]->{'taxkey'} eq "1"
 
 535               || $trans->[$j]->{'taxkey'} eq "10"
 
 536               || $trans->[$j]->{'taxkey'} eq "11")) {
 
 538         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
 
 540         $absumsatz               += $trans->[$j]->{'amount'};
 
 541         $new_trans{'amount'}      = $trans->[$j]->{'amount'} * (-1);
 
 542         $new_trans{'umsatz'}      = abs($trans->[$j]->{'amount'}) * $ml;
 
 543         $trans->[$j]->{'umsatz'}  = abs($trans->[$j]->{'amount'}) * $ml;
 
 545         push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
 
 547       } elsif (($j != $notsplitindex) && !$trans->[$j]->{is_tax}) {
 
 548         my %tax_info = $taxkeys->get_full_tax_info('transdate' => $trans->[$j]->{transdate},
 
 549                                                    'deliverydate' => $trans->[$j]->{deliverydate});
 
 552         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
 
 554         my $tax_rate              = $tax_info{taxkeys}->{ $trans->[$j]->{'taxkey'} }->{taxrate};
 
 555         $new_trans{'net_amount'}  = $trans->[$j]->{'amount'} * -1;
 
 556         $new_trans{'tax_rate'}    = 1 + $tax_rate;
 
 558         if (!$trans->[$j]->{'invoice'}) {
 
 559           $new_trans{'amount'}      = $form->round_amount(-1 * ($trans->[$j]->{amount} + $trans->[$j]->{tax_amount}), 2);
 
 560           $new_trans{'umsatz'}      = abs($new_trans{'amount'}) * $ml;
 
 561           $trans->[$j]->{'umsatz'}  = $new_trans{'umsatz'};
 
 562           $absumsatz               += -1 * $new_trans{'amount'};
 
 565           my $unrounded             = $trans->[$j]->{'amount'} * (1 + $tax_rate) * -1 + $rounding_error;
 
 566           my $rounded               = $form->round_amount($unrounded, 2);
 
 568           $rounding_error           = $unrounded - $rounded;
 
 569           $new_trans{'amount'}      = $rounded;
 
 570           $new_trans{'umsatz'}      = abs($rounded) * $ml;
 
 571           $trans->[$j]->{'umsatz'}  = $new_trans{umsatz};
 
 572           $absumsatz               -= $rounded;
 
 575         push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
 
 576         push @taxed, $self->{DATEV}->[-1];
 
 582     while ((abs($absumsatz) >= 0.01) && (abs($absumsatz) < 1.00)) {
 
 583       if ($idx >= scalar @taxed) {
 
 584         last if (!$correction);
 
 590       my $transaction = $taxed[$idx]->[0];
 
 592       my $old_amount     = $transaction->{amount};
 
 593       my $old_correction = $correction;
 
 596       if (!$transaction->{diff}) {
 
 597         @possible_diffs = (0.01, -0.01);
 
 599         @possible_diffs = ($transaction->{diff});
 
 602       foreach my $diff (@possible_diffs) {
 
 603         my $net_amount = $form->round_amount(($transaction->{amount} + $diff) / $transaction->{tax_rate}, 2);
 
 604         next if ($net_amount != $transaction->{net_amount});
 
 606         $transaction->{diff}    = $diff;
 
 607         $transaction->{amount} += $diff;
 
 608         $transaction->{umsatz} += $diff;
 
 618     $absumsatz = $form->round_amount($absumsatz, 2);
 
 619     if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
 
 620       $self->add_error("Datev-Export fehlgeschlagen! Bei Transaktion $trans->[0]->{trans_id} ($absumsatz)");
 
 622     } elsif (abs($absumsatz) >= 0.01) {
 
 623       $self->add_net_gross_differences($absumsatz);
 
 629   $::lxdebug->leave_sub;
 
 632 sub make_kne_data_header {
 
 633   $main::lxdebug->enter_sub();
 
 635   my ($self, $form) = @_;
 
 638   my $stamm = $self->get_datev_stamm;
 
 640   my $jahr = $self->from ? $self->from->year : DateTime->today->year;
 
 643   my $header  = "\x1D\x181";
 
 644   $header    .= _fill($stamm->{datentraegernr}, 3, ' ', 'left');
 
 645   $header    .= ($self->fromto) ? "11" : "13"; # Anwendungsnummer
 
 646   $header    .= _fill($stamm->{dfvkz}, 2, '0');
 
 647   $header    .= _fill($stamm->{beraternr}, 7, '0');
 
 648   $header    .= _fill($stamm->{mandantennr}, 5, '0');
 
 649   $header    .= _fill($stamm->{abrechnungsnr} . $jahr, 6, '0');
 
 651   $header .= $self->from ? $self->from->strftime('%d%m%y') : '';
 
 652   $header .= $self->to   ? $self->to->strftime('%d%m%y')   : '';
 
 656     $header .= $primanota;
 
 659   $header .= _fill($stamm->{passwort}, 4, '0');
 
 660   $header .= " " x 16;       # Anwendungsinfo
 
 661   $header .= " " x 16;       # Inputinfo
 
 665   my $versionssatz  = $self->exporttype == DATEV_ET_BUCHUNGEN ? "\xB5" . "1," : "\xB6" . "1,";
 
 667   my $query         = qq|SELECT accno FROM chart LIMIT 1|;
 
 668   my $ref           = selectfirst_hashref_query($form, $self->dbh, $query);
 
 670   $versionssatz    .= length $ref->{accno};
 
 671   $versionssatz    .= ",";
 
 672   $versionssatz    .= length $ref->{accno};
 
 673   $versionssatz    .= ",SELF" . "\x1C\x79";
 
 675   $header          .= $versionssatz;
 
 677   $main::lxdebug->leave_sub();
 
 683   $main::lxdebug->enter_sub();
 
 685   my ($date, $six) = @_;
 
 687   my ($day, $month, $year) = split(/\./, $date);
 
 690     $day = substr($day, 1, 1);
 
 692   if (length($month) < 2) {
 
 693     $month = "0" . $month;
 
 695   if (length($year) > 2) {
 
 696     $year = substr($year, -2, 2);
 
 700     $date = $day . $month . $year;
 
 702     $date = $day . $month;
 
 705   $main::lxdebug->leave_sub();
 
 710 sub trim_leading_zeroes {
 
 718 sub make_ed_versionset {
 
 719   $main::lxdebug->enter_sub();
 
 721   my ($self, $header, $filename, $blockcount) = @_;
 
 723   my $versionset  = "V" . substr($filename, 2, 5);
 
 724   $versionset    .= substr($header, 6, 22);
 
 727     $versionset .= "0000" . substr($header, 28, 19);
 
 729     my $datum = " " x 16;
 
 730     $versionset .= $datum . "001" . substr($header, 28, 4);
 
 733   $versionset .= _fill($blockcount, 5, '0');
 
 734   $versionset .= "001";
 
 736   $versionset .= substr($header, -12, 10) . "    ";
 
 737   $versionset .= " " x 53;
 
 739   $main::lxdebug->leave_sub();
 
 745   $main::lxdebug->enter_sub();
 
 747   my ($self, $form, $fileno) = @_;
 
 749   my $stamm = $self->get_datev_stamm;
 
 751   my $ev_header  = _fill($stamm->{datentraegernr}, 3, ' ', 'left');
 
 753   $ev_header    .= _fill($stamm->{beraternr}, 7, ' ', 'left');
 
 754   $ev_header    .= _fill($stamm->{beratername}, 9, ' ', 'left');
 
 756   $ev_header    .= (_fill($fileno, 5, '0')) x 2;
 
 757   $ev_header    .= " " x 95;
 
 759   $main::lxdebug->leave_sub();
 
 764 sub kne_buchungsexport {
 
 765   $main::lxdebug->enter_sub();
 
 773   my $filename    = "ED00000";
 
 778   my $fromto = $self->fromto;
 
 780   $self->_get_transactions($fromto);
 
 782   return if $self->errors;
 
 786   while (scalar(@{ $self->{DATEV} || [] })) {
 
 789     my $ed_filename = $self->export_path . $filename;
 
 790     push(@filenames, $filename);
 
 791     my $header = $self->make_kne_data_header($form);
 
 793     my $kne_file = SL::DATEV::KNEFile->new();
 
 794     $kne_file->add_block($header);
 
 796     while (scalar(@{ $self->{DATEV} }) > 0) {
 
 797       my $transaction = shift @{ $self->{DATEV} };
 
 798       my $trans_lines = scalar(@{$transaction});
 
 807       my $buchungstext   = "";
 
 809       my $datevautomatik = 0;
 
 813       my $iconv          = $::locale->{iconv_utf8};
 
 814       my %umlaute = ($iconv->convert('ä') => 'ae',
 
 815                      $iconv->convert('ö') => 'oe',
 
 816                      $iconv->convert('ü') => 'ue',
 
 817                      $iconv->convert('Ä') => 'Ae',
 
 818                      $iconv->convert('Ö') => 'Oe',
 
 819                      $iconv->convert('Ü') => 'Ue',
 
 820                      $iconv->convert('ß') => 'sz');
 
 821       for (my $i = 0; $i < $trans_lines; $i++) {
 
 822         if ($trans_lines == 2) {
 
 823           if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
 
 824             $umsatz = $transaction->[$i]->{'amount'};
 
 827           if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
 
 828             $umsatz = $transaction->[$i]->{'umsatz'};
 
 831         if ($transaction->[$i]->{'datevautomatik'}) {
 
 834         if ($transaction->[$i]->{'taxkey'}) {
 
 835           $taxkey = $transaction->[$i]->{'taxkey'};
 
 837         if ($transaction->[$i]->{'charttax'}) {
 
 838           $charttax = $transaction->[$i]->{'charttax'};
 
 840         if ($transaction->[$i]->{'amount'} > 0) {
 
 847       # Umwandlung von Umlauten und Sonderzeichen in erlaubte Zeichen bei Textfeldern
 
 848       foreach my $umlaut (keys(%umlaute)) {
 
 849         $transaction->[$haben]->{'invnumber'} =~ s/${umlaut}/${umlaute{$umlaut}}/g;
 
 850         $transaction->[$haben]->{'name'}      =~ s/${umlaut}/${umlaute{$umlaut}}/g;
 
 853       $transaction->[$haben]->{'invnumber'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
 
 854       $transaction->[$haben]->{'name'}      =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
 
 856       $transaction->[$haben]->{'invnumber'} =  substr($transaction->[$haben]->{'invnumber'}, 0, 12);
 
 857       $transaction->[$haben]->{'name'}      =  substr($transaction->[$haben]->{'name'}, 0, 30);
 
 858       $transaction->[$haben]->{'invnumber'} =~ s/\ *$//;
 
 859       $transaction->[$haben]->{'name'}      =~ s/\ *$//;
 
 861       if ($trans_lines >= 2) {
 
 863         $gegenkonto = "a" . trim_leading_zeroes($transaction->[$haben]->{'accno'});
 
 864         $konto      = "e" . trim_leading_zeroes($transaction->[$soll]->{'accno'});
 
 865         if ($transaction->[$haben]->{'invnumber'} ne "") {
 
 866           $belegfeld1 = "\xBD" . $transaction->[$haben]->{'invnumber'} . "\x1C";
 
 869         $datum .= &datetofour($transaction->[$haben]->{'transdate'}, 0);
 
 870         $waehrung = "\xB3" . "EUR" . "\x1C";
 
 871         if ($transaction->[$haben]->{'name'} ne "") {
 
 872           $buchungstext = "\x1E" . $transaction->[$haben]->{'name'} . "\x1C";
 
 874         if ($transaction->[$haben]->{'duedate'} ne "") {
 
 875           $belegfeld2 = "\xBE" . &datetofour($transaction->[$haben]->{'duedate'}, 1) . "\x1C";
 
 879       $umsatz       = $kne_file->format_amount(abs($umsatz), 0);
 
 880       $umsatzsumme += $umsatz;
 
 881       $kne_file->add_block("+" . $umsatz);
 
 883       # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
 
 884       # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
 
 885       # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
 
 886       # Skript angelegt werden.
 
 887       # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
 
 888       # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
 
 889       # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
 
 890       # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
 
 891       # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
 
 893       if (   ( $datevautomatik || $taxkey)
 
 894           && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
 
 895 #         $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4"));
 
 896         $kne_file->add_block("\x6C${taxkey}");
 
 899       $kne_file->add_block($gegenkonto);
 
 900       $kne_file->add_block($belegfeld1);
 
 901       $kne_file->add_block($belegfeld2);
 
 902       $kne_file->add_block($datum);
 
 903       $kne_file->add_block($konto);
 
 904       $kne_file->add_block($buchungstext);
 
 905       $kne_file->add_block($waehrung . "\x79");
 
 908     my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
 
 910     $kne_file->add_block($mandantenendsumme);
 
 913     open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
 
 914     print(ED $kne_file->get_data());
 
 917     $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
 
 921   #Make EV Verwaltungsdatei
 
 922   my $ev_header = $self->make_ev_header($form, $fileno);
 
 923   my $ev_filename = $self->export_path . $evfile;
 
 924   push(@filenames, $evfile);
 
 925   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
 
 926   print(EV $ev_header);
 
 928   foreach my $file (@ed_versionset) {
 
 929     print(EV $ed_versionset[$file]);
 
 934   $self->add_filenames(@filenames);
 
 936   $main::lxdebug->leave_sub();
 
 938   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
 
 941 sub kne_stammdatenexport {
 
 942   $main::lxdebug->enter_sub();
 
 947   $self->get_datev_stamm->{abrechnungsnr} = "99";
 
 951   my $filename    = "ED00000";
 
 957   my $remaining_bytes = 256;
 
 958   my $total_bytes     = 256;
 
 959   my $buchungssatz    = "";
 
 961   my $ed_filename = $self->export_path . $filename;
 
 962   push(@filenames, $filename);
 
 963   open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
 
 964   my $header = $self->make_kne_data_header($form);
 
 965   $remaining_bytes -= length($header);
 
 969   my (@where, @values) = ((), ());
 
 970   if ($self->accnofrom) {
 
 971     push @where, 'c.accno >= ?';
 
 972     push @values, $self->accnofrom;
 
 974   if ($self->accnoto) {
 
 975     push @where, 'c.accno <= ?';
 
 976     push @values, $self->accnoto;
 
 979   my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
 
 981   my $query     = qq|SELECT c.accno, c.description
 
 986   my $sth = $self->dbh->prepare($query);
 
 987   $sth->execute(@values) || $form->dberror($query);
 
 989   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
 
 990     if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
 
 991       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
 
 992       $buchungssatz .= "\x00" x $fuellzeichen;
 
 994       $total_bytes = ($blockcount) * 256;
 
 996     $buchungssatz .= "t" . $ref->{'accno'};
 
 997     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
 
 998     $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
 
 999     $ref->{'description'} = substr($ref->{'description'}, 0, 40);
 
1000     $ref->{'description'} =~ s/\ *$//;
 
1003         ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
 
1006       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
 
1007       $buchungssatz .= "\x00" x $fuellzeichen;
 
1009       $total_bytes = ($blockcount) * 256;
 
1011     $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
 
1012     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
 
1017   print(ED $buchungssatz);
 
1018   $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
 
1019   my $dateiende = "\x00" x $fuellzeichen;
 
1021   print(ED $dateiende);
 
1024   #Make EV Verwaltungsdatei
 
1026     $self->make_ed_versionset($header, $filename, $blockcount);
 
1028   my $ev_header = $self->make_ev_header($form, $fileno);
 
1029   my $ev_filename = $self->export_path . $evfile;
 
1030   push(@filenames, $evfile);
 
1031   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
 
1032   print(EV $ev_header);
 
1034   foreach my $file (@ed_versionset) {
 
1035     print(EV $ed_versionset[$file]);
 
1039   $self->add_filenames(@filenames);
 
1041   $main::lxdebug->leave_sub();
 
1043   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
 
1047   clean_temporary_directories();
 
1058 SL::DATEV - kivitendo DATEV Export module
 
1062   use SL::DATEV qw(:CONSTANTS);
 
1064   my $datev = SL::DATEV->new(
 
1065     exporttype => DATEV_ET_BUCHUNGEN,
 
1066     format     => DATEV_FORMAT_KNE,
 
1071   my $datev = SL::DATEV->new(
 
1072     exporttype => DATEV_ET_STAMM,
 
1073     format     => DATEV_FORMAT_KNE,
 
1074     accnofrom  => $start_account_number,
 
1075     accnoto    => $end_account_number,
 
1078   # get or set datev stamm
 
1079   my $hashref = $datev->get_datev_stamm;
 
1080   $datev->save_datev_stamm($hashref);
 
1082   # manually clean up temporary directories
 
1083   $datev->clean_temporary_directories;
 
1088   if ($datev->errors) {
 
1089     die join "\n", $datev->error;
 
1092   # get relevant data for saving the export:
 
1093   my $dl_token = $datev->download_token;
 
1094   my $path     = $datev->export_path;
 
1095   my @files    = $datev->filenames;
 
1097   # retrieving an export at a later time
 
1098   my $datev = SL::DATEV->new(
 
1099     download_token => $dl_token_from_user,
 
1102   my $path     = $datev->export_path;
 
1103   my @files    = glob("$path/*");
 
1107 This module implements the DATEV export standard. For usage see above.
 
1115 Generic constructor. See section attributes for information about hat to pass.
 
1117 =item get_datev_stamm
 
1119 Loads DATEV Stammdaten and returns as hashref.
 
1121 =item save_datev_stamm HASHREF
 
1123 Saves DATEV Stammdaten from provided hashref.
 
1127 See L<CONSTANTS> for possible values
 
1129 =item has_exporttype
 
1131 Returns true if an exporttype has been set. Without exporttype most report functions won't work.
 
1135 Specifies the designated format of the export. Currently only KNE export is implemented.
 
1137 See L<CONSTANTS> for possible values
 
1141 Returns true if a format has been set. Without format most report functions won't work.
 
1143 =item download_token
 
1145 Returns a download token for this DATEV object.
 
1147 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
 
1151 Returns an export_path for this DATEV object.
 
1153 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
 
1157 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.
 
1159 =item net_gross_differences
 
1161 If there were any net gross differences during calculation they will be collected here.
 
1163 =item sum_net_gross_differences
 
1165 Sum of all differences.
 
1167 =item clean_temporary_directories
 
1169 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.
 
1173 Returns a list of errors that occured. If no errors occured, the export was a success.
 
1177 Exports data. You have to have set L<exporttype> and L<format> or an error will
 
1178 occur. OBE exports are currently not implemented.
 
1184 This is a list of attributes set in either the C<new> or a method of the same name.
 
1190 Set a database handle to use in the process. This allows for an export to be
 
1191 done on a transaction in progress without committing first.
 
1195 See L<CONSTANTS> for possible values. This MUST be set before export is called.
 
1199 See L<CONSTANTS> for possible values. This MUST be set before export is called.
 
1201 =item download_token
 
1203 Can be set on creation to retrieve a prior export for download.
 
1209 Set boundary dates for the export. Currently thse MUST be set for the export to work.
 
1215 Set boundary account numbers for the export. Only useful for a stammdaten export.
 
1221 =head2 Supplied to L<exporttype>
 
1225 =item DATEV_ET_BUCHUNGEN
 
1227 =item DATEV_ET_STAMM
 
1231 =head2 Supplied to L<format>.
 
1235 =item DATEV_FORMAT_KNE
 
1237 =item DATEV_FORMAT_OBE
 
1241 =head1 ERROR HANDLING
 
1243 This module will die in the following cases:
 
1249 No or unrecognized exporttype or format was provided for an export
 
1253 OBE rxport was called, which is not yet implemented.
 
1261 Errors that occur during th actual export will be collected in L<errors>. The following types can occur at the moment:
 
1267 C<Unbalanced Ledger!>. Exactly that, your ledger is unbalanced. Should never occur.
 
1271 C<Datev-Export fehlgeschlagen! Bei Transaktion %d (%f).>  This error occurs if a
 
1272 transaction could not be reliably sorted out, or had rounding errors over the acceptable threshold.
 
1276 =head1 BUGS AND CAVEATS
 
1282 Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to to get a working export.
 
1286 OBE export is currently not implemented.
 
1292 - handling of export_path and download token is a bit dodgy, clean that up.
 
1296 L<SL::DATEV::KNEFile>
 
1300 Philip Reetz E<lt>p.reetz@linet-services.deE<gt>,
 
1302 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
 
1304 Jan Büren E<lt>jan@lx-office-hosting.deE<gt>,
 
1306 Geoffrey Richardson E<lt>information@lx-office-hosting.deE<gt>,
 
1308 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,